#!/usr/bin/env python3
"""Dependency-light runtime helpers for persistent StickerMan project scripts."""

from __future__ import annotations

import hashlib
import json
import os
import re
import struct
import tempfile
import wave
from pathlib import Path
from typing import Any, Iterable

SHARED_ROOT = Path("/data/video-pipeline").resolve()
STAGES = (
    "SCRIPT",
    "MANIFEST",
    "TTS",
    "ASR",
    "TIMELINE",
    "CANARY",
    "VISUAL_QA",
    "GPU",
    "BOUNDARY",
    "PACKAGE",
    "UPLOAD",
    "RELEASE_AUDIT",
    "CALENDAR",
    "FINAL",
)


def atomic_write_json(path: Path, value: Any) -> None:
    path = path.resolve()
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(value, handle, ensure_ascii=False, indent=2)
            handle.write("\n")
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temp_name, path)
    finally:
        try:
            os.unlink(temp_name)
        except FileNotFoundError:
            pass


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def require_project_root(path: Path) -> Path:
    root = path.resolve()
    root.relative_to(SHARED_ROOT)
    config_path = root / "project.json"
    if not config_path.is_file():
        raise ValueError(f"missing project.json: {root}")
    config = json.loads(config_path.read_text(encoding="utf-8"))
    configured = Path(config.get("project_path", root)).resolve()
    if configured != root:
        raise ValueError(f"project root mismatch: {configured} != {root}")
    if not re.match(r"^[0-9]{3}-", root.name):
        raise ValueError(f"invalid numbered project root: {root.name}")
    return root


def require_beneath(path: Path, root: Path, *, must_exist: bool = False) -> Path:
    resolved = path.resolve()
    resolved.relative_to(root.resolve())
    resolved.relative_to(SHARED_ROOT)
    if must_exist and (not resolved.is_file() or resolved.stat().st_size <= 0):
        raise ValueError(f"missing or empty input: {resolved}")
    return resolved


def valid_wav(path: Path) -> bool:
    try:
        with wave.open(str(path), "rb") as handle:
            return handle.getframerate() > 0 and handle.getnframes() > 0
    except (OSError, EOFError, wave.Error):
        return False


def valid_asr(path: Path) -> bool:
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
        return (
            float(value.get("duration", 0)) > 0
            and bool(value.get("segments"))
            and any(bool(segment.get("words")) for segment in value["segments"])
        )
    except (OSError, ValueError, TypeError, json.JSONDecodeError):
        return False


def valid_png(path: Path, *, min_width: int = 1024, min_height: int = 576, min_size: int = 10_000) -> bool:
    try:
        data = path.read_bytes()
        if len(data) <= min_size or data[:8] != b"\x89PNG\r\n\x1a\n":
            return False
        if data[12:16] != b"IHDR" or data[-8:-4] != b"IEND":
            return False
        width, height = struct.unpack(">II", data[16:24])
        return width >= min_width and height >= min_height
    except (OSError, ValueError, struct.error):
        return False


def missing_ids(items: Iterable[dict[str, Any]], path_key: str, validator) -> list[int]:
    return [int(item["id"]) for item in items if not validator(Path(item[path_key]))]


def load_state(root: Path) -> dict[str, Any]:
    state_path = root / "logs" / "workflow_state.json"
    if not state_path.exists():
        return {"version": 1, "stages": {stage: "pending" for stage in STAGES}}
    value = json.loads(state_path.read_text(encoding="utf-8"))
    stages = value.setdefault("stages", {})
    for stage in STAGES:
        stages.setdefault(stage, "pending")
    return value


def save_stage(root: Path, stage: str, status: str, evidence: dict[str, Any] | None = None) -> None:
    if stage not in STAGES or status not in {"pending", "in_progress", "PASS", "blocked", "failed"}:
        raise ValueError(f"invalid state: {stage}={status}")
    state = load_state(root)
    state["stages"][stage] = status
    if evidence is not None:
        state.setdefault("evidence", {})[stage] = evidence
    atomic_write_json(root / "logs" / "workflow_state.json", state)


def first_incomplete(root: Path) -> str | None:
    stages = load_state(root)["stages"]
    return next((stage for stage in STAGES if stages.get(stage) != "PASS"), None)


def normalize_content(value: str) -> str:
    return re.sub(r"\s+", " ", (value or "").strip()).casefold()


def merge_intervals(intervals: Iterable[tuple[float, float]], *, fps: float = 30.0) -> list[tuple[float, float]]:
    merged: list[tuple[float, float]] = []
    tolerance = 1 / fps + 1e-4
    for start, end in sorted(intervals):
        if merged and start <= merged[-1][1] + tolerance:
            merged[-1] = (merged[-1][0], max(merged[-1][1], end))
        else:
            merged.append((start, end))
    return merged


def raw_postiz_headers(api_key: str) -> dict[str, str]:
    if not api_key:
        raise ValueError("POSTIZ_API_KEY is empty")
    return {"Authorization": api_key, "Content-Type": "application/json"}
