# Copyright (C) 2026 GoodData Corporation

"""Validation policy config loaded from `.aida/validation_policy.yaml`.

This config drives the unified `validate` tool:
- routing of changed paths into domains/roots
- mapping domains to pipelines
- pipeline steps as (command_id, processor_id) pairs
- optional conditional codegen triggers (e.g. protos) and per-scope policies
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Any, cast

import yaml

from aida_mcp.utils.aida_paths import get_repo_config_root

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class CodegenTrigger:
    id: str
    match_globs: list[str]
    # Pipeline id that should run before validations when trigger matches.
    pipeline: str
    # Scopes in which this trigger is active (default-on behavior should be expressed here).
    scopes: list[str]


@dataclass(frozen=True, slots=True)
class PipelineStep:
    command_id: str
    processor_id: str
    # Optional small config passed to processor (per-step override).
    processor_config: dict[str, object]


@dataclass(frozen=True, slots=True)
class Pipeline:
    id: str
    steps: list[PipelineStep]


@dataclass(frozen=True, slots=True)
class Route:
    domain: str
    pipeline: str


@dataclass(frozen=True, slots=True)
class ValidationPolicyConfig:
    codegen: list[CodegenTrigger]
    routes: list[Route]
    pipelines: dict[str, Pipeline]


def _default_config() -> ValidationPolicyConfig:
    return ValidationPolicyConfig(codegen=[], routes=[], pipelines={})


def default_policy_path(workspace_root: Path) -> Path:
    return get_repo_config_root(workspace_root) / "validation_policy.yaml"


def load_validation_policy(workspace_root: Path, *, config_path: Path | None = None) -> ValidationPolicyConfig:
    """Load validation policy with safe defaults.

    This loader is intentionally defensive: config errors should not crash the server startup.
    """
    cfg = _default_config()
    path = config_path or default_policy_path(workspace_root)
    if not path.exists():
        logger.info("validation_policy.yaml not found at %s; using empty defaults", path)
        return cfg

    try:
        raw_text = path.read_text(encoding="utf-8")
        loaded: Any = yaml.safe_load(raw_text)
    except Exception as exc:
        logger.warning("Failed to read validation policy at %s; using defaults (%s)", path, exc)
        return cfg

    if not isinstance(loaded, dict):
        logger.warning("validation policy at %s is not a mapping; using defaults", path)
        return cfg

    root = cast(dict[str, Any], loaded)
    policy_any: Any = root.get("validation_policy")
    if not isinstance(policy_any, dict):
        logger.warning("validation policy at %s missing 'validation_policy' root; using defaults", path)
        return cfg

    data = cast(dict[str, Any], policy_any)

    def _string_list(v: Any) -> list[str]:
        if not isinstance(v, list):
            return []
        items = cast(list[object], v)
        if not all(isinstance(x, str) for x in items):
            return []
        return cast(list[str], items)

    # codegen triggers
    codegen: list[CodegenTrigger] = []
    codegen_any: Any = data.get("codegen", [])
    if isinstance(codegen_any, list):
        for entry_any in cast(list[object], codegen_any):
            if not isinstance(entry_any, dict):
                continue
            entry = cast(dict[str, Any], entry_any)
            id_any: Any = entry.get("id")
            pipeline_any: Any = entry.get("pipeline")
            globs = _string_list(entry.get("match_globs"))
            scopes = _string_list(entry.get("scopes"))
            if not isinstance(id_any, str) or not id_any.strip():
                continue
            if not isinstance(pipeline_any, str) or not pipeline_any.strip():
                continue
            if not globs:
                continue
            codegen.append(
                CodegenTrigger(
                    id=id_any.strip(),
                    match_globs=globs,
                    pipeline=pipeline_any.strip(),
                    scopes=scopes or ["pre_push"],
                )
            )

    # routes
    routes: list[Route] = []
    routes_any: Any = data.get("routes", [])
    if isinstance(routes_any, list):
        for r_any in cast(list[object], routes_any):
            if not isinstance(r_any, dict):
                continue
            r = cast(dict[str, Any], r_any)
            domain_any: Any = r.get("domain")
            pipeline_any = r.get("pipeline")
            if (
                isinstance(domain_any, str)
                and domain_any.strip()
                and isinstance(pipeline_any, str)
                and pipeline_any.strip()
            ):
                routes.append(Route(domain=domain_any.strip(), pipeline=pipeline_any.strip()))

    # pipelines
    pipelines: dict[str, Pipeline] = {}
    pipelines_any: Any = data.get("pipelines", {})
    if isinstance(pipelines_any, dict):
        pmap = cast(dict[object, object], pipelines_any)
        for pid_obj, pval in pmap.items():
            pid = str(pid_obj).strip()
            if not pid or not isinstance(pval, dict):
                continue
            p_raw = cast(dict[str, Any], {str(k): v for k, v in cast(dict[object, object], pval).items()})
            steps_any = p_raw.get("steps", [])
            if not isinstance(steps_any, list):
                continue
            steps: list[PipelineStep] = []
            for s_any in cast(list[object], steps_any):
                if not isinstance(s_any, dict):
                    continue
                s_raw = cast(dict[str, Any], s_any)
                cmd_any: Any = s_raw.get("command_id")
                proc_any: Any = s_raw.get("processor_id")
                if not (
                    isinstance(cmd_any, str) and cmd_any.strip() and isinstance(proc_any, str) and proc_any.strip()
                ):
                    continue
                pc_any: Any = s_raw.get("processor_config", {})
                processor_config: dict[str, object] = {}
                if isinstance(pc_any, dict):
                    processor_config = {str(k): v for k, v in cast(dict[object, object], pc_any).items()}
                steps.append(
                    PipelineStep(
                        command_id=cmd_any.strip(),
                        processor_id=proc_any.strip(),
                        processor_config=processor_config,
                    )
                )
            pipelines[pid] = Pipeline(id=pid, steps=steps)

    return ValidationPolicyConfig(codegen=codegen, routes=routes, pipelines=pipelines)
