"""Immutable run layout and no-clobber transaction helpers for OID v2."""
from __future__ import annotations

import os
import re
import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping

from CCD_analysis.utils.run_contract import deterministic_json_bytes, sha256_bytes

RUNS_ROOT = Path(__file__).resolve().parents[1] / "runs"
_RUN_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{7,159}$")
_REQUIRED_DIRS = ("raw", "aligned", "analysis", "validation", "reports")


def canonical_json(value: Any) -> str:
    return deterministic_json_bytes(value).decode("utf-8").removesuffix("\n")


def config_hash(config: Mapping[str, Any], length: int = 12) -> str:
    return sha256_bytes(deterministic_json_bytes(config))[:length]


def make_run_id(scene: str, purpose: str, config: Mapping[str, Any], git_hash: str, *, now: datetime | None = None) -> str:
    timestamp = (now or datetime.now(timezone.utc)).strftime("%Y%m%dt%H%M%Sz")
    clean = lambda text: re.sub(r"[^a-z0-9_.-]+", "-", text.lower()).strip("-.")
    return f"{clean(scene)}.{clean(purpose)}.{timestamp}.{config_hash(config)}.{git_hash[:8]}"


def validate_run_id(run_id: str) -> str:
    if not _RUN_ID_RE.fullmatch(run_id) or ".." in run_id:
        raise ValueError(f"invalid run_id: {run_id!r}")
    return run_id


@dataclass(frozen=True)
class RunPaths:
    run_id: str
    root: Path
    partial: Path
    final: Path

    @classmethod
    def create(cls, run_id: str, *, runs_root: str | Path = RUNS_ROOT, dry_run: bool = False) -> "RunPaths":
        validate_run_id(run_id)
        root = Path(runs_root).resolve()
        final = root / run_id
        partial = root / f"{run_id}.partial.{os.getpid()}"
        if final.exists() or partial.exists() or any(root.glob(f"{run_id}.partial.*")):
            raise FileExistsError(f"run already exists or is partial: {run_id}")
        paths = cls(run_id, root, partial, final)
        if not dry_run:
            partial.mkdir(parents=True, exist_ok=False)
            for name in _REQUIRED_DIRS:
                (partial / name).mkdir()
        return paths

    def child(self, relative: str | Path, *, final: bool = False) -> Path:
        base = self.final if final else self.partial
        candidate = (base / relative).resolve()
        try:
            candidate.relative_to(base.resolve())
        except ValueError as exc:
            raise ValueError("path escapes run directory") from exc
        return candidate

    def commit(self) -> Path:
        if self.final.exists():
            raise FileExistsError(f"final run exists: {self.final}")
        if not self.partial.is_dir():
            raise FileNotFoundError(f"partial run missing: {self.partial}")
        if not (self.partial / "manifest.json").is_file():
            raise RuntimeError("manifest.json must be written before commit")
        if not (self.partial / "_SUCCESS").is_file():
            raise RuntimeError("_SUCCESS must be written last before commit")
        os.replace(self.partial, self.final)
        return self.final

    def quarantine(self, reason: str) -> Path:
        if not self.partial.exists():
            return self.partial
        suffix = re.sub(r"[^a-z0-9_.-]+", "-", reason.lower()).strip("-.") or "failed"
        destination = self.root / f"{self.run_id}.quarantine.{suffix}.{os.getpid()}"
        os.replace(self.partial, destination)
        return destination

    def discard_empty(self) -> None:
        if self.partial.exists() and not any(self.partial.iterdir()):
            shutil.rmtree(self.partial)
