from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
import networkx as nx

from . import __version__ as DIDACTOPUS_VERSION
from .artifact_schemas import ConceptsFile, PackManifest, ProjectsFile, RoadmapFile, RubricsFile

REQUIRED_FILES = ["pack.yaml", "concepts.yaml", "roadmap.yaml", "projects.yaml", "rubrics.yaml"]


def _parse_version(version: str) -> tuple[int, ...]:
    return tuple(int("".join(ch for ch in chunk if ch.isdigit()) or 0) for chunk in version.split("."))


def _version_in_range(version: str, min_version: str, max_version: str) -> bool:
    return _parse_version(min_version) <= _parse_version(version) <= _parse_version(max_version)


@dataclass
class PackValidationResult:
    pack_dir: Path
    manifest: PackManifest | None = None
    is_valid: bool = False
    errors: list[str] = field(default_factory=list)
    loaded_files: dict[str, Any] = field(default_factory=dict)


def _load_yaml(path: Path) -> dict[str, Any]:
    data = yaml.safe_load(path.read_text(encoding="utf-8"))
    if data is None:
        return {}
    if not isinstance(data, dict):
        raise ValueError(f"{path.name} must contain a YAML mapping at top level")
    return data


def validate_pack(pack_dir: str | Path) -> PackValidationResult:
    pack_path = Path(pack_dir)
    result = PackValidationResult(pack_dir=pack_path)
    for filename in REQUIRED_FILES:
        if not (pack_path / filename).exists():
            result.errors.append(f"missing required file: {filename}")
    if result.errors:
        return result
    try:
        result.manifest = PackManifest.model_validate(_load_yaml(pack_path / "pack.yaml"))
        if not _version_in_range(DIDACTOPUS_VERSION, result.manifest.didactopus_min_version, result.manifest.didactopus_max_version):
            result.errors.append(
                f"incompatible with Didactopus core version {DIDACTOPUS_VERSION}; "
                f"supported range is {result.manifest.didactopus_min_version}..{result.manifest.didactopus_max_version}"
            )
        result.loaded_files["concepts"] = ConceptsFile.model_validate(_load_yaml(pack_path / "concepts.yaml"))
        result.loaded_files["roadmap"] = RoadmapFile.model_validate(_load_yaml(pack_path / "roadmap.yaml"))
        result.loaded_files["projects"] = ProjectsFile.model_validate(_load_yaml(pack_path / "projects.yaml"))
        result.loaded_files["rubrics"] = RubricsFile.model_validate(_load_yaml(pack_path / "rubrics.yaml"))
    except Exception as exc:
        result.errors.append(str(exc))
    result.is_valid = not result.errors
    return result


def discover_domain_packs(base_dirs: list[str | Path]) -> list[PackValidationResult]:
    results = []
    for base_dir in base_dirs:
        base = Path(base_dir)
        if not base.exists():
            continue
        for pack_dir in sorted(p for p in base.iterdir() if p.is_dir()):
            results.append(validate_pack(pack_dir))
    return results


def check_pack_dependencies(results: list[PackValidationResult]) -> list[str]:
    errors = []
    manifest_by_name = {r.manifest.name: r.manifest for r in results if r.manifest is not None}
    for result in results:
        if result.manifest is None:
            continue
        for dep in result.manifest.dependencies:
            dep_manifest = manifest_by_name.get(dep.name)
            if dep_manifest is None:
                errors.append(f"pack '{result.manifest.name}' depends on missing pack '{dep.name}'")
                continue
            if not _version_in_range(dep_manifest.version, dep.min_version, dep.max_version):
                errors.append(
                    f"pack '{result.manifest.name}' requires '{dep.name}' version "
                    f"{dep.min_version}..{dep.max_version}, but found {dep_manifest.version}"
                )
    return errors


def build_dependency_graph(results: list[PackValidationResult]) -> nx.DiGraph:
    graph = nx.DiGraph()
    valid = [r for r in results if r.manifest is not None and r.is_valid]
    for result in valid:
        graph.add_node(result.manifest.name)
    for result in valid:
        for dep in result.manifest.dependencies:
            if dep.name in graph:
                graph.add_edge(dep.name, result.manifest.name)
    return graph


def detect_dependency_cycles(results: list[PackValidationResult]) -> list[list[str]]:
    return [cycle for cycle in nx.simple_cycles(build_dependency_graph(results))]


def topological_pack_order(results: list[PackValidationResult]) -> list[str]:
    return list(nx.topological_sort(build_dependency_graph(results)))
