"""Configuration loading."""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


def get_memnixa_home() -> Path:
    """Return the user-level Memnixa home directory."""
    override = os.getenv("MEMNIXA_HOME")
    if override:
        return Path(override).expanduser().resolve()
    return (Path.home() / ".memnixa").resolve()


def get_default_config_path() -> Path:
    """Return the default config path in the user-level Memnixa home."""
    return get_memnixa_home() / "config.json"


def get_default_database_path() -> Path:
    """Return the default SQLite database path."""
    return get_memnixa_home() / "memnixa.db"


def get_cli_history_path() -> Path:
    """Return the default CLI history file path."""
    return get_memnixa_home() / "cli_history"


def ensure_memnixa_home() -> Path:
    """Create the user-level Memnixa home if needed."""
    path = get_memnixa_home()
    path.mkdir(parents=True, exist_ok=True)
    return path


@dataclass(slots=True)
class ProviderConfig:
    """One configured model endpoint."""

    id: str
    preset: str
    api_key: str = ""
    api_base: str | None = None
    model: str | None = None
    label: str | None = None
    context_window_tokens: int | None = None
    max_output_tokens: int | None = None

    @classmethod
    def from_mapping(
        cls,
        data: dict[str, Any],
        *,
        fallback_id: int,
    ) -> "ProviderConfig":
        return cls(
            id=str(data.get("id") or fallback_id),
            preset=str(data.get("preset") or data.get("provider") or "openai_chatgpt"),
            api_key=str(data.get("api_key") or ""),
            api_base=str(data["api_base"]) if data.get("api_base") else None,
            model=str(data["model"]) if data.get("model") else None,
            label=str(data["label"]) if data.get("label") else None,
            context_window_tokens=(
                int(data["context_window_tokens"])
                if data.get("context_window_tokens") not in (None, "")
                else None
            ),
            max_output_tokens=(
                int(data["max_output_tokens"])
                if data.get("max_output_tokens") not in (None, "")
                else None
            ),
        )


@dataclass(slots=True)
class FeishuChannelConfig:
    """Feishu channel configuration."""

    enabled: bool = False
    app_id: str = ""
    app_secret: str = ""
    encrypt_key: str | None = None
    verification_token: str | None = None
    group_policy: str = "mention"

    @classmethod
    def from_mapping(cls, data: dict[str, Any] | None) -> "FeishuChannelConfig":
        payload = data or {}
        return cls(
            enabled=bool(payload.get("enabled", False)),
            app_id=str(payload.get("app_id") or ""),
            app_secret=str(payload.get("app_secret") or ""),
            encrypt_key=str(payload["encrypt_key"])
            if payload.get("encrypt_key")
            else None,
            verification_token=str(payload["verification_token"])
            if payload.get("verification_token")
            else None,
            group_policy=str(payload.get("group_policy") or "mention"),
        )


@dataclass(slots=True)
class QQChannelConfig:
    """QQ channel configuration."""

    enabled: bool = False
    app_id: str = ""
    secret: str = ""

    @classmethod
    def from_mapping(cls, data: dict[str, Any] | None) -> "QQChannelConfig":
        payload = data or {}
        return cls(
            enabled=bool(payload.get("enabled", False)),
            app_id=str(payload.get("app_id") or ""),
            secret=str(payload.get("secret") or ""),
        )


@dataclass(slots=True)
class ChannelsConfig:
    """Gateway channel settings."""

    feishu: FeishuChannelConfig = field(default_factory=FeishuChannelConfig)
    qq: QQChannelConfig = field(default_factory=QQChannelConfig)

    @classmethod
    def from_mapping(cls, data: dict[str, Any] | None) -> "ChannelsConfig":
        payload = data or {}
        return cls(
            feishu=FeishuChannelConfig.from_mapping(payload.get("feishu")),
            qq=QQChannelConfig.from_mapping(payload.get("qq")),
        )


