"""FastAPI service implementing the HTTP API (CONTRACT §3) + SSE (§4).

Endpoints:
  GET  /health            → {"status":"ok"}
  POST /runs              → multipart (file, month, provider); starts an async run
  GET  /runs/{id}         → full run snapshot (run row + planned_lines/invoices/results)
  GET  /runs/{id}/stream  → text/event-stream SSE of the live run conversation
  GET  /runs?limit=       → run list for the left rail

Runs execute as asyncio background tasks; the blocking pipeline runs in a worker
thread and streams events back through the in-process SSE broker.
"""

from __future__ import annotations

import asyncio
import logging
import tempfile
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional

from fastapi import FastAPI, Form, HTTPException, UploadFile
from fastapi.responses import StreamingResponse

from app.config import get_settings
from app.db import build_store
from app.runner import execute_online_run, parse_month
from app.streaming import broker, format_sse_event
from app.telemetry.otel import setup_telemetry

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Wire settings, telemetry and the persistence store for the app lifetime."""
    settings = get_settings()
    setup_telemetry(settings)
    app.state.settings = settings
    app.state.store = build_store(settings)
    app.state.tmpdir = tempfile.mkdtemp(prefix="invoice_match_")
    logger.info("Service started (provider default=%s).", settings.provider_default)
    try:
        yield
    finally:
        app.state.store.close()


app = FastAPI(title="Invoice Match Service", version="1.0.0", lifespan=lifespan)


@app.get("/health")
async def health() -> dict[str, str]:
    """Liveness probe (CONTRACT §3)."""
    return {"status": "ok"}


@app.post("/runs")
async def create_run(
    file: UploadFile,
    month: str = Form(...),
    provider: Optional[str] = Form(None),
) -> dict[str, object]:
    """Accept a workbook + month + provider and start an async reconciliation run."""
    settings = app.state.settings
    store = app.state.store

    try:
        year, month_num = parse_month(month)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

    chosen_provider = (provider or settings.provider_default).lower()
    if chosen_provider not in ("bedrock", "anthropic", "openai"):
        raise HTTPException(status_code=422, detail=f"unknown provider: {provider}")

    # Persist the upload to a stable temp path for the worker thread to read.
    suffix = Path(file.filename or "upload.xlsx").suffix or ".xlsx"
    fd_path = Path(app.state.tmpdir) / f"run_{month}_{file.filename or 'upload'}{suffix}"
    content = await file.read()
    fd_path.write_bytes(content)

    model = settings.model_id_for(chosen_provider)
    run_id = store.create_run(
        f"{year}-{month_num:02d}", chosen_provider, model, file.filename or "upload.xlsx"
    )

    loop = asyncio.get_running_loop()

    def emit(event: str, data: dict) -> None:
        broker.publish_threadsafe(loop, run_id, event, data)

    async def run_task() -> None:
        try:
            await asyncio.to_thread(
                execute_online_run,
                settings,
                store,
                run_id,
                str(fd_path),
                year,
                month_num,
                chosen_provider,
                emit,
            )
        except Exception:  # already recorded as failed + run.error emitted
            logger.exception("Background run %s failed", run_id)
        finally:
            await broker.close(run_id)
            try:
                fd_path.unlink(missing_ok=True)
            except OSError:
                pass

    asyncio.create_task(run_task())
    return {"run_id": run_id, "status": "pending"}


@app.get("/runs")
async def list_runs(limit: int = 50) -> dict[str, object]:
    """Return the most recent runs for the left rail (CONTRACT §3)."""
    return {"runs": app.state.store.list_runs(limit=limit)}


@app.get("/runs/{run_id}")
async def get_run(run_id: int) -> dict[str, object]:
    """Return the full run snapshot (CONTRACT §3)."""
    run = app.state.store.get_run(run_id)
    if run is None:
        raise HTTPException(status_code=404, detail="run not found")
    return run


@app.get("/runs/{run_id}/stream")
async def stream_run(run_id: int) -> StreamingResponse:
    """Stream the run's SSE events (CONTRACT §4)."""

    async def event_generator():
        async for event, data in broker.subscribe(run_id):
            yield format_sse_event(event, data)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )
