"""provider_admin.py — LLM provider CRUD operations.

Extracted from gui_backend.py to ensure PyInstaller bundles the code.
gui_backend.py (entry point) delegates to these functions.

Concurrency: uses a module-level _CONFIG_LOCK (threading.Lock) to protect
all read-modify-write cycles on config files. All writes use atomic
tmp+replace to prevent corruption from concurrent processes.
"""
from __future__ import annotations

import json
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any

import config_manager

_logger = logging.getLogger("provider_admin")
CONFIG_DIR = config_manager.get_config_dir()

# File-level lock for in-process concurrency (multiple threads/request handlers)
_CONFIG_LOCK = threading.Lock()


def _safe_load_json(path: Path, fallback_corrupt: bool = False) -> dict | None:
    """Load JSON with corruption detection. Falls back to .bak on parse error."""
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as e:
        _logger.error("Corrupted config file: %s — %s", path, e)
        # Try .bak recovery
        bak = path.with_suffix(path.suffix + ".bak")
        if bak.exists():
            try:
                _logger.warning("Recovering from backup: %s", bak)
                return json.loads(bak.read_text(encoding="utf-8"))
            except Exception:
                pass
        # Rename corrupted file out of the way
        try:
            corrupt = path.with_suffix(path.suffix + f".corrupted_{int(time.time())}")
            path.rename(corrupt)
            _logger.warning("Renamed corrupted file to %s", corrupt)
        except Exception:
            pass
        return {} if fallback_corrupt else None


def _log_audit(action: str, target: str, detail: str = "") -> None:
    """Log provider CRUD operations for audit trail."""
    _logger.info("[AUDIT] %s | target=%s | detail=%s", action, target, detail[:200])


def _load_custom_providers() -> dict:
    path = CONFIG_DIR / "custom_providers.json"
    result = _safe_load_json(path, fallback_corrupt=True)
    return result if result else {"providers": {}}


def _save_custom_providers(data: dict) -> None:
    path = CONFIG_DIR / "custom_providers.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    # Create .bak before writing
    if path.exists():
        try:
            bak = path.with_suffix(path.suffix + ".bak")
            bak.write_bytes(path.read_bytes())
        except Exception:
            pass
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
    os.replace(tmp, path)  # atomic on Windows (Python 3.3+)


def _write_json_atomic(path: Path, payload: dict) -> Path:
    """Atomic write with .bak backup and corruption prevention."""
    # Create .bak before writing
    if path.exists():
        try:
            bak = path.with_suffix(path.suffix + ".bak")
            bak.write_bytes(path.read_bytes())
        except Exception:
            pass
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
    tmp.replace(path)
    return path


def _mask_api_key(key: str) -> str:
    if not key:
        return ""
    if len(key) <= 8:
        return key[:4] + "****"
    return key[:4] + "****" + key[-4:]


def _resolve_openclaw_path() -> Path | None:
    """Return path to openclaw.json if it exists (legacy backward compat only)."""
    try:
        p = config_manager.get_llm_config_dir() / "openclaw.json"
        if p.exists():
            return p
    except Exception:
        pass
    home_openclaw = Path.home() / ".openclaw" / "openclaw.json"
    if home_openclaw.exists():
        return home_openclaw
    return None


def list_providers() -> dict:
    """Return all providers with system + custom merged, no duplicates."""
    providers: dict[str, dict] = {}

    # System providers: llm_providers.json (authoritative)
    llm_path = CONFIG_DIR / "llm_providers.json"
    if llm_path.exists():
        try:
            llm_data = _safe_load_json(llm_path, fallback_corrupt=True)
            if llm_data:
                llm_prov = llm_data.get("providers", {})
                for name, info in llm_prov.items():
                    if isinstance(info, dict):
                        providers[name] = {**info, "_source": "llm_providers"}
        except Exception:
            pass

    # Legacy system providers: openclaw.json (fallback only — don't override llm_providers)
    oc_path = _resolve_openclaw_path()
    if oc_path:
        try:
            cfg = json.loads(oc_path.read_text(encoding="utf-8"))
            sys_prov = cfg.get("models", {}).get("providers", {})
            for name, info in sys_prov.items():
                if isinstance(info, dict) and name not in providers:
                    providers[name] = {**info, "_source": "openclaw"}
        except Exception:
            pass

    # Custom overrides: merge on top
    custom_data = _load_custom_providers()
    custom_prov = custom_data.get("providers", {})
    for name, info in custom_prov.items():
        if isinstance(info, dict):
            if name in providers:
                for k, v in info.items():
                    if v:
                        providers[name][k] = v
                providers[name]["_has_override"] = True
            else:
                providers[name] = {**info, "_source": "custom"}

    result = []
    for name, info in providers.items():
        result.append({
            "name": name,
            "baseUrl": info.get("baseUrl", ""),
            "api": info.get("api", ""),
            "apiKey": _mask_api_key(info.get("apiKey", "")),
            "hasKey": bool(info.get("apiKey", "")),
            "models": info.get("models", []),
            "custom": info.get("_source") == "custom",
            "hasOverride": info.get("_has_override", False),
        })
    return {"success": True, "providers": result}


