"""Configuration schema using Pydantic."""

import os
from pathlib import Path
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.alias_generators import to_camel
from pydantic_settings import BaseSettings


def _normalize_control_plane_url(value: str | None) -> str:
    if not isinstance(value, str):
        return "wss://api.madpodai.com"

    normalized = value.strip()
    if normalized.startswith("https://"):
        return "wss://" + normalized[len("https://") :]
    if normalized.startswith("http://"):
        return "ws://" + normalized[len("http://") :]
    return normalized or "wss://api.madpodai.com"


class Base(BaseModel):
    """Base model that accepts both camelCase and snake_case keys."""

    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)


class MadpodConfig(Base):
    """Madpod control-plane channel configuration."""

    enabled: bool = True
    control_plane_url: str = Field(
        default_factory=lambda: _normalize_control_plane_url(
            os.environ.get("MADPOD_CONTROL_PLANE_WS_URL")
        )
    )
    agent_id: str = Field(
        default_factory=lambda: os.environ.get("MADPOD_AGENT_ID", "")
    )
    gateway_token: str = Field(
        default_factory=lambda: (os.environ.get("MADPOD_GATEWAY_TOKEN") or "")
    )

    @field_validator("control_plane_url", mode="before")
    @classmethod
    def normalize_control_plane_url(cls, value: str) -> str:
        """Accept HTTP(S) control plane URLs and coerce them to WebSocket URLs."""
        return _normalize_control_plane_url(value)


class ChannelsConfig(Base):
    """Configuration for agent channels."""

    send_progress: bool = True   # stream agent's text progress to the channel
    send_tool_hints: bool = False  # stream tool-call hints (e.g. read_file("…"))
    madpod: MadpodConfig = Field(default_factory=MadpodConfig)


class AgentDefaults(Base):
    """Default agent configuration."""

    workspace: str = "~/.nanobot/workspace"
    model: str = "anthropic/claude-opus-4-5"
    provider: str = (
        "auto"  # Provider name (e.g. "anthropic", "openrouter") or "auto" for auto-detection
    )
    max_tokens: int = 8192
    temperature: float = 0.1
    max_tool_iterations: int = 40
    memory_window: int = 100
    reasoning_effort: str | None = None  # low / medium / high — enables LLM thinking mode


class AgentRoleConfig(Base):
    """Optional role-specific model overrides."""

    model: str | None = None
    provider: str | None = None
    api_base: str | None = None
    max_tokens: int | None = None
    reasoning_effort: str | None = None


class AgentRolesConfig(Base):
    """Role-specific agent model configuration."""

    planner: AgentRoleConfig = Field(default_factory=AgentRoleConfig)
    executor: AgentRoleConfig = Field(default_factory=AgentRoleConfig)
    summarizer: AgentRoleConfig = Field(default_factory=AgentRoleConfig)


class AgentsConfig(Base):
    """Agent configuration."""

    defaults: AgentDefaults = Field(default_factory=AgentDefaults)
    roles: AgentRolesConfig = Field(default_factory=AgentRolesConfig)


class ProviderConfig(Base):
    """LLM provider configuration."""

    api_key: str = ""
    api_base: str | None = None
    extra_headers: dict[str, str] | None = None  # Custom headers (e.g. APP-Code for AiHubMix)


class ProvidersConfig(Base):
    """Configuration for LLM providers."""

    custom: ProviderConfig = Field(default_factory=ProviderConfig)  # Any OpenAI-compatible endpoint
    azure_openai: ProviderConfig = Field(default_factory=ProviderConfig)  # Azure OpenAI (model = deployment name)
    anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
    openai: ProviderConfig = Field(default_factory=ProviderConfig)
    openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
    deepseek: ProviderConfig = Field(default_factory=ProviderConfig)
    groq: ProviderConfig = Field(default_factory=ProviderConfig)
    zhipu: ProviderConfig = Field(default_factory=ProviderConfig)
    dashscope: ProviderConfig = Field(default_factory=ProviderConfig)  # 阿里云通义千问
    vllm: ProviderConfig = Field(default_factory=ProviderConfig)
    gemini: ProviderConfig = Field(default_factory=ProviderConfig)
    moonshot: ProviderConfig = Field(default_factory=ProviderConfig)
    minimax: ProviderConfig = Field(default_factory=ProviderConfig)
    aihubmix: ProviderConfig = Field(default_factory=ProviderConfig)  # AiHubMix API gateway
    siliconflow: ProviderConfig = Field(default_factory=ProviderConfig)  # SiliconFlow (硅基流动)
    volcengine: ProviderConfig = Field(default_factory=ProviderConfig)  # VolcEngine (火山引擎)
    openai_codex: ProviderConfig = Field(default_factory=ProviderConfig)  # OpenAI Codex (OAuth)
    github_copilot: ProviderConfig = Field(default_factory=ProviderConfig)  # Github Copilot (OAuth)


