from __future__ import annotations

from copy import deepcopy
from typing import Any

from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
from ..json_types import JSONObject
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
from ..solution.compiler.icon_utils import workspace_icon_config
from .app_tools import _analysis_supported_for_view_type
from .base import ToolBase
from .qingbi_report_tools import QingbiReportTools, _coerce_tool_error


class ResourceReadTools(ToolBase):
    """资源读取工具（中文名：统一只读聚合）。

    类型：只读聚合工具。
    主要职责：
    1. 聚合应用、视图、图表等读取能力；
    2. 为上层流程提供统一资源读取入口；
    3. 避免多工具重复初始化造成的调用复杂度。
    """

    def __init__(self, sessions, backend) -> None:
        """执行内部辅助逻辑。"""
        super().__init__(sessions, backend)
        self.charts = QingbiReportTools(sessions, backend)

    def portal_list(self, *, profile: str) -> JSONObject:
        """执行门户相关逻辑。"""
        def runner(session_profile, context):
            raw_items = self.backend.request("GET", context, "/dash")
            items = _normalize_portal_list_items(raw_items)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "warnings": [],
                "verification": {"portal_list_loaded": True},
                "data": {
                    "items": items,
                    "total": len(items),
                },
            }

        return self._run(profile, runner, tool_name='资源读取-门户列表')

    def portal_get(self, *, profile: str, dash_key: str) -> JSONObject:
        """执行门户相关逻辑。"""
        self._require_dash_key(dash_key)

        def runner(session_profile, context):
            result = self.backend.request("GET", context, f"/dash/{dash_key}", params={"beingDraft": False})
            dash_name = str(result.get("dashName") or "").strip() or None
            dash_icon = str(result.get("dashIcon") or "").strip() or None
            package_tag_ids = [
                tag_id
                for tag_id in (
                    _coerce_positive_int((tag or {}).get("tagId"))
                    for tag in (result.get("tags") or [])
                    if isinstance(tag, dict)
                )
                if tag_id is not None
            ]
            components = _normalize_user_portal_components(result.get("components"))
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "warnings": [],
                "verification": {"portal_exists": True},
                "data": {
                    "dash_key": dash_key,
                    "dash_name": dash_name,
                    "dash_icon": dash_icon,
                    "icon_config": workspace_icon_config(dash_icon),
                    "package_tag_ids": package_tag_ids,
                    "component_count": len(components),
                    "components": components,
                },
            }

        return self._run(profile, runner, tool_name='资源读取-门户详情')

    def view_get(self, *, profile: str, view_id: str) -> JSONObject:
        """执行视图相关逻辑。"""
        self._require_view_id(view_id)
        view_key = _extract_custom_view_key(view_id)
        system_view = _lookup_system_view_descriptor(view_id)
        if view_key is None and system_view is None:
            raise_tool_error(
                QingflowApiError.config_error(
                    "view_get only accepts accessible view_id values such as `custom:VIEW_KEY` or `system:all`; use app_get or portal_get first to find a valid view_id."
                )
            )
        if system_view is not None:
            export_capability = _view_export_capability_payload(
                supported=_export_supported_for_view_type(system_view["view_type"])
            )
            return self._run(
                profile,
                lambda session_profile, _context: {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "ok": True,
                    "warnings": [
                        {
                            "code": "VIEW_APP_KEY_UNRESOLVED",
                            "message": f"view_get could not resolve app_key for system view `{view_id}`; keep using the app_key from the parent app context.",
                        },
                        {
                            "code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
                            "message": f"view_get supports exporting `{view_id}`, but the export call still needs the parent app context `app_key`.",
                        },
                    ],
                    "verification": {
                        "view_exists": True,
                        "descriptor_only": True,
                        "export_route_supported": export_capability["supported"],
                        "export_permission_verified": export_capability["permission_verified"],
                    },
                    "data": {
                        "app_key": None,
                        "view_id": view_id,
                        "view_key": None,
                        "view_name": system_view["view_name"],
                        "view_type": system_view["view_type"],
                        "visible_columns": [],
                        "analysis_supported": system_view["analysis_supported"],
                        "export_capability": export_capability,
                    },
                },
            tool_name='资源读取-视图详情',
            )

        def runner(session_profile, context):
            raw_view_type = None
            warnings: list[JSONObject] = []
            verification = {
                "view_exists": True,
                "config_verified": True,
                "base_info_verified": True,
                "questions_verified": True,
            }
            try:
                config = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
                if not isinstance(config, dict):
                    config = {}
            except QingflowApiError as error:
                if not _is_optional_view_config_error(error):
                    raise
                config = {}
                verification["config_verified"] = False
                warnings.append(
                    {
                        "code": "VIEW_CONFIG_UNAVAILABLE",
                        "message": "view_get used baseInfo because viewConfig is unavailable for this user.",
                        "backend_code": error.backend_code,
                        "http_status": error.http_status,
                        "request_id": error.request_id,
                    }
                )
            try:
                base_info = self.backend.request("GET", context, f"/view/{view_key}/viewConfig/baseInfo")
            except QingflowApiError as error:
                if not _is_optional_view_base_error(error):
                    raise
                base_info = {}
                verification["base_info_verified"] = False
                warnings.append(
                    {
                        "code": "VIEW_BASE_INFO_UNAVAILABLE",
                        "message": "view_get used viewConfig because view baseInfo is unavailable for this user.",
                        "backend_code": error.backend_code,
                        "http_status": error.http_status,
                        "request_id": error.request_id,
                    }
                )
            questions: list[dict[str, Any]] = []
            try:
                questions_payload = self.backend.request("GET", context, f"/view/{view_key}/question")
                if isinstance(questions_payload, list):
                    questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
            except QingflowApiError as error:
                if not _is_optional_view_question_error(error):
                    raise
                verification["questions_verified"] = False
                warnings.append(
                    {
                        "code": "VIEW_QUESTIONS_UNAVAILABLE",
                        "message": "view_get could not load visible columns because question readback is unavailable.",
                        "backend_code": error.backend_code,
                        "http_status": error.http_status,
                        "request_id": error.request_id,
                    }
                )

            raw_view_type = (
                str(base_info.get("viewgraphType") or "").strip()
                or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
            )
            export_capability = _view_export_capability_payload(
                supported=_export_supported_for_view_type(raw_view_type or None)
            )
            verification["export_route_supported"] = export_capability["supported"]
            verification["export_permission_verified"] = export_capability["permission_verified"]
            resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
            if not resolved_app_key:
                resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
            if not resolved_app_key:
                resolved_app_key = self._resolve_app_key_from_form_id(
                    context=context,
                    form_id=_coerce_positive_int(base_info.get("formId") or config.get("formId")),
                )
            if not resolved_app_key:
                warnings.append(
                    {
                        "code": "VIEW_APP_KEY_UNRESOLVED",
                        "message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
                    }
                )
                if export_capability["supported"]:
                    warnings.append(
                        {
                            "code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
                            "message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
                        }
                    )
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "warnings": warnings,
                "verification": verification,
                "data": {
                    "app_key": resolved_app_key,
                    "view_id": view_id,
                    "view_key": view_key,
                    "view_name": str(
                        base_info.get("viewgraphName") or config.get("viewgraphName") or config.get("viewName") or view_key
                    ).strip()
                    or view_key,
                    "view_type": _normalize_view_type(raw_view_type),
                    "visible_columns": [
                        str(item.get("queTitle") or item.get("title") or "").strip()
                        for item in questions
                        if str(item.get("queTitle") or item.get("title") or "").strip()
                    ],
                    "analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
                    "export_capability": export_capability,
                },
            }

        return self._run(profile, runner, tool_name='资源读取-视图详情')

    def chart_get(self, *, profile: str, chart_id: str) -> JSONObject:
        """执行图表相关逻辑。"""
        self._require_chart_id(chart_id)

        def runner(session_profile, _context):
            warnings: list[JSONObject] = []
            verification = {
                "chart_exists": True,
                "chart_base_loaded": False,
                "chart_data_loaded": False,
                "chart_config_loaded": False,
            }
            transport_errors: list[JSONObject] = []
            base: dict[str, Any] = {}
            try:
                base_result = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
                if isinstance(base_result, dict):
                    base = deepcopy(base_result)
                    verification["chart_base_loaded"] = True
            except (QingflowApiError, RuntimeError) as error:
                api_error = _coerce_tool_error(error)
                if api_error is None or not _is_optional_chart_base_error(api_error):
                    raise
                transport_errors.append(_transport_error_payload(stage="base_info", error=api_error))
                warnings.append(
                    {
                        "code": "CHART_BASE_INFO_UNAVAILABLE",
                        "message": "chart_get could not load chart base info; continuing with chart data when available.",
                        **_transport_warning_fields(api_error),
                    }
                )
            data: Any = None
            data_config: dict[str, Any] = {}
            try:
                data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
                verification["chart_data_loaded"] = True
                if isinstance(data, dict) and isinstance(data.get("config"), dict):
                    data_config = deepcopy(data.get("config"))
                    verification["chart_config_loaded"] = True
            except (QingflowApiError, RuntimeError) as error:
                api_error = _coerce_tool_error(error)
                if api_error is None or not _is_optional_chart_data_error(api_error):
                    raise
                transport_errors.append(_transport_error_payload(stage="data", error=api_error))
                warnings.append(
                    {
                        "code": "CHART_DATA_UNAVAILABLE",
                        "message": "chart_get could not load chart data; returning chart metadata and config when available.",
                        **_transport_warning_fields(api_error),
                    }
                )
            if not data_config:
                try:
                    config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
                except (QingflowApiError, RuntimeError) as error:
                    api_error = _coerce_tool_error(error)
                    if api_error is None or not _is_optional_chart_config_error(api_error):
                        raise
                    transport_errors.append(_transport_error_payload(stage="config", error=api_error))
                    warnings.append(
                        {
                            "code": "CHART_CONFIG_UNAVAILABLE",
                            "message": "chart_get could not load chart config; returning chart base info or data when available.",
                            **_transport_warning_fields(api_error),
                        }
                    )
                    config_result = {}
                if isinstance(config_result, dict) and config_result:
                    data_config = deepcopy(config_result)
                    verification["chart_config_loaded"] = True
            if not any(
                bool(verification[key])
                for key in ("chart_base_loaded", "chart_data_loaded", "chart_config_loaded")
            ):
                return {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "ok": False,
                    "status": "failed",
                    "error_code": "CHART_READ_UNAVAILABLE",
                    "message": "chart_get could not load chart base info, data, or config in this permission context.",
                    "warnings": warnings,
                    "verification": verification,
                    "details": {
                        "chart_id": chart_id,
                        "transport_errors": transport_errors,
                    },
                }
            data_payload: dict[str, Any] = {
                "chart_id": chart_id,
                "chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
                "chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
                "data_source_type": str(base.get("dataSourceType") or "").strip() or None,
                "data_source_id": str(base.get("dataSourceId") or "").strip() or None,
            }
            if verification["chart_data_loaded"]:
                data_payload["data"] = deepcopy(data) if isinstance(data, dict) else {"value": data}
            if data_config and not verification["chart_data_loaded"]:
                data_payload["config"] = deepcopy(data_config)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "warnings": warnings,
                "verification": verification,
                "data": data_payload,
            }

        return self._run(profile, runner, tool_name='资源读取-报表详情')

    def _require_dash_key(self, dash_key: str) -> None:
        """执行内部辅助逻辑。"""
        if not dash_key:
            raise_tool_error(QingflowApiError.config_error("dash_key is required"))

    def _require_view_id(self, view_id: str) -> None:
        """执行内部辅助逻辑。"""
        if not view_id:
            raise_tool_error(QingflowApiError.config_error("view_id is required"))

    def _require_chart_id(self, chart_id: str) -> None:
        """执行内部辅助逻辑。"""
        if not chart_id:
            raise_tool_error(QingflowApiError.config_error("chart_id is required"))

    def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
        try:
            form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
        except QingflowApiError as exc:
            if not _is_optional_view_metadata_resolution_error(exc):
                raise
            return None
        if not isinstance(form_payload, dict):
            return None
        app_key = str(form_payload.get("appKey") or "").strip()
        return app_key or None

    def _resolve_app_key_from_form_id(self, *, context: Any, form_id: int | None) -> str | None:
        """执行内部辅助逻辑。"""
        if form_id is None:
            return None
        try:
            payload = self.backend.request("GET", context, "/tag/apps")
        except QingflowApiError as exc:
            if not _is_optional_view_metadata_resolution_error(exc):
                raise
            payload = None
        app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
        if app_key:
            return app_key
        return None


