# Copyright (C) 2026 GoodData Corporation

"""Loader and cache for validation registry definitions."""

from __future__ import annotations

import logging
import os
from collections.abc import Mapping, MutableMapping
from pathlib import Path
from typing import cast

import yaml

from aida_mcp.tools.core.validation_registry_model import CommandSpec, ProcessorSpec, ValidationRegistry
from aida_mcp.tools.core.validation_registry_parsing import (
    parse_command_spec,
    parse_processor_spec,
    string_list,
    string_mapping,
)
from aida_mcp.utils.aida_paths import DOT_AIDA

logger = logging.getLogger(__name__)

ENV_VALIDATION_REGISTRY_PATH = "AIDA_VALIDATION_REGISTRY_PATH"
ENV_WORKSPACE_ROOT = "AIDA_WORKSPACE_ROOT"


def workspace_root_from_env() -> Path | None:
    override = os.environ.get(ENV_WORKSPACE_ROOT)
    if not override:
        return None
    try:
        candidate = Path(override).expanduser()
        if not candidate.is_absolute():
            candidate = (Path.cwd() / candidate).resolve()
        return candidate if candidate.exists() else None
    except Exception:
        return None


def default_registry_path() -> Path:
    env_path = os.environ.get(ENV_VALIDATION_REGISTRY_PATH)
    if isinstance(env_path, str) and env_path.strip():
        p = Path(env_path.strip()).expanduser()
        if not p.is_absolute():
            p = (Path.cwd() / p).resolve()
        return p

    ws = workspace_root_from_env()
    if ws is not None:
        cand0 = ws / DOT_AIDA / "validation_registry.yaml"
        if cand0.exists():
            return cand0

    cwd = Path.cwd().resolve()
    cand3 = cwd / DOT_AIDA / "validation_registry.yaml"
    return cand3


def load_validation_registry(config_path: Path | None = None) -> ValidationRegistry:
    if config_path is None:
        config_path = default_registry_path()
    if not config_path.exists():
        logger.info("Validation registry config not found: %s", config_path)
        return ValidationRegistry(commands={}, processors={})
    try:
        return _load_registry_with_includes(config_path)
    except Exception as exc:
        logger.warning("Failed to load validation registry from %s: %s", config_path, exc)
        return ValidationRegistry(commands={}, processors={})


def _load_registry_with_includes(config_path: Path) -> ValidationRegistry:
    seen: set[Path] = set()
    commands: MutableMapping[str, CommandSpec] = {}
    processors: MutableMapping[str, ProcessorSpec] = {}

    def _load_one(path: Path) -> None:
        resolved = path.resolve()
        if resolved in seen:
            return
        seen.add(resolved)
        try:
            raw_text = resolved.read_text(encoding="utf-8")
            loaded: object = yaml.safe_load(raw_text)
        except Exception as exc:
            logger.warning("Skipping registry file %s (read/parse error): %s", resolved, exc)
            return
        root = string_mapping(loaded)
        reg_any = root.get("registry")
        reg = string_mapping(reg_any)
        base_dir = resolved.parent
        includes = string_list(reg.get("includes"))
        for inc in includes:
            if not inc:
                continue
            inc_path = Path(inc)
            if not inc_path.is_absolute():
                inc_path = (base_dir / inc_path).resolve()
            _load_one(inc_path)
        commands_any = reg.get("commands")
        if isinstance(commands_any, Mapping):
            for cid, cval in string_mapping(cast(object, commands_any)).items():
                spec = parse_command_spec(cval)
                if spec is not None:
                    commands[cid] = spec
        processors_any = reg.get("processors")
        if isinstance(processors_any, Mapping):
            for pid, pval in string_mapping(cast(object, processors_any)).items():
                spec = parse_processor_spec(pval)
                if spec is not None:
                    processors[pid] = spec

    _load_one(config_path)
    return ValidationRegistry(commands=commands, processors=processors)


_cached_registry: ValidationRegistry | None = None
_cached_registry_path: Path | None = None
_cached_registry_mtime: float | None = None


def get_validation_registry() -> ValidationRegistry:
    global _cached_registry, _cached_registry_mtime, _cached_registry_path
    config_path = default_registry_path()
    try:
        mtime = config_path.stat().st_mtime if config_path.exists() else None
    except Exception:
        mtime = None
    if _cached_registry is None or _cached_registry_path != config_path or _cached_registry_mtime != mtime:
        _cached_registry = load_validation_registry(config_path=config_path)
        _cached_registry_path = config_path
        _cached_registry_mtime = mtime
    return _cached_registry
