from __future__ import annotations

from typing import TYPE_CHECKING, Any

from aicd.config import (
    DEFAULT_PROFILE_CONTEXT_TOKENS,
    LLMProfile,
    PROTO_OLLAMA,
    apply_active_profile_to_llm,
    save_config,
)
from aicd.config_settings import (
    _coerce_llm_proto,
    format_llm_profiles_lines,
    switch_llm_profile_by_token,
)
from aicd.tools.ollama_context import fetch_ollama_context_window_tokens

if TYPE_CHECKING:
    from aicd.agent.tool_router import ToolRouter


def _persist_and_maybe_reload(router: ToolRouter, persist: bool) -> None:
    if persist:
        save_config(router._layout["config"], router._cfg)
        router.schedule_llm_hot_reload()


def _resolve_profile_index(
    cfg: Any, token: str
) -> tuple[bool, str, int | None]:
    t = (token or "").strip()
    if not t:
        return False, "empty target (index or name)", None
    if not cfg.llm_profiles:
        return False, "no profiles", None
    if t.isdigit():
        i = int(t, 10)
        if i < 1 or i > len(cfg.llm_profiles):
            return (
                False,
                f"index must be 1–{len(cfg.llm_profiles)}",
                None,
            )
        return True, "", i - 1
    for j, p in enumerate(cfg.llm_profiles):
        if p.name == t:
            return True, "", j
    tl = t.lower()
    for j, p in enumerate(cfg.llm_profiles):
        if p.name.lower() == tl:
            return True, "", j
    return False, f"profile not found: {t!r}", None


