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

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


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.resolve().relative_to(ROOT.resolve())
    return path


def atomic_json(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    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 terminal_receipt(manifest, key, canon):
    path = resolve(manifest, key)
    value = json.loads(path.read_text(encoding="utf-8"))
    if value.get("status") != "completed" or value.get("verified") is not True or value.get("source_canon_sha256") != canon:
        raise RuntimeError(f"{key} not terminal on current canon")
    return path, value


def calendar_readback(schedule):
    publisher_path = ROOT / "script/publish-postiz.py"
    spec = importlib.util.spec_from_file_location("gma006_publisher", publisher_path)
    publisher = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(publisher)
    values = publisher.config()
    base = values["POSTIZ_BASE_URL"].rstrip("/")
    slot = publisher.parse_date(schedule["slot_utc"])
    found = publisher.posts_at_slot(base, values["POSTIZ_API_KEY"], slot)
    expected = {publisher.FB, publisher.YT}
    if set(found) != expected or any(row.get("state") not in {"QUEUE", "PUBLISHED"} for row in found.values()):
        raise RuntimeError("current calendar readback does not contain exact two target posts")
    return found


def main():
    manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
    canon = manifest.get("canon_sha256")
    if not canon:
        raise RuntimeError("canon missing")
    transcode_path, transcode = terminal_receipt(manifest, "transcode_receipt", canon)
    media_path, media = terminal_receipt(manifest, "postiz_media_receipt", canon)
    schedule_path, schedule = terminal_receipt(manifest, "postiz_schedule_receipt", canon)
    ready_path, _ = terminal_receipt(manifest, "publish_ready_receipt", canon)
    upload = resolve(manifest, "final_upload")
    final_master = resolve(manifest, "final_master")
    footage = resolve(manifest, "footage")
    cleanup_path = resolve(manifest, "cleanup_receipt")
    completion_path = resolve(manifest, "completion_receipt")
    upload_hash = sha256(upload)
    if upload.stat().st_size >= 1_000_000_000:
        raise RuntimeError("retained upload not under one billion bytes")
    if upload_hash != transcode.get("artifact_sha256") or upload.stat().st_size != transcode.get("artifact_bytes"):
        raise RuntimeError("transcode/upload lineage failed")
    if upload_hash != media.get("video_sha256") or media.get("video_path") != str(upload):
        raise RuntimeError("Postiz media/upload lineage failed")
    video = media.get("video") or {}
    if not video.get("id") or not video.get("path"):
        raise RuntimeError("remote video object invalid")
    media_link = schedule.get("media_receipt") or {}
    if media_link.get("path") != str(media_path) or media_link.get("sha256") != sha256(media_path):
        raise RuntimeError("schedule/media receipt hash link failed")
    current_posts = calendar_readback(schedule)

    if cleanup_path.exists():
        cleanup = json.loads(cleanup_path.read_text(encoding="utf-8"))
        if cleanup.get("status") == "completed" and cleanup.get("verified") is True:
            if any(path.exists() for path in (final_master, footage)) or not upload.exists() or sha256(upload) != cleanup.get("retained_upload", {}).get("sha256"):
                raise RuntimeError("terminal cleanup receipt does not match current filesystem")
        elif cleanup.get("status") == "running" and cleanup.get("source_canon_sha256") == canon:
            deletion = cleanup.get("deletion_set") or []
            expected_paths = {str(final_master), str(footage)}
            if {row.get("path") for row in deletion} != expected_paths:
                raise RuntimeError("running cleanup deletion set drift")
        else:
            raise RuntimeError("existing cleanup receipt not safely resumable")
    else:
        if not final_master.is_file() or not footage.is_file():
            raise RuntimeError("cleanup targets missing before deletion plan lock")
        deletion = [
            {"key": "final_master", "path": str(final_master), "sha256": sha256(final_master), "bytes": final_master.stat().st_size},
            {"key": "footage", "path": str(footage), "sha256": sha256(footage), "bytes": footage.stat().st_size},
        ]
        cleanup = {
            "status": "running", "verified": False, "source_canon_sha256": canon,
            "deletion_set": deletion,
            "retained_upload": {"path": str(upload), "sha256": upload_hash, "bytes": upload.stat().st_size},
            "transcode_receipt": {"path": str(transcode_path), "sha256": sha256(transcode_path)},
            "postiz_media_receipt": {"path": str(media_path), "sha256": sha256(media_path)},
            "postiz_schedule_receipt": {"path": str(schedule_path), "sha256": sha256(schedule_path)},
            "calendar_readback_before_cleanup": current_posts,
            "started_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        }
        atomic_json(cleanup_path, cleanup)

    deletion = cleanup["deletion_set"]
    for row in deletion:
        path = Path(row["path"])
        path.resolve().relative_to(ROOT.resolve())
        if path == upload:
            raise RuntimeError("deletion set includes retained upload")
        if path.exists():
            if sha256(path) != row["sha256"] or path.stat().st_size != row["bytes"]:
                raise RuntimeError(f"cleanup target drift: {path}")
            path.unlink()
    if any(Path(row["path"]).exists() for row in deletion):
        raise RuntimeError("cleanup target remains")
    if not upload.is_file() or sha256(upload) != upload_hash:
        raise RuntimeError("retained upload drift after cleanup")
    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
    cleanup.update({
        "status": "completed", "verified": True,
        "deleted": deletion, "bytes_freed": sum(row["bytes"] for row in deletion),
        "retained_upload": {"path": str(upload), "sha256": upload_hash, "bytes": upload.stat().st_size},
        "retained": [
            manifest["active_paths"][key] for key in (
                "final_upload", "intro", "tts_audio", "intro_audio", "intro_normalized", "layout",
                "metadata", "story_canon", "spoken_narration", "promotion_receipt",
            )
        ],
        "calendar_readback_after_cleanup": calendar_readback(schedule), "completed_at": now,
    })
    atomic_json(cleanup_path, cleanup)

    required_receipts = [
        "promotion_receipt", "tts_pronunciation_receipt", "tts_receipt", "artwork_generation_receipt",
        "artwork_visual_receipt", "layout_receipt", "intro_receipt", "footage_receipt", "final_receipt",
        "final_qa_receipt", "transcode_receipt", "metadata_receipt", "publish_ready_receipt",
        "postiz_identity_receipt", "postiz_media_receipt", "postiz_schedule_receipt", "cleanup_receipt",
    ]
    receipt_hashes = {}
    for key in required_receipts:
        path, _ = terminal_receipt(manifest, key, canon)
        receipt_hashes[key] = {"path": str(path.relative_to(ROOT)), "sha256": sha256(path)}
    retained = {}
    for key in ("final_upload", "intro_normalized", "metadata", "story_canon", "spoken_narration", "tts_audio"):
        path = resolve(manifest, key)
        if not path.is_file() or path.stat().st_size <= 0:
            raise RuntimeError(f"retained artifact missing: {key}")
        retained[key] = {"path": str(path.relative_to(ROOT)), "sha256": sha256(path), "bytes": path.stat().st_size}
    step_names = list(manifest["steps"])
    for key in step_names:
        manifest["steps"][key] = "completed"
    blockers = [key for key, value in manifest["steps"].items() if value != "completed"]
    if blockers:
        raise RuntimeError(f"step reconciliation failed: {blockers}")
    completion = {
        "status": "completed", "verified": True, "source_canon_sha256": canon,
        "receipt_hashes": receipt_hashes, "retained_artifacts": retained,
        "deleted_targets_absent": {row["path"]: not Path(row["path"]).exists() for row in deletion},
        "calendar_readback": cleanup["calendar_readback_after_cleanup"],
        "post_ids": {key: value.get("id") for key, value in cleanup["calendar_readback_after_cleanup"].items()},
        "step_reconciliation": {"all_completed": True, "steps": manifest["steps"]},
        "completed_at": now,
    }
    atomic_json(completion_path, completion)
    manifest["status"] = "completed"
    manifest["steps"]["upload"] = "completed"
    manifest["steps"]["cleanup"] = "completed"
    manifest["steps"]["completion"] = "completed"
    manifest["cleanup"] = {"status": "completed", "verified": True, "receipt": str(cleanup_path.relative_to(ROOT)), "bytes_freed": cleanup["bytes_freed"], "completed_at": now}
    manifest["completion"] = {"status": "completed", "verified": True, "receipt": str(completion_path.relative_to(ROOT)), "completed_at": now}
    manifest["updated_at"] = now
    atomic_json(MANIFEST, manifest)
    print(json.dumps({"status": "completed", "verified": True, "bytes_freed": cleanup["bytes_freed"], "completion_receipt": str(completion_path)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
