#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
#
# SecuBox-Deb :: secubox-ytsas-billet (#1244)
#
# ytsas -> billets : publie un BILLET enrichi (multiliens) pour une capture
# CONSERVEE, exactement comme le podcaster le fait a l'import
# (packages/secubox-podcaster/api/importer.py::_add_billet). Cote HOTE : ytsas
# vit en LXC (pas d'acces a la DB billets), mais le drain de conserve
# (secubox-ytsas-conserve) tourne sur l'hote — c'est lui qui appelle ce script,
# apres avoir mirore la video vers PeerTube.
#
# Le billet porte : le titre (h1, dont derive le titre du billet), et les
# multiliens 🎬 Source / 📺 PeerTube / ▶ Flux souverain (cache box, sans pub ni
# Google — #1240). IDEMPOTENT : on ne recree pas si un billet reference deja
# cette source (ref_url).
#
# --repair : backfill. Parcourt la liste ytsas et cree les billets manquants
# des captures CONSERVEES (kept) et completes.

import argparse
import html
import json
import os
import re
import secrets
import sqlite3
import subprocess
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone

BILLETS_DB = os.environ.get("BILLETS_DB", "/var/lib/secubox/billets/billets.db")


def _ytsas_public() -> str:
    """YTSaS de CETTE box (#1723) — les billets pointaient vers ytsas.gk2."""
    try:
        from secubox_core.auth import hote_box
        h = hote_box("ytsas")
    except Exception:  # noqa: BLE001 — bibliothèque absente : nom local
        h = ""
    return f"https://{h or 'ytsas.localhost'}"


YTSAS_BASE = os.environ.get("YTSAS_PUBLIC_URL") or _ytsas_public()
YTSAS_LIST = os.environ.get("YTSAS_LIST_URL", "http://10.100.0.180:8091/api/v1/ytsas/list")

_B32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"


def _ulid() -> str:
    t = int(time.time() * 1000)
    ts = "".join(_B32[(t >> (10 * (5 - i))) & 0x1F] for i in range(6))
    return ts + "".join(secrets.choice(_B32) for _ in range(20))


def _slug(s: str, extra: str = "") -> str:
    base = re.sub(r"[^a-z0-9]+", "-", (s or "").lower()).strip("-")[:60] or "item"
    return f"{base}-{extra}" if extra else base


def _chown() -> None:
    # ytsas-conserve tourne en root ; on rend la DB a secubox pour que billets
    # puisse continuer d'ecrire.
    for suf in ("", "-wal", "-shm"):
        p = BILLETS_DB + suf
        if os.path.exists(p):
            subprocess.run(["chown", "secubox:secubox", p], check=False)


def stream_url(vid: str) -> str:
    return f"{YTSAS_BASE}/api/v1/ytsas/stream/{vid}"


def _ytid(u: str) -> str:
    m = re.search(r"(?:v=|youtu\.be/|/embed/|/shorts/)([A-Za-z0-9_-]{6,})", u or "")
    return m.group(1)[:11] if m else ""


def _peertube_embed(u: str) -> str:
    """Dérive l'URL d'INTÉGRATION PeerTube (/videos/embed/<id>) — les pages
    /w/ et /videos/watch/ refusent le cadrage (X-Frame-Options)."""
    for seg in ("/videos/watch/", "/w/"):
        i = (u or "").find(seg)
        if i >= 0:
            return u[:i] + "/videos/embed/" + u[i + len(seg):]
    return u or ""


def _embed(source: str, peertube: str, vid: str):
    """L'ITEM EMBARQUABLE SÛR (#1244) : le billet doit PORTER un lecteur, pas
    seulement des liens en texte. PeerTube souverain si miroité, sinon
    youtube-nocookie (première vue). Rend (embed_html, provider, embed_url)."""
    if peertube:
        return (
            '<iframe class="sbx-embed" src="%s" allowfullscreen loading="lazy"></iframe>'
            % html.escape(_peertube_embed(peertube)),
            "peertube", peertube)
    yid = _ytid(source) or (vid if re.fullmatch(r"[A-Za-z0-9_-]{6,}", vid or "") else "")
    if yid:
        return (
            '<iframe class="sbx-embed" src="https://www.youtube-nocookie.com/embed/%s"'
            ' allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope;'
            ' picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"'
            ' allowfullscreen loading="lazy"></iframe>' % html.escape(yid),
            "youtube", source or ("https://www.youtube.com/watch?v=" + yid))
    return ("", "", peertube or source or "")


def _fmt_duree(sec) -> str:
    try:
        s = int(sec)
    except Exception:
        return ""
    if s <= 0:
        return ""
    h, r = divmod(s, 3600)
    m, s = divmod(r, 60)
    return (f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}")


