from __future__ import annotations

from pathlib import Path

import yaml

from tests.pipeline_setup_v3.core.models import (
    CaseExpectations,
    CaseInput,
    LlmExpectation,
    PipelineExpectation,
    RetrievalExpectation,
    RouterExpectation,
    V3Case,
)


class CaseDirectoryLoader:
    def load(self, cases_dir: Path) -> list[V3Case]:
        if cases_dir.is_file():
            return self._load_file(cases_dir)
        files = sorted(path for path in cases_dir.rglob("*.yaml") if path.is_file())
        if not files:
            raise ValueError(f"No YAML case files found in: {cases_dir}")
        cases: list[V3Case] = []
        for path in files:
            cases.extend(self._load_file(path))
        return cases

    def _load_file(self, path: Path) -> list[V3Case]:
        payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
        if not isinstance(payload, dict):
            raise ValueError(f"Invalid case file {path}: expected mapping")
        defaults = payload.get("defaults") or {}
        items = payload.get("cases") or []
        if not isinstance(items, list):
            raise ValueError(f"Invalid case file {path}: `cases` must be a list")
        return [self._to_case(path, raw, defaults) for raw in items]

    def _to_case(self, path: Path, raw: object, defaults: dict) -> V3Case:
        if not isinstance(raw, dict):
            raise ValueError(f"Invalid case in {path}: expected object")
        merged_input = {**dict(defaults.get("input") or {}), **dict(raw.get("input") or {})}
        runner = self._normalize_runner(str(raw.get("runner") or defaults.get("runner") or "agent_runtime").strip())
        mode = str(raw.get("mode") or defaults.get("mode") or "").strip()
        case_id = str(raw.get("id") or "").strip()
        query = str(raw.get("query") or "").strip()
        if not case_id or not query or not runner or not mode:
            raise ValueError(f"Invalid case in {path}: `id`, `query`, `runner`, `mode` are required")
        expected = dict(raw.get("expected") or {})
        return V3Case(
            case_id=case_id,
            runner=runner,  # type: ignore[arg-type]
            mode=mode,  # type: ignore[arg-type]
            query=query,
            source_file=path,
            input=self._to_input(path.parent, merged_input),
            expectations=self._to_expectations(expected),
            notes=str(raw.get("notes") or ""),
            tags=tuple(str(item).strip() for item in raw.get("tags", []) if str(item).strip()),
        )

    def _to_input(self, base_dir: Path, raw: dict) -> CaseInput:
        repo_path_raw = str(raw.get("repo_path") or "").strip()
        repo_path = None
        if repo_path_raw:
            repo_path = Path(repo_path_raw).expanduser()
            if not repo_path.is_absolute():
                repo_path = (base_dir / repo_path).resolve()
        return CaseInput(
            repo_path=repo_path,
            project_id=str(raw.get("project_id") or "").strip() or None,
            rag_session_id=str(raw.get("rag_session_id") or "").strip() or None,
        )

    def _to_expectations(self, raw: dict) -> CaseExpectations:
        router = dict(raw.get("router") or {})
        retrieval = dict(raw.get("retrieval") or {})
        llm = dict(raw.get("llm") or {})
        pipeline = dict(raw.get("pipeline") or {})
        return CaseExpectations(
            router=RouterExpectation(
                intent=str(router.get("intent") or "").strip() or None,
                sub_intent=str(router.get("sub_intent") or "").strip() or None,
                graph_id=str(router.get("graph_id") or "").strip() or None,
                conversation_mode=str(router.get("conversation_mode") or "").strip() or None,
            ),
            retrieval=RetrievalExpectation(
                non_empty=retrieval.get("non_empty"),
                min_rows=int(retrieval["min_rows"]) if retrieval.get("min_rows") is not None else None,
                direct_symbol_test_hits_max=(
                    int(retrieval["direct_symbol_test_hits_max"])
                    if retrieval.get("direct_symbol_test_hits_max") is not None
                    else None
                ),
                path_scope_contains=tuple(str(item) for item in retrieval.get("path_scope_contains") or []),
                symbol_candidates_contain=tuple(str(item) for item in retrieval.get("symbol_candidates_contain") or []),
                layers_include=tuple(str(item) for item in retrieval.get("layers_include") or []),
            ),
            llm=LlmExpectation(
                non_empty=llm.get("non_empty"),
                contains_all=tuple(str(item) for item in llm.get("contains_all") or []),
                excludes=tuple(str(item) for item in llm.get("excludes") or []),
            ),
            pipeline=PipelineExpectation(answer_mode=str(pipeline.get("answer_mode") or "").strip() or None),
        )

    def _normalize_runner(self, value: str) -> str:
        if value in {"agent_runtime", "runtime", "code_qa_eval"}:
            return "agent_runtime"
        return value
