#!/usr/bin/env python3
import hashlib
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
import requests

ROOT = Path("/data/video-pipeline/GacMaiAudio/project/gacmai_20260717_104203")
ART = ROOT / "artifacts"
VIDEO = ROOT / "render/final_postiz_nvenc.mp4"
THUMB = ROOT / "assets/images/intro_1920x1080.png"
META = ART / "metadata_receipt.json"
RECEIPT = ART / "postiz_publish_receipt.json"
CALENDAR = ART / "postiz_live_calendar_evidence.json"
EXPECTED_VIDEO_SHA = "4e2739e39010e7cd3e4a9a07937f91943cd89d2e8178a30daab16be51b145ee2"
EXPECTED_THUMB_SHA = "11a26caff06d1fef0db949336f48852b07cffc7d89e1822341bf402b47890465"
YOUTUBE = "cmriltjyk0013j7c8l7dp3o5i"
FACEBOOK = "cmrilrryn0011j7c8kfzgx9zq"
TARGETS = {YOUTUBE: "youtube", FACEBOOK: "facebook"}
BASE = os.environ["POSTIZ_BASE_URL"].rstrip("/")
HEADERS = {"Authorization": os.environ["POSTIZ_API_KEY"]}
TZ = ZoneInfo("Asia/Ho_Chi_Minh")
SLOT_TIMES = ((9, 20), (19, 0))


def api(method, path, **kwargs):
    response = requests.request(method, BASE + path, headers=HEADERS, **kwargs)
    response.raise_for_status()
    return response


def save(receipt):
    receipt["updated_at"] = datetime.now(timezone.utc).isoformat()
    RECEIPT.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")


def instant(value):
    return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)


def same_instant(a, b):
    return bool(a and b) and instant(a) == instant(b)


def fetch_posts(start, end):
    body = api("GET", "/posts", params={"startDate": start.isoformat(), "endDate": end.isoformat()}, timeout=60).json()
    posts = body.get("posts", body if isinstance(body, list) else [])
    if not isinstance(posts, list):
        raise RuntimeError("Unexpected /posts response schema")
    return posts


def select_slot(posts, now):
    evidence = []
    target_matches = [p for p in posts if (p.get("integration") or {}).get("id") in TARGETS]
    for offset in range(8):
        day = (now + timedelta(days=offset)).date()
        for hour, minute in SLOT_TIMES:
            candidate = datetime(day.year, day.month, day.day, hour, minute, tzinfo=TZ)
            if candidate <= now:
                continue
            utc = candidate.astimezone(timezone.utc)
            busy = [p for p in target_matches if same_instant(p.get("publishDate"), utc.isoformat())]
            busy_ids = [(p.get("integration") or {}).get("id") for p in busy]
            evidence.append({"local": candidate.isoformat(), "utc": utc.isoformat().replace("+00:00", "Z"), "busy_integration_ids": busy_ids})
            if not busy_ids:
                return utc.isoformat().replace("+00:00", "Z"), evidence, len(target_matches)
    raise RuntimeError("No common-free fixed slot found")


