from __future__ import annotations

import asyncio
from pathlib import Path
from typing import TYPE_CHECKING, Any

from aicd.agent.tool_args_coerce import (
    coerce_tool_json_array,
    coerce_tool_json_block_object,
    coerce_tool_json_object,
)
from aicd.tools.code_intel import graph_json_format, mermaid_block, python_outline
from aicd.tools.document_outline import build_outline_payload, write_outline_json
from aicd.tools import docx_compose as docx_compose_mod
from aicd.tools import docx_to_pdf as docx_to_pdf_mod
from aicd.tools import mermaid_png as mermaid_png_mod

if TYPE_CHECKING:
    from aicd.agent.tool_router import ToolRouter


def _doc_convert_markdown(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    return router._docs.convert_to_markdown(
        args["source_path"], args["output_dir"]
    )


def _doc_extract(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    sn = args.get("sheet_names")
    if sn is not None:
        co = coerce_tool_json_array(sn, "sheet_names", allow_empty=True)
        if isinstance(co, dict):
            return co
        sn = co
    names = [str(x) for x in sn] if sn else None
    return router._docs.convert_document(
        args["source_path"],
        args["output_dir"],
        output_format=str(args.get("output_format", "markdown")),
        page_start=args.get("page_start"),
        page_end=args.get("page_end"),
        use_ocr=bool(args.get("use_ocr", False)),
        sheet_names=names,
    )


async def _browser_cdp_navigate(
    router: ToolRouter, args: dict[str, Any]
) -> dict[str, Any]:
    return await router._browser.navigate(
        args["url"], wait_until=str(args.get("wait_until", "load"))
    )


def _diagram_mermaid(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    return mermaid_block(args.get("title"), args["mermaid_body"])


def _diagram_graph_json(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    n = coerce_tool_json_array(args.get("nodes"), "nodes", allow_empty=True)
    if isinstance(n, dict):
        return n
    e = coerce_tool_json_array(args.get("edges"), "edges", allow_empty=True)
    if isinstance(e, dict):
        return e
    return graph_json_format(n, e)


def _code_outline_python(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    return python_outline(args["path"])


async def _diagram_mermaid_png(
    router: ToolRouter, args: dict[str, Any]
) -> dict[str, Any]:
    """Playwright 同步 API 不可在 asyncio 事件循环中直接调用，放到线程里执行。"""
    out = router._resolve_path(str(args["output_png_path"]))
    theme_vars = args.get("theme_variables")
    if theme_vars is not None:
        tv_obj, tv_err = coerce_tool_json_object(theme_vars, "theme_variables")
        if tv_err:
            return {"ok": False, "error": tv_err}
        theme_vars = tv_obj or None
    body = str(args["mermaid_body"])
    theme = str(args.get("theme", "neutral"))
    scale = float(args.get("scale", 3))
    policy = router._policy.check_allowed

    def _sync() -> dict[str, Any]:
        return mermaid_png_mod.render_mermaid_to_png(
            body,
            out,
            policy_check=policy,
            theme=theme,
            theme_variables=theme_vars,
            scale=scale,
        )

    return await asyncio.to_thread(_sync)


def _docx_compose(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    path = router._resolve_path(str(args["path"]))
    raw = args.get("blocks")
    coerced = coerce_tool_json_array(raw, "blocks", allow_empty=True)
    if isinstance(coerced, dict):
        return coerced
    raw = coerced
    blocks_out: list[dict[str, Any]] = []
    for bi, b in enumerate(raw):
        blk, berr = coerce_tool_json_block_object(b, "blocks", bi)
        if berr:
            return {"ok": False, "error": berr}
        bt = str(blk.get("type") or "").lower()
        if bt == "image" and blk.get("path"):
            blocks_out.append(
                {**blk, "path": str(router._resolve_path(str(blk["path"])))}
            )
        else:
            blocks_out.append(dict(blk))
    de = args.get("default_east_asia_font")
    dl = args.get("default_latin_font")
    dc = args.get("default_color_hex")
    db = args.get("default_body_font_pt")
    east = str(de).strip() if isinstance(de, str) and de.strip() else None
    lat = str(dl).strip() if isinstance(dl, str) and dl.strip() else None
    col = str(dc).strip() if isinstance(dc, str) and dc.strip() else None
    body_pt: float | None = None
    if db is not None and str(db).strip() != "":
        try:
            body_pt = float(db)
        except (TypeError, ValueError):
            return {"ok": False, "error": "default_body_font_pt must be a number"}
    return docx_compose_mod.docx_compose(
        path,
        blocks_out,
        policy_check=router._policy.check_allowed,
        append=bool(args.get("append", False)),
        default_east_asia_font=east,
        default_latin_font=lat,
        default_color_hex=col,
        default_body_font_pt=body_pt,
    )


def _docx_export_pdf(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    src = router._resolve_path(str(args["docx_path"]))
    op = args.get("output_pdf_path")
    out: Path | None = None
    if op is not None and str(op).strip():
        out = router._resolve_path(str(op).strip())
        if out.suffix.lower() != ".pdf":
            return {"ok": False, "error": "output_pdf_path must end with .pdf"}
    pref = args.get("prefer")
    pref_s = str(pref).strip().lower() if pref is not None and str(pref).strip() else None
    if pref_s is not None and pref_s not in ("libreoffice", "docx2pdf"):
        return {
            "ok": False,
            "error": "prefer must be libreoffice, docx2pdf, or omitted",
        }
    return docx_to_pdf_mod.export_docx_to_pdf(
        src,
        out,
        policy_check=router._policy.check_allowed,
        prefer=pref_s,
    )


def _document_outline(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    p = router._resolve_path(str(args["path"]))
    if not p.is_file():
        return {"ok": False, "error": f"not a file: {p}"}
    suf = p.suffix.lower()
    if suf not in {".md", ".markdown", ".html", ".htm", ".txt"}:
        return {
            "ok": False,
            "error": f"unsupported extension {suf!r}; use .md/.html/.txt",
        }
    max_lv = int(args.get("max_heading_level", 6))
    max_lv = max(1, min(6, max_lv))
    payload = build_outline_payload(p, max_heading_level=max_lv)
    out: dict[str, Any] = {"ok": True, **payload}
    if bool(args.get("save_to_tmp", True)):
        sub = router._ai_tmp / "outlines"
        stem = p.stem[:80] or "doc"
        dest = sub / f"{stem}.json"
        write_outline_json(payload, dest)
        out["saved_path"] = str(dest)
    return out


HANDLERS: dict[str, Any] = {
    "doc_convert_markdown": _doc_convert_markdown,
    "doc_extract": _doc_extract,
    "browser_cdp_navigate": _browser_cdp_navigate,
    "diagram_mermaid": _diagram_mermaid,
    "diagram_mermaid_png": _diagram_mermaid_png,
    "diagram_graph_json": _diagram_graph_json,
    "docx_compose": _docx_compose,
    "docx_export_pdf": _docx_export_pdf,
    "code_outline_python": _code_outline_python,
    "document_outline": _document_outline,
}
