"""
Phase 49: Centralized Configuration Management.

Platform-wide configuration with dot-notation keys, validation, change history,
environment overrides, and export/import.

Features:
  - GET/SET: Thread-safe get/set with dot-notation keys
  - SECTIONS: Group keys by prefix (rate_limit.*, auth.*, etc.)
  - DEFAULTS: Built-in defaults used when no explicit value is set
  - VALIDATION: Register validators that run before set() writes
  - HISTORY: Every set() logs old/new value, actor, timestamp
  - ENV OVERRIDE: Load from BRYNQ_* environment variables
  - EXPORT/IMPORT: Full config round-trip as dict

Storage:
  state/broker/config/platform.json    — current configuration
  state/broker/config/history.jsonl    — change log
"""

import json
import os
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Optional

import builtins as _builtins

from brynq.server import BROKER_DIR, _write_json_sync, _append_jsonl_sync

# Preserve the built-in ``set`` type — our module-level ``set()`` function
# shadows it, so internal code uses ``_settype`` for the collection.
_settype = _builtins.set

# ── Paths ──────────────────────────────────────────────────────────────────

CONFIG_DIR = BROKER_DIR / "config"
CONFIG_FILE = CONFIG_DIR / "platform.json"
HISTORY_FILE = CONFIG_DIR / "history.jsonl"

CONFIG_DIR.mkdir(parents=True, exist_ok=True)

# ── Thread Safety ─────────────────────────────────────────────────────────

_lock = threading.Lock()

# ── Internal State ────────────────────────────────────────────────────────

_defaults: dict[str, dict] = {}      # key -> {"value": ..., "description": ...}
_validators: dict[str, Callable] = {}  # key -> validator_fn(value) -> bool

# ── Built-in Defaults ────────────────────────────────────────────────────

_BUILTIN_DEFAULTS = {
    "rate_limit.max_per_minute": {
        "value": 100,
        "description": "Maximum API requests per minute",
    },
    "auth.session_ttl_hours": {
        "value": 168,
        "description": "Session time-to-live in hours (default 7 days)",
    },
    "platform.name": {
        "value": "Brynq",
        "description": "Platform display name",
    },
    "platform.version": {
        "value": "1.0",
        "description": "Platform version string",
    },
    "marketplace.platform_fee_pct": {
        "value": 15,
        "description": "Marketplace platform fee percentage (0-100)",
    },
    "sandbox.default_trust_level": {
        "value": "untrusted",
        "description": "Default trust level for new sandbox environments",
    },
}


# ── Built-in Validators ──────────────────────────────────────────────────


def _validate_positive_int(value: Any) -> bool:
    """Value must be a positive integer."""
    return isinstance(value, int) and value > 0


def _validate_fee_pct(value: Any) -> bool:
    """Value must be a number between 0 and 100 inclusive."""
    return isinstance(value, (int, float)) and 0 <= value <= 100


_BUILTIN_VALIDATORS = {
    "rate_limit.max_per_minute": _validate_positive_int,
    "auth.session_ttl_hours": _validate_positive_int,
    "marketplace.platform_fee_pct": _validate_fee_pct,
}


# ── Initialisation ───────────────────────────────────────────────────────


def _init() -> None:
    """Register built-in defaults and validators (idempotent)."""
    for key, meta in _BUILTIN_DEFAULTS.items():
        if key not in _defaults:
            _defaults[key] = meta
    for key, fn in _BUILTIN_VALIDATORS.items():
        if key not in _validators:
            _validators[key] = fn


_init()


# ── Persistence Helpers ───────────────────────────────────────────────────