def _normalize_portal_list_items(raw_items: Any) -> list[dict[str, Any]]:
    if not isinstance(raw_items, list):
        return []
    items: list[dict[str, Any]] = []
    for item in raw_items:
        if not isinstance(item, dict):
            continue
        dash_key = str(item.get("dashKey") or "").strip()
        dash_name = str(item.get("dashName") or "").strip()
        dash_icon = str(item.get("dashIcon") or "").strip() or None
        package_tag_ids = [
            tag_id
            for tag_id in (
                _coerce_positive_int((tag or {}).get("tagId"))
                for tag in (item.get("tags") or [])
                if isinstance(tag, dict)
            )
            if tag_id is not None
        ]
        if not any((dash_key, dash_name, dash_icon, package_tag_ids)):
            continue
        items.append(
            {
                "dash_key": dash_key or None,
                "dash_name": dash_name or None,
                "dash_icon": dash_icon,
                "icon_config": workspace_icon_config(dash_icon),
                "package_tag_ids": package_tag_ids,
            }
        )
    return items


def _normalize_user_portal_components(components: Any) -> list[dict[str, Any]]:
    if not isinstance(components, list):
        return []
    items: list[dict[str, Any]] = []
    config_key_map = {
        "grid": "gridConfig",
        "link": "linkConfig",
        "text": "textConfig",
        "filter": "filterConfig",
        "chart": "chartConfig",
        "view": "viewgraphConfig",
    }
    for index, component in enumerate(components):
        if not isinstance(component, dict):
            continue
        source_type = _normalize_portal_component_source_type(component.get("type"))
        title = _extract_portal_component_title(component, source_type=source_type)
        summary: dict[str, Any] = {
            "order": index,
            "source_type": source_type,
            "title": title,
        }
        position = component.get("position")
        if isinstance(position, dict):
            summary["position"] = deepcopy(position)
        config_key = config_key_map.get(source_type, "")
        config = component.get(config_key) if isinstance(component.get(config_key), dict) else {}
        if source_type == "chart":
            summary["chart_ref"] = {
                "chart_id": str(config.get("biChartId") or "").strip() or None,
                "chart_name": str(config.get("chartComponentTitle") or title or "").strip() or None,
            }
        elif source_type == "view":
            view_key = str(config.get("viewgraphKey") or "").strip() or None
            summary["view_ref"] = {
                "app_key": str(config.get("appKey") or "").strip() or None,
                "view_id": f"custom:{view_key}" if view_key else None,
                "view_key": view_key,
                "view_name": str(config.get("viewgraphName") or title or "").strip() or None,
            }
        elif source_type in {"grid", "link", "text", "filter"} and config:
            summary["config"] = deepcopy(config)
        items.append(summary)
    return items


