"""运行时合并 agent/llm 配置补丁（斜杠命令与 agent_settings_update 工具）。"""

from __future__ import annotations

import copy
from typing import Any

from aicd.config import MAX_AGENT_TOOL_ROUNDS, AppConfig, LLMConfig


def _clamp_int(v: int, lo: int, hi: int) -> int:
    return max(lo, min(hi, v))


def _clamp_agent_heartbeat(sec: int) -> int:
    """0=关闭；正数夹紧到 [5, 3600]（秒）。"""
    if sec <= 0:
        return 0
    return max(5, min(3600, int(sec)))


def _parse_float(raw: str) -> float | None:
    try:
        return float(raw.strip())
    except (TypeError, ValueError):
        return None


def _parse_int(raw: str) -> int | None:
    try:
        return int(raw.strip(), 10)
    except (TypeError, ValueError):
        return None


def set_llm_scalar_from_slash(cfg: LLMConfig, key: str, value_str: str) -> tuple[bool, str]:
    """``/llm <key> <value>`` 解析；``key`` 已小写。"""
    k = key.strip().lower().replace("-", "_")
    aliases = {
        "temp": "temperature",
        "topp": "top_p",
        "top_p": "top_p",
        "freq": "frequency_penalty",
        "frequencypenalty": "frequency_penalty",
        "frequency_penalty": "frequency_penalty",
        "pres": "presence_penalty",
        "presencepenalty": "presence_penalty",
        "presence_penalty": "presence_penalty",
        "maxtokens": "max_output_tokens",
        "max_output_tokens": "max_output_tokens",
        "maxoutputtokens": "max_output_tokens",
        "repetition": "repetition_penalty",
        "repetitionpenalty": "repetition_penalty",
        "repeat_penalty": "repetition_penalty",
        "repetition_penalty": "repetition_penalty",
        "timeout": "timeout_sec",
        "timeoutsec": "timeout_sec",
        "model": "model",
        "modelid": "model",
        "maxretries": "max_retries",
        "retrydelay": "retry_base_delay_sec",
        "retrybasedelaysec": "retry_base_delay_sec",
    }
    field = aliases.get(k, k)
    if field == "model":
        cfg.model = value_str.strip()
        return True, f"llm.model={cfg.model!r}"
    if field in ("max_retries", "maxretries", "retries"):
        n = _parse_int(value_str)
        if n is None or n < 0:
            return False, "max_retries must be a non-negative integer (0–20)"
        cfg.max_retries = _clamp_int(n, 0, 20)
        return True, f"llm.max_retries={cfg.max_retries}"
    if field in ("retry_base_delay_sec", "retrybasedelay", "retry_delay"):
        f = _parse_float(value_str)
        if f is None or f <= 0:
            return False, "retry_base_delay_sec must be > 0"
        cfg.retry_base_delay_sec = float(f)
        return True, f"llm.retry_base_delay_sec={cfg.retry_base_delay_sec}"
    if field == "timeout_sec":
        f = _parse_float(value_str)
        if f is None or f <= 0:
            return False, "timeout_sec must be a positive number"
        cfg.timeout_sec = float(f)
        return True, f"llm.timeout_sec={cfg.timeout_sec}"
    if field == "temperature":
        f = _parse_float(value_str)
        if f is None or not 0 <= f <= 2:
            return False, "temperature must be a number in [0, 2]"
        cfg.temperature = float(f)
        return True, f"llm.temperature={cfg.temperature}"
    if field == "top_p":
        f = _parse_float(value_str)
        if f is None or not 0 < f <= 1:
            return False, "top_p must be in (0, 1]"
        cfg.top_p = float(f)
        return True, f"llm.top_p={cfg.top_p}"
    if field == "frequency_penalty":
        f = _parse_float(value_str)
        if f is None or not -2 <= f <= 2:
            return False, "frequency_penalty must be in [-2, 2]"
        cfg.frequency_penalty = float(f)
        return True, f"llm.frequency_penalty={cfg.frequency_penalty}"
    if field == "presence_penalty":
        f = _parse_float(value_str)
        if f is None or not -2 <= f <= 2:
            return False, "presence_penalty must be in [-2, 2]"
        cfg.presence_penalty = float(f)
        return True, f"llm.presence_penalty={cfg.presence_penalty}"
    if field == "max_output_tokens":
        n = _parse_int(value_str)
        if n is None or n < 1:
            return False, "max_output_tokens must be a positive integer"
        cfg.max_output_tokens = int(n)
        return True, f"llm.max_output_tokens={cfg.max_output_tokens}"
    if field == "repetition_penalty":
        f = _parse_float(value_str)
        if f is None or f <= 0:
            return False, "repetition_penalty must be a positive number"
        cfg.repetition_penalty = float(f)
        return True, f"llm.repetition_penalty={cfg.repetition_penalty}"
    return False, f"unknown llm key: {key!r} (try temperature, topP, maxOutputTokens, …)"


