"""Self-hosted sharing — clips on YOUR bucket, YOUR domain, no service.

For users comfortable setting up an S3-compatible bucket (R2 / S3 / B2 /
MinIO) behind a public domain: the app uploads the clip, a poster frame and
a self-contained share page directly, and the link lives entirely on the
user's infrastructure. No accounts, no tier caps, no quotas — it's their
storage. A local ledger tracks what was published so shares can be listed
and deleted.

Deliberately a deep-settings feature: setup asks for endpoint/bucket/keys/
public base, verifies with a real upload fetched back over the public URL,
and (politely, optionally) invites a donation — self-hosters cost the
project the most support and pay it nothing, which is fine and by design.
"""
import hashlib
import json
import os
import re
import secrets
import subprocess
import time

from fastapi import APIRouter, HTTPException, Request

from clipshelf import config, sigv4
from clipshelf.config import DATA

LEDGER = DATA / "selfhost_shares.json"
DONATE_URL = os.getenv("CLIPSHELF_DONATE_URL", "")   # set at release; empty hides the link

router = APIRouter(tags=["share"])


def _cfg() -> dict:
    c = config.CFG.get("selfhost") or {}
    return {k: str(c.get(k) or "") for k in
            ("endpoint", "bucket", "access_key", "secret_key", "public_base",
             "prefix", "region")}


def configured() -> bool:
    c = _cfg()
    return all(c[k] for k in ("endpoint", "bucket", "access_key", "secret_key", "public_base"))


def _key(name: str) -> str:
    prefix = re.sub(r"[^\w/-]", "", _cfg()["prefix"]).strip("/")
    return f"{prefix}/{name}" if prefix else name


def _public(name: str) -> str:
    return _cfg()["public_base"].rstrip("/") + "/" + _key(name)


def _ledger() -> list:
    try:
        return json.loads(LEDGER.read_text(encoding="utf-8"))
    except Exception:
        return []


def _ledger_write(rows: list):
    tmp = LEDGER.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(rows, indent=1), encoding="utf-8")
    tmp.replace(LEDGER)


def _page_html(slug: str, title: str, meta: dict) -> str:
    esc = lambda s: str(s or "").replace("&", "&amp;").replace("<", "&lt;").replace('"', "&quot;")
    t = esc(title or "clip")
    video, poster = _public(f"{slug}.mp4"), _public(f"{slug}.jpg")
    return f"""<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{t}</title>
<meta property="og:title" content="{t}">
<meta property="og:type" content="video.other">
<meta property="og:video" content="{esc(video)}">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="{meta.get('width') or 1280}">
<meta property="og:video:height" content="{meta.get('height') or 720}">
<meta property="og:image" content="{esc(poster)}">
<meta name="twitter:card" content="player">
<style>
body{{margin:0;background:#08080d;color:#f4f2ee;font:15px system-ui,sans-serif;
display:flex;flex-direction:column;align-items:center;min-height:100vh}}
main{{width:min(960px,100%);padding:24px 16px}}
video{{width:100%;border-radius:14px;background:#000}}
h1{{font-size:1.1rem;margin:14px 2px 2px}}
.meta{{color:#9a97a5;font-size:.8rem;margin:4px 2px}}
footer{{margin-top:auto;padding:14px;color:#57545f;font-size:.72rem}}
</style></head><body><main>
<video src="{esc(video)}" poster="{esc(poster)}" controls playsinline></video>
<h1>{t}</h1>
<div class="meta">{meta.get('height') or '?'}p{round(meta.get('fps') or 0)} · {meta.get('duration_s', 0):.0f}s</div>
</main>
<footer>self-hosted clip</footer>
</body></html>"""


def _probe(path: str) -> dict:
    r = subprocess.run(
        ["ffprobe", "-v", "error", "-print_format", "json",
         "-show_format", "-show_streams", path],
        capture_output=True, text=True, timeout=120, encoding="utf-8", errors="replace")
    j = json.loads(r.stdout or "{}")
    v = next((s for s in j.get("streams", []) if s.get("codec_type") == "video"), {})
    num, _, den = (v.get("avg_frame_rate") or "0/1").partition("/")
    try:
        fps = float(num) / float(den or 1)
    except ZeroDivisionError:
        fps = 0.0
    return {"duration_s": float(j.get("format", {}).get("duration") or 0),
            "width": int(v.get("width") or 0), "height": int(v.get("height") or 0),
            "fps": fps}


def _s3(method_fn, name, *args, **kw):
    c = _cfg()
    return method_fn(c["endpoint"], c["bucket"], _key(name), *args,
                     access_key=c["access_key"], secret_key=c["secret_key"],
                     region=c["region"] or "auto", **kw)