class HeartbeatConfig(Base):
    """Heartbeat service configuration."""

    enabled: bool = True
    interval_s: int = 30 * 60  # 30 minutes


class GatewayConfig(Base):
    """Gateway/server configuration."""

    host: str = "0.0.0.0"
    port: int = 18790
    heartbeat: HeartbeatConfig = Field(default_factory=HeartbeatConfig)


class HooksConfig(Base):
    """Configuration for runtime hooks."""

    pre_tool_use: list[str] = Field(default_factory=list, alias="PreToolUse")
    post_tool_use: list[str] = Field(default_factory=list, alias="PostToolUse")
    on_error: list[str] = Field(default_factory=list, alias="OnError")


class WebSearchConfig(Base):
    """Web search tool configuration."""

    api_key: str = ""  # Brave Search API key
    max_results: int = 5
    engines: list[str] = Field(
        default_factory=lambda: ["duckduckgo_html", "bing_html"]
    )
    searxng_base_url: str | None = None
    summary_model: str | None = None
    summary_enabled: bool = True
    http_timeout_s: int = 15
    browser_timeout_s: int = 30
    concurrency: int = 2
    browser_fallback_enabled: bool = True


class WebToolsConfig(Base):
    """Web tools configuration."""

    proxy: str | None = (
        None  # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080"
    )
    search: WebSearchConfig = Field(default_factory=WebSearchConfig)


class BrowserToolsConfig(Base):
    """Interactive browser tool configuration."""

    enabled: bool = True
    backend: Literal["playwright", "playwright_mcp"] = "playwright"
    headless: bool | None = None  # None = headed when DISPLAY exists, else headless
    viewport_width: int = 1280
    viewport_height: int = 800
    action_timeout_s: int = 15
    navigation_timeout_s: int = 30
    snapshot_max_elements: int = 40
    snapshot_text_chars: int = 1200
    profile_subdir: str = "chrome/browser"
    output_subdir: str = "logs/browser"
    mcp_server_name: str = "playwright"


class ExecToolConfig(Base):
    """Shell exec tool configuration."""

    timeout: int = 60
    path_append: str = ""


class MCPServerConfig(Base):
    """MCP server connection configuration (stdio or HTTP)."""

    type: Literal["stdio", "sse", "streamableHttp"] | None = None  # auto-detected if omitted
    command: str = ""  # Stdio: command to run (e.g. "npx")
    args: list[str] = Field(default_factory=list)  # Stdio: command arguments
    env: dict[str, str] = Field(default_factory=dict)  # Stdio: extra env vars
    url: str = ""  # HTTP/SSE: endpoint URL
    headers: dict[str, str] = Field(default_factory=dict)  # HTTP/SSE: custom headers
    tool_timeout: int = 30  # seconds before a tool call is cancelled
    hide_from_model: bool = False  # Keep wrapped tools callable but omit them from the model tool list


class ToolsConfig(Base):
    """Tools configuration."""

    web: WebToolsConfig = Field(default_factory=WebToolsConfig)
    browser: BrowserToolsConfig = Field(default_factory=BrowserToolsConfig)
    exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
    restrict_to_workspace: bool = False  # If true, restrict all tool access to workspace directory
    mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)


class ResolvedAgentRole(Base):
    """Fully resolved settings for an agent role."""

    model: str
    provider: str | None = None
    api_base: str | None = None
    max_tokens: int
    reasoning_effort: str | None = None


