"""Manifest chain resolution and merge for MAID Runner v2."""

from __future__ import annotations

from pathlib import Path
from typing import Optional, Union

from maid_runner.core.manifest import load_manifest
from maid_runner.core.types import ArtifactSpec, FileMode, Manifest


class ManifestChain:
    def __init__(
        self,
        manifest_dir: Union[str, Path],
        project_root: Union[str, Path] = ".",
    ):
        self._manifest_dir = Path(manifest_dir)
        self._project_root = Path(project_root)
        if not self._manifest_dir.exists():
            raise FileNotFoundError(
                f"Manifest directory not found: {self._manifest_dir}"
            )
        self._manifests: list[Manifest] | None = None
        self._superseded_set: set[str] | None = None
        self._superseded_by_map: dict[str, str] | None = None

    def _load(self) -> None:
        paths = _discover_manifest_files(self._manifest_dir)
        manifests = []
        for p in paths:
            try:
                manifests.append(load_manifest(p))
            except Exception:
                continue
        self._manifests = manifests
        self._resolve_supersession()

    def _resolve_supersession(self) -> None:
        assert self._manifests is not None
        superseded: set[str] = set()
        superseded_by: dict[str, str] = {}

        for m in self._manifests:
            for slug in m.supersedes:
                superseded.add(slug)
                superseded_by[slug] = m.slug

        self._superseded_set = superseded
        self._superseded_by_map = superseded_by

    def _ensure_loaded(self) -> None:
        if self._manifests is None:
            self._load()

    @property
    def all_manifests(self) -> list[Manifest]:
        self._ensure_loaded()
        assert self._manifests is not None
        return list(self._manifests)

    def active_manifests(self) -> list[Manifest]:
        self._ensure_loaded()
        assert self._manifests is not None
        assert self._superseded_set is not None
        active = [m for m in self._manifests if m.slug not in self._superseded_set]
        return _sort_manifests(active)

    def superseded_manifests(self) -> list[Manifest]:
        self._ensure_loaded()
        assert self._manifests is not None
        assert self._superseded_set is not None
        return [m for m in self._manifests if m.slug in self._superseded_set]

    def is_superseded(self, slug: str) -> bool:
        self._ensure_loaded()
        assert self._superseded_set is not None
        return slug in self._superseded_set

    def superseded_by(self, slug: str) -> Optional[str]:
        self._ensure_loaded()
        assert self._superseded_by_map is not None
        return self._superseded_by_map.get(slug)

    def manifests_for_file(self, path: str) -> list[Manifest]:
        result = []
        for m in self.active_manifests():
            if path in m.all_writable_paths:
                result.append(m)
        return result

    def merged_artifacts_for(self, path: str) -> list[ArtifactSpec]:
        manifests = self.manifests_for_file(path)
        if not manifests:
            return []

        merged: dict[str, ArtifactSpec] = {}
        # Manifests are sorted by created timestamp; later ones override
        for m in manifests:
            for fs in m.all_file_specs:
                if fs.path == path:
                    for artifact in fs.artifacts:
                        merged[artifact.merge_key()] = artifact

        return list(merged.values())

    def file_mode_for(self, path: str) -> Optional[FileMode]:
        modes: set[FileMode] = set()
        for m in self.active_manifests():
            for fs in m.all_file_specs:
                if fs.path == path:
                    modes.add(fs.mode)
            for ds in m.files_delete:
                if ds.path == path:
                    modes.add(FileMode.DELETE)

        if not modes:
            return None

        # Strictest wins: CREATE > SNAPSHOT > DELETE > EDIT > READ
        priority = [
            FileMode.CREATE,
            FileMode.SNAPSHOT,
            FileMode.DELETE,
            FileMode.EDIT,
            FileMode.READ,
        ]
        for mode in priority:
            if mode in modes:
                return mode

        return modes.pop()

    def all_tracked_paths(self) -> set[str]:
        paths: set[str] = set()
        for m in self.active_manifests():
            paths |= m.all_writable_paths
            paths |= set(m.files_read)
        return paths

    def all_read_only_paths(self) -> set[str]:
        """Return paths that appear only in read sections (no writable manifest)."""
        all_writable: set[str] = set()
        all_read: set[str] = set()
        for m in self.active_manifests():
            all_writable |= m.all_writable_paths
            all_read |= set(m.files_read)
        return all_read - all_writable

    def validate_supersession_integrity(self) -> list[str]:
        self._ensure_loaded()
        assert self._manifests is not None
        errors: list[str] = []
        slug_set = {m.slug for m in self._manifests}

        # Check for non-existent superseded manifests
        for m in self._manifests:
            for s in m.supersedes:
                if s not in slug_set:
                    errors.append(
                        f"Manifest '{m.slug}' supersedes non-existent manifest '{s}'"
                    )

        # Check for circular supersession
        supersedes_graph: dict[str, list[str]] = {}
        for m in self._manifests:
            if m.supersedes:
                supersedes_graph[m.slug] = list(m.supersedes)

        if _has_cycle(supersedes_graph, slug_set):
            errors.append("Circular supersession detected in manifest chain")

        return errors

    def reload(self) -> None:
        self._manifests = None
        self._superseded_set = None
        self._superseded_by_map = None


def _discover_manifest_files(manifest_dir: Path) -> list[Path]:
    patterns = ["*.manifest.yaml", "*.manifest.yml", "*.manifest.json"]
    files: list[Path] = []
    for pattern in patterns:
        files.extend(manifest_dir.glob(pattern))
    return sorted(files)


def _sort_manifests(manifests: list[Manifest]) -> list[Manifest]:
    def sort_key(m: Manifest) -> tuple[int, str, str]:
        if m.created:
            return (0, m.created, m.slug)
        return (1, "", m.slug)

    return sorted(manifests, key=sort_key)


def _has_cycle(graph: dict[str, list[str]], all_slugs: set[str]) -> bool:
    visited: set[str] = set()
    in_stack: set[str] = set()

    def dfs(node: str) -> bool:
        if node in in_stack:
            return True
        if node in visited:
            return False
        visited.add(node)
        in_stack.add(node)
        for neighbor in graph.get(node, []):
            if neighbor in all_slugs and dfs(neighbor):
                return True
        in_stack.discard(node)
        return False

    for slug in graph:
        if dfs(slug):
            return True
    return False