def set_agent_scalar_from_slash(cfg: AppConfig, key: str, value_str: str) -> tuple[bool, str]:
    k = key.strip().lower().replace("-", "_")
    n = _parse_int(value_str)
    if n is None:
        return False, "value must be an integer"
    if k in ("rounds", "max_tool_rounds", "maxtoolrounds"):
        cfg.agent.max_tool_rounds = _clamp_int(n, 1, MAX_AGENT_TOOL_ROUNDS)
        return True, f"agent.max_tool_rounds={cfg.agent.max_tool_rounds}"
    if k in ("subrounds", "sub_rounds", "subagent", "sub_agent_max_tool_rounds", "subagentmaxtoolrounds"):
        cfg.agent.sub_agent_max_tool_rounds = _clamp_int(n, 1, MAX_AGENT_TOOL_ROUNDS)
        return True, f"agent.sub_agent_max_tool_rounds={cfg.agent.sub_agent_max_tool_rounds}"
    if k in ("heartbeat", "idle_heartbeat", "idleheartbeat", "idle_heartbeat_sec"):
        cfg.agent.idle_heartbeat_sec = _clamp_agent_heartbeat(n)
        return True, f"agent.idle_heartbeat_sec={cfg.agent.idle_heartbeat_sec} (0=关闭, 否则 5–3600 秒)"
    if k in (
        "subheartbeat",
        "sub_heartbeat",
        "sub_agent_idle_heartbeat",
        "subagentidleheartbeat",
        "sub_agent_idle_heartbeat_sec",
    ):
        cfg.agent.sub_agent_idle_heartbeat_sec = _clamp_agent_heartbeat(n)
        return (
            True,
            f"agent.sub_agent_idle_heartbeat_sec={cfg.agent.sub_agent_idle_heartbeat_sec} "
            "(0=关闭；子任务仍有 running/pending 时启用协调心跳)",
        )
    return False, f"unknown agent key: {key!r} (rounds | subRounds | heartbeat | subHeartbeat)"