def upload(path, key, mime, receipt):
    prior = receipt["uploads"].get(key)
    if prior and prior.get("id") and prior.get("path"):
        return prior
    receipt["upload_in_progress"] = {"key": key, "filename": path.name, "size": path.stat().st_size, "mime": mime, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
    save(receipt)
    with path.open("rb") as stream:
        response = api("POST", "/upload", files={"file": (path.name, stream, mime)}, timeout=3600)
    body = response.json()
    item = body.get("output", body.get("data", body)) if isinstance(body, dict) else body
    if isinstance(item, list):
        item = item[0]
    if not isinstance(item, dict) or not item.get("id") or not item.get("path"):
        raise RuntimeError(f"Upload response lacks id/path: {str(body)[:800]}")
    receipt["uploads"][key] = {"id": item["id"], "path": item["path"], "filename": path.name, "http_status": response.status_code}
    receipt.pop("upload_in_progress", None)
    save(receipt)
    return receipt["uploads"][key]


def main():
    metadata = json.loads(META.read_text())
    gate = metadata.get("gate", {})
    required_true = (
        "exact_title_suffix",
        "youtube_hook_synopsis_cta_brand_copyright",
        "facebook_copy_is_adapted",
        "spoiler_light",
        "project_specific",
        "tags_deduplicated",
    )
    if (
        metadata.get("status") != "passed"
        or not all(gate.get(name) is True for name in required_true)
        or gate.get("inherited_metadata") is not False
    ):
        raise RuntimeError("Metadata gate failed")
    title = metadata["youtube"]["title"]
    story_title = "Nửa Nhịp Không Thuộc Về Ai"
    if title != story_title + " | Truyện Audio Full":
        raise RuntimeError("YouTube title gate failed")
    if VIDEO.stat().st_size >= 1073741824:
        raise RuntimeError("Video exceeds Postiz limit")
    if hashlib.sha256(VIDEO.read_bytes()).hexdigest() != EXPECTED_VIDEO_SHA or hashlib.sha256(THUMB.read_bytes()).hexdigest() != EXPECTED_THUMB_SHA:
        raise RuntimeError("Media checksum gate failed")
    calendar = json.loads(CALENDAR.read_text())
    if calendar.get("status") != "passed" or calendar.get("canonical_slots") != ["09:20", "19:00"]:
        raise RuntimeError("Calendar evidence gate failed")
    locked_slot = calendar["chosen"]["utc"]
    now = datetime.now(TZ)
    integrations_body = api("GET", "/integrations", timeout=30).json()
    integrations = integrations_body if isinstance(integrations_body, list) else integrations_body.get("integrations", integrations_body.get("data", []))
    by_id = {item.get("id"): item for item in integrations}
    integration_evidence = {}
    for integration_id, provider in TARGETS.items():
        item = by_id.get(integration_id)
        if not item or item.get("disabled"):
            raise RuntimeError(f"Integration gate failed: {integration_id}")
        integration_evidence[integration_id] = {"provider": provider, "name": item.get("name"), "disabled": item.get("disabled", False)}
    posts = fetch_posts(now, now + timedelta(days=8))
    slot, candidates, target_match_count = select_slot(posts, now)
    if slot != locked_slot:
        raise RuntimeError(f"Live nearest slot changed: locked={locked_slot} current={slot}; rerun read-only calendar probe")
    receipt = {"project_id": ROOT.name, "title": story_title, "youtube_title": title, "canonical_slot_utc": slot, "canonical_slot_local": instant(slot).astimezone(TZ).isoformat(), "metadata_receipt": str(META), "metadata_receipt_sha256": hashlib.sha256(META.read_bytes()).hexdigest(), "calendar_evidence_sha256": hashlib.sha256(CALENDAR.read_bytes()).hexdigest(), "video_sha256": hashlib.sha256(VIDEO.read_bytes()).hexdigest(), "video_size": VIDEO.stat().st_size, "postiz_limit": 1073741824, "integrations": integration_evidence, "uploads": {}, "channels": {}, "calendar_evidence": {"checked_at_local": now.isoformat(), "api_post_count": len(posts), "target_match_count": target_match_count, "candidates": candidates}}
    save(receipt)
    existing = [p for p in posts if (p.get("integration") or {}).get("id") in TARGETS and same_instant(p.get("publishDate"), slot) and story_title in (p.get("content") or "")]
    if len({(p.get("integration") or {}).get("id") for p in existing}) != 2:
        video = upload(VIDEO, "video", "video/mp4", receipt)
        thumb = upload(THUMB, "thumbnail", "image/png", receipt)
        posts = fetch_posts(datetime.now(TZ), datetime.now(TZ) + timedelta(days=8))
        occupied = [p for p in posts if (p.get("integration") or {}).get("id") in TARGETS and same_instant(p.get("publishDate"), slot)]
        if occupied:
            raise RuntimeError("Selected slot became occupied before create")
        tags = [{"value": tag, "label": tag} for tag in metadata["youtube"]["tags"]]
        payload = {"type": "schedule", "date": slot, "shortLink": False, "tags": [], "posts": [
            {"integration": {"id": YOUTUBE}, "value": [{"content": metadata["youtube"]["description"], "image": [video]}], "settings": {"__type": "youtube", "title": title, "type": "public", "selfDeclaredMadeForKids": "no", "thumbnail": thumb, "tags": tags}},
            {"integration": {"id": FACEBOOK}, "value": [{"content": metadata["facebook"]["caption"], "image": [video]}], "settings": {"__type": "facebook", "post_type": "post"}},
        ]}
        response = api("POST", "/posts", json=payload, timeout=180)
        receipt["create_http_status"] = response.status_code
        receipt["create_response"] = response.json()
        save(receipt)
    posts = fetch_posts(datetime.now(TZ), datetime.now(TZ) + timedelta(days=8))
    found = [p for p in posts if (p.get("integration") or {}).get("id") in TARGETS and same_instant(p.get("publishDate"), slot) and story_title in (p.get("content") or "")]
    by_channel = {(p.get("integration") or {}).get("id"): p for p in found}
    errors = []
    for integration_id, provider in TARGETS.items():
        post = by_channel.get(integration_id)
        if not post:
            errors.append(f"missing readback {integration_id}")
            continue
        missing_response = api("GET", f"/posts/{post['id']}/missing", timeout=30)
        missing = missing_response.json()
        receipt["channels"][integration_id] = {"post_id": post["id"], "group_id": post.get("group"), "provider": provider, "state": post.get("state"), "scheduled_date": post.get("publishDate"), "missing_http_status": missing_response.status_code, "missing": missing}
        if post.get("state") != "QUEUE" or missing != []:
            errors.append(f"post validation failed {integration_id}")
    receipt["final_readback_match_count"] = len(found)
    receipt["creation_complete"] = not errors and len(found) == 2
    receipt["readback_errors"] = errors
    save(receipt)
    if errors or len(found) != 2:
        raise RuntimeError(str(errors))
    print(json.dumps({"status": "passed", "slot": slot, "local": receipt["canonical_slot_local"], "channels": receipt["channels"]}, ensure_ascii=False))


if __name__ == "__main__":
    main()