def _llm_profiles_list(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    _ = args
    cfg = router._cfg
    return {
        "ok": True,
        "activeLlmProfile": cfg.active_llm_profile,
        "profiles": [p.to_mapping() for p in cfg.llm_profiles],
        "listing": format_llm_profiles_lines(cfg),
    }


def _llm_profile_activate(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    token = str(args.get("name_or_index") or "").strip()
    ok, msg = switch_llm_profile_by_token(router._cfg, token)
    if ok:
        _persist_and_maybe_reload(router, bool(args.get("persist", True)))
    return {"ok": ok, "message": msg}


def _llm_profile_add(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    cfg = router._cfg
    name = str(args.get("name") or "").strip()
    if not name:
        return {"ok": False, "error": "name is required"}
    for p in cfg.llm_profiles:
        if p.name == name:
            return {"ok": False, "error": f"profile name already exists: {name!r}"}
    proto = _coerce_llm_proto(str(args.get("type") or args.get("proto") or ""))
    base_url = str(args.get("baseUrl") or args.get("url") or "").strip()
    api_key = str(args.get("apiKey") or args.get("key") or "").strip()
    model = str(args.get("modelId") or args.get("model") or "").strip() or "gpt-4o-mini"
    vm = args.get("visionModel") or args.get("vision_model")
    vision_model = str(vm).strip() if vm not in (None, "") else None

    ctx: int | None = None
    ctx_raw = args.get("contextWindowTokens")
    if ctx_raw is not None and str(ctx_raw).strip().lower() not in (
        "",
        "auto",
        "null",
        "none",
    ):
        try:
            iv = int(ctx_raw)
            ctx = iv if iv > 0 else None
        except (TypeError, ValueError):
            ctx = None

    if proto == PROTO_OLLAMA:
        got = fetch_ollama_context_window_tokens(
            base_url=base_url, model=model, timeout_sec=float(cfg.llm.timeout_sec)
        )
        if got is not None:
            ctx = got
        elif ctx is None:
            ctx = DEFAULT_PROFILE_CONTEXT_TOKENS
    elif ctx is None:
        ctx = DEFAULT_PROFILE_CONTEXT_TOKENS

    prof = LLMProfile(
        name=name,
        proto=proto,
        base_url=base_url,
        api_key=api_key,
        model=model,
        context_window_tokens=ctx,
        vision_model=vision_model,
    )
    cfg.llm_profiles.append(prof)
    if bool(args.get("activate", False)):
        cfg.active_llm_profile = name
        apply_active_profile_to_llm(cfg)
    _persist_and_maybe_reload(router, bool(args.get("persist", True)))
    return {
        "ok": True,
        "profile": prof.to_mapping(),
        "message": f"added profile {name!r}",
    }


def _profile_as_raw(p: LLMProfile) -> dict[str, Any]:
    d = p.to_mapping()
    # from_mapping 接受 modelId；确保字段齐全
    return {
        "name": d.get("name") or p.name,
        "proto": p.proto,
        "baseUrl": p.base_url,
        "apiKey": p.api_key,
        "modelId": p.model,
        **({"visionModel": d["visionModel"]} if d.get("visionModel") else {}),
        **(
            {"contextWindowTokens": d["contextWindowTokens"]}
            if "contextWindowTokens" in d
            else {}
        ),
    }


def _llm_profile_update(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    cfg = router._cfg
    token = str(args.get("target") or args.get("name_or_index") or "").strip()
    ok_i, err, idx = _resolve_profile_index(cfg, token)
    if not ok_i or idx is None:
        return {"ok": False, "error": err}
    patch = args.get("patch")
    if not isinstance(patch, dict):
        return {"ok": False, "error": "patch must be an object"}
    p = cfg.llm_profiles[idx]
    raw = _profile_as_raw(p)
    old_name = p.name
    for rk, rv in patch.items():
        kk = str(rk)
        if kk in ("name",):
            raw["name"] = str(rv).strip()
        elif kk in ("proto", "type"):
            raw["proto"] = _coerce_llm_proto(str(rv))
        elif kk in ("baseUrl", "base_url", "url"):
            raw["baseUrl"] = str(rv).strip()
        elif kk in ("apiKey", "api_key", "key"):
            raw["apiKey"] = str(rv).strip()
        elif kk in ("modelId", "model_id", "model"):
            raw["modelId"] = str(rv).strip()
        elif kk in ("visionModel", "vision_model", "vision"):
            if rv in (None, ""):
                raw.pop("visionModel", None)
            else:
                raw["visionModel"] = str(rv).strip()
        elif kk in ("contextWindowTokens", "context_window_tokens", "context"):
            if rv is None or str(rv).strip().lower() in ("", "auto", "null", "none"):
                raw.pop("contextWindowTokens", None)
            else:
                try:
                    iv = int(rv)
                    if iv > 0:
                        raw["contextWindowTokens"] = iv
                    else:
                        raw.pop("contextWindowTokens", None)
                except (TypeError, ValueError):
                    return {"ok": False, "error": f"invalid contextWindowTokens: {rv!r}"}
        else:
            return {"ok": False, "error": f"unknown patch key: {kk!r}"}

    new_p = LLMProfile.from_mapping(raw)
    if new_p.name != old_name and cfg.active_llm_profile == old_name:
        cfg.active_llm_profile = new_p.name
    cfg.llm_profiles[idx] = new_p

    if (
        new_p.proto == PROTO_OLLAMA
        and bool(args.get("refreshOllamaContext", False))
    ):
        got = fetch_ollama_context_window_tokens(
            base_url=new_p.base_url,
            model=new_p.model,
            timeout_sec=float(cfg.llm.timeout_sec),
        )
        if got is not None:
            new_p = LLMProfile(
                name=new_p.name,
                proto=new_p.proto,
                base_url=new_p.base_url,
                api_key=new_p.api_key,
                model=new_p.model,
                context_window_tokens=got,
                vision_model=new_p.vision_model,
            )
            cfg.llm_profiles[idx] = new_p

    apply_active_profile_to_llm(cfg)
    _persist_and_maybe_reload(router, bool(args.get("persist", True)))
    return {"ok": True, "profile": new_p.to_mapping()}


def _llm_profile_delete(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    cfg = router._cfg
    if len(cfg.llm_profiles) <= 1:
        return {"ok": False, "error": "不能删除最后一个 profile；请先添加再删。"}
    token = str(args.get("name_or_index") or "").strip()
    ok_i, err, idx = _resolve_profile_index(cfg, token)
    if not ok_i or idx is None:
        return {"ok": False, "error": err}
    victim = cfg.llm_profiles[idx]
    if victim.name == cfg.active_llm_profile:
        alt = next(i for i in range(len(cfg.llm_profiles)) if i != idx)
        cfg.active_llm_profile = cfg.llm_profiles[alt].name
    del cfg.llm_profiles[idx]
    apply_active_profile_to_llm(cfg)
    _persist_and_maybe_reload(router, bool(args.get("persist", True)))
    return {"ok": True, "message": f"removed profile {victim.name!r}"}


def _llm_profile_duplicate(router: ToolRouter, args: dict[str, Any]) -> dict[str, Any]:
    cfg = router._cfg
    token = str(args.get("source") or args.get("name_or_index") or "").strip()
    ok_i, err, idx = _resolve_profile_index(cfg, token)
    if not ok_i or idx is None:
        return {"ok": False, "error": err}
    new_name = str(args.get("new_name") or "").strip()
    if not new_name:
        return {"ok": False, "error": "new_name is required"}
    for p in cfg.llm_profiles:
        if p.name == new_name:
            return {"ok": False, "error": f"profile name already exists: {new_name!r}"}
    src = cfg.llm_profiles[idx]
    raw = _profile_as_raw(src)
    raw["name"] = new_name
    new_p = LLMProfile.from_mapping(raw)
    cfg.llm_profiles.append(new_p)
    if bool(args.get("activate", False)):
        cfg.active_llm_profile = new_name
        apply_active_profile_to_llm(cfg)
    _persist_and_maybe_reload(router, bool(args.get("persist", True)))
    return {
        "ok": True,
        "profile": new_p.to_mapping(),
        "message": f"duplicated → {new_name!r}",
    }


HANDLERS: dict[str, Any] = {
    "llm_profiles_list": _llm_profiles_list,
    "llm_profile_activate": _llm_profile_activate,
    "llm_profile_add": _llm_profile_add,
    "llm_profile_update": _llm_profile_update,
    "llm_profile_delete": _llm_profile_delete,
    "llm_profile_duplicate": _llm_profile_duplicate,
}
