#!/usr/bin/env python3
import datetime
import hashlib
import json
import os
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "script/project-manifest.json"
DELETE_KEYS = ("final_master", "footage")


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


def resolve(manifest, key):
    value = manifest.get("active_paths", {}).get(key)
    if not value:
        raise RuntimeError(f"active_paths.{key} missing")
    path = Path(value)
    path = path if path.is_absolute() else ROOT / path
    path = path.resolve()
    path.relative_to(ROOT.resolve())
    return path


def load(path):
    value = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(value, dict):
        raise RuntimeError(f"JSON object required: {path}")
    return value


def atomic_json(path, value):
    part = path.with_suffix(path.suffix + ".part")
    part.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    os.replace(part, path)


def main():
    project = load(MANIFEST)
    canon = project["canon_sha256"]
    paths = {key: resolve(project, key) for key in (
        "final_upload", "final_master", "footage", "transcode_receipt",
        "postiz_media_receipt", "postiz_schedule_receipt", "cleanup_receipt",
    )}
    upload = paths["final_upload"]
    if not upload.is_file() or upload.stat().st_size <= 0 or upload.stat().st_size >= 1_000_000_000:
        raise RuntimeError("retained upload copy missing or too large")
    upload_hash = sha256(upload)
    transcode = load(paths["transcode_receipt"])
    media = load(paths["postiz_media_receipt"])
    schedule = load(paths["postiz_schedule_receipt"])
    for label, receipt in (("transcode", transcode), ("media", media), ("schedule", schedule)):
        if receipt.get("status") != "completed" or receipt.get("verified") is not True or receipt.get("source_canon_sha256") != canon:
            raise RuntimeError(f"{label} receipt is not terminal for current canon")
    if transcode.get("artifact_sha256") != upload_hash or transcode.get("artifact_bytes") != upload.stat().st_size:
        raise RuntimeError("retained upload differs from transcode receipt")
    if media.get("video_sha256") != upload_hash or media.get("video_bytes") != upload.stat().st_size or Path(str(media.get("video_path", ""))).resolve() != upload:
        raise RuntimeError("retained upload differs from media receipt")
    remote = media.get("video") or {}
    if not remote.get("id") or not remote.get("path"):
        raise RuntimeError("remote media object incomplete")
    if (schedule.get("media_receipt") or {}).get("sha256") != sha256(paths["postiz_media_receipt"]):
        raise RuntimeError("schedule is not linked to active media receipt")
    posts = schedule.get("posts") or {}
    ids = schedule.get("integration_ids") or {}
    expected = {ids.get("facebook"), ids.get("youtube")}
    if None in expected or set(posts) != expected or len(posts) != 2:
        raise RuntimeError("schedule does not contain exact two target integrations")
    states = {row.get("state") for row in posts.values()}
    dates = {row.get("publishDate") for row in posts.values()}
    if not states.issubset({"QUEUE", "PUBLISHED"}) or len(dates) != 1 or None in dates:
        raise RuntimeError("schedule readback is not terminal")

    cleanup = paths["cleanup_receipt"]
    if cleanup.exists():
        old = load(cleanup)
        if old.get("status") == "completed" and old.get("verified") is True:
            if old.get("upload_copy", {}).get("sha256") != upload_hash or any(Path(row["path"]).exists() for row in old.get("delete_targets", [])):
                raise RuntimeError("completed cleanup receipt no longer matches disk")
            print(json.dumps({"status": "completed", "verified": True, "idempotent": True, "receipt": str(cleanup)}, ensure_ascii=False))
            return
        if old.get("status") != "running" or old.get("verified") is not False or old.get("source_canon_sha256") != canon:
            raise RuntimeError("unsupported cleanup resume state")
        records = old.get("delete_targets") or []
        if not records:
            raise RuntimeError("locked cleanup set empty")
        for row in records:
            target = Path(row["path"]).resolve()
            target.relative_to(ROOT.resolve())
            if target == upload:
                raise RuntimeError("cleanup target aliases upload")
            if target.exists() and sha256(target) != row["sha256"]:
                raise RuntimeError("cleanup target hash drift")
        running = old
    else:
        records = []
        for key in DELETE_KEYS:
            target = paths[key]
            if target == upload or not target.is_file():
                raise RuntimeError(f"cleanup target invalid: {key}")
            records.append({"key": key, "path": str(target), "bytes": target.stat().st_size, "sha256": sha256(target), "exists_before": True})
        now = datetime.datetime.now(datetime.timezone.utc).isoformat()
        running = {
            "status": "running", "verified": False, "source_canon_sha256": canon,
            "active_version": project.get("active_version"),
            "postiz_media_receipt": {"path": str(paths["postiz_media_receipt"]), "sha256": sha256(paths["postiz_media_receipt"])},
            "postiz_schedule_receipt": {"path": str(paths["postiz_schedule_receipt"]), "sha256": sha256(paths["postiz_schedule_receipt"])},
            "upload_copy": {"path": str(upload), "sha256": upload_hash, "bytes": upload.stat().st_size},
            "delete_targets": records, "started_at": now,
        }
        atomic_json(cleanup, running)
    for row in records:
        Path(row["path"]).unlink(missing_ok=True)
    checks = {
        "upload_copy_exists": upload.is_file(),
        "upload_copy_hash_unchanged": sha256(upload) == upload_hash,
        "all_targets_absent": all(not Path(row["path"]).exists() for row in records),
    }
    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
    result = {
        **running, "status": "completed" if all(checks.values()) else "failed", "verified": all(checks.values()),
        "checks": checks, "deleted": [{**row, "exists_after": Path(row["path"]).exists()} for row in records],
        "bytes_freed": sum(row["bytes"] for row in records),
        "kept": [{"key": "final_upload", "path": str(upload), "sha256": upload_hash, "bytes": upload.stat().st_size}],
        "completed_at": now, "hash_method": "sha256 streaming 8 MiB",
    }
    atomic_json(cleanup, result)
    if not result["verified"]:
        raise RuntimeError(f"cleanup verification failed: {checks}")
    latest = load(MANIFEST)
    latest["steps"]["upload"] = "completed"
    latest["steps"]["cleanup"] = "completed"
    latest["cleanup"] = {"status": "completed", "verified": True, "receipt": str(cleanup.relative_to(ROOT)), "upload_copy_retained": str(upload.relative_to(ROOT)), "deleted_keys": list(DELETE_KEYS), "bytes_freed": result["bytes_freed"], "completed_at": now}
    latest["updated_at"] = now
    atomic_json(MANIFEST, latest)
    print(json.dumps({"status": "completed", "verified": True, "deleted": [row["path"] for row in records], "bytes_freed": result["bytes_freed"], "upload_copy_retained": str(upload), "upload_copy_sha256": upload_hash}, ensure_ascii=False))


if __name__ == "__main__":
    main()
