"""
Instance configuration loader.

Loads instance.yaml from CONFIG_DIR (env var) or ./config/ fallback.
Used by both webapp and src modules for instance-specific settings.

Supports ${ENV_VAR} syntax in YAML values for secret interpolation.
Actual secret values are stored in .env (gitignored), while the YAML
structure stays in instance.yaml.
"""

import logging
import os
import re
from pathlib import Path
from typing import Any

import yaml

logger = logging.getLogger(__name__)

CONFIG_DIR = Path(os.environ.get("CONFIG_DIR", "./config"))

_ENV_PATTERN = re.compile(r"\$\{([^}]+)\}")

SUPPORTED_CONFIG_VERSIONS = {1}


def _resolve_env_refs(value: Any, _path: str = "") -> Any:
    """Resolve ${ENV_VAR} references in config values.

    Walks the config tree recursively. String values containing ${VAR}
    are replaced with the corresponding environment variable value.
    Logs a warning for unset variables so misconfiguration is visible.
    Non-string values pass through unchanged.
    """
    if isinstance(value, str):
        missing_vars: list[str] = []

        def replacer(match: re.Match) -> str:
            env_key = match.group(1)
            env_val = os.environ.get(env_key)
            if env_val is None:
                missing_vars.append(env_key)
                return ""
            return env_val

        resolved = _ENV_PATTERN.sub(replacer, value)
        for var in missing_vars:
            logger.warning(
                "Environment variable %s not set (referenced in config %s)",
                var,
                _path or "value",
            )
        return resolved
    if isinstance(value, dict):
        return {
            k: _resolve_env_refs(v, _path=f"{_path}.{k}" if _path else k)
            for k, v in value.items()
        }
    if isinstance(value, list):
        return [
            _resolve_env_refs(item, _path=f"{_path}[{i}]")
            for i, item in enumerate(value)
        ]
    return value


def _validate_config_version(config: dict) -> None:
    """Validate config_version field in the loaded config.

    Reads config_version from the config dict. If missing, logs a warning
    and defaults to 0. If the version is not in SUPPORTED_CONFIG_VERSIONS,
    logs a warning but does NOT raise — existing deployments without the
    field must not crash on upgrade. The operator is nudged to add the
    field via the warning message.
    """
    version = config.get("config_version")
    if version is None:
        logger.warning(
            "config_version not set in instance.yaml; defaulting to 0. "
            "Add config_version: 1 to your config for forward compatibility."
        )
        version = 0
    if version not in SUPPORTED_CONFIG_VERSIONS:
        logger.warning(
            "Unsupported config_version: %s. Supported versions: %s. "
            "Update your instance.yaml config_version field.",
            version,
            sorted(SUPPORTED_CONFIG_VERSIONS),
        )


def load_instance_config() -> dict[str, Any]:
    """Load instance configuration from instance.yaml.

    Search order:
    1. $CONFIG_DIR/instance.yaml
    2. ./config/instance.yaml

    Raises:
        FileNotFoundError: If instance.yaml not found.
        yaml.YAMLError: If YAML is invalid.
        ValueError: If config is empty or missing required fields.
    """
    path = CONFIG_DIR / "instance.yaml"
    if not path.exists():
        # Fallback to local config dir
        path = Path("./config/instance.yaml")

    if not path.exists():
        raise FileNotFoundError(
            "Instance configuration not found. "
            "Copy config/instance.yaml.example to config/instance.yaml "
            "and fill in your values."
        )

    with open(path) as f:
        config = yaml.safe_load(f)

    if not config:
        raise ValueError("instance.yaml is empty")

    config = _resolve_env_refs(config)
    _validate_config_version(config)
    _validate_config(config)
    logger.info("Instance config loaded from %s", path)
    return config


def _validate_config(config: dict) -> None:
    """Validate required configuration fields.

    Raises:
        ValueError: If required fields are missing or empty.
    """
    required_paths = [
        ("instance", "name"),
        ("auth", "allowed_domain"),
        ("server", "host"),
        ("server", "hostname"),
    ]

    # Secret fields that must resolve to non-empty values (from .env)
    required_secrets = [
        ("auth", "webapp_secret_key"),
    ]

    missing = []
    for keys in required_paths + required_secrets:
        value = config
        path_str = ".".join(keys)
        for key in keys:
            if not isinstance(value, dict) or key not in value:
                missing.append(path_str)
                break
            value = value[key]
        else:
            if not value:
                missing.append(path_str)

    if missing:
        raise ValueError(
            f"Missing required instance config fields: {', '.join(missing)}. "
            f"Check config/instance.yaml and .env"
        )


def get_instance_value(config: dict, *keys: str, default: Any = None) -> Any:
    """Get a nested value from instance config.

    Args:
        config: Instance config dict.
        *keys: Path of keys (e.g., "instance", "name").
        default: Default value if path not found.

    Returns:
        Config value or default.
    """
    value = config
    for key in keys:
        if not isinstance(value, dict) or key not in value:
            return default
        value = value[key]
    return value if value is not None else default