@dataclass(slots=True)
class MemoryConfig:
    """Long-term memory configuration."""

    enabled: bool = False
    extraction_provider_id: str | None = None
    extraction_timeout_seconds: int = 20
    retrieval_limit: int = 5
    max_injected_chars: int = 2400

    @classmethod
    def from_mapping(cls, data: dict[str, Any] | None) -> "MemoryConfig":
        payload = data or {}
        provider_id = payload.get("extraction_provider_id")
        return cls(
            enabled=bool(payload.get("enabled", False)),
            extraction_provider_id=(
                str(provider_id).strip() if provider_id not in (None, "") else None
            ),
            extraction_timeout_seconds=max(
                1, int(payload.get("extraction_timeout_seconds") or 20)
            ),
            retrieval_limit=max(1, int(payload.get("retrieval_limit") or 5)),
            max_injected_chars=max(256, int(payload.get("max_injected_chars") or 2400)),
        )


def build_example_config() -> dict[str, Any]:
    """Return the built-in example config template."""
    return {
        "default_provider": "1",
        "providers": [
            {
                "id": "1",
                "label": "Zhipu Coding Plan",
                "preset": "zhipu_coding_plan",
                "api_key": "YOUR_API_KEY",
                "api_base": "https://open.bigmodel.cn/api/coding/paas/v4",
                "model": "glm-4.7",
                "context_window_tokens": 128000,
                "max_output_tokens": 8192,
            },
            {
                "id": "2",
                "label": "OpenAI ChatGPT",
                "preset": "openai_chatgpt",
                "api_key": "YOUR_OPENAI_API_KEY",
                "model": "gpt-4.1-mini",
                "context_window_tokens": 128000,
                "max_output_tokens": 8192,
            },
            {
                "id": "3",
                "label": "SiliconFlow",
                "preset": "siliconflow",
                "api_key": "YOUR_SILICONFLOW_API_KEY",
                "api_base": "https://api.siliconflow.cn/v1",
                "model": "deepseek-ai/DeepSeek-V3",
                "context_window_tokens": 128000,
                "max_output_tokens": 8192,
            },
        ],
        "workspace": ".",
        "database_path": "~/.memnixa/memnixa.db",
        "max_iterations": 8,
        "history_limit": 24,
        "context_window_tokens": 16384,
        "max_output_tokens": 4096,
        "context_compact_threshold_ratio": 0.88,
        "context_warn_threshold_ratio": 0.8,
        "context_safety_margin_tokens": 1024,
        "context_compaction_max_rounds": 3,
        "system_prompt": (
            "You are Memnixa, a concise coding agent. "
            "Use tools when needed, cite tool results plainly, "
            "and finish once the task is complete."
        ),
        "jina_api_key": "YOUR_JINA_API_KEY",
        "gateway_host": "127.0.0.1",
        "gateway_port": 8787,
        "gateway_url": "http://127.0.0.1:8787",
        "cli_via": "local",
        "channels": {
            "feishu": {
                "enabled": False,
                "app_id": "YOUR_FEISHU_APP_ID",
                "app_secret": "YOUR_FEISHU_APP_SECRET",
                "group_policy": "mention",
            },
            "qq": {
                "enabled": False,
                "app_id": "YOUR_QQ_APP_ID",
                "secret": "YOUR_QQ_SECRET",
            },
        },
        "memory": {
            "enabled": False,
            "extraction_provider_id": "2",
            "extraction_timeout_seconds": 20,
            "retrieval_limit": 5,
            "max_injected_chars": 2400,
        },
    }


def merge_missing_fields(
    current: dict[str, Any],
    example: dict[str, Any],
) -> tuple[dict[str, Any], list[str]]:
    """Merge missing keys from *example* into *current* recursively."""
    merged = dict(current)
    added_paths: list[str] = []

    for key, example_value in example.items():
        if key not in merged:
            merged[key] = example_value
            added_paths.append(key)
            continue

        current_value = merged[key]
        if isinstance(current_value, dict) and isinstance(example_value, dict):
            nested, nested_paths = merge_missing_fields(current_value, example_value)
            merged[key] = nested
            added_paths.extend([f"{key}.{path}" for path in nested_paths])

    return merged, added_paths


