from __future__ import annotations

from typing import TYPE_CHECKING, Any
from urllib.parse import quote
from urllib.parse import urlencode

import httpx

from aicd.network_policy import outbound_tool_url_blocked_reason
from aicd.tools.github_repo_read import github_repo_read
from aicd.tools.web_extract import extract_main_text_from_html
from aicd.version import VERSION

if TYPE_CHECKING:
    from aicd.agent.tool_router import ToolRouter


def _web_user_agent(router: "ToolRouter") -> str:
    ua = (router._cfg.integrations.web_fetch.user_agent or "").strip()
    if ua:
        return ua
    return f"AICD/{VERSION} (https://github.com/)"

_MAX_WEB_OUT_CHARS = 500_000


def _normalize_github_path(path: str) -> tuple[str | None, str | None]:
    p = path.strip()
    if not p.startswith("/"):
        p = "/" + p
    low = p.lower()
    if ".." in p or low.startswith("//"):
        return None, "invalid path: must start with / and not contain .."
    return p, None


async def _web_fetch(router: "ToolRouter", args: dict[str, Any]) -> dict[str, Any]:
    wf = router._cfg.integrations.web_fetch
    url = str(args.get("url") or "").strip()
    if not url or not (url.startswith("http://") or url.startswith("https://")):
        return {
            "ok": False,
            "error": "url must be http(s)",
            "hint": "仅允许公网/内网 http(s)；敏感内网请确认合规",
        }
    br = outbound_tool_url_blocked_reason(
        url, block_external=router._cfg.agent.block_external_network
    )
    if br:
        return {
            "ok": False,
            "error": br,
            "network_policy": "block_external_network",
        }
    extract = str(args.get("extract") or "main_text").strip().lower()
    if extract in ("main", "article", "readable"):
        extract = "main_text"
    timeout = float(args.get("timeout_sec") or wf.default_timeout_sec)
    max_bytes = int(args.get("max_response_bytes") or wf.max_response_bytes)
    max_bytes = max(4096, min(8_000_000, max_bytes))
    mtc = args.get("max_text_chars")
    if isinstance(mtc, (int, float)):
        max_out = max(2000, min(2_000_000, int(mtc)))
    elif isinstance(mtc, str) and mtc.strip():
        try:
            max_out = max(2000, min(2_000_000, int(mtc.strip(), 10)))
        except ValueError:
            max_out = _MAX_WEB_OUT_CHARS
    else:
        max_out = _MAX_WEB_OUT_CHARS

    headers = {"User-Agent": _web_user_agent(router), "Accept": "*/*"}
    follow = bool(args.get("follow_redirects", True))
    try:
        async with httpx.AsyncClient(
            timeout=timeout,
            headers=headers,
            follow_redirects=follow,
        ) as client:
            r = await client.get(url)
        err_body = None
        try:
            err_body = r.text if not r.is_success else None
        except Exception:  # noqa: BLE001
            err_body = None
        if not r.is_success:
            return {
                "ok": False,
                "status_code": r.status_code,
                "error": f"HTTP {r.status_code}",
                "hint": err_body[:2000] if err_body else None,
            }
        content = r.content[:max_bytes]
        truncated_bytes = len(r.content) > len(content)
        ct = (r.headers.get("content-type") or "").split(";")[0].strip().lower()
        final_url = str(r.url)
        text_body = content.decode("utf-8", errors="replace")
        if extract == "raw" or ct not in ("text/html", "application/xhtml+xml"):
            if len(text_body) > max_out:
                text_body = text_body[:max_out]
                truncated_text = True
            else:
                truncated_text = False
            return {
                "ok": True,
                "url": url,
                "final_url": final_url,
                "content_type": ct or None,
                "extract": extract,
                "text": text_body,
                "truncated": truncated_text or truncated_bytes,
                "truncated_response_bytes": truncated_bytes,
                "response_bytes": len(content),
                "note": None,
            }

        main_text, meta = extract_main_text_from_html(
            text_body, final_url or url, max_chars=max_out
        )
        out: dict[str, Any] = {
            "ok": True,
            "url": url,
            "final_url": final_url,
            "content_type": ct,
            "extract": "main_text",
            "text": main_text,
            "engine": meta.get("engine"),
            "truncated": bool(meta.get("truncated")) or truncated_bytes,
            "truncated_response_bytes": truncated_bytes,
            "response_bytes": len(content),
        }
        if meta.get("install_suggestions"):
            out["install_suggestions"] = meta["install_suggestions"]
        if meta.get("hint"):
            out["hint"] = meta["hint"]
        if meta.get("trafilatura_missing"):
            out["trafilatura_installed"] = False
        else:
            out["trafilatura_installed"] = meta.get("engine") == "trafilatura"
        return out
    except httpx.TimeoutException:
        return {"ok": False, "error": "timeout", "hint": "可增大 integrations.webFetch.defaultTimeoutSec"}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


