"""HTTP API (FastAPI).   uvicorn copilot.api:app --port 8000

POST /ask        question → cited answer + trace_id          (rate-limited per user, in LLM tokens)
GET  /search     hybrid search over the lessons
POST /feedback   👍/👎 for a trace_id; also sent to Langfuse as a score when it is configured
GET  /healthz    liveness + index size

The endpoint functions are plain `def`: retrieval is CPU-bound NumPy work, so FastAPI runs them in its
thread pool instead of blocking the event loop (see the FastAPI course).
"""

from __future__ import annotations

import os
from contextlib import asynccontextmanager
from typing import Literal

from fastapi import FastAPI, HTTPException, Query, Request
from opentelemetry.propagate import extract
from opentelemetry.trace import SpanKind
from pydantic import BaseModel, Field

from . import build
from .guardrails import RateLimiter


class AskRequest(BaseModel):
    question: str = Field(min_length=3, max_length=2_000)
    user_id: str = Field(default="anonymous", max_length=128)
    session_id: str | None = Field(default=None, max_length=128)
    course: str | None = Field(default=None, description="Limit to one course folder, e.g. 'sql'")


class Source(BaseModel):
    url: str
    title: str
    section: str


class AskResponse(BaseModel):
    answer: str
    grounded: bool
    sources: list[Source]
    trace_id: str
    input_tokens: int
    output_tokens: int
    cost_usd: float
    latency_ms: int


class Feedback(BaseModel):
    trace_id: str = Field(pattern=r"^[0-9a-f]{32}$")
    value: Literal[0, 1]
    comment: str | None = Field(default=None, max_length=1_000)


def _langfuse_score(fb: Feedback) -> bool:
    """Send feedback to Langfuse as a score on the same trace (OTel trace ids are Langfuse trace ids)."""
    if not os.environ.get("LANGFUSE_SECRET_KEY"):
        return False
    try:
        from langfuse import get_client
    except ImportError:
        return False
    get_client().create_score(trace_id=fb.trace_id, name="user_feedback", value=fb.value,
                              data_type="BOOLEAN", comment=fb.comment)
    return True


def create_app(copilot_factory=build, limiter: RateLimiter | None = None) -> FastAPI:
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        app.state.copilot = copilot_factory()                 # build the index once, at startup
        app.state.limiter = limiter or RateLimiter()
        app.state.feedback = []
        yield
        app.state.copilot.tel.shutdown()                      # flush telemetry on shutdown

    app = FastAPI(title="docs-copilot", version="1.0.0", lifespan=lifespan)

    @app.post("/ask", response_model=AskResponse)
    def ask(body: AskRequest, request: Request) -> AskResponse:
        copilot = request.app.state.copilot
        if not request.app.state.limiter.allow(body.user_id, estimated_tokens=1_500):
            raise HTTPException(status_code=429, detail="rate limit exceeded", headers={"Retry-After": "5"})
        with copilot.tel.tracer.start_as_current_span(
                "POST /ask", kind=SpanKind.SERVER, context=extract(request.headers),
                attributes={"http.request.method": "POST", "http.route": "/ask"}):
            a = copilot.ask(body.question, user_id=body.user_id, session_id=body.session_id, course=body.course)
        return AskResponse(answer=a.text, grounded=a.grounded,
                           sources=[Source(url=c.url, title=c.title, section=c.section) for c in a.sources],
                           trace_id=a.trace_id, input_tokens=a.input_tokens, output_tokens=a.output_tokens,
                           cost_usd=round(a.cost_usd, 8), latency_ms=round(a.latency_s * 1000))

    @app.get("/search", response_model=list[Source])
    def search(request: Request, q: str = Query(min_length=2, max_length=500),
               k: int = Query(default=5, ge=1, le=20), course: str | None = None) -> list[Source]:
        hits = request.app.state.copilot.index.search(q, k=k, course=course)
        return [Source(url=h.chunk.url, title=h.chunk.title, section=h.chunk.section) for h in hits]

    @app.post("/feedback", status_code=202)
    def feedback(fb: Feedback, request: Request) -> dict:
        request.app.state.feedback.append(fb)
        return {"stored": True, "sent_to_langfuse": _langfuse_score(fb)}

    @app.get("/healthz")
    def healthz(request: Request) -> dict:
        return {"status": "ok", "chunks": len(request.app.state.copilot.index)}

    return app


app = create_app()