def apply_nested_settings_patch(cfg: AppConfig, patch: dict[str, Any]) -> tuple[bool, str]:
    """
    ``patch`` 形如 ``{"llm": {"temperature": 0.5}, "agent": {"maxToolRounds": 30}}``。
    仅更新出现的字段；校验失败则不改配置并返回错误说明。
    """
    if not isinstance(patch, dict):
        return False, "patch must be an object"
    errs: list[str] = []
    backup = copy.deepcopy(cfg)

    try:
        llm_p = patch.get("llm")
        if isinstance(llm_p, dict):
            for rk, rv in llm_p.items():
                key = str(rk).replace("-", "_")
                camel_map = {
                    "baseUrl": "base_url",
                    "apiKey": "api_key",
                    "modelId": "model",
                    "timeoutSec": "timeout_sec",
                    "topP": "top_p",
                    "frequencyPenalty": "frequency_penalty",
                    "presencePenalty": "presence_penalty",
                    "maxOutputTokens": "max_output_tokens",
                    "repetitionPenalty": "repetition_penalty",
                    "extraBody": "extra_body",
                    "visionModel": "vision_model",
                    "maxRetries": "max_retries",
                    "retryBaseDelaySec": "retry_base_delay_sec",
                }
                key = camel_map.get(key, key)
                if key == "model":
                    cfg.llm.model = str(rv).strip()
                elif key == "vision_model":
                    cfg.llm.vision_model = (
                        str(rv).strip() if str(rv).strip() else None
                    )
                elif key == "proto":
                    cfg.llm.proto = str(rv).strip()
                elif key == "base_url":
                    cfg.llm.base_url = str(rv).strip()
                elif key == "api_key":
                    cfg.llm.api_key = str(rv).strip()
                elif key == "timeout_sec":
                    cfg.llm.timeout_sec = float(rv)
                    if cfg.llm.timeout_sec <= 0:
                        raise ValueError("timeout_sec must be > 0")
                elif key == "max_retries":
                    cfg.llm.max_retries = _clamp_int(int(rv), 0, 20)
                elif key == "retry_base_delay_sec":
                    cfg.llm.retry_base_delay_sec = max(0.1, float(rv))
                elif key == "temperature":
                    cfg.llm.temperature = float(rv)
                    if not 0 <= cfg.llm.temperature <= 2:
                        raise ValueError("temperature in [0,2]")
                elif key == "top_p":
                    cfg.llm.top_p = float(rv)
                    if not 0 < cfg.llm.top_p <= 1:
                        raise ValueError("top_p in (0,1]")
                elif key == "frequency_penalty":
                    cfg.llm.frequency_penalty = float(rv)
                    if not -2 <= cfg.llm.frequency_penalty <= 2:
                        raise ValueError("frequency_penalty in [-2,2]")
                elif key == "presence_penalty":
                    cfg.llm.presence_penalty = float(rv)
                    if not -2 <= cfg.llm.presence_penalty <= 2:
                        raise ValueError("presence_penalty in [-2,2]")
                elif key == "max_output_tokens":
                    cfg.llm.max_output_tokens = int(rv)
                    if cfg.llm.max_output_tokens < 1:
                        raise ValueError("max_output_tokens >= 1")
                elif key == "repetition_penalty":
                    cfg.llm.repetition_penalty = float(rv)
                    if cfg.llm.repetition_penalty <= 0:
                        raise ValueError("repetition_penalty > 0")
                elif key == "extra_body":
                    if rv is not None and not isinstance(rv, dict):
                        raise ValueError("extra_body must be an object")
                    cfg.llm.extra_body = dict(rv) if rv else {}
                elif key == "clear_sampling":
                    if rv:
                        cfg.llm.temperature = None
                        cfg.llm.top_p = None
                        cfg.llm.frequency_penalty = None
                        cfg.llm.presence_penalty = None
                        cfg.llm.max_output_tokens = None
                        cfg.llm.repetition_penalty = None
                else:
                    errs.append(f"unknown llm field: {rk!r}")

        ag_p = patch.get("agent")
        if isinstance(ag_p, dict):
            for rk, rv in ag_p.items():
                k = str(rk)
                if k in ("maxToolRounds", "max_tool_rounds"):
                    cfg.agent.max_tool_rounds = _clamp_int(
                        int(rv), 1, MAX_AGENT_TOOL_ROUNDS
                    )
                elif k in ("subAgentMaxToolRounds", "sub_agent_max_tool_rounds"):
                    cfg.agent.sub_agent_max_tool_rounds = _clamp_int(
                        int(rv), 1, MAX_AGENT_TOOL_ROUNDS
                    )
                elif k in ("contextBudgetTokens", "context_budget_tokens"):
                    cfg.agent.context_budget_tokens = max(4096, int(rv))
                elif k in ("chatHistoryMaxMessages", "chat_history_max_messages"):
                    cfg.agent.chat_history_max_messages = max(
                        10, min(20_000, int(rv))
                    )
                elif k in ("idleHeartbeatSec", "idle_heartbeat_sec"):
                    cfg.agent.idle_heartbeat_sec = _clamp_agent_heartbeat(int(rv))
                elif k in (
                    "subAgentIdleHeartbeatSec",
                    "sub_agent_idle_heartbeat_sec",
                ):
                    cfg.agent.sub_agent_idle_heartbeat_sec = _clamp_agent_heartbeat(
                        int(rv)
                    )
                else:
                    errs.append(f"unknown agent field: {rk!r}")
    except (TypeError, ValueError) as e:
        # restore
        cfg.llm = backup.llm
        cfg.agent = backup.agent
        return False, str(e)

    if errs:
        cfg.llm = backup.llm
        cfg.agent = backup.agent
        return False, "; ".join(errs)
    return True, "ok"


def format_settings_summary(cfg: AppConfig) -> str:
    llm = cfg.llm
    lines = [
        "[agent]",
        f"  max_tool_rounds={cfg.agent.max_tool_rounds}",
        f"  sub_agent_max_tool_rounds={cfg.agent.sub_agent_max_tool_rounds}",
        f"  idle_heartbeat_sec={cfg.agent.idle_heartbeat_sec} (主对话空闲定时检查；0=关)",
        f"  sub_agent_idle_heartbeat_sec={cfg.agent.sub_agent_idle_heartbeat_sec} (子 Agent 协调；0=关)",
        f"  context_budget_tokens={cfg.agent.context_budget_tokens}",
        f"  chat_history_max_messages={cfg.agent.chat_history_max_messages}",
        "[llm sampling]",
        f"  temperature={llm.temperature}",
        f"  top_p={llm.top_p}",
        f"  frequency_penalty={llm.frequency_penalty}",
        f"  presence_penalty={llm.presence_penalty}",
        f"  max_output_tokens={llm.max_output_tokens}",
        f"  repetition_penalty={llm.repetition_penalty}",
        f"  extra_body keys={list(llm.extra_body.keys()) if llm.extra_body else []}",
        "[llm core]",
        f"  model={llm.model!r} vision_model={llm.vision_model!r} timeout_sec={llm.timeout_sec}",
        f"  max_retries={llm.max_retries} retry_base_delay_sec={llm.retry_base_delay_sec}",
    ]
    return "\n".join(lines)
