#!/usr/bin/env python3
"""YAML config loader with merge hierarchy, hardware tier detection, and SIGHUP reload."""

import copy
import os
import signal
import threading
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

import yaml

from utils.resource import get_hardware_tier

CONFIG_DIR = "config"
MERGE_ORDER = [
    "hardware_tiers.yaml",
    "bigbrother.yaml",
    "modules.yaml",
    "stealth.yaml",
]


def deep_merge(base: dict, override: dict) -> dict:
    result = copy.deepcopy(base)
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = deep_merge(result[key], value)
        else:
            result[key] = copy.deepcopy(value)
    return result


class ConfigLoader:
    def __init__(
        self,
        config_dir: str = CONFIG_DIR,
        cli_overrides: Optional[Dict[str, Any]] = None,
        auto_detect_tier: bool = True,
    ):
        self.config_dir = Path(config_dir)
        self.cli_overrides = cli_overrides or {}
        self.auto_detect_tier = auto_detect_tier
        self._config: Dict[str, Any] = {}
        self._lock = threading.Lock()
        self._reload_callbacks: List[Callable] = []
        self._loaded = False

    def load(self) -> Dict[str, Any]:
        with self._lock:
            self._config = self._build_config()
            self._loaded = True
            return copy.deepcopy(self._config)

    def _build_config(self) -> Dict[str, Any]:
        merged: Dict[str, Any] = {}

        for filename in MERGE_ORDER:
            filepath = self.config_dir / filename
            if filepath.exists():
                try:
                    with open(filepath, "r") as f:
                        data = yaml.safe_load(f) or {}
                    merged = deep_merge(merged, data)
                except (yaml.YAMLError, IOError):
                    continue

        # Apply hardware tier defaults
        if self.auto_detect_tier:
            merged = self._apply_tier_limits(merged)

        # Apply CLI overrides last (highest priority)
        if self.cli_overrides:
            merged = deep_merge(merged, self.cli_overrides)

        # Validate
        self._validate(merged)

        return merged

    def _apply_tier_limits(self, config: dict) -> dict:
        tier = config.get("device", {}).get("platform", "auto")
        if tier == "auto":
            tier = get_hardware_tier()
            config.setdefault("device", {})["platform"] = tier

        # Extract tier-specific limits from hardware_tiers section
        tiers = config.pop("hardware_tiers", {})
        tier_config = tiers.get(tier, tiers.get("generic", {}))

        if tier_config:
            config = deep_merge(config, tier_config)

        # Apply well-known tier constraints
        if tier == "pi_zero":
            config.setdefault("capture", {})["pcap_compression_level"] = 3
            config.setdefault("security", {})["encryption_key_derive"] = "pbkdf2"
            # Limit passive modules
            limits = config.setdefault("limits", {})
            limits.setdefault("max_passive_modules", 4)
            limits.setdefault("max_active_modules", 0)
        elif tier == "opi_zero3":
            config.setdefault("capture", {})["pcap_compression_level"] = 19
            config.setdefault("security", {})["encryption_key_derive"] = "argon2id"
            limits = config.setdefault("limits", {})
            limits.setdefault("max_passive_modules", 16)
            limits.setdefault("max_active_modules", 6)
        else:
            config.setdefault("capture", {})["pcap_compression_level"] = 19
            config.setdefault("security", {})["encryption_key_derive"] = "argon2id"
            limits = config.setdefault("limits", {})
            limits.setdefault("max_passive_modules", 16)
            limits.setdefault("max_active_modules", -1)  # unlimited

        return config

    def _validate(self, config: dict) -> None:
        device = config.get("device", {})
        platform = device.get("platform", "")
        if platform and platform not in ("auto", "opi_zero3", "pi_zero", "generic"):
            raise ValueError(f"Invalid platform: {platform}")

        capture = config.get("capture", {})
        comp_level = capture.get("pcap_compression_level")
        if comp_level is not None and not (1 <= comp_level <= 22):
            raise ValueError(f"Invalid compression level: {comp_level}")

        bettercap = config.get("bettercap", {})
        port = bettercap.get("api_port")
        if port is not None and not (1 <= port <= 65535):
            raise ValueError(f"Invalid bettercap API port: {port}")

        security = config.get("security", {})
        kdf = security.get("encryption_key_derive")
        if kdf and kdf not in ("argon2id", "pbkdf2"):
            raise ValueError(f"Invalid key derivation method: {kdf}")

    def get(self, key_path: str, default: Any = None) -> Any:
        with self._lock:
            keys = key_path.split(".")
            current = self._config
            for k in keys:
                if isinstance(current, dict) and k in current:
                    current = current[k]
                else:
                    return default
            return copy.deepcopy(current)

    def reload(self) -> Dict[str, Any]:
        config = self.load()
        for cb in self._reload_callbacks:
            try:
                cb(config)
            except Exception:
                pass
        return config

    def on_reload(self, callback: Callable):
        self._reload_callbacks.append(callback)

    def register_sighup(self):
        def _handler(signum, frame):
            self.reload()
        signal.signal(signal.SIGHUP, _handler)

    @property
    def config(self) -> Dict[str, Any]:
        with self._lock:
            if not self._loaded:
                return self.load()
            return copy.deepcopy(self._config)


def load_config(
    config_dir: str = CONFIG_DIR,
    cli_overrides: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    loader = ConfigLoader(config_dir=config_dir, cli_overrides=cli_overrides)
    return loader.load()
