"""YAML loading and environment expansion for edge-AI configuration."""

from __future__ import annotations

import os
import re
from collections.abc import Mapping
from pathlib import Path
from typing import Any

import yaml  # type: ignore[import-untyped]

from .models import AppConfig


_ENV_EXACT_PATTERN = re.compile(r"^env://(?P<name>[A-Za-z_][A-Za-z0-9_]*)$")
_ENV_TEMPLATE_PATTERN = re.compile(
    r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)" r"(?::-(?P<default>[^}]*))?\}"
)


class ConfigLoadError(ValueError):
    """Raised when a configuration document cannot be read or expanded."""


class MissingEnvironmentVariable(ConfigLoadError):
    """Raised when a required environment variable is not available."""


def expand_environment(
    value: Any,
    environ: Mapping[str, str] | None = None,
) -> Any:
    """Recursively expand environment references in a YAML-compatible value.

    Supported string forms are:

    * ``${NAME}`` -- ``NAME`` is required;
    * ``${NAME:-default}`` -- use ``default`` when ``NAME`` is unset or empty;
    * ``env://NAME`` -- resolve the entire value from required ``NAME``.

    Expansion traverses mappings, lists, and tuples without mutating the input.
    Environment substitution deliberately returns strings; Pydantic performs
    the final conversion according to the target configuration field.
    """

    environment = os.environ if environ is None else environ

    if isinstance(value, str):
        return _expand_string(value, environment)
    if isinstance(value, Mapping):
        return {
            key: expand_environment(item, environment) for key, item in value.items()
        }
    if isinstance(value, list):
        return [expand_environment(item, environment) for item in value]
    if isinstance(value, tuple):
        return tuple(expand_environment(item, environment) for item in value)
    return value


def load_config_data(
    data: Mapping[str, Any],
    *,
    environ: Mapping[str, str] | None = None,
) -> AppConfig:
    """Expand and validate an already parsed configuration mapping."""

    if not isinstance(data, Mapping):
        raise ConfigLoadError("configuration root must be a YAML mapping")
    expanded = expand_environment(data, environ)
    return AppConfig.model_validate(expanded)


def load_config_text(
    text: str,
    *,
    environ: Mapping[str, str] | None = None,
) -> AppConfig:
    """Parse, expand, and validate a YAML configuration string."""

    try:
        data = yaml.safe_load(text)
    except yaml.YAMLError as exc:
        raise ConfigLoadError(f"invalid YAML configuration: {exc}") from exc

    if not isinstance(data, Mapping):
        raise ConfigLoadError("configuration root must be a YAML mapping")
    return load_config_data(data, environ=environ)


def load_config(
    path: str | os.PathLike[str],
    *,
    environ: Mapping[str, str] | None = None,
) -> AppConfig:
    """Load, expand, and validate an edge-AI YAML configuration file."""

    config_path = Path(path)
    try:
        text = config_path.read_text(encoding="utf-8")
    except OSError as exc:
        raise ConfigLoadError(
            f"unable to read configuration file {config_path}: {exc}"
        ) from exc

    try:
        return load_config_text(text, environ=environ)
    except ConfigLoadError as exc:
        raise ConfigLoadError(f"failed to load {config_path}: {exc}") from exc


def _expand_string(value: str, environ: Mapping[str, str]) -> str:
    exact_match = _ENV_EXACT_PATTERN.fullmatch(value)
    if exact_match:
        name = exact_match.group("name")
        if name not in environ:
            raise MissingEnvironmentVariable(
                f"required environment variable {name!r} is not set"
            )
        return environ[name]

    def replace(match: re.Match[str]) -> str:
        name = match.group("name")
        default = match.group("default")
        current = environ.get(name)
        if current is not None and (current != "" or default is None):
            return current
        if default is not None:
            return default
        raise MissingEnvironmentVariable(
            f"required environment variable {name!r} is not set"
        )

    return _ENV_TEMPLATE_PATTERN.sub(replace, value)


__all__ = [
    "ConfigLoadError",
    "MissingEnvironmentVariable",
    "expand_environment",
    "load_config",
    "load_config_data",
    "load_config_text",
]
