from __future__ import annotations

from typing import Any

from mcp.server.fastmcp import FastMCP

from ..config import DEFAULT_PROFILE
from ..errors import QingflowApiError, raise_tool_error
from ..json_types import JSONObject
from ..list_type_labels import get_record_list_type_label
from .base import ToolBase


class ApprovalTools(ToolBase):
    def __init__(self, sessions, backend) -> None:  # type: ignore[no-untyped-def]
        super().__init__(sessions, backend)
        self._form_id_cache: dict[str, int] = {}

    def register(self, mcp: FastMCP) -> None:
        @mcp.tool()
        def record_comment_add(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_comment_add(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool()
        def record_comment_list(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            apply_id: int = 0,
            page_size: int = 20,
            list_type: int | None = None,
            page_num: int | None = 1,
        ) -> dict[str, Any]:
            return self.record_comment_list(
                profile=profile,
                app_key=app_key,
                apply_id=apply_id,
                page_size=page_size,
                list_type=list_type,
                page_num=page_num,
            )

        @mcp.tool()
        def record_comment_mention_candidates(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            apply_id: int = 0,
            page_size: int = 20,
            page_num: int = 1,
            list_type: int | None = None,
            keyword: str | None = None,
        ) -> dict[str, Any]:
            return self.record_comment_mention_candidates(
                profile=profile,
                app_key=app_key,
                apply_id=apply_id,
                page_size=page_size,
                page_num=page_num,
                list_type=list_type,
                keyword=keyword,
            )

        @mcp.tool()
        def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
            return self.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=apply_id)

        @mcp.tool()
        def record_comment_stats(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
            return self.record_comment_stats(profile=profile, app_key=app_key, apply_id=apply_id)

        @mcp.tool(description=self._high_risk_tool_description(operation="approve", target="workflow record"))
        def record_approve(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_approve(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool(description=self._high_risk_tool_description(operation="reject", target="workflow record"))
        def record_reject(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_reject(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool()
        def record_rollback_candidates(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, audit_node_id: int = 0) -> dict[str, Any]:
            return self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=apply_id, audit_node_id=audit_node_id)

        @mcp.tool()
        def record_rollback(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_rollback(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool()
        def record_transfer(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_transfer(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool()
        def record_transfer_candidates(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            apply_id: int = 0,
            page_size: int = 20,
            page_num: int = 1,
            audit_node_id: int = 0,
            keyword: str | None = None,
        ) -> dict[str, Any]:
            return self.record_transfer_candidates(
                profile=profile,
                app_key=app_key,
                apply_id=apply_id,
                page_size=page_size,
                page_num=page_num,
                audit_node_id=audit_node_id,
                keyword=keyword,
            )

        @mcp.tool()
        def record_reassign_get(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict[str, Any]:
            return self.record_reassign_get(profile=profile, app_key=app_key, apply_id=apply_id)

        @mcp.tool()
        def record_reassign(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_reassign(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

        @mcp.tool()
        def record_countersign_candidates(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            apply_id: int = 0,
            page_size: int = 20,
            page_num: int = 1,
            audit_node_id: int = 0,
            search_key: str | None = None,
        ) -> dict[str, Any]:
            return self.record_countersign_candidates(
                profile=profile,
                app_key=app_key,
                apply_id=apply_id,
                page_size=page_size,
                page_num=page_num,
                audit_node_id=audit_node_id,
                search_key=search_key,
            )

        @mcp.tool()
        def record_countersign(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0, payload: dict[str, Any] | None = None) -> dict[str, Any]:
            return self.record_countersign(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})

    def record_comment_add(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        self._validate_comment_payload(payload)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment", json_body=payload)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_comment_list(self, *, profile: str, app_key: str, apply_id: int, page_size: int = 20, list_type: int | None = None, page_num: int | None = 1) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)

        def runner(session_profile, context):
            params: dict[str, Any] = {"pageSize": page_size}
            if list_type is not None:
                params["listType"] = list_type
            if page_num is not None:
                params["pageNum"] = page_num
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment", params=params)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "app_key": app_key,
                "apply_id": apply_id,
                "list_type": list_type,
                "list_type_label": get_record_list_type_label(list_type),
                "page": result,
            }

        return self._run(profile, runner)

    def record_comment_mention_candidates(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        page_size: int = 20,
        page_num: int = 1,
        list_type: int | None = None,
        keyword: str | None = None,
    ) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)

        def runner(session_profile, context):
            params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num}
            if list_type is not None:
                params["listType"] = list_type
            if keyword:
                params["keyword"] = keyword
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/member", params=params)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "app_key": app_key,
                "apply_id": apply_id,
                "list_type": list_type,
                "list_type_label": get_record_list_type_label(list_type),
                "page": result,
            }

        return self._run(profile, runner)

    def record_comment_mark_read(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/comment/read")
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_comment_stats(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)

        def runner(session_profile, context):
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/comment/statistic")
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_approve(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)

        def runner(session_profile, context):
            approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
            result = self.backend.request("POST", context, "/workflow/engine/approval/approve", json_body=approval_body)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "app_key": app_key,
                "apply_id": apply_id,
                "form_id": approval_body["formId"],
                "node_id": approval_body["nodeId"],
                "result": result,
                "request_route": self._request_route_payload(context),
            }

        return self._run(profile, runner)

    def record_reject(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)

        def runner(session_profile, context):
            approval_body = self._normalize_approval_payload(profile, context, app_key, apply_id, body)
            result = self.backend.request("POST", context, "/workflow/engine/approval/reject", json_body=approval_body)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "app_key": app_key,
                "apply_id": apply_id,
                "form_id": approval_body["formId"],
                "node_id": approval_body["nodeId"],
                "result": result,
                "request_route": self._request_route_payload(context),
            }

        return self._run(profile, runner)

    def record_rollback_candidates(self, *, profile: str, app_key: str, apply_id: int, audit_node_id: int) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        if audit_node_id <= 0:
            raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))

        def runner(session_profile, context):
            result = self.backend.request(
                "GET",
                context,
                f"/app/{app_key}/apply/{apply_id}/revertNode",
                params={"auditNodeId": audit_node_id},
            )
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_rollback(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)
        self._validate_audit_payload(body)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/rollback", json_body=body)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_transfer(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)
        self._validate_audit_payload(body, require_uid=True)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/transfer", json_body=body)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_transfer_candidates(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        page_size: int = 20,
        page_num: int = 1,
        audit_node_id: int = 0,
        keyword: str | None = None,
    ) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        if audit_node_id <= 0:
            raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))

        def runner(session_profile, context):
            params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
            if keyword:
                params["keyword"] = keyword
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/transfer/member", params=params)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}

        return self._run(profile, runner)

    def record_reassign_get(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)

        def runner(session_profile, context):
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/reassign")
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_reassign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/reassign", json_body=body)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def record_countersign_candidates(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        page_size: int = 20,
        page_num: int = 1,
        audit_node_id: int = 0,
        search_key: str | None = None,
    ) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        if audit_node_id <= 0:
            raise_tool_error(QingflowApiError.config_error("audit_node_id must be positive"))

        def runner(session_profile, context):
            params: dict[str, Any] = {"pageSize": page_size, "pageNum": page_num, "auditNodeId": audit_node_id}
            if search_key:
                params["searchKey"] = search_key
            result = self.backend.request("GET", context, f"/app/{app_key}/apply/{apply_id}/countersign/member", params=params)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "page": result}

        return self._run(profile, runner)

    def record_countersign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
        self._require_app_and_apply(app_key, apply_id)
        body = self._require_dict(payload)
        self._validate_countersign_payload(body)

        def runner(session_profile, context):
            result = self.backend.request("POST", context, f"/app/{app_key}/apply/{apply_id}/countersign", json_body=body)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "apply_id": apply_id, "result": result}

        return self._run(profile, runner)

    def _require_app_and_apply(self, app_key: str, apply_id: int) -> None:
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if apply_id <= 0:
            raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))

    def _validate_comment_payload(self, payload: dict[str, Any]) -> None:
        comment_detail = payload.get("commentDetail")
        if not isinstance(comment_detail, dict):
            raise_tool_error(QingflowApiError.config_error("payload.commentDetail must be an object"))
        if not comment_detail.get("commentMsg"):
            raise_tool_error(QingflowApiError.config_error("payload.commentDetail.commentMsg is required"))

    def _normalize_approval_payload(self, profile: str, context, app_key: str, apply_id: int, payload: dict[str, Any]) -> JSONObject:  # type: ignore[no-untyped-def]
        body: JSONObject = dict(payload)
        self._normalize_alias(body, "auditFeedback", "audit_feedback")
        self._normalize_alias(body, "uploadFileSize", "upload_file_size")
        self._normalize_alias(body, "handSignImageUrl", "hand_sign_image_url")
        self._normalize_alias(body, "beingSaveSignature", "being_save_signature")
        self._normalize_alias(body, "applyId", "apply_id")
        self._normalize_alias(body, "formId", "form_id")

        node_id = self._extract_node_id(body)
        body["nodeId"] = node_id
        body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
        body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))

        self._validate_approval_payload(body)
        return body

    def _extract_node_id(self, payload: JSONObject) -> int:
        node_id = payload.get("nodeId")
        audit_node_id = payload.pop("auditNodeId", None)
        if node_id is None:
            node_id = audit_node_id
        elif audit_node_id is not None and node_id != audit_node_id:
            raise_tool_error(QingflowApiError.config_error("payload.nodeId and payload.auditNodeId must match when both are provided"))
        if not isinstance(node_id, int) or node_id <= 0:
            raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
        return node_id

    def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int:  # type: ignore[no-untyped-def]
        if explicit_form_id is not None:
            if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
                raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
            form_id = self._get_form_id(profile, context, app_key)
            if form_id != explicit_form_id:
                raise_tool_error(
                    QingflowApiError.config_error(
                        f"payload.formId={explicit_form_id} does not match app_key '{app_key}' formId={form_id}"
                    )
                )
            return explicit_form_id
        return self._get_form_id(profile, context, app_key)

    def _get_form_id(self, profile: str, context, app_key: str) -> int:  # type: ignore[no-untyped-def]
        cache_key = f"{profile}:{app_key}"
        cached = self._form_id_cache.get(cache_key)
        if cached is not None:
            return cached
        result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
        form_id = result.get("formId") if isinstance(result, dict) else None
        if not isinstance(form_id, int) or form_id <= 0:
            raise_tool_error(QingflowApiError.config_error(f"cannot resolve formId for app_key '{app_key}'"))
        self._form_id_cache[cache_key] = form_id
        return form_id

    def _match_or_fill_int(self, payload: JSONObject, *, field_name: str, expected_value: int) -> int:
        current = payload.get(field_name)
        if current is None:
            return expected_value
        if not isinstance(current, int) or current <= 0:
            raise_tool_error(QingflowApiError.config_error(f"payload.{field_name} must be a positive integer"))
        if current != expected_value:
            raise_tool_error(QingflowApiError.config_error(f"payload.{field_name}={current} does not match apply_id={expected_value}"))
        return current

    def _normalize_alias(self, payload: JSONObject, canonical_key: str, alias_key: str) -> None:
        alias_value = payload.pop(alias_key, None)
        if canonical_key not in payload and alias_value is not None:
            payload[canonical_key] = alias_value
        elif alias_value is not None and payload.get(canonical_key) != alias_value:
            raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))

    def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
        self._reject_unsupported_fields(payload)
        if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
            raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
        if not isinstance(payload.get("applyId"), int) or payload["applyId"] <= 0:
            raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
        if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
            raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))

    def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
        self._reject_unsupported_fields(payload)
        if require_uid and not payload.get("uid"):
            raise_tool_error(QingflowApiError.config_error("payload.uid is required"))

    def _validate_countersign_payload(self, payload: dict[str, Any]) -> None:
        self._reject_unsupported_fields(payload)
        members = payload.get("countersignMembers")
        if not isinstance(members, list) or not members:
            raise_tool_error(QingflowApiError.config_error("payload.countersignMembers must be a non-empty array"))

    def _reject_unsupported_fields(self, payload: dict[str, Any]) -> None:
        if payload.get("handSignImageUrl"):
            raise_tool_error(QingflowApiError.not_supported("NOT_SUPPORTED_IN_V1: handSignImageUrl is not supported"))

    def _request_route_payload(self, context) -> JSONObject:  # type: ignore[no-untyped-def]
        describe_route = getattr(self.backend, "describe_route", None)
        if callable(describe_route):
            payload = describe_route(context)
            if isinstance(payload, dict):
                return payload
        return {
            "base_url": context.base_url,
            "qf_version": context.qf_version,
            "qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
        }