class Config(BaseSettings):
    """Root configuration for madpod-agent."""

    agents: AgentsConfig = Field(default_factory=AgentsConfig)
    channels: ChannelsConfig = Field(default_factory=ChannelsConfig)
    providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
    gateway: GatewayConfig = Field(default_factory=GatewayConfig)
    hooks: HooksConfig = Field(default_factory=HooksConfig)
    tools: ToolsConfig = Field(default_factory=ToolsConfig)

    @property
    def workspace_path(self) -> Path:
        """Get expanded workspace path."""
        return Path(self.agents.defaults.workspace).expanduser()

    def _match_provider(
        self,
        model: str | None = None,
        forced_provider: str | None = None,
    ) -> tuple["ProviderConfig | None", str | None]:
        """Match provider config and its registry name. Returns (config, spec_name)."""
        from madpod_agent.providers.registry import PROVIDERS

        forced = forced_provider if forced_provider is not None else self.agents.defaults.provider
        if forced != "auto":
            p = getattr(self.providers, forced, None)
            return (p, forced) if p else (None, None)

        model_lower = (model or self.agents.defaults.model).lower()
        model_normalized = model_lower.replace("-", "_")
        model_prefix = model_lower.split("/", 1)[0] if "/" in model_lower else ""
        normalized_prefix = model_prefix.replace("-", "_")

        def _kw_matches(kw: str) -> bool:
            kw = kw.lower()
            return kw in model_lower or kw.replace("-", "_") in model_normalized

        # Explicit provider prefix wins — prevents `github-copilot/...codex` matching openai_codex.
        for spec in PROVIDERS:
            p = getattr(self.providers, spec.name, None)
            if p and model_prefix and normalized_prefix == spec.name:
                if spec.is_oauth or p.api_key:
                    return p, spec.name

        # Match by keyword (order follows PROVIDERS registry)
        for spec in PROVIDERS:
            p = getattr(self.providers, spec.name, None)
            if p and any(_kw_matches(kw) for kw in spec.keywords):
                if spec.is_oauth or p.api_key:
                    return p, spec.name

        # Fallback: gateways first, then others (follows registry order)
        # OAuth providers are NOT valid fallbacks — they require explicit model selection
        for spec in PROVIDERS:
            if spec.is_oauth:
                continue
            p = getattr(self.providers, spec.name, None)
            if p and p.api_key:
                return p, spec.name
        return None, None

    def get_provider(
        self,
        model: str | None = None,
        forced_provider: str | None = None,
    ) -> ProviderConfig | None:
        """Get matched provider config (api_key, api_base, extra_headers). Falls back to first available."""
        p, _ = self._match_provider(model, forced_provider)
        return p

    def get_provider_name(
        self,
        model: str | None = None,
        forced_provider: str | None = None,
    ) -> str | None:
        """Get the registry name of the matched provider (e.g. "deepseek", "openrouter")."""
        _, name = self._match_provider(model, forced_provider)
        return name

    def get_api_key(
        self,
        model: str | None = None,
        forced_provider: str | None = None,
    ) -> str | None:
        """Get API key for the given model. Falls back to first available key."""
        p = self.get_provider(model, forced_provider)
        return p.api_key if p else None

    def get_api_base(
        self,
        model: str | None = None,
        forced_provider: str | None = None,
    ) -> str | None:
        """Get API base URL for the given model. Applies default URLs for known gateways."""
        from madpod_agent.providers.registry import find_by_name

        p, name = self._match_provider(model, forced_provider)
        if p and p.api_base:
            return p.api_base
        # Only gateways get a default api_base here. Standard providers
        # (like Moonshot) set their base URL via env vars in _setup_env
        # to avoid polluting the global litellm.api_base.
        if name:
            spec = find_by_name(name)
            if spec and spec.is_gateway and spec.default_api_base:
                return spec.default_api_base
        return None

    def resolve_role(
        self,
        role: Literal["planner", "executor", "summarizer"],
        *,
        fallback_model: str | None = None,
    ) -> ResolvedAgentRole:
        """Resolve role settings with defaults and optional fallback model."""
        override = getattr(self.agents.roles, role)
        model = (
            str(override.model or fallback_model or self.agents.defaults.model).strip()
            or self.agents.defaults.model
        )
        provider = override.provider
        api_base = (
            override.api_base
            if override.api_base is not None
            else self.get_api_base(model, provider)
        )
        max_tokens = override.max_tokens or self.agents.defaults.max_tokens
        reasoning_effort = (
            override.reasoning_effort
            if override.reasoning_effort is not None
            else self.agents.defaults.reasoning_effort
        )
        return ResolvedAgentRole(
            model=model,
            provider=provider,
            api_base=api_base,
            max_tokens=max_tokens,
            reasoning_effort=reasoning_effort,
        )

    model_config = ConfigDict(env_prefix="NANOBOT_", env_nested_delimiter="__")
