"""Client for the hosted share service.

Registers the "share" publisher (next to local export) and owns the free-
tier pre-upload transcode: the server enforces ≤1080p30/≤90s, so the client
encodes DOWN to compliance locally — your infra never transcodes anything.
Connection state (service url + JWT) lives in config.json via
POST /api/share/login.
"""
import json
import os
import subprocess
import tempfile
import urllib.error
import urllib.request

from fastapi import APIRouter, HTTPException, Request

from clipshelf import config
from clipshelf.routers.editor_common import resolve_media

# Where clips go when you have not said otherwise. Same reasoning as the
# updater's default: the service has a home now, and making someone type a
# URL to use the feature is friction with nothing on the other side of it.
# Still overridable — pointing this at your own domain is a supported
# setup, not a workaround.
DEFAULT_SHARE_URL = "https://clipshelf.gg"

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


def _base() -> str:
    return str(config.CFG.get("share_url") or DEFAULT_SHARE_URL).rstrip("/")


def _token() -> str:
    return str(config.CFG.get("share_token") or "")


def connected() -> bool:
    return bool(_base() and _token())


def _api(method: str, path: str, body=None, token=None, timeout=60):
    headers = {}
    data = None
    if body is not None:
        data = json.dumps(body).encode()
        headers["Content-Type"] = "application/json"
    tok = token if token is not None else _token()
    if tok:
        headers["Authorization"] = f"Bearer {tok}"
    req = urllib.request.Request(_base() + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            payload = r.read()
            return json.loads(payload) if payload else {}
    except urllib.error.HTTPError as e:
        try:
            detail = json.loads(e.read()).get("detail", "")
        except Exception:
            detail = ""
        raise HTTPException(e.code, detail or f"share service error {e.code}")
    except Exception as e:
        raise HTTPException(502, f"share service unreachable: {e}")


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, "bytes": os.path.getsize(path)}


def encode_for_tier(src: str, caps: dict) -> str:
    """Return a path compliant with the account's caps — the source itself
    when it already complies, else a local transcode (tmp file the caller
    deletes). Duration violations are the user's edit to make, never a
    silent truncation."""
    p = _probe(src)
    if p["duration_s"] > caps["max_duration_s"] + 0.5:
        raise HTTPException(400, f"clip is {p['duration_s']:.0f}s — your tier caps at "
                                 f"{caps['max_duration_s']:.0f}s; trim it first")
    if p["height"] <= caps["max_height"] and p["fps"] <= caps["max_fps"]:
        return src
    out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
    vf = f"scale=-2:{min(p['height'], caps['max_height'])},format=yuv420p"
    r = subprocess.run(
        ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", "-i", src,
         "-map", "0:v:0", "-map", "0:a:0?", "-vf", vf,
         "-r", str(int(min(p["fps"], caps["max_fps"] - 1)) or 30),
         "-c:v", "libx264", "-preset", "medium", "-crf", "21",
         "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", out],
        capture_output=True, timeout=3600)
    if r.returncode != 0:
        os.unlink(out)
        raise HTTPException(500, "share transcode failed")
    return out


def publish_file(path: str, title: str) -> dict:
    me = _api("GET", "/v1/me")
    caps = me["caps"]
    upload_src = encode_for_tier(path, caps)
    try:
        p = _probe(upload_src)
        up = _api("POST", "/v1/uploads", {
            "filename": os.path.basename(path), "title": title[:140],
            "bytes": p["bytes"], "duration_s": p["duration_s"],
            "height": p["height"], "fps": p["fps"]})
        with open(upload_src, "rb") as fh:
            req = urllib.request.Request(up["put_url"], data=fh.read(), method="PUT")
            urllib.request.urlopen(req, timeout=1800)
        done = _api("POST", f"/v1/uploads/{up['upload_id']}/complete", body={}, timeout=300)
    finally:
        if upload_src != path:
            os.unlink(upload_src)
    return {"slug": done["slug"], "share": _base() + done["url"],
            "transcoded": upload_src != path}


class SharePublisher:
    ID = "share"
    LABEL = "Share online"

    def ready(self):
        return connected()

    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 _api("GET", "/v1/shares")["shares"]

    def delete(self, slug):
        _api("DELETE", f"/v1/shares/{slug}")


# ------------------------------------------------------------------ routes
@router.post("/api/share/login")
async def share_login(request: Request):
    """Connect this install to a share service: log in (or register) and
    persist url + token in config.json."""
    body = await request.json()
    url = str(body.get("url") or _base() or "").rstrip("/")
    if not url.startswith("http"):
        raise HTTPException(400, "need the share service url")
    config.CFG["share_url"] = url
    creds = {"email": str(body.get("email") or ""), "password": str(body.get("password") or "")}
    if body.get("register"):
        creds["handle"] = str(body.get("handle") or "")
        tok = _api("POST", "/v1/auth/register", creds, token="")["token"]
    else:
        tok = _api("POST", "/v1/auth/login", creds, token="")["token"]
    config.CFG["share_token"] = tok
    config.save(config.CFG)
    return {"status": "success", "me": _api("GET", "/v1/me")}


@router.post("/api/share/logout")
def share_logout():
    config.CFG["share_token"] = ""
    config.save(config.CFG)
    return {"status": "success"}


@router.get("/api/share/me")
def share_me():
    if not connected():
        return {"connected": False}
    return {"connected": True, "url": _base(), "me": _api("GET", "/v1/me")}


@router.post("/api/share/publish")
async def share_publish(request: Request):
    """Share a finished render ({job}) or any local clip url ({src}).

    Routing: explicit {"via": "hosted"|"selfhost"} wins; otherwise the
    hosted account when connected, else the user's own domain when
    self-hosting is configured."""
    from clipshelf import selfhost
    body = await request.json()
    via = str(body.get("via") or "")
    if not via:
        via = "hosted" if connected() else ("selfhost" if selfhost.configured() else "")
    if via == "hosted" and not connected():
        raise HTTPException(400, "not connected to a share service — log in first")
    if via == "selfhost" and not selfhost.configured():
        raise HTTPException(400, "self-hosting isn't configured — see advanced sharing settings")
    if not via:
        raise HTTPException(400, "no share destination — log in to a share service, "
                                 "or configure your own domain in advanced settings")
    title = str(body.get("title") or "clip")
    if body.get("job"):
        from clipshelf.config import RENDERS
        from clipshelf.routers.editor_common import slugify
        path = RENDERS / f"{slugify(str(body['job']))}.mp4"
        if not path.exists():
            raise HTTPException(404, "render not found")
        path = str(path)
    elif body.get("src"):
        try:
            path = resolve_media(str(body["src"]))
        except ValueError as e:
            raise HTTPException(400, str(e))
    else:
        raise HTTPException(400, "need job or src")
    import asyncio
    fn = selfhost.publish_file if via == "selfhost" else publish_file
    res = await asyncio.to_thread(fn, path, title)
    if body.get("src"):     # remember it so the clips archive can show the link
        from clipshelf import clips_api
        clips_api.record_share(str(body["src"]), res["share"], via, res.get("slug", ""))
    return {"status": "success", "via": via, **res}


@router.get("/api/share/list")
def share_list():
    if not connected():
        return {"shares": []}
    return _api("GET", "/v1/shares")


@router.delete("/api/share/{slug}")
def share_delete(slug: str):
    if not connected():
        raise HTTPException(400, "not connected")
    _api("DELETE", f"/v1/shares/{slug}")
    return {"status": "success"}