def save_provider(name: str, base_url: str, api: str, api_key: str, models: list) -> dict:
    """Save a provider. System providers updated in-place, no custom override.

    Thread-safe: acquires _CONFIG_LOCK before any file I/O.
    """
    if not name or not base_url or not api:
        return {"success": False, "error": "name, baseUrl, api required"}
    if api not in ("openai-responses", "openai-completions", "anthropic-messages"):
        return {"success": False, "error": f"Unsupported API: {api}"}

    url = base_url.rstrip("/")
    if api in ("openai-responses", "openai-completions"):
        if not url.endswith("/v1"):
            url = url + "/v1"

    with _CONFIG_LOCK:
        is_system = False

        # Check llm_providers.json first
        llm_path = CONFIG_DIR / "llm_providers.json"
        if llm_path.exists():
            try:
                cfg = json.loads(llm_path.read_text(encoding="utf-8"))
                if name in cfg.get("providers", {}):
                    is_system = True
                    cfg["providers"][name]["apiKey"] = api_key
                    cfg["providers"][name]["baseUrl"] = url
                    if models:
                        cfg["providers"][name]["models"] = models
                    _write_json_atomic(llm_path, cfg)
                    _log_audit("SAVE", name, f"system provider, api={api}, url={url}")
            except Exception as e:
                _logger.error("Failed to save system provider '%s': %s", name, e)

        # Check openclaw.json (legacy)
        if not is_system:
            oc_path = _resolve_openclaw_path()
            if oc_path:
                try:
                    cfg = json.loads(oc_path.read_text(encoding="utf-8"))
                    if name in cfg.get("models", {}).get("providers", {}):
                        is_system = True
                        cfg["models"]["providers"][name]["apiKey"] = api_key
                        cfg["models"]["providers"][name]["baseUrl"] = url
                        if models:
                            cfg["models"]["providers"][name]["models"] = models
                        _write_json_atomic(oc_path, cfg)
                        _log_audit("SAVE", name, f"legacy openclaw provider, api={api}")
                except Exception as e:
                    _logger.error("Failed to save legacy provider '%s': %s", name, e)

        if not is_system:
            custom = _load_custom_providers()
            custom.setdefault("providers", {})[name] = {
                "baseUrl": url, "api": api, "apiKey": api_key, "models": models,
            }
            _save_custom_providers(custom)
            _log_audit("SAVE", name, f"custom provider, api={api}, url={url}")

    # After saving a provider, re-sync agent_models.json so new models appear
    # in the role model dropdown immediately.
    try:
        from src.llm import _resync_available_models
        _resync_available_models()
    except Exception:
        pass

    return {"success": True, "message": f"供应商 '{name}' 已保存"}


def delete_provider(name: str) -> dict:
    """Delete a provider from all config sources.

    Thread-safe: acquires _CONFIG_LOCK before any file I/O.
    """
    deleted_from = []

    with _CONFIG_LOCK:
        # custom_providers.json
        custom = _load_custom_providers()
        if name in custom.get("providers", {}):
            del custom["providers"][name]
            _save_custom_providers(custom)
            deleted_from.append("custom_providers")

        # llm_providers.json
        llm_path = CONFIG_DIR / "llm_providers.json"
        if llm_path.exists():
            try:
                cfg = json.loads(llm_path.read_text(encoding="utf-8"))
                if name in cfg.get("providers", {}):
                    cfg["providers"][name]["apiKey"] = ""
                    _write_json_atomic(llm_path, cfg)
                    deleted_from.append("llm_providers")
            except Exception as e:
                _logger.error("Failed to delete provider '%s': %s", name, e)

        # openclaw.json — clear apiKey, don't delete system provider
        oc_path = _resolve_openclaw_path()
        if oc_path:
            try:
                cfg = json.loads(oc_path.read_text(encoding="utf-8"))
                if name in cfg.get("models", {}).get("providers", {}):
                    cfg["models"]["providers"][name]["apiKey"] = ""
                    _write_json_atomic(oc_path, cfg)
                    deleted_from.append("openclaw")
            except Exception as e:
                _logger.error("Failed to delete legacy provider '%s': %s", name, e)

    if deleted_from:
        _log_audit("DELETE", name, f"cleared from: {', '.join(deleted_from)}")
        return {"success": True, "message": f"已清除 '{name}' (来源: {', '.join(deleted_from)})"}
    return {"success": False, "error": f"供应商 '{name}' 不存在"}


def _load_all_providers_merged() -> dict[str, dict]:
    """Load merged providers from all sources — used for validation lookup."""
    all_providers: dict = {}

    # llm_providers.json (authoritative)
    llm_data = _safe_load_json(CONFIG_DIR / "llm_providers.json", fallback_corrupt=True)
    if llm_data:
        all_providers.update(llm_data.get("providers", {}))

    # openclaw.json (legacy fallback, don't override llm_providers)
    oc_path = _resolve_openclaw_path()
    if oc_path:
        try:
            cfg = json.loads(oc_path.read_text(encoding="utf-8"))
            for name, info in cfg.get("models", {}).get("providers", {}).items():
                if name not in all_providers:
                    all_providers[name] = info
        except Exception:
            pass

    # custom_providers.json (overrides)
    custom = _load_custom_providers()
    for name, info in custom.get("providers", {}).items():
        if name in all_providers:
            existing = all_providers[name]
            if isinstance(existing, dict):
                existing.update({k: v for k, v in info.items() if v})
        else:
            all_providers[name] = info

    return all_providers


def read_allowed_models() -> list[str]:
    """Build the list of allowed model IDs from all config sources."""
    all_providers = _load_all_providers_merged()

    model_ids: list[str] = []
    seen: set[str] = set()
    for provider_name, info in all_providers.items():
        if not isinstance(info, dict):
            continue
        for model in info.get("models", []) or []:
            raw = str(model.get("id", "") or "").strip()
            if not raw:
                continue
            full = f"{provider_name}/{raw}" if "/" not in raw else raw
            if full not in seen:
                seen.add(full)
                model_ids.append(full)
    return model_ids


def save_agent_models(agents: list[dict]) -> dict:
    """Validate and save agent model bindings.

    For each agent, verifies that:
    1. The model has a valid "provider/model" format
    2. The provider exists in any config source
    3. The specific model ID is in that provider's model list

    Thread-safe: acquires _CONFIG_LOCK.
    """
    allowed = read_allowed_models()
    providers = _load_all_providers_merged()
    config_file = CONFIG_DIR / "agent_models.json"

    normalized = []
    for agent in agents:
        role = str(agent.get("role", "") or "").strip()
        model = str(agent.get("model", "") or "").strip()
        if "/" not in model:
            model = f"deepseek/{model}"
        if allowed and model not in allowed:
            parts = model.split("/", 1)
            provider_name = parts[0]
            model_id = parts[1] if len(parts) > 1 else model
            provider_models = providers.get(provider_name, {}).get("models", [])
            valid_ids = [m.get("id", "") for m in provider_models if isinstance(m, dict)]
            if valid_ids:
                return {
                    "success": False,
                    "error": f"模型 '{model_id}' 不在供应商 '{provider_name}' 的模型列表中",
                    "valid_models": [f"{provider_name}/{mid}" for mid in valid_ids],
                }
            return {
                "success": False,
                "error": f"模型 '{model}' 不在系统允许列表中（供应商不存在或模型无效）",
            }
        entry: dict[str, Any] = {"role": role, "model": model, "status": agent.get("status", "ready")}
        if agent.get("temperature") is not None:
            entry["temperature"] = agent["temperature"]
        if agent.get("max_tokens") is not None:
            entry["max_tokens"] = agent["max_tokens"]
        if agent.get("timeout_sec") is not None:
            entry["timeout_sec"] = agent["timeout_sec"]
        if agent.get("description"):
            entry["description"] = agent["description"]
        normalized.append(entry)

    config_file.parent.mkdir(parents=True, exist_ok=True)
    with _CONFIG_LOCK:
        _write_json_atomic(config_file, normalized if isinstance(normalized, list) else [normalized])
    _log_audit("AGENT_MODELS", "agent_models.json", f"{len(normalized)} roles saved")
    return {"success": True, "message": "模型配置已保存"}