def _load_config() -> dict:
    """Load current config from disk.  Returns {} on missing/corrupt file."""
    try:
        if CONFIG_FILE.exists():
            return json.loads(CONFIG_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return {}


def _save_config(data: dict) -> None:
    """Persist config dict to disk."""
    _write_json_sync(CONFIG_FILE, data)


def _load_history(limit: int = 0) -> list[dict]:
    """Load change history from JSONL.  Optional *limit* returns last N."""
    if not HISTORY_FILE.exists():
        return []
    entries: list[dict] = []
    for line in HISTORY_FILE.read_text("utf-8").strip().splitlines():
        try:
            entries.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    if limit > 0:
        entries = entries[-limit:]
    return entries


# ── Public API — Get / Set ───────────────────────────────────────────────


def get(key: str, default: Any = None) -> Any:
    """Return the value for *key*.

    Resolution order:
      1. Explicit value in platform.json
      2. Registered default
      3. Caller-supplied *default*
    """
    with _lock:
        cfg = _load_config()
        if key in cfg:
            return cfg[key]
        if key in _defaults:
            return _defaults[key]["value"]
        return default


def set(key: str, value: Any, description: str = "",  # noqa: A001
        changed_by: str = "system") -> dict:
    """Set *key* to *value*.  Validates, persists, and logs the change.

    Returns a result dict with ``ok`` or ``error``.
    """
    with _lock:
        # Validation
        validator = _validators.get(key)
        if validator and not validator(value):
            return {"error": f"Validation failed for key '{key}'", "key": key, "value": value}

        cfg = _load_config()
        old_value = cfg.get(key)
        cfg[key] = value
        _save_config(cfg)

        # History entry
        entry = {
            "key": key,
            "old_value": old_value,
            "new_value": value,
            "changed_by": changed_by,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "description": description,
        }
        _append_jsonl_sync(HISTORY_FILE, entry)

        return {"ok": True, "key": key, "value": value, "old_value": old_value}


# ── Public API — Sections ────────────────────────────────────────────────


def get_section(section: str) -> dict:
    """Return all keys whose dot-notation prefix matches *section*.

    For key ``rate_limit.max_per_minute`` the section is ``rate_limit``.
    Result merges defaults with explicit values (explicit wins).
    """
    prefix = section + "."
    result: dict[str, Any] = {}

    # Defaults first
    for key, meta in _defaults.items():
        if key.startswith(prefix):
            result[key] = meta["value"]

    # Explicit values override
    cfg = _load_config()
    for key, value in cfg.items():
        if key.startswith(prefix):
            result[key] = value

    return result


def list_sections() -> list[str]:
    """Return sorted, deduplicated list of section prefixes."""
    keys = _settype()
    for key in _defaults:
        if "." in key:
            keys.add(key.rsplit(".", 1)[0])
    cfg = _load_config()
    for key in cfg:
        if "." in key:
            keys.add(key.rsplit(".", 1)[0])
    return sorted(keys)


# ── Public API — Defaults ────────────────────────────────────────────────


def register_default(key: str, value: Any, description: str = "") -> dict:
    """Register a default value for *key*.

    Defaults are returned by ``get()`` when no explicit value is set.
    """
    with _lock:
        _defaults[key] = {"value": value, "description": description}
        return {"ok": True, "key": key, "value": value, "description": description}


def get_all_defaults() -> dict:
    """Return a copy of the full defaults registry."""
    return {k: dict(v) for k, v in _defaults.items()}


# ── Public API — Validation ──────────────────────────────────────────────


def register_validator(key: str, validator_fn: Callable[[Any], bool]) -> dict:
    """Attach a validation function to *key*.

    The function receives the proposed value and must return ``True`` to
    allow the write.
    """
    with _lock:
        _validators[key] = validator_fn
        return {"ok": True, "key": key}


# ── Public API — Change History ──────────────────────────────────────────


def get_history(key: Optional[str] = None, limit: int = 20) -> list[dict]:
    """Return change history, optionally filtered by *key*.

    Returns the most recent *limit* entries (default 20).
    """
    entries = _load_history()
    if key:
        entries = [e for e in entries if e.get("key") == key]
    if limit > 0:
        entries = entries[-limit:]
    return entries


# ── Public API — Environment Override ────────────────────────────────────


def load_from_env(prefix: str = "BRYNQ_") -> dict:
    """Read environment variables starting with *prefix* and apply them.

    Translation: ``BRYNQ_RATE_LIMIT_MAX_PER_MINUTE=200`` becomes
    ``rate_limit.max_per_minute = 200``.

    Returns a summary of applied overrides.
    """
    applied: list[dict] = []
    errors: list[dict] = []

    for env_key, env_value in os.environ.items():
        if not env_key.startswith(prefix):
            continue
        # Strip prefix, lowercase, replace _ with .
        # But we need to handle multi-level keys: RATE_LIMIT_MAX_PER_MINUTE
        # Strategy: find the longest matching known key prefix
        raw = env_key[len(prefix):].lower()
        config_key = _env_key_to_config_key(raw)
        parsed_value = _parse_env_value(env_value)
        result = set(config_key, parsed_value, description=f"env override from {env_key}",
                     changed_by="env")
        if "error" in result:
            errors.append({"env_key": env_key, "error": result["error"]})
        else:
            applied.append({"env_key": env_key, "config_key": config_key, "value": parsed_value})

    return {"applied": applied, "errors": errors, "count": len(applied)}


def _env_key_to_config_key(raw: str) -> str:
    """Convert an underscore-delimited env suffix to a dot-notation config key.

    Uses known section prefixes to find the best split point.  Falls back to
    treating the first segment as the section.

    Examples:
        rate_limit_max_per_minute -> rate_limit.max_per_minute
        auth_session_ttl_hours    -> auth.session_ttl_hours
        platform_name             -> platform.name
    """
    # Collect all known section prefixes from defaults + stored config
    known_sections = _settype()
    for key in _defaults:
        if "." in key:
            known_sections.add(key.split(".")[0])
    cfg = _load_config()
    for key in cfg:
        if "." in key:
            known_sections.add(key.split(".")[0])

    # Try to match the longest known section prefix (underscore form)
    parts = raw.split("_")
    best_split = 1  # default: first segment is section
    for i in range(1, len(parts)):
        candidate_section = "_".join(parts[:i])
        if candidate_section in known_sections:
            best_split = i

    section = "_".join(parts[:best_split])
    remainder = "_".join(parts[best_split:])
    if remainder:
        return f"{section}.{remainder}"
    return section


def _parse_env_value(raw: str) -> Any:
    """Try to parse an environment value as int, float, bool, or leave as str."""
    # Try numeric first so "0" / "1" stay as ints rather than bools
    try:
        return int(raw)
    except ValueError:
        pass
    try:
        return float(raw)
    except ValueError:
        pass
    if raw.lower() in ("true", "yes"):
        return True
    if raw.lower() in ("false", "no"):
        return False
    return raw


# ── Public API — Export / Import ─────────────────────────────────────────


def export_config() -> dict:
    """Return the full effective configuration.

    Merges defaults with explicit values (explicit wins).
    """
    result: dict[str, Any] = {}
    for key, meta in _defaults.items():
        result[key] = meta["value"]
    cfg = _load_config()
    result.update(cfg)
    return result


def import_config(data: dict, merge: bool = True,
                  changed_by: str = "import") -> dict:
    """Load configuration from *data*.

    If *merge* is True (default), existing keys not in *data* are preserved.
    If *merge* is False, the config is replaced entirely.

    Returns a summary of the operation.
    """
    applied: list[str] = []
    errors: list[dict] = []

    with _lock:
        cfg = _load_config() if merge else {}

        for key, value in data.items():
            validator = _validators.get(key)
            if validator and not validator(value):
                errors.append({"key": key, "error": f"Validation failed for '{key}'"})
                continue
            old_value = cfg.get(key)
            cfg[key] = value
            applied.append(key)

            entry = {
                "key": key,
                "old_value": old_value,
                "new_value": value,
                "changed_by": changed_by,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "description": f"import (merge={merge})",
            }
            _append_jsonl_sync(HISTORY_FILE, entry)

        _save_config(cfg)

    return {"ok": True, "applied": applied, "errors": errors,
            "count": len(applied)}


# ── Reset (for testing) ──────────────────────────────────────────────────


def _reset() -> None:
    """Clear all runtime state and re-register built-ins.  For tests only."""
    global _defaults, _validators
    _defaults = {}
    _validators = {}
    _init()