def _is_optional_chart_base_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404, 81007} or error.http_status == 404


def _is_optional_chart_data_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404, 44011, 81007, 81011} or error.http_status == 404


def _is_optional_chart_config_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404, 44011, 81007, 81011} or error.http_status == 404


def _is_optional_view_base_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404} or error.http_status == 404


def _is_optional_view_config_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404} or error.http_status == 404


def _is_optional_view_question_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404} or error.http_status == 404


def _is_optional_view_metadata_resolution_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    backend_code = _coerce_backend_code(error)
    return backend_code in {40002, 40027, 404} or error.http_status == 404


def _coerce_backend_code(error: QingflowApiError) -> int | None:
    return backend_code_int(error)


def _transport_error_payload(*, stage: str, error: QingflowApiError) -> JSONObject:
    payload: JSONObject = {
        "stage": stage,
        "category": error.category,
        "message": error.message,
    }
    if error.backend_code is not None:
        payload["backend_code"] = error.backend_code
    if error.http_status is not None:
        payload["http_status"] = error.http_status
    if error.request_id:
        payload["request_id"] = error.request_id
    if error.details:
        payload["details"] = deepcopy(error.details)
    return payload


def _transport_warning_fields(error: QingflowApiError) -> JSONObject:
    payload: JSONObject = {}
    if error.backend_code is not None:
        payload["backend_code"] = error.backend_code
    if error.http_status is not None:
        payload["http_status"] = error.http_status
    if error.request_id:
        payload["request_id"] = error.request_id
    if error.details:
        payload["details"] = deepcopy(error.details)
    return payload