def _details_peertube(peertube_url: str) -> dict:
    """DETAILS DU MEDIA DEPUIS PEERTUBE (#1244), source SOUVERAINE (box-interne,
    aucun contact YouTube). Le billet est créé APRÈS le miroir : PeerTube porte
    déjà titre, description, durée, chaîne. Best-effort : jamais d'échec dur."""
    m = re.search(r"/(?:w|videos/watch)/([^/?#]+)", peertube_url or "")
    if not m:
        return {}
    uuid = m.group(1)
    origin = re.split(r"/(?:w|videos/watch)/", peertube_url, 1)[0]
    try:
        req = urllib.request.Request(
            origin + "/api/v1/videos/" + urllib.parse.quote(uuid),
            headers={"Accept": "application/json"})
        with urllib.request.urlopen(req, timeout=10) as r:
            d = json.loads(r.read().decode("utf-8"))
        return {
            "description": (d.get("description") or "").strip(),
            "duree": _fmt_duree(d.get("duration")),
            "chaine": ((d.get("channel") or {}).get("displayName") or "").strip(),
        }
    except Exception:  # noqa: BLE001 — best-effort
        return {}


def _iso(unix) -> str:
    """Horodatage ISO depuis un unix ts (added_at) — pour dater le billet du
    moment de la CAPTURE, pas du backfill (sinon 91 billets tous a « maintenant »
    noieraient le fil)."""
    try:
        return datetime.fromtimestamp(int(unix), tz=timezone.utc).isoformat()
    except Exception:
        return ""


def creer(vid, title, source, stream, peertube=None, quand=None) -> str:
    if not os.path.exists(BILLETS_DB):
        return "no-db"
    con = sqlite3.connect(BILLETS_DB, timeout=30)
    try:
        con.execute("PRAGMA journal_mode=WAL")
        # IDEMPOTENT : deja un billet pour cette source ?
        if source and con.execute(
                "SELECT 1 FROM billet WHERE ref_url=? LIMIT 1", (source,)).fetchone():
            return "exists"
        now = quand or datetime.now(timezone.utc).isoformat()
        bid = _ulid()
        slug = _slug(title, bid[-6:].lower())
        titre = (title or "").strip().replace("\n", " ")
        # DETAILS DU MEDIA (#1244), comme le podcaster : chaîne, durée, et la
        # description — depuis PeerTube (souverain) quand la vidéo est miroitée.
        det = _details_peertube(peertube) if peertube else {}
        # Le titre en 1re ligne : le billet n'a pas de colonne title, il derive de
        # la premiere ligne du corps (cf. podcaster #1238d). La description suit.
        parts = [f"# {titre}\n\n" if titre else ""]
        if det.get("description"):
            parts.append(det["description"][:4000] + "\n\n")
        meta = []
        if det.get("chaine"):
            meta.append(f"👤 {det['chaine']}")
        if det.get("duree"):
            meta.append(f"⏱ {det['duree']}")
        parts.append("\n---\n")
        if meta:
            parts.append("\n" + " · ".join(meta) + "\n")
        if source:
            parts.append(f"\n🎬 **Source :** {source}\n")
        if peertube:
            parts.append(f"\n📺 **Vidéo (PeerTube) :** {peertube}\n")
        if stream:
            parts.append(f"\n▶ **Flux souverain (box) :** {stream}\n")
        body = "".join(parts)
        # L'ITEM EMBARQUABLE (#1244) : le billet PORTE un lecteur (PeerTube souverain
        # si miroité, sinon youtube-nocookie), pas seulement des liens texte.
        emb_html, emb_provider, emb_url = _embed(source, peertube, vid)
        con.execute(
            "INSERT INTO billet(id,created_at,updated_at,published_at,body,ref_url,"
            "embed_url,embed_html,embed_provider,slug,status,style) "
            "VALUES(?,?,?,?,?,?,?,?,?,?, 'published','default')",
            (bid, now, now, now, body, source or None,
             emb_url or None, emb_html or None, emb_provider or None, slug))
        con.commit()
        return "created:" + slug
    finally:
        con.close()
        _chown()


def repair() -> None:
    try:
        with urllib.request.urlopen(YTSAS_LIST, timeout=15) as r:
            data = json.loads(r.read().decode("utf-8"))
    except Exception as e:  # noqa: BLE001
        print("repair: liste ytsas injoignable:", e)
        return
    rows = data if isinstance(data, list) else (data.get("items") or [])
    n = 0
    for it in rows:
        if str(it.get("kept")) not in ("1", "True", "true"):
            continue
        if not it.get("complete"):
            continue
        vid = it.get("id")
        if not vid:
            continue
        res = creer(vid, it.get("title") or vid, it.get("url"),
                    stream_url(vid), it.get("peertube_url"),
                    quand=_iso(it.get("added_at")) or None)
        if str(res).startswith("created"):
            n += 1
    print(f"repair: {n} billet(s) cree(s)")


def main() -> None:
    ap = argparse.ArgumentParser(description="ytsas -> billet enrichi (#1244)")
    ap.add_argument("--repair", action="store_true", help="backfill des captures conservees")
    ap.add_argument("--id")
    ap.add_argument("--title")
    ap.add_argument("--source")
    ap.add_argument("--peertube")
    ap.add_argument("--stream")
    a = ap.parse_args()
    if a.repair:
        repair()
        return
    if not a.id:
        ap.error("--id requis (ou --repair)")
    print(creer(a.id, a.title or a.id, a.source, a.stream or stream_url(a.id), a.peertube))


if __name__ == "__main__":
    main()
