"""Public, login-free surface for shared download links (the `/watch/{token}` player page).

These routes are deliberately OUTSIDE the auth/`require_human` gate — the unguessable token IS the
credential (a capability URL). They only ever expose one file's stream + minimal metadata, never
the app or any account data. Password-protected links exchange the password once at `/unlock` for
a short-lived signed grant that the media URL carries (a `<video src>` can't send a header).
"""

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.config import settings
from app.db import get_db
from app.downloads import links as linksmod
from app.downloads import storage
from app.models import DownloadJob, DownloadLink, MediaAsset
from app.ratelimit import RateLimiter
from app.security import verify_password

router = APIRouter(prefix="/api/public", tags=["public"])

# Slow password guessing per link (the token itself is 192-bit, so it needs no throttle).
_unlock_limit = RateLimiter(max_events=10, window_seconds=300)


def _link_or_404(db: Session, token: str) -> DownloadLink:
    link = db.execute(
        select(DownloadLink).where(DownloadLink.token == token)
    ).scalar_one_or_none()
    if link is None or linksmod.is_expired(link):
        raise HTTPException(status_code=404, detail="This link is no longer available.")
    return link


def _ready_target(db: Session, link: DownloadLink) -> tuple[DownloadJob, MediaAsset]:
    job = db.get(DownloadJob, link.job_id)
    asset = db.get(MediaAsset, job.asset_id) if job and job.asset_id else None
    if job is None or asset is None or asset.status != "ready" or not asset.rel_path:
        raise HTTPException(status_code=404, detail="This video is no longer available.")
    # Verify the file is actually on disk here (not just flagged ready), so the watch page never
    # loads a player for a missing file — meta and file agree.
    path = storage.safe_abs_path(settings.download_root, asset.rel_path)
    if path is None:
        raise HTTPException(status_code=404, detail="This video is no longer available.")
    return job, asset


def _ready_asset(db: Session, link: DownloadLink) -> MediaAsset:
    return _ready_target(db, link)[1]


def _file_url(token: str, grant: str | None = None) -> str:
    base = f"/api/public/watch/{token}/file"
    return f"{base}?g={grant}" if grant else base


def _poster_url(token: str, asset, grant: str | None = None) -> str | None:
    if not asset.poster_path:
        return None
    base = f"/api/public/watch/{token}/poster.jpg"
    return f"{base}?g={grant}" if grant else base


@router.get("/watch/{token}")
def watch_meta(token: str, db: Session = Depends(get_db)) -> dict:
    """Public metadata for the watch page. For a password link, returns only `needs_password`
    until the viewer unlocks it (no title/thumbnail leak)."""
    link = _link_or_404(db, token)
    if link.password_hash:
        return {"needs_password": True}
    job, asset = _ready_target(db, link)
    link.view_count += 1
    db.commit()
    return {
        "needs_password": False,
        **linksmod.public_meta(link, job, asset, _file_url(token), _poster_url(token, asset)),
    }


@router.post("/watch/{token}/unlock")
def watch_unlock(token: str, payload: dict, db: Session = Depends(get_db)) -> dict:
    """Exchange the link password for a signed grant carried by the media URL."""
    if not _unlock_limit.allow(token):
        raise HTTPException(status_code=429, detail="Too many attempts. Try again later.")
    link = _link_or_404(db, token)
    if not link.password_hash:
        # Not a password link — nothing to unlock; just hand back the plain meta.
        job, asset = _ready_target(db, link)
        return {
            "needs_password": False,
            **linksmod.public_meta(link, job, asset, _file_url(token), _poster_url(token, asset)),
        }
    if not verify_password((payload.get("password") or ""), link.password_hash):
        raise HTTPException(status_code=403, detail="Wrong password.")
    job, asset = _ready_target(db, link)
    link.view_count += 1
    db.commit()
    grant = linksmod.make_grant(token)
    return {
        "needs_password": False,
        **linksmod.public_meta(
            link, job, asset, _file_url(token, grant), _poster_url(token, asset, grant)
        ),
    }


@router.get("/watch/{token}/file")
def watch_file(token: str, g: str | None = None, db: Session = Depends(get_db)):
    """Range-aware stream of the shared file. Inline by default (plays in the page); an
    `allow_download` link serves it as an attachment. Password links require a valid grant."""
    link = _link_or_404(db, token)
    if link.password_hash and not linksmod.check_grant(token, g):
        raise HTTPException(status_code=403, detail="This link needs a password.")
    asset = _ready_asset(db, link)
    path = storage.safe_abs_path(settings.download_root, asset.rel_path)
    if path is None:
        raise HTTPException(status_code=404, detail="This video is no longer available.")

    filename = storage.download_filename(asset.title or "video", asset.container, path)
    disposition = "attachment" if link.allow_download else "inline"
    # Starlette's FileResponse honours the Range header (206) for seeking/scrubbing.
    return FileResponse(path, filename=filename, content_disposition_type=disposition)


@router.get("/watch/{token}/poster.jpg")
def watch_poster(token: str, g: str | None = None, db: Session = Depends(get_db)):
    """The generated poster frame for a shared thumbnail-less video (used as the page/video poster
    and the link-preview image). Password links require a valid grant, like the file."""
    link = _link_or_404(db, token)
    if link.password_hash and not linksmod.check_grant(token, g):
        raise HTTPException(status_code=403, detail="This link needs a password.")
    job, asset = _ready_target(db, link)
    if not asset.poster_path:
        raise HTTPException(status_code=404, detail="No poster.")
    path = storage.safe_abs_path(settings.download_root, asset.poster_path)
    if path is None:
        raise HTTPException(status_code=404, detail="No poster.")
    return FileResponse(path, media_type="image/jpeg")