def sync_default_config() -> tuple[Path, bool, list[str]]:
    """Create or incrementally update the user-level config file."""
    ensure_memnixa_home()
    config_path = get_default_config_path()
    example = build_example_config()
    created = not config_path.exists()

    current: dict[str, Any] = {}
    if config_path.exists():
        loaded = json.loads(config_path.read_text(encoding="utf-8"))
        if not isinstance(loaded, dict):
            raise ValueError("default config must be a JSON object")
        current = loaded

    merged, added_paths = merge_missing_fields(current, example)
    config_path.write_text(
        json.dumps(merged, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    return config_path, created, added_paths


def ensure_config_file(config_path: str | Path | None) -> Path:
    """Return a writable config path, creating a template file when needed."""
    if config_path is None:
        path, _, _ = sync_default_config()
        return path

    path = Path(config_path).expanduser().resolve()
    if path.exists():
        return path

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(build_example_config(), ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    return path


def load_config_payload(config_path: str | Path | None) -> tuple[Path, dict[str, Any]]:
    """Load the raw JSON payload from a config file."""
    path = ensure_config_file(config_path)
    loaded = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(loaded, dict):
        raise ValueError("config must be a JSON object")
    return path, loaded


def save_config_payload(path: Path, payload: dict[str, Any]) -> None:
    """Persist a config payload to disk."""
    path.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def add_provider_config(
    config_path: str | Path | None,
    provider: ProviderConfig,
    *,
    set_default: bool = False,
) -> tuple[Path, AppConfig]:
    """Append one provider config to the config file and reload it."""
    path, payload = load_config_payload(config_path)
    providers = _extract_provider_payloads(payload)
    existing_ids = {str(item.get("id") or "").strip() for item in providers}
    if provider.id in existing_ids:
        raise ValueError(f"provider id '{provider.id}' already exists")

    providers.append(_provider_to_mapping(provider))
    payload["providers"] = providers
    if set_default or len(providers) == 1:
        payload["default_provider"] = provider.id

    save_config_payload(path, payload)
    return path, load_config(path, require_api_key=False)


@dataclass(slots=True)
class AppConfig:
    """Runtime configuration."""

    provider: str = "openai_chatgpt"
    api_key: str = ""
    api_base: str | None = None
    model: str | None = None
    providers: list[ProviderConfig] = field(default_factory=list)
    default_provider: str = "1"
    workspace: Path = Path(".")
    database_path: Path | None = None
    session_file: Path | None = None
    max_iterations: int = 8
    history_limit: int = 24
    context_window_tokens: int = 16384
    max_output_tokens: int = 4096
    context_compact_threshold_ratio: float = 0.88
    context_warn_threshold_ratio: float = 0.8
    context_safety_margin_tokens: int = 1024
    context_compaction_max_rounds: int = 3
    system_prompt: str | None = None
    jina_api_key: str = ""
    request_timeout: int = 120
    gateway_host: str = "127.0.0.1"
    gateway_port: int = 8787
    gateway_url: str | None = None
    cli_via: str = "local"
    channels: ChannelsConfig = field(default_factory=ChannelsConfig)
    memory: MemoryConfig = field(default_factory=MemoryConfig)

    @classmethod
    def from_mapping(cls, data: dict[str, Any]) -> "AppConfig":
        """Build config from a user mapping."""
        workspace = Path(data.get("workspace") or ".").expanduser().resolve()
        provider_list = cls._parse_provider_list(data)
        default_provider = str(data.get("default_provider") or "1")
        if provider_list:
            resolved_default = cls._pick_provider(provider_list, default_provider)
            default_provider = resolved_default.id
            provider = resolved_default.preset
            api_key = resolved_default.api_key
            api_base = resolved_default.api_base
            model = resolved_default.model
        else:
            provider = str(data.get("provider") or "openai_chatgpt")
            api_key = str(data.get("api_key") or "")
            api_base = str(data["api_base"]) if data.get("api_base") else None
            model = str(data["model"]) if data.get("model") else None
        memory = MemoryConfig.from_mapping(data.get("memory"))
        if memory.enabled:
            if not memory.extraction_provider_id:
                raise ValueError(
                    "memory.extraction_provider_id is required when memory.enabled is true"
                )
            cls._require_provider(
                provider_list or cls._parse_provider_list(data),
                memory.extraction_provider_id,
                field_name="memory.extraction_provider_id",
            )

        database_value = (
            data.get("database_path")
            or data.get("session_file")
            or str(get_default_database_path())
        )
        database_path = Path(database_value).expanduser()
        if not database_path.is_absolute():
            database_path = workspace / database_path

        return cls(
            provider=provider,
            api_key=api_key,
            api_base=api_base,
            model=model,
            providers=provider_list,
            default_provider=default_provider,
            workspace=workspace,
            database_path=database_path.resolve(),
            session_file=database_path.resolve(),
            max_iterations=int(data.get("max_iterations") or 8),
            history_limit=int(data.get("history_limit") or 24),
            context_window_tokens=int(data.get("context_window_tokens") or 16384),
            max_output_tokens=int(data.get("max_output_tokens") or 4096),
            context_compact_threshold_ratio=float(
                data.get("context_compact_threshold_ratio") or 0.88
            ),
            context_warn_threshold_ratio=float(
                data.get("context_warn_threshold_ratio") or 0.8
            ),
            context_safety_margin_tokens=int(
                data.get("context_safety_margin_tokens") or 1024
            ),
            context_compaction_max_rounds=int(
                data.get("context_compaction_max_rounds") or 3
            ),
            system_prompt=str(data["system_prompt"])
            if data.get("system_prompt")
            else None,
            jina_api_key=str(data.get("jina_api_key") or ""),
            request_timeout=int(data.get("request_timeout") or 120),
            gateway_host=str(data.get("gateway_host") or "127.0.0.1"),
            gateway_port=int(data.get("gateway_port") or 8787),
            gateway_url=str(data["gateway_url"]) if data.get("gateway_url") else None,
            cli_via=str(data.get("cli_via") or "local"),
            channels=ChannelsConfig.from_mapping(data.get("channels")),
            memory=memory,
        )

    @staticmethod
    def _parse_provider_list(data: dict[str, Any]) -> list[ProviderConfig]:
        raw_providers = data.get("providers")
        if not isinstance(raw_providers, list):
            return []
        providers: list[ProviderConfig] = []
        for index, item in enumerate(raw_providers, start=1):
            if not isinstance(item, dict):
                continue
            providers.append(ProviderConfig.from_mapping(item, fallback_id=index))
        return providers

    @staticmethod
    def _pick_provider(
        providers: list[ProviderConfig],
        provider_id: str | None,
    ) -> ProviderConfig:
        if provider_id:
            for provider in providers:
                if provider.id == str(provider_id):
                    return provider
        if not providers:
            raise ValueError("at least one provider must be configured")
        return providers[0]

    @staticmethod
    def _require_provider(
        providers: list[ProviderConfig],
        provider_id: str,
        *,
        field_name: str,
    ) -> ProviderConfig:
        if not providers:
            raise ValueError(
                f"{field_name} requires at least one configured provider"
            )
        for provider in providers:
            if provider.id == str(provider_id):
                return provider
        available = ", ".join(provider.id for provider in providers)
        raise ValueError(
            f"{field_name} refers to unknown provider '{provider_id}', "
            f"expected one of: {available}"
        )

    @property
    def storage_path(self) -> Path:
        """Return the resolved SQLite database path."""
        if self.database_path is not None:
            return self.database_path
        if self.session_file is not None:
            return self.session_file
        return get_default_database_path()

    def list_provider_configs(self) -> list[ProviderConfig]:
        """Return all configured providers."""
        if self.providers:
            return list(self.providers)
        return [
            ProviderConfig(
                id="1",
                preset=self.provider,
                api_key=self.api_key,
                api_base=self.api_base,
                model=self.model,
            )
        ]

    def get_provider_config(self, provider_id: str | None = None) -> ProviderConfig:
        """Resolve a configured provider by id."""
        providers = self.list_provider_configs()
        target_id = str(provider_id or self.default_provider or "1")
        for provider in providers:
            if provider.id == target_id:
                return provider
        return providers[0]


def _provider_to_mapping(provider: ProviderConfig) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "id": provider.id,
        "preset": provider.preset,
        "api_key": provider.api_key,
    }
    if provider.label:
        payload["label"] = provider.label
    if provider.api_base:
        payload["api_base"] = provider.api_base
    if provider.model:
        payload["model"] = provider.model
    if provider.context_window_tokens is not None:
        payload["context_window_tokens"] = provider.context_window_tokens
    if provider.max_output_tokens is not None:
        payload["max_output_tokens"] = provider.max_output_tokens
    return payload


def _extract_provider_payloads(payload: dict[str, Any]) -> list[dict[str, Any]]:
    raw_providers = payload.get("providers")
    if isinstance(raw_providers, list):
        return [dict(item) for item in raw_providers if isinstance(item, dict)]

    legacy_keys = ("provider", "api_key", "api_base", "model")
    if not any(payload.get(key) for key in legacy_keys):
        return []

    config = AppConfig.from_mapping(payload)
    return [_provider_to_mapping(item) for item in config.list_provider_configs()]


def load_config(
    config_path: str | Path | None,
    *,
    require_api_key: bool = True,
) -> AppConfig:
    """Load config from JSON file and environment."""
    payload: dict[str, Any] = {}
    path: Path | None = None
    if config_path:
        path = Path(config_path).expanduser().resolve()
    else:
        current_dir_path = Path("config.json")
        user_home_path = get_default_config_path()
        if current_dir_path.exists():
            path = current_dir_path.resolve()
        elif user_home_path.exists():
            path = user_home_path

    if path:
        payload = json.loads(path.read_text(encoding="utf-8"))

    env_map = {
        "provider": os.getenv("MEMNIXA_PROVIDER"),
        "api_key": os.getenv("MEMNIXA_API_KEY"),
        "api_base": os.getenv("MEMNIXA_API_BASE"),
        "model": os.getenv("MEMNIXA_MODEL"),
        "default_provider": os.getenv("MEMNIXA_DEFAULT_PROVIDER"),
        "workspace": os.getenv("MEMNIXA_WORKSPACE"),
        "database_path": os.getenv("MEMNIXA_DATABASE_PATH"),
        "session_file": os.getenv("MEMNIXA_SESSION_FILE"),
        "max_iterations": os.getenv("MEMNIXA_MAX_ITERATIONS"),
        "history_limit": os.getenv("MEMNIXA_HISTORY_LIMIT"),
        "context_window_tokens": os.getenv("MEMNIXA_CONTEXT_WINDOW_TOKENS"),
        "max_output_tokens": os.getenv("MEMNIXA_MAX_OUTPUT_TOKENS"),
        "context_compact_threshold_ratio": os.getenv(
            "MEMNIXA_CONTEXT_COMPACT_THRESHOLD_RATIO"
        ),
        "context_warn_threshold_ratio": os.getenv(
            "MEMNIXA_CONTEXT_WARN_THRESHOLD_RATIO"
        ),
        "context_safety_margin_tokens": os.getenv(
            "MEMNIXA_CONTEXT_SAFETY_MARGIN_TOKENS"
        ),
        "context_compaction_max_rounds": os.getenv(
            "MEMNIXA_CONTEXT_COMPACTION_MAX_ROUNDS"
        ),
        "system_prompt": os.getenv("MEMNIXA_SYSTEM_PROMPT"),
        "jina_api_key": os.getenv("MEMNIXA_JINA_API_KEY")
        or os.getenv("JINA_API_KEY"),
        "request_timeout": os.getenv("MEMNIXA_REQUEST_TIMEOUT"),
        "gateway_host": os.getenv("MEMNIXA_GATEWAY_HOST"),
        "gateway_port": os.getenv("MEMNIXA_GATEWAY_PORT"),
        "gateway_url": os.getenv("MEMNIXA_GATEWAY_URL"),
        "cli_via": os.getenv("MEMNIXA_CLI_VIA"),
    }
    for key, value in env_map.items():
        if value not in (None, ""):
            payload[key] = value

    config = AppConfig.from_mapping(payload)
    if require_api_key:
        default_provider = config.get_provider_config()
        if not default_provider.api_key:
            raise ValueError(
                "a default provider api_key is required. "
                "Set it in config JSON or MEMNIXA_API_KEY."
            )
    return config
