#!/usr/bin/env python3
import hashlib
import json
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path

PROJECT = Path(__file__).resolve().parents[1]
CONFIG = Path.home() / ".config/huyen-an-audio/postiz.env"
RECEIPT = PROJECT / "log/postiz-schedule.json"
ARCHIVED = PROJECT / "log/archive/pre-pronunciation-v1/log__postiz-schedule.json"
MEDIA_RECEIPT = PROJECT / "log/postiz-media.json"
FB = "cmrq10vod000hj7cbt8zvuj6a"
YT = "cmrpr0j9u000bj7cboqonlx4b"
TARGETS = (FB, YT)
OLD_IDS = {
    FB: "cmrtmi8d2002yj7cb74ajq330",
    YT: "cmrtmi8dr002zj7cbc4pxzxyb",
}
SLOT = datetime.fromisoformat("2026-07-23T12:00:00+00:00")
CANON = "7f44aafb6907de39dcb0c218bcd13865247ee3adceca356aacaa6377da7b9999"


def sha(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 load_env():
    values = {}
    for raw in CONFIG.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if line and not line.startswith("#") and "=" in line:
            key, value = line.split("=", 1)
            values[key.strip()] = value.strip().strip('"').strip("'")
    return values


def request(base, key, method, path, payload=None):
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
    headers = {"Authorization": key}
    if body is not None:
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(base.rstrip("/") + path, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=120) as response:
            raw = response.read()
            if not raw:
                return {"http_status": response.status}
            return json.loads(raw.decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode(errors="replace")[:2000]
        raise RuntimeError(f"Postiz HTTP {exc.code} for {method} {path}: {detail}") from exc


def parse(value):
    stamp = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    return stamp.replace(tzinfo=timezone.utc) if stamp.tzinfo is None else stamp.astimezone(timezone.utc)


def rows(payload):
    if isinstance(payload, dict):
        return payload.get("posts", payload.get("data", []))
    return payload


def integration_id(post):
    integration = post.get("integration") or {}
    return integration.get("id") if isinstance(integration, dict) else None


def exact_calendar(base, key):
    query = urllib.parse.urlencode({
        "startDate": (SLOT - timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
        "endDate": (SLOT + timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
    })
    result = {target: [] for target in TARGETS}
    for post in rows(request(base, key, "GET", "/posts?" + query)):
        target = integration_id(post)
        raw_date = post.get("publishDate") or post.get("date")
        if target not in result or not raw_date or abs((parse(raw_date) - SLOT).total_seconds()) >= 1:
            continue
        result[target].append({
            "id": post.get("id"),
            "state": str(post.get("state", "")).upper(),
            "publishDate": parse(raw_date).isoformat(),
        })
    return result


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


def exactly_two_new_queues(found, old_ids):
    for target in TARGETS:
        target_rows = found[target]
        if len(target_rows) != 1 or target_rows[0]["state"] != "QUEUE":
            return False
        if target_rows[0]["id"] == old_ids[target]:
            return False
    return True


def main():
    required = [ARCHIVED, MEDIA_RECEIPT, PROJECT / "log/upload-transcode.json", PROJECT / "output/info.txt"]
    if any(not path.is_file() for path in required):
        raise RuntimeError("replacement prerequisites missing")
    gate = subprocess.run([sys.executable, str(PROJECT / "script/assert-publish-ready.py")], cwd=PROJECT)
    if gate.returncode:
        raise RuntimeError("publish-ready gate failed")

    env = load_env()
    base = env["POSTIZ_BASE_URL"]
    key = env["POSTIZ_API_KEY"]
    if (env["POSTIZ_FACEBOOK_INTEGRATION_ID"], env["POSTIZ_YOUTUBE_INTEGRATION_ID"]) != TARGETS:
        raise RuntimeError("integration IDs differ from authority")
    if request(base, key, "GET", "/is-connected").get("connected") is not True:
        raise RuntimeError("Postiz is not connected")
    integrations = request(base, key, "GET", "/integrations")
    integration_rows = integrations.get("integrations", integrations.get("data", integrations)) if isinstance(integrations, dict) else integrations
    current = {row.get("id"): row for row in integration_rows}
    if any(target not in current or current[target].get("disabled") is True for target in TARGETS):
        raise RuntimeError("target integration missing or disabled")

    archived = json.loads(ARCHIVED.read_text(encoding="utf-8"))
    media = json.loads(MEDIA_RECEIPT.read_text(encoding="utf-8"))
    video_path = PROJECT / "output/final-upload.mp4"
    thumb_path = PROJECT / "image/intro-poster-1920x1080.png"
    if archived.get("verified") is not True or archived.get("slot_utc") != SLOT.isoformat():
        raise RuntimeError("archived schedule authority mismatch")
    if media.get("verified") is not True or media.get("source_canon_sha256") != CANON:
        raise RuntimeError("media receipt invalid")
    video = media["media"]["video"]
    thumbnail = media["media"]["thumbnail"]
    if video.get("sha256") != sha(video_path) or thumbnail.get("sha256") != sha(thumb_path):
        raise RuntimeError("media receipt hashes do not match local assets")

    info = (PROJECT / "output/info.txt").read_text(encoding="utf-8")
    title = info.split("TITLE\n", 1)[1].split("\n\nDESCRIPTION\n", 1)[0].strip()
    description = info.split("\n\nDESCRIPTION\n", 1)[1].strip()
    payload = {
        "type": "schedule",
        "date": SLOT.isoformat().replace("+00:00", "Z"),
        "shortLink": False,
        "tags": [],
        "posts": [
            {
                "integration": {"id": FB},
                "value": [{"content": description, "image": [{"id": video["id"], "path": video["path"]}]}],
                "settings": {"__type": "facebook"},
            },
            {
                "integration": {"id": YT},
                "value": [{"content": description, "image": [{"id": video["id"], "path": video["path"]}]}],
                "settings": {
                    "__type": "youtube",
                    "title": title,
                    "type": "public",
                    "selfDeclaredMadeForKids": "no",
                    "thumbnail": {"id": thumbnail["id"], "path": thumbnail["path"]},
                    "tags": [],
                },
            },
        ],
    }

    receipt = {
        "version": 2,
        "verified": False,
        "status": "prepared",
        "replacement": "pronunciation_v2",
        "source_canon_sha256": CANON,
        "slot_local": "2026-07-23T19:00:00+07:00",
        "slot_utc": SLOT.isoformat(),
        "old_posts": OLD_IDS,
        "deleted_old_posts": [],
        "media": {"video": video, "thumbnail": thumbnail},
        "posts": {},
    }
    if RECEIPT.is_file():
        prior = json.loads(RECEIPT.read_text(encoding="utf-8"))
        if prior.get("version") == 2 and prior.get("replacement") == "pronunciation_v2":
            receipt = prior
            found = exact_calendar(base, key)
            if prior.get("verified") is True and exactly_two_new_queues(found, OLD_IDS):
                print(json.dumps({"verified": True, "resumed": True, "posts": found}, ensure_ascii=False))
                return 0
            if prior.get("status") == "submitting" and exactly_two_new_queues(found, OLD_IDS):
                receipt.update({"verified": True, "status": "completed", "posts": found, "resumed_after_uncertain_post": True})
                save(receipt)
                print(json.dumps({"verified": True, "resumed": True, "posts": found}, ensure_ascii=False))
                return 0

    found = exact_calendar(base, key)
    old_present = {
        target: any(row["id"] == OLD_IDS[target] and row["state"] == "QUEUE" for row in found[target])
        for target in TARGETS
    }
    allowed_ids = set(OLD_IDS.values())
    foreign = [row for target in TARGETS for row in found[target] if row["id"] not in allowed_ids]
    if foreign and receipt.get("status") not in {"submitting"}:
        raise RuntimeError("foreign or unexpected occupancy exists at replacement slot")

    save(receipt)
    for target in TARGETS:
        current_found = exact_calendar(base, key)
        if any(row["id"] == OLD_IDS[target] for row in current_found[target]):
            request(base, key, "DELETE", "/posts/" + OLD_IDS[target])
            receipt.setdefault("deleted_old_posts", []).append({
                "id": OLD_IDS[target],
                "integration_id": target,
                "deleted_at": datetime.now(timezone.utc).isoformat(),
            })
            receipt["status"] = "deleting_old_posts"
            save(receipt)

    after_delete = exact_calendar(base, key)
    if any(any(row["id"] in allowed_ids for row in after_delete[target]) for target in TARGETS):
        raise RuntimeError("one or more old exact post IDs remain after delete")
    if any(after_delete[target] for target in TARGETS):
        raise RuntimeError("replacement slot is not empty after deleting old posts")

    receipt.update({"status": "submitting", "verified": False, "calendar_after_delete": after_delete, "submitting_at": datetime.now(timezone.utc).isoformat()})
    save(receipt)
    request(base, key, "POST", "/posts", payload)
    found_new = exact_calendar(base, key)
    verified = exactly_two_new_queues(found_new, OLD_IDS)
    receipt.update({
        "verified": verified,
        "status": "completed" if verified else "partial_failure",
        "posts": found_new,
        "checked_at": datetime.now(timezone.utc).isoformat(),
    })
    save(receipt)
    print(json.dumps({"verified": verified, "slot_local": receipt["slot_local"], "posts": found_new}, ensure_ascii=False))
    return 0 if verified else 1


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(f"Postiz replacement blocked: {exc}", file=sys.stderr)
        sys.exit(1)