def _normalize_portal_component_source_type(value: Any) -> str:
    raw = str(value or "").strip()
    mapping = {
        "2": "grid",
        "4": "link",
        "5": "text",
        "6": "filter",
        "9": "chart",
        "10": "view",
        "grid": "grid",
        "link": "link",
        "text": "text",
        "filter": "filter",
        "chart": "chart",
        "bi": "chart",
        "view": "view",
        "viewgraph": "view",
    }
    return mapping.get(raw, raw.lower() or "unknown")


def _extract_portal_component_title(component: dict[str, Any], *, source_type: str) -> str | None:
    config_key_map = {
        "grid": "gridConfig",
        "link": "linkConfig",
        "text": "textConfig",
        "filter": "filterConfig",
        "chart": "chartConfig",
        "view": "viewgraphConfig",
    }
    config_key = config_key_map.get(source_type, "")
    config = component.get(config_key) if isinstance(component.get(config_key), dict) else {}
    title_candidates = {
        "grid": ["gridTitle"],
        "link": ["title", "linkTitle"],
        "text": ["title", "textTitle"],
        "filter": ["title", "filterTitle"],
        "chart": ["chartComponentTitle", "componentTitle", "title"],
        "view": ["componentTitle", "viewgraphName", "title"],
    }
    for key in title_candidates.get(source_type, []):
        title = str(config.get(key) or "").strip()
        if title:
            return title
    return None


def _extract_custom_view_key(view_id: str) -> str | None:
    value = str(view_id or "").strip()
    if not value.startswith("custom:"):
        return None
    view_key = value.split(":", 1)[1].strip()
    return view_key or None


def _lookup_system_view_descriptor(view_id: str) -> dict[str, Any] | None:
    normalized = str(view_id or "").strip()
    for system_view_id, _list_type, name in SYSTEM_VIEW_DEFINITIONS:
        if system_view_id != normalized:
            continue
        return {
            "view_name": name,
            "view_type": "system",
            "analysis_supported": True,
        }
    return None


def _view_export_capability_payload(*, supported: bool) -> JSONObject:
    return {
        "supported": supported,
        "tool": "record_export_start",
        "format": "xlsx",
        "async": True,
        "requires_app_key": True,
        "permission_verified": False,
    }


def _export_supported_for_view_type(view_type: str | None) -> bool:
    return _analysis_supported_for_view_type(view_type)


def _normalize_view_type(view_type: Any) -> str | None:
    value = str(view_type or "").strip()
    if not value:
        return None
    if value.lower().endswith("view"):
        value = value[:-4]
    return value or None


def _coerce_positive_int(value: Any) -> int | None:
    try:
        number = int(value)
    except (TypeError, ValueError):
        return None
    return number if number > 0 else None


def _find_visible_app_key_by_form_id(payload: Any, *, form_id: int) -> str | None:
    if isinstance(payload, list):
        for item in payload:
            resolved = _find_visible_app_key_by_form_id(item, form_id=form_id)
            if resolved:
                return resolved
        return None
    if not isinstance(payload, dict):
        return None
    candidate_form_id = _coerce_positive_int(payload.get("formId") or payload.get("form_id"))
    if candidate_form_id == form_id:
        app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
        if app_key:
            return app_key
    for value in payload.values():
        if isinstance(value, (list, dict)):
            resolved = _find_visible_app_key_by_form_id(value, form_id=form_id)
            if resolved:
                return resolved
    return None
