"""Douanier — ParVagues Audio-Intelligence API (the customs gate).

Walking skeleton (#22 scaffold + #36 vertical): a versioned FastAPI app with a
health check and ONE real, synchronous endpoint — /v1/analyze/emotion — guarded
by a hardcoded dev bearer. It proves the three risky seams end-to-end (engines
import · service runs · a client can auth & call) before we invest in the
SQLite auth/quota/jobs/cache machinery (#23–#27) that hardens it later.

  run:  cd armada/api && ~/.virtualenvs/douanier/bin/uvicorn app:app --port 8780
"""
import tempfile
from pathlib import Path

from fastapi import FastAPI, APIRouter, UploadFile, File, Header, HTTPException, Depends, Response
from fastapi.middleware.cors import CORSMiddleware

import auth
import cache
import config
from engines import ears

app = FastAPI(
    title="Douanier — ParVagues Audio-Intelligence API",
    version="0.1.0",
    description="Customs gate over the fleet's audio-intelligence engines. "
                "Versioned at /v1; SRE maps the public api.nech.pl path in. See SRE.md.",
    docs_url=f"/{config.VERSION}/docs",
    openapi_url=f"/{config.VERSION}/openapi.json",
)

# hexa lives in Shipow's Vercel world → allow cross-origin calls. Tighten to his
# origins once known (kept open in dev so the walking skeleton is easy to hit).
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)

v1 = APIRouter(prefix=f"/{config.VERSION}")


# ── auth (#23) — SQLite bearer tokens + per-engine scopes ───────────────────────
def require_scope(scope: str | None):
    """Dependency factory: resolve the bearer token to a Principal and (if `scope`
    is given) assert it carries that scope or the '*' wildcard. scope=None means
    "any valid token". 401 unknown/expired/revoked, 403 scoped out. The dev
    bootstrap (DOUANIER_DEV_TOKEN) is a local-only wildcard."""
    def dep(authorization: str = Header(default="")) -> auth.Principal:
        kind, _, token = authorization.partition(" ")
        if kind.lower() != "bearer" or not token:
            raise HTTPException(401, "missing bearer token")
        principal = auth.verify(token)
        if principal is None:
            raise HTTPException(401, "invalid, expired, or revoked token")
        if scope is not None and not principal.has_scope(scope):
            raise HTTPException(403, f"token lacks scope '{scope}'")
        return principal
    return dep


# ── health ──────────────────────────────────────────────────────────────────
@v1.get("/healthz")
def healthz():
    ok, hint = ears.available()
    return {
        "service": config.SERVICE,
        "version": config.VERSION,
        "status": "ok",
        "dev_token_set": bool(config.DEV_TOKEN),
        "engines": {"emotion": {"selected": config.EMOTION_ENGINE,
                                "available": ok, "hint": hint}},
    }


@v1.get("/me")
def whoami(principal: auth.Principal = Depends(require_scope("*"))):
    """Echo the calling token's identity — handy for onboarding smoke tests."""
    return {"account": principal.account, "scopes": sorted(principal.scopes),
            "expires_at": principal.expires_at}


# ── #36 walking-skeleton endpoint: synchronous emotion read ─────────────────────
@v1.post("/analyze/emotion")
async def analyze_emotion(response: Response, file: UploadFile = File(...),
                          _p: auth.Principal = Depends(require_scope("emotion"))):
    """Upload a short audio clip → valence/arousal + top emotions (CLAP).

    Content-addressed cache (#26): the same audio returns instantly on a repeat
    (X-Douanier-Cache: hit) instead of paying the ~CLAP compute again. Still
    synchronous on a miss; #28 layers the async job queue under this.
    """
    data = await file.read()
    limit = config.MAX_UPLOAD_MB * 1024 * 1024
    if len(data) > limit:
        raise HTTPException(413, f"file too large (> {config.MAX_UPLOAD_MB} MB)")
    suffix = Path(file.filename or "clip").suffix or ".wav"
    with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as tmp:
        tmp.write(data)
        tmp.flush()
        cid = cache.content_id(tmp.name)
        params = {"engine": config.EMOTION_ENGINE}
        hit = cache.get(cid, "emotion", params)
        if hit:
            response.headers["X-Douanier-Cache"] = "hit"
            return {"filename": file.filename, "content_id": cid, "emotion": hit["result"]}
        try:
            read = ears.emotion_read(tmp.name)
        except Exception as e:
            raise HTTPException(422, f"emotion analysis failed: {e}")
    cache.put(cid, "emotion", result=read, params=params)
    response.headers["X-Douanier-Cache"] = "miss"
    return {"filename": file.filename, "content_id": cid, "emotion": read}


app.include_router(v1)
