from __future__ import annotations

import base64
import json
import mimetypes
import random
import string
from pathlib import Path
from typing import Any
from urllib.parse import quote

from mcp.server.fastmcp import FastMCP

from ..config import DEFAULT_PROFILE
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
from ..json_types import JSONObject
from .base import ToolBase


ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES = {40118}
LEGACY_OSS_FORM_REQUIRED_KEYS = ("key", "policy", "signature", "ossAccessKeyId")


class FileTools(ToolBase):
    """文件工具（中文名：上传与下载中转）。

    类型：文件传输工具。
    主要职责：
    1. 申请上传凭证与上传地址；
    2. 支持本地路径、URL、Base64 内容三类上传输入；
    3. 返回标准化文件元信息供导入与记录附件使用。
    """

    def register(self, mcp: FastMCP) -> None:
        """注册当前工具到 MCP 服务。"""
        @mcp.tool()
        def file_get_upload_info(
            profile: str = DEFAULT_PROFILE,
            upload_kind: str = "attachment",
            file_name: str = "",
            file_size: int = 0,
            upload_mark: str | None = None,
            content_type: str | None = None,
            bucket_type: str | None = None,
            path_id: int | None = None,
            file_related_url: str | None = None,
        ) -> dict[str, Any]:
            return self.file_get_upload_info(
                profile=profile,
                upload_kind=upload_kind,
                file_name=file_name,
                file_size=file_size,
                upload_mark=upload_mark,
                content_type=content_type,
                bucket_type=bucket_type,
                path_id=path_id,
                file_related_url=file_related_url,
            )

        @mcp.tool()
        def file_upload_local(
            profile: str = DEFAULT_PROFILE,
            upload_kind: str = "attachment",
            file_path: str = "",
            upload_mark: str | None = None,
            content_type: str | None = None,
            bucket_type: str | None = None,
            path_id: int | None = None,
            file_related_url: str | None = None,
        ) -> dict[str, Any]:
            return self.file_upload_local(
                profile=profile,
                upload_kind=upload_kind,
                file_path=file_path,
                upload_mark=upload_mark,
                content_type=content_type,
                bucket_type=bucket_type,
                path_id=path_id,
                file_related_url=file_related_url,
            )

    def file_get_upload_info(
        self,
        *,
        profile: str,
        upload_kind: str,
        file_name: str,
        file_size: int,
        upload_mark: str | None = None,
        content_type: str | None = None,
        bucket_type: str | None = None,
        path_id: int | None = None,
        file_related_url: str | None = None,
    ) -> dict[str, Any]:
        """执行文件处理相关逻辑。"""
        def runner(session_profile, context):
            upload_info = self._request_upload_info_with_fallback(
                context,
                upload_kind=upload_kind,
                file_name=file_name,
                file_size=file_size,
                upload_mark=upload_mark,
                content_type=content_type,
                bucket_type=bucket_type,
                path_id=path_id,
                file_related_url=file_related_url,
            )
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "upload_kind": upload_kind,
                "requested_upload_kind": upload_info["requested_upload_kind"],
                "effective_upload_kind": upload_info["effective_upload_kind"],
                "upload_fallback_applied": upload_info["fallback_applied"],
                "upload_fallback_reason": upload_info["fallback_reason"],
                "file_name": file_name,
                "file_size": file_size,
                "request_route": self.backend.describe_route(context),
                "result": upload_info["result"],
            }

        return self._run(profile, runner, tool_name='文件上传信息')

    def file_upload_local(
        self,
        *,
        profile: str,
        upload_kind: str,
        file_path: str,
        upload_mark: str | None = None,
        content_type: str | None = None,
        bucket_type: str | None = None,
        path_id: int | None = None,
        file_related_url: str | None = None,
    ) -> dict[str, Any]:
        """执行文件处理相关逻辑。"""
        path = Path(file_path).expanduser()
        if not path.is_file():
            raise_tool_error(QingflowApiError.config_error("file_path must point to an existing file"))
        file_name = path.name
        file_size = path.stat().st_size
        resolved_content_type = content_type or mimetypes.guess_type(file_name)[0] or "application/octet-stream"

        def runner(session_profile, context):
            upload_info = self._request_upload_info_with_fallback(
                context,
                upload_kind=upload_kind,
                file_name=file_name,
                file_size=file_size,
                upload_mark=upload_mark,
                content_type=resolved_content_type,
                bucket_type=bucket_type,
                path_id=path_id,
                file_related_url=file_related_url,
            )
            result = upload_info["result"]
            if not isinstance(result, dict):
                raise QingflowApiError.config_error("upload endpoint did not return a structured upload payload")
            content = path.read_bytes()
            upload_protocol = "binary_put"
            if self._is_legacy_oss_form_upload(result):
                upload_result = self._upload_legacy_oss_form(
                    result,
                    file_name=file_name,
                    content=content,
                    content_type=resolved_content_type,
                )
                upload_protocol = "oss_form_post"
            else:
                upload_url = str(result.get("uploadUrl") or "").strip()
                if not upload_url:
                    raise QingflowApiError.config_error("upload endpoint did not return uploadUrl")
                upload_result = self.backend.upload_binary(upload_url, content, content_type=resolved_content_type)
            download_url = result.get("downloadUrl")
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "upload_kind": upload_kind,
                "requested_upload_kind": upload_info["requested_upload_kind"],
                "effective_upload_kind": upload_info["effective_upload_kind"],
                "upload_fallback_applied": upload_info["fallback_applied"],
                "upload_fallback_reason": upload_info["fallback_reason"],
                "file_name": file_name,
                "file_size": file_size,
                "content_type": resolved_content_type,
                "request_route": self.backend.describe_route(context),
                "upload_protocol": upload_protocol,
                "result": result,
                "upload_result": upload_result,
                "download_url": download_url,
                "write_executed": True,
                "safe_to_retry": False,
                "attachment_value": {
                    "value": download_url,
                    "otherInfo": file_name,
                    "name": file_name,
                },
                "comment_file_info": {
                    "url": download_url,
                    "name": file_name,
                    "uploadFileSize": file_size,
                },
            }

        return self._run(profile, runner, tool_name='本地文件上传')

    def _build_file_upload_bo(
        self,
        *,
        upload_kind: str,
        file_name: str,
        file_size: int,
        upload_mark: str | None,
        content_type: str | None,
        bucket_type: str | None,
        path_id: int | None,
        file_related_url: str | None,
    ) -> dict[str, Any]:
        """执行内部辅助逻辑。"""
        if not file_name:
            raise_tool_error(QingflowApiError.config_error("file_name is required"))
        if file_size <= 0:
            raise_tool_error(QingflowApiError.config_error("file_size must be positive"))
        if upload_kind == "attachment" and not upload_mark:
            raise_tool_error(QingflowApiError.config_error("upload_mark is required for attachment uploads and should usually be the app_key"))
        payload: dict[str, Any] = {
            "fileName": file_name,
            "fileSize": file_size,
        }
        if upload_mark:
            payload["uploadMark"] = upload_mark
        if content_type:
            payload["contentType"] = content_type
        if bucket_type:
            payload["bucketType"] = bucket_type
        if path_id is not None:
            payload["pathId"] = path_id
        if file_related_url:
            payload["fileRelatedUrl"] = file_related_url
        return payload

    def _resolve_upload_endpoint(self, upload_kind: str) -> str:
        """执行内部辅助逻辑。"""
        normalized = upload_kind.strip().lower()
        if normalized == "attachment":
            return "/upload/puburl"
        if normalized == "login":
            return "/upload/url"
        if normalized == "anonymous":
            return "/upload/anonymousurl"
        raise_tool_error(QingflowApiError.config_error("upload_kind must be one of: attachment, login, anonymous"))
        raise AssertionError("unreachable")

    def _encode_formula(self, formula: str) -> str:
        """执行内部辅助逻辑。"""
        encoded = base64.b64encode(quote(formula, safe="").encode("utf-8")).decode("utf-8")
        return f"{self._random_string(16)}{encoded}{self._random_string(16)}"

    def _random_string(self, length: int) -> str:
        """执行内部辅助逻辑。"""
        alphabet = string.ascii_letters + string.digits
        return "".join(random.choice(alphabet) for _ in range(length))

    def _request_upload_info_with_fallback(
        self,
        context,
        *,
        upload_kind: str,
        file_name: str,
        file_size: int,
        upload_mark: str | None,
        content_type: str | None,
        bucket_type: str | None,
        path_id: int | None,
        file_related_url: str | None,
    ) -> dict[str, Any]:
        """执行内部辅助逻辑。"""
        requested_kind = upload_kind.strip().lower()
        attempted_kinds = [requested_kind]
        if requested_kind == "attachment":
            attempted_kinds.append("login")
        last_error: QingflowApiError | None = None
        for current_kind in attempted_kinds:
            try:
                result = self._request_upload_info(
                    context,
                    upload_kind=current_kind,
                    file_name=file_name,
                    file_size=file_size,
                    upload_mark=upload_mark,
                    content_type=content_type,
                    bucket_type=bucket_type,
                    path_id=path_id,
                    file_related_url=file_related_url,
                )
                return {
                    "requested_upload_kind": requested_kind,
                    "effective_upload_kind": current_kind,
                    "fallback_applied": current_kind != requested_kind,
                    "fallback_reason": (
                        f"backend rejected '{requested_kind}' upload info; retried with '{current_kind}'"
                        if current_kind != requested_kind
                        else None
                    ),
                    "result": result,
                }
            except QingflowApiError as error:
                last_error = error
                if not self._should_retry_attachment_upload_info(requested_kind, current_kind, error):
                    raise
        assert last_error is not None
        raise last_error

    def _request_upload_info(
        self,
        context,
        *,
        upload_kind: str,
        file_name: str,
        file_size: int,
        upload_mark: str | None,
        content_type: str | None,
        bucket_type: str | None,
        path_id: int | None,
        file_related_url: str | None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        endpoint = self._resolve_upload_endpoint(upload_kind)
        file_upload_bo = self._build_file_upload_bo(
            upload_kind=upload_kind,
            file_name=file_name,
            file_size=file_size,
            upload_mark=upload_mark,
            content_type=content_type,
            bucket_type=bucket_type,
            path_id=path_id,
            file_related_url=file_related_url,
        )
        encrypted_payload = {
            "upload": self._encode_formula(json.dumps(file_upload_bo, ensure_ascii=False, separators=(",", ":"))),
            "fileName": file_name,
            "fileSize": file_size,
        }
        if path_id is not None:
            encrypted_payload["pathId"] = path_id
        if file_related_url:
            encrypted_payload["fileRelatedUrl"] = file_related_url
        if bucket_type:
            encrypted_payload["bucketType"] = bucket_type
        result = self.backend.request("POST", context, endpoint, json_body=encrypted_payload)
        if not isinstance(result, dict):
            raise QingflowApiError.config_error("upload endpoint did not return a structured upload payload")
        return result

    def _should_retry_attachment_upload_info(
        self,
        requested_kind: str,
        attempted_kind: str,
        error: QingflowApiError,
    ) -> bool:
        """执行内部辅助逻辑。"""
        if is_auth_like_error(error):
            return False
        return (
            requested_kind == "attachment"
            and attempted_kind == "attachment"
            and backend_code_int(error) in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
        )

    def _is_legacy_oss_form_upload(self, payload: JSONObject) -> bool:
        """执行内部辅助逻辑。"""
        return all(str(payload.get(key) or "").strip() for key in LEGACY_OSS_FORM_REQUIRED_KEYS)

    def _upload_legacy_oss_form(
        self,
        upload_info: JSONObject,
        *,
        file_name: str,
        content: bytes,
        content_type: str,
    ) -> dict[str, Any]:
        """执行内部辅助逻辑。"""
        upload_url = self._resolve_legacy_oss_form_url(upload_info)
        if not upload_url:
            raise QingflowApiError.config_error("legacy upload payload is missing host/uploadAccelerateHost")
        form_fields = {
            "key": str(upload_info["key"]),
            "policy": str(upload_info["policy"]),
            "signature": str(upload_info["signature"]),
            "OSSAccessKeyId": str(upload_info["ossAccessKeyId"]),
        }
        callback = str(upload_info.get("callback") or "").strip()
        if callback:
            form_fields["callback"] = callback
        return self.backend.upload_form_file(
            upload_url,
            form_fields=form_fields,
            file_field="file",
            file_name=file_name,
            content=content,
            content_type=content_type,
        )

    def _resolve_legacy_oss_form_url(self, upload_info: JSONObject) -> str:
        """执行内部辅助逻辑。"""
        for key in ("uploadAccelerateHost", "host", "uploadUrl"):
            value = str(upload_info.get(key) or "").strip()
            if not value:
                continue
            if value.startswith(("http://", "https://")):
                return value
            return f"https://{value.lstrip('/')}"
        return ""
