"""The ``SourceProvider`` seam — one interface, two implementations.

``DiskProvider`` reads (and, locally, writes) a project's working-tree
``fed-resources/`` now; ``PackageProvider`` will read published package data
later. The web and the sync engine go through the read interface so the
disk -> package switch is additive (spec §3).
"""

from __future__ import annotations

import subprocess
from abc import ABC, abstractmethod
from pathlib import Path


class SourceProvider(ABC):
    """Read-side contract: everything the engine/web needs to compose a project view."""

    project_root: Path

    @abstractmethod
    def manifest_bytes(self) -> bytes | None: ...

    @abstractmethod
    def generated_bytes(self, relpath: str) -> bytes | None: ...

    @abstractmethod
    def list_generated(self) -> list[str]:
        """Relative paths under ``generated/`` (excluding ``manifest.json``), sorted."""

    @abstractmethod
    def has_fedres(self) -> bool: ...

    @abstractmethod
    def head_commit(self) -> str | None: ...


class DiskProvider(SourceProvider):
    """Reads/writes a local working tree's ``fed-resources/`` (force-sync reflects instantly)."""

    def __init__(self, project_root: str | Path) -> None:
        self.project_root = Path(project_root)

    # --- locations ---
    @property
    def fedres_dir(self) -> Path:
        return self.project_root / "fed-resources"

    @property
    def generated_dir(self) -> Path:
        return self.fedres_dir / "generated"

    @property
    def source_dir(self) -> Path:
        return self.fedres_dir / "source"

    @property
    def feedback_dir(self) -> Path:
        return self.fedres_dir / "feedback"

    @property
    def live_dir(self) -> Path:
        return self.fedres_dir / "live"

    # --- read interface ---
    def has_fedres(self) -> bool:
        return self.fedres_dir.is_dir()

    def manifest_bytes(self) -> bytes | None:
        path = self.generated_dir / "manifest.json"
        return path.read_bytes() if path.is_file() else None

    def generated_bytes(self, relpath: str) -> bytes | None:
        path = self.generated_dir / relpath
        return path.read_bytes() if path.is_file() else None

    def list_generated(self) -> list[str]:
        if not self.generated_dir.is_dir():
            return []
        return sorted(
            str(p.relative_to(self.generated_dir))
            for p in self.generated_dir.rglob("*.json")
            if p.is_file() and p.name != "manifest.json"
        )

    def head_commit(self) -> str | None:
        try:
            out = subprocess.run(  # noqa: S603
                ["git", "-C", str(self.project_root), "rev-parse", "HEAD"],  # noqa: S607
                capture_output=True,
                text=True,
                check=True,
                timeout=5,
            )
        except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
            return None
        return out.stdout.strip() or None

    # --- source (Class-A authored single-source) ---
    def source_bytes(self, name: str) -> bytes | None:
        path = self.source_dir / name
        return path.read_bytes() if path.is_file() else None

    def list_source(self) -> list[str]:
        if not self.source_dir.is_dir():
            return []
        return sorted(p.name for p in self.source_dir.iterdir() if p.is_file())

    # --- write side (local mode only; package mode is read-only) ---
    def write_generated(self, files: dict[str, bytes]) -> None:
        """Replace ``generated/`` with exactly ``files`` (relpath -> bytes), atomically per file."""
        self.generated_dir.mkdir(parents=True, exist_ok=True)
        keep = set(files)
        # prune files no longer generated (but never touch runs/ history)
        for existing in self.generated_dir.rglob("*.json"):
            rel = str(existing.relative_to(self.generated_dir))
            if rel not in keep and not rel.startswith("runs/"):
                existing.unlink()
        for relpath, content in files.items():
            target = self.generated_dir / relpath
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_bytes(content)


class PackageProvider(SourceProvider):
    """Reads published package data (post-v1). The seam is named; the impl lands with package mode."""

    def __init__(self, *args: object, **kwargs: object) -> None:
        raise NotImplementedError(
            "PackageProvider lands with package mode (post-v1, plan M7); use DiskProvider for now"
        )

    def manifest_bytes(self) -> bytes | None:  # pragma: no cover
        raise NotImplementedError

    def generated_bytes(self, relpath: str) -> bytes | None:  # pragma: no cover
        raise NotImplementedError

    def list_generated(self) -> list[str]:  # pragma: no cover
        raise NotImplementedError

    def has_fedres(self) -> bool:  # pragma: no cover
        raise NotImplementedError

    def head_commit(self) -> str | None:  # pragma: no cover
        raise NotImplementedError
