#!/usr/bin/env python3
from __future__ import annotations

import datetime
import hashlib
import json
import os
import stat
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
import zoneinfo
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PROJECT_MANIFEST = ROOT / "script/project-manifest.json"
ENV_FILE = Path("/home/hermes/.config/gac-mai-audio/postiz.env")
FB = "cmrilrryn0011j7c8kfzgx9zq"
YT = "cmriltjyk0013j7c8l7dp3o5i"
BRAND = "Gác Mái Audio"
TZ = zoneinfo.ZoneInfo("Asia/Ho_Chi_Minh")


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


def resolve_active(manifest: dict, key: str) -> Path:
    value = (manifest.get("active_paths") or {}).get(key)
    if not isinstance(value, str) or not value:
        raise RuntimeError(f"active_paths.{key} is missing")
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    try:
        path.resolve().relative_to(ROOT.resolve())
    except ValueError as exc:
        raise RuntimeError(f"active path escapes project: {key}") from exc
    return path


def atomic_json(path: Path, value: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    data = (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    try:
        with os.fdopen(fd, "wb") as handle:
            handle.write(data)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(name, path)
    finally:
        if os.path.exists(name):
            os.unlink(name)


def config() -> dict[str, str]:
    if not ENV_FILE.exists():
        raise RuntimeError("Postiz configuration file is missing")
    if stat.S_IMODE(ENV_FILE.stat().st_mode) != 0o600:
        raise RuntimeError("Postiz configuration file permissions must be 0600")
    file_values: dict[str, str] = {}
    for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            key, value = line.split("=", 1)
            file_values[key.strip()] = value.strip().strip("\"'")
    values = {key: os.environ.get(key, value) for key, value in file_values.items()}
    required = ("POSTIZ_BASE_URL", "POSTIZ_API_KEY", "POSTIZ_FACEBOOK_INTEGRATION_ID", "POSTIZ_YOUTUBE_INTEGRATION_ID")
    missing = [key for key in required if not values.get(key)]
    if missing:
        raise RuntimeError(f"Postiz configuration is missing variables: {missing}")
    return values


def decode_json_body(raw: bytes) -> object:
    text = raw.decode("utf-8", errors="replace")
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return {"raw": text[:2000]}


def request_json(base: str, key: str, path: str, *, payload: dict | None = None, timeout: int = 120) -> object:
    data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
    headers = {"Authorization": key}
    method = "GET"
    if data is not None:
        headers["Content-Type"] = "application/json"
        method = "POST"
    req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as response:
            return decode_json_body(response.read())
    except urllib.error.HTTPError as exc:
        body = decode_json_body(exc.read())
        raise RuntimeError(f"Postiz HTTP {exc.code}: {json.dumps(body, ensure_ascii=False)[:2000]}") from exc


def parse_date(value: str) -> datetime.datetime:
    if value.endswith("Z"):
        value = value[:-1] + "+00:00"
    parsed = datetime.datetime.fromisoformat(value)
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=datetime.timezone.utc)
    return parsed.astimezone(datetime.timezone.utc).replace(microsecond=0)


def posts_list(value: object) -> list[dict]:
    if isinstance(value, list):
        return [item for item in value if isinstance(item, dict)]
    if isinstance(value, dict):
        for key in ("posts", "data"):
            items = value.get(key)
            if isinstance(items, list):
                return [item for item in items if isinstance(item, dict)]
    return []


def integration_id(post: dict) -> str:
    return str(((post.get("integration") or {}).get("id") or post.get("integrationId") or ""))


def find_slot(base: str, key: str) -> tuple[datetime.datetime, dict]:
    now = datetime.datetime.now(TZ)
    end = now + datetime.timedelta(days=30)
    query = urllib.parse.urlencode({
        "startDate": now.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
        "endDate": end.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
    })
    posts = posts_list(request_json(base, key, "/posts?" + query, timeout=60))
    occupied = {FB: set(), YT: set()}
    ignored = 0
    for post in posts:
        target = integration_id(post)
        if target not in occupied:
            ignored += 1
            continue
        if str(post.get("state") or "").upper() not in {"QUEUE", "PUBLISHED"}:
            continue
        value = post.get("publishDate") or post.get("date")
        if value:
            occupied[target].add(parse_date(str(value)))
    for offset in range(31):
        day = now.date() + datetime.timedelta(days=offset)
        for hour, minute in ((9, 20), (19, 0)):
            local = datetime.datetime(day.year, day.month, day.day, hour, minute, tzinfo=TZ)
            if local <= now:
                continue
            utc = local.astimezone(datetime.timezone.utc).replace(microsecond=0)
            if utc not in occupied[FB] and utc not in occupied[YT]:
                return utc, {
                    "local": local.isoformat(), "utc": utc.isoformat().replace("+00:00", "Z"),
                    "facebook_occupied": len(occupied[FB]), "youtube_occupied": len(occupied[YT]),
                    "other_posts_ignored": ignored,
                }
    raise RuntimeError("no common target slot found in 30 days")


def posts_at_slot(base: str, key: str, slot: datetime.datetime) -> dict[str, dict]:
    start = slot - datetime.timedelta(minutes=1)
    end = slot + datetime.timedelta(minutes=1)
    query = urllib.parse.urlencode({
        "startDate": start.isoformat().replace("+00:00", "Z"),
        "endDate": end.isoformat().replace("+00:00", "Z"),
    })
    found: dict[str, dict] = {}
    for post in posts_list(request_json(base, key, "/posts?" + query, timeout=60)):
        target = integration_id(post)
        value = post.get("publishDate") or post.get("date")
        state = str(post.get("state") or "").upper()
        if target in {FB, YT} and value and parse_date(str(value)) == slot and state in {"QUEUE", "PUBLISHED"}:
            found[target] = {"id": post.get("id"), "state": state, "publishDate": value}
    return found


def curl_escape(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


def upload(base: str, key: str, path: Path) -> dict:
    # The real key is sent only through curl config stdin; it never appears in argv or receipts.
    config_text = (
        f'header = "Authorization: {curl_escape(key)}"\n'
        f'form = "file=@{curl_escape(str(path))}"\n'
    )
    completed = subprocess.run([
        "curl", "--fail-with-body", "--silent", "--show-error", "--config", "-", base + "/upload",
    ], input=config_text, text=True, capture_output=True)
    if completed.returncode != 0:
        raise RuntimeError(f"upload failed: {completed.stderr[-2000:]}")
    value = json.loads(completed.stdout)
    media = value.get("media") if isinstance(value, dict) and isinstance(value.get("media"), dict) else value
    if not isinstance(media, dict) or not media.get("id") or not media.get("path"):
        raise RuntimeError("upload response lacks media id/path")
    return {"id": media["id"], "path": media["path"]}


def identity_gate(project: dict, paths: dict[str, Path], values: dict[str, str], base: str, key: str, ready: dict) -> dict:
    if values["POSTIZ_FACEBOOK_INTEGRATION_ID"] != FB or values["POSTIZ_YOUTUBE_INTEGRATION_ID"] != YT:
        raise RuntimeError("effective Postiz integration configuration drift")
    connected_obj = request_json(base, key, "/is-connected", timeout=60)
    connected = isinstance(connected_obj, dict) and connected_obj.get("connected") is True
    raw = request_json(base, key, "/integrations", timeout=60)
    if isinstance(raw, list):
        integrations = raw
    elif isinstance(raw, dict):
        integrations = raw.get("integrations", raw.get("data", []))
    else:
        integrations = []
    results = []
    for platform, target in (("facebook", FB), ("youtube", YT)):
        item = next((entry for entry in integrations if isinstance(entry, dict) and str(entry.get("id")) == target), None)
        display = (item or {}).get("name") or (item or {}).get("displayName") or ""
        identifier = str((item or {}).get("identifier") or (item or {}).get("provider") or "")
        provider_match = platform in identifier.casefold()
        disabled = None if item is None else item.get("disabled")
        brand_match = display == BRAND
        results.append({
            "platform": platform, "id": target, "identifier": identifier,
            "display_name": display, "disabled": disabled, "brand_match": brand_match,
            "provider_match": provider_match, "exists": item is not None,
        })
    allowed = connected and all(item["exists"] and item["disabled"] is False and item["brand_match"] and item["provider_match"] for item in results)
    receipt = {
        "status": "completed" if allowed else "failed", "verified": allowed,
        "publish_allowed": allowed, "source_canon_sha256": ready["source_canon_sha256"],
        "publish_ready_receipt": {"path": str(paths["ready"]), "sha256": sha256(paths["ready"])},
        "checked_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "target_brand": BRAND, "connected": connected,
        "effective_integration_ids": {"facebook": FB, "youtube": YT},
        "integrations": results, "user_override": False,
        "reason": "exact IDs, providers, enabled state and display names match" if allowed else "identity/configuration mismatch",
    }
    atomic_json(paths["identity"], receipt)
    if not allowed:
        raise RuntimeError("Postiz Identity Gate failed; upload is blocked")
    return receipt


def main() -> int:
    project = json.loads(PROJECT_MANIFEST.read_text(encoding="utf-8"))
    paths = {
        "ready": resolve_active(project, "publish_ready_receipt"),
        "promotion": resolve_active(project, "promotion_receipt"),
        "transcode": resolve_active(project, "transcode_receipt"),
        "video": resolve_active(project, "final_upload"),
        "thumbnail": resolve_active(project, "intro_normalized"),
        "metadata": resolve_active(project, "metadata"),
        "metadata_receipt": resolve_active(project, "metadata_receipt"),
        "identity": resolve_active(project, "postiz_identity_receipt"),
        "media": resolve_active(project, "postiz_media_receipt"),
        "schedule": resolve_active(project, "postiz_schedule_receipt"),
    }
    if paths["schedule"].exists():
        raise RuntimeError("Postiz schedule receipt already exists and requires audit; refusing duplicate create")
    ready = json.loads(paths["ready"].read_text(encoding="utf-8"))
    promotion = json.loads(paths["promotion"].read_text(encoding="utf-8"))
    transcode = json.loads(paths["transcode"].read_text(encoding="utf-8"))
    metadata_receipt = json.loads(paths["metadata_receipt"].read_text(encoding="utf-8"))
    canon = project.get("canon_sha256")
    if ready.get("status") != "completed" or ready.get("verified") is not True or ready.get("source_canon_sha256") != canon:
        raise RuntimeError("publish-ready receipt is absent, failed or stale")
    if promotion.get("verified") is not True or promotion.get("source_canon_sha256") != canon:
        raise RuntimeError("promotion receipt is absent, failed or stale")
    if transcode.get("status") != "completed" or transcode.get("verified") is not True or transcode.get("source_canon_sha256") != canon:
        raise RuntimeError("transcode receipt is absent, failed or stale")
    if metadata_receipt.get("verified") is not True or metadata_receipt.get("source_canon_sha256") != canon:
        raise RuntimeError("metadata receipt is absent, failed or stale")
    for path in (paths["video"], paths["thumbnail"], paths["metadata"]):
        if not path.exists() or path.stat().st_size <= 0:
            raise RuntimeError(f"publish artifact missing or empty: {path}")
    video_hash = sha256(paths["video"])
    thumb_hash = sha256(paths["thumbnail"])
    metadata_hash = sha256(paths["metadata"])
    if paths["video"].suffix.casefold() != ".mp4" or paths["video"].stat().st_size >= 1_000_000_000:
        raise RuntimeError("upload copy is not a valid MP4 under one billion bytes")
    if transcode.get("artifact_sha256") != video_hash or transcode.get("artifact_bytes") != paths["video"].stat().st_size:
        raise RuntimeError("upload copy differs from transcode receipt")
    if metadata_receipt.get("artifact_sha256") != metadata_hash:
        raise RuntimeError("metadata differs from verified receipt")
    ready_upload = (ready.get("artifacts") or {}).get("upload") or {}
    ready_thumb = (ready.get("artifacts") or {}).get("thumbnail") or {}
    if ready_upload.get("sha256") != video_hash or ready_thumb.get("sha256") != thumb_hash:
        raise RuntimeError("video/thumbnail differs from publish-ready artifacts")
    if any(ROOT.glob("DO_NOT_PUBLISH*")) or (ROOT / "publish_block").exists():
        raise RuntimeError("publish block marker exists")

    values = config()
    base = values["POSTIZ_BASE_URL"].rstrip("/")
    key = values["POSTIZ_API_KEY"]
    identity = identity_gate(project, paths, values, base, key, ready)
    _, calendar_preflight = find_slot(base, key)

    media_receipt = json.loads(paths["media"].read_text(encoding="utf-8")) if paths["media"].exists() else {}
    same_canon = media_receipt.get("source_canon_sha256") == canon
    if same_canon and media_receipt.get("video_sha256") == video_hash:
        video_media = media_receipt.get("video")
    else:
        video_media = upload(base, key, paths["video"])
        media_receipt = {
            "status": "video_uploaded", "verified": False, "source_canon_sha256": canon,
            "identity_receipt": {"path": str(paths["identity"]), "sha256": sha256(paths["identity"])},
            "video_path": str(paths["video"]), "video_sha256": video_hash,
            "video_bytes": paths["video"].stat().st_size, "video": video_media,
            "video_uploaded_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        }
        atomic_json(paths["media"], media_receipt)
    if not isinstance(video_media, dict) or not video_media.get("id") or not video_media.get("path"):
        raise RuntimeError("video media checkpoint is invalid")

    if same_canon and media_receipt.get("thumbnail_sha256") == thumb_hash:
        thumb_media = media_receipt.get("thumbnail")
    else:
        thumb_media = upload(base, key, paths["thumbnail"])
        media_receipt.update({
            "status": "completed", "verified": True,
            "thumbnail_path": str(paths["thumbnail"]), "thumbnail_sha256": thumb_hash,
            "thumbnail_bytes": paths["thumbnail"].stat().st_size, "thumbnail": thumb_media,
            "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        })
        atomic_json(paths["media"], media_receipt)
    if not isinstance(thumb_media, dict) or not thumb_media.get("id") or not thumb_media.get("path"):
        raise RuntimeError("thumbnail media checkpoint is invalid")

    slot, calendar_before_create = find_slot(base, key)
    existing = posts_at_slot(base, key, slot)
    if existing:
        if set(existing) == {FB, YT}:
            created: object = {"skipped": "both posts already exist"}
        else:
            raise RuntimeError("partial posts already exist at target slot; refusing blind create")
    else:
        text = paths["metadata"].read_text(encoding="utf-8")
        title = text.splitlines()[0].strip()
        if not 2 <= len(title) <= 100:
            raise RuntimeError("YouTube title length invalid")
        payload = {
            "type": "schedule", "date": slot.isoformat().replace("+00:00", "Z"),
            "shortLink": False, "tags": [],
            "posts": [
                {"integration": {"id": FB}, "value": [{"content": text, "image": [video_media]}], "settings": {"__type": "facebook"}},
                {"integration": {"id": YT}, "value": [{"content": text, "image": [video_media]}], "settings": {"__type": "youtube", "title": title, "type": "public", "selfDeclaredMadeForKids": "no", "thumbnail": thumb_media, "tags": []}},
            ],
        }
        parsed = json.loads(json.dumps(payload, ensure_ascii=False))
        ids = {post["integration"]["id"] for post in parsed["posts"]}
        media_valid = all((post["value"][0]["image"][0].get("id") and post["value"][0]["image"][0].get("path")) for post in parsed["posts"])
        if len(parsed["posts"]) != 2 or ids != {FB, YT} or not media_valid or parsed["posts"][1]["settings"]["tags"] != []:
            raise RuntimeError("serialized Postiz payload validation failed")
        created = request_json(base, key, "/posts", payload=parsed, timeout=120)

    found = posts_at_slot(base, key, slot)
    verified = set(found) == {FB, YT}
    receipt = {
        "status": "completed" if verified else "partial_failure", "verified": verified,
        "source_canon_sha256": canon,
        "slot_local": slot.astimezone(TZ).isoformat(), "slot_utc": slot.isoformat().replace("+00:00", "Z"),
        "identity_receipt": {"path": str(paths["identity"]), "sha256": sha256(paths["identity"]), "publish_allowed": identity["publish_allowed"]},
        "calendar_preflight": calendar_preflight, "calendar_before_create": calendar_before_create,
        "media_receipt": {"path": str(paths["media"]), "sha256": sha256(paths["media"])},
        "created_response": created, "posts": found,
        "integration_ids": {"facebook": FB, "youtube": YT},
        "verified_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    }
    atomic_json(paths["schedule"], receipt)
    print(json.dumps({
        "status": receipt["status"], "verified": verified,
        "slot_local": receipt["slot_local"], "slot_utc": receipt["slot_utc"],
        "posts": found, "receipt": str(paths["schedule"]),
    }, ensure_ascii=False))
    return 0 if verified else 1


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(json.dumps({"status": "failed", "verified": False, "error": f"{type(exc).__name__}: {exc}"}, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
