"""Configuration loading with YAML + ENV + defaults."""

import os
from copy import deepcopy
from pathlib import Path

import yaml

DEFAULTS = {
    "data_dir": "~/.kb",
    "embedding": {
        "model": "all-MiniLM-L6-v2",
        "query_prefix": "",
        "passage_prefix": "",
    },
    "search": {
        "default_top": 10,
        "default_format": "json",
        "rrf_k": 60,
    },
    "chunking": {
        "defaults": {
            "max_tokens": 512,
            "overlap_tokens": 50,
        },
        "pdf": {
            "strategy": "hierarchy",
            "max_tokens": 1024,
        },
        "markdown": {
            "strategy": "header",
            "min_tokens": 50,
            "max_tokens": 1024,
        },
        "code": {
            "strategy": "ast",
            "include_context": True,
            "max_tokens": 1024,
        },
        "note": {
            "strategy": "whole",
        },
    },
    "ingestion": {
        "workers": 4,
        "batch_size": 50,
        "enable_ocr": "auto",
    },
}

# ENV variable mapping: ENV_NAME -> config dotted key
ENV_MAP = {
    "KB_DATA_DIR": "data_dir",
    "KB_MODEL": "embedding.model",
    "KB_DEFAULT_TOP": "search.default_top",
    "KB_DEFAULT_FORMAT": "search.default_format",
}

# Type coercions for ENV values
ENV_TYPES = {
    "search.default_top": int,
}


def _deep_merge(base: dict, override: dict) -> dict:
    """Deep merge override into base, returning a new dict."""
    result = 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] = deepcopy(value)
    return result


def _set_nested(d: dict, dotted_key: str, value):
    """Set a value in a nested dict using a dotted key path."""
    keys = dotted_key.split(".")
    for key in keys[:-1]:
        d = d.setdefault(key, {})
    d[keys[-1]] = value


def _get_nested(d: dict, dotted_key: str, default=None):
    """Get a value from a nested dict using a dotted key path."""
    keys = dotted_key.split(".")
    for key in keys:
        if not isinstance(d, dict) or key not in d:
            return default
        d = d[key]
    return d


def get_data_dir(cfg: dict) -> Path:
    """Resolve the data directory from config."""
    return Path(cfg["data_dir"]).expanduser()


def get_config_path(cfg: dict) -> Path:
    """Path to the YAML config file."""
    return get_data_dir(cfg) / "config.yaml"


def get_db_path(cfg: dict) -> Path:
    """Path to the SQLite database."""
    return get_data_dir(cfg) / "kb.db"


def load_config(config_path: Path | None = None) -> dict:
    """Load config with precedence: ENV > YAML > defaults.

    CLI flags are applied by the caller after this returns.
    """
    cfg = deepcopy(DEFAULTS)

    # Determine config file path (ENV can override data_dir which affects path)
    if config_path is None:
        data_dir = os.environ.get("KB_DATA_DIR", DEFAULTS["data_dir"])
        config_path = Path(data_dir).expanduser() / "config.yaml"

    # Load YAML if it exists
    if config_path.is_file():
        with open(config_path) as f:
            yaml_cfg = yaml.safe_load(f) or {}
        cfg = _deep_merge(cfg, yaml_cfg)

    # Apply ENV overrides
    for env_name, dotted_key in ENV_MAP.items():
        env_val = os.environ.get(env_name)
        if env_val is not None:
            coerce = ENV_TYPES.get(dotted_key, str)
            _set_nested(cfg, dotted_key, coerce(env_val))

    return cfg


def save_config_value(config_path: Path, dotted_key: str, value: str):
    """Set a single value in the YAML config file."""
    config_path.parent.mkdir(parents=True, exist_ok=True)

    existing = {}
    if config_path.is_file():
        with open(config_path) as f:
            existing = yaml.safe_load(f) or {}

    # Try numeric coercion
    try:
        value = int(value)
    except ValueError:
        try:
            value = float(value)
        except ValueError:
            if value.lower() in ("true", "false"):
                value = value.lower() == "true"

    _set_nested(existing, dotted_key, value)

    with open(config_path, "w") as f:
        yaml.dump(existing, f, default_flow_style=False, sort_keys=False)


def config_with_sources(config_path: Path | None = None) -> list[tuple[str, str, str]]:
    """Return a flat list of (dotted_key, value, source) tuples for display."""
    if config_path is None:
        data_dir = os.environ.get("KB_DATA_DIR", DEFAULTS["data_dir"])
        config_path = Path(data_dir).expanduser() / "config.yaml"

    yaml_cfg = {}
    if config_path.is_file():
        with open(config_path) as f:
            yaml_cfg = yaml.safe_load(f) or {}

    # Build reverse ENV map for source detection
    env_keys = {v: k for k, v in ENV_MAP.items()}

    def _flatten(d, prefix=""):
        items = []
        for k, v in d.items():
            key = f"{prefix}.{k}" if prefix else k
            if isinstance(v, dict):
                items.extend(_flatten(v, key))
            else:
                # Determine source
                env_name = env_keys.get(key)
                if env_name and os.environ.get(env_name) is not None:
                    source = f"env ({env_name})"
                elif _get_nested(yaml_cfg, key) is not None:
                    source = "config.yaml"
                else:
                    source = "default"
                items.append((key, str(v), source))
        return items

    cfg = load_config(config_path)
    return _flatten(cfg)