async def _github_api(router: "ToolRouter", args: dict[str, Any]) -> dict[str, Any]:
    if router._cfg.agent.block_external_network:
        return {
            "ok": False,
            "error": "block_external_network=ON 时已禁止 github_api（须访问 api.github.com）",
            "network_policy": "block_external_network",
        }
    gh = router._cfg.integrations.github
    base = (gh.api_base_url or "https://api.github.com").strip().rstrip("/")
    path, perr = _normalize_github_path(str(args.get("path") or ""))
    if perr:
        return {"ok": False, "error": perr}
    assert path is not None
    q = args.get("query")
    qstr = ""
    if isinstance(q, dict) and q:
        flat = {str(k): str(v) for k, v in q.items() if v is not None and str(v) != ""}
        qstr = "?" + urlencode(flat, quote_via=quote, safe="/")
    url = base + path + qstr
    method = str(args.get("method") or "GET").upper()
    token = (gh.token or "").strip()
    headers: dict[str, str] = {
        "Accept": gh.accept or "application/vnd.github+json",
        "User-Agent": _web_user_agent(router),
    }
    if token:
        headers["Authorization"] = f"Bearer {token}"
    jb = args.get("json_body")
    data = args.get("data")
    timeout = float(args.get("timeout_sec") or 60.0)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            kw: dict[str, Any] = {"method": method, "url": url, "headers": headers}
            if jb is not None:
                kw["json"] = jb
            elif data is not None:
                kw["content"] = str(data).encode("utf-8")
            resp = await client.request(**kw)
        text = resp.text[:500_000]
        hint = None
        lowct = (resp.headers.get("content-type") or "").lower()
        if resp.status_code == 401:
            hint = "401: 在 config.yaml integrations.github.token 或环境变量 GITHUB_TOKEN 配置 PAT"
        elif resp.status_code == 403 and "rate limit" in text.lower():
            hint = "速率限制：配置 token 可提高限额"
        parsed_json = None
        if "json" in lowct:
            try:
                import json

                parsed_json = json.loads(resp.text)
            except Exception:  # noqa: BLE001
                parsed_json = None
        return {
            "ok": resp.is_success,
            "status_code": resp.status_code,
            "url": url,
            "method": method,
            "headers": {
                k: v
                for k, v in resp.headers.items()
                if k.lower() in ("x-ratelimit-remaining", "link", "content-type")
            },
            "text": text,
            "json": parsed_json,
            "truncated": len(resp.text) > len(text),
            "hint": hint,
            "github_token_configured": bool(token),
        }
    except httpx.TimeoutException:
        return {"ok": False, "error": "timeout"}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


async def _github_repo_read(router: "ToolRouter", args: dict[str, Any]) -> dict[str, Any]:
    gh = router._cfg.integrations.github
    mtc = args.get("max_text_chars")
    if isinstance(mtc, (int, float)):
        max_tc = max(1000, min(2_000_000, int(mtc)))
    elif isinstance(mtc, str) and mtc.strip():
        try:
            max_tc = max(1000, min(2_000_000, int(mtc.strip(), 10)))
        except ValueError:
            max_tc = 400_000
    else:
        max_tc = 400_000
    return await github_repo_read(
        owner=str(args.get("owner") or ""),
        repo=str(args.get("repo") or ""),
        path=str(args.get("path") or ""),
        block_external_network=router._cfg.agent.block_external_network,
        ref=(
            str(r).strip()
            if (r := args.get("ref")) is not None and str(r).strip()
            else None
        ),
        api_base_url=gh.api_base_url or "https://api.github.com",
        token=gh.token or "",
        user_agent=_web_user_agent(router),
        timeout_sec=float(args.get("timeout_sec") or 60.0),
        max_text_chars=max_tc,
        max_directory_entries=int(args.get("max_directory_entries") or 200),
    )


HANDLERS: dict[str, Any] = {
    "web_fetch": _web_fetch,
    "github_api": _github_api,
    "github_repo_read": _github_repo_read,
}