def publish_file(path: str, title: str) -> dict:
    if not configured():
        raise HTTPException(400, "self-hosting isn't configured")
    slug = secrets.token_urlsafe(6).replace("_", "a").replace("-", "b")
    meta = _probe(path)
    # poster frame
    import tempfile
    with tempfile.NamedTemporaryFile(suffix=".jpg") as pj:
        subprocess.run(["ffmpeg", "-y", "-v", "error",
                        "-ss", f"{min(1.0, meta['duration_s'] / 3):.2f}",
                        "-i", path, "-frames:v", "1", "-vf", "scale=640:-2", pj.name],
                       capture_output=True, timeout=60)
        poster = open(pj.name, "rb").read()
    with open(path, "rb") as fh:
        raw = fh.read()
    try:
        _s3(sigv4.put, f"{slug}.mp4", raw, content_type="video/mp4")
        if poster:
            _s3(sigv4.put, f"{slug}.jpg", poster, content_type="image/jpeg")
        _s3(sigv4.put, f"{slug}.html",
            _page_html(slug, title, meta).encode(), content_type="text/html; charset=utf-8")
    except Exception as e:
        raise HTTPException(502, f"upload to your bucket failed: {e}")
    rows = _ledger()
    rows.insert(0, {"slug": slug, "title": title[:140], "bytes": len(raw),
                    "url": _public(f"{slug}.html"), "ts": int(time.time()),
                    **{k: meta[k] for k in ("duration_s", "width", "height", "fps")}})
    _ledger_write(rows)
    return {"slug": slug, "share": _public(f"{slug}.html"), "transcoded": False,
            "selfhost": True}


class SelfhostPublisher:
    ID = "selfhost"
    LABEL = "Your own domain"

    def ready(self):
        return configured()

    def publish(self, clip_path, meta):
        res = publish_file(str(clip_path), str((meta or {}).get("title") or "clip"))
        return {"url": res["share"], "id": res["slug"]}

    def list_published(self):
        return _ledger()

    def delete(self, slug):
        for ext in ("html", "mp4", "jpg"):
            _s3(sigv4.delete, f"{slug}.{ext}")
        _ledger_write([r for r in _ledger() if r["slug"] != slug])


# ------------------------------------------------------------------ routes
@router.get("/api/share/selfhost")
def selfhost_status():
    c = _cfg()
    return {"configured": configured(), "donate_url": DONATE_URL,
            "settings": {k: (c[k] if k != "secret_key" else ("•••" if c[k] else ""))
                         for k in c}}


@router.post("/api/share/selfhost/setup")
async def selfhost_setup(request: Request):
    """Save S3 + public-domain settings, then VERIFY end-to-end: upload a
    probe object and fetch it back over the plain public URL. Failing the
    fetch fails setup — a share link that only works signed is no share."""
    body = await request.json()
    cur = config.CFG.get("selfhost") or {}
    for k in ("endpoint", "bucket", "access_key", "secret_key", "public_base",
              "prefix", "region"):
        if k in body:
            cur[k] = str(body[k]).strip()
    config.CFG["selfhost"] = cur
    if not configured():
        config.save(config.CFG)
        raise HTTPException(400, "need endpoint, bucket, access_key, secret_key and public_base")

    probe_name = f"_verify-{secrets.token_hex(4)}.txt"
    payload = f"product selfhost verify {time.time()}".encode()
    try:
        _s3(sigv4.put, probe_name, payload, content_type="text/plain")
    except Exception as e:
        raise HTTPException(400, f"bucket upload failed — check endpoint/keys: {e}")
    import urllib.request
    try:
        got = urllib.request.urlopen(_public(probe_name), timeout=20).read()
        if got != payload:
            raise ValueError("public URL served different content")
    except Exception as e:
        _s3(sigv4.delete, probe_name)
        raise HTTPException(400,
            f"uploaded fine, but the public URL didn't serve it back ({e}). "
            "Make the bucket publicly readable (R2: enable public access or a "
            "custom domain) and check public_base points at it.")
    _s3(sigv4.delete, probe_name)
    config.save(config.CFG)
    return {"status": "success", "configured": True, "donate_url": DONATE_URL}


@router.post("/api/share/selfhost/disable")
def selfhost_disable():
    config.CFG["selfhost"] = {}
    config.save(config.CFG)
    return {"status": "success"}


@router.get("/api/share/selfhost/list")
def selfhost_list():
    return {"shares": _ledger()}


@router.delete("/api/share/selfhost/{slug}")
def selfhost_delete(slug: str):
    SelfhostPublisher().delete(re.sub(r"[^A-Za-z0-9]", "", slug))
    return {"status": "success"}
