"""Application FastAPI : pilote le pipeline et sert l'UI.

Toutes les routes lourdes (analyse, casting, rendu) sont *enfilees* dans
l'orchestrateur et rendent la main immediatement ; l'avancement arrive par
WebSocket. Les operations rapides (preview de voix) tournent dans un threadpool.
"""
from __future__ import annotations

import asyncio
import io
from pathlib import Path
from typing import Optional

import soundfile as sf
from fastapi import FastAPI, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from ..config import DATA_DIR, book_data_dir, book_output_dir, ensure_dirs
from ..epub.parser import load_book, load_chapter_text, parse_epub
from ..models import Cast, ChapterAnalysis, Pronunciation
from ..pipeline.orchestrator import load_state, orchestrator
from ..settings import Settings, get_settings, save_settings
from ..store import artifacts
from ..util import slugify
from .ws import manager

app = FastAPI(title="InkFlow API")
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)


@app.on_event("startup")
async def _startup() -> None:
    ensure_dirs()
    manager.bind_loop(asyncio.get_running_loop())
    orchestrator.set_broadcaster(manager.broadcast_threadsafe)


# --- Helpers -----------------------------------------------------------------

def _list_book_slugs() -> list[str]:
    if not DATA_DIR.exists():
        return []
    return sorted(p.parent.name for p in DATA_DIR.glob("*/book.json"))


def _book_summary(slug: str) -> dict:
    book = load_book(slug)
    state = load_state(slug)
    rendered = sum(1 for r in state.render.values() if r.mp3)
    return {
        "slug": slug,
        "title": book.title,
        "author": book.author,
        "chapters": len(book.render_chapters),
        "rendered": rendered,
        "cover": f"/api/books/{slug}/cover" if book.cover_file else None,
    }


# --- Bibliotheque / upload ---------------------------------------------------

@app.get("/api/books")
def list_books() -> list[dict]:
    return [_book_summary(s) for s in _list_book_slugs()]


@app.post("/api/books")
async def upload_book(file: UploadFile) -> dict:
    ensure_dirs()
    uploads = DATA_DIR / "_uploads"
    uploads.mkdir(parents=True, exist_ok=True)
    dest = uploads / (file.filename or "livre.epub")
    dest.write_bytes(await file.read())
    book = await asyncio.to_thread(parse_epub, dest)
    # Initialise l'etat.
    load_state(book.slug)
    return {"slug": book.slug, "title": book.title}


@app.get("/api/books/{slug}")
def get_book(slug: str) -> dict:
    _require(slug)
    book = load_book(slug)
    return {"book": book.model_dump(mode="json"),
            "state": load_state(slug).model_dump(mode="json")}


@app.get("/api/books/{slug}/cover")
def get_cover(slug: str):
    book = load_book(slug)
    if not book.cover_file:
        raise HTTPException(404, "pas de couverture")
    return FileResponse(str(book_data_dir(slug) / book.cover_file))


@app.get("/api/books/{slug}/chapters/{index}")
def get_chapter(slug: str, index: int) -> dict:
    _require(slug)
    book = load_book(slug)
    ch = next((c for c in book.chapters if c.index == index), None)
    if ch is None:
        raise HTTPException(404, "chapitre inconnu")
    out: dict = {"chapter": ch.model_dump(mode="json")}
    apath = artifacts.analysis_path(slug, index)
    if apath.exists():
        out["analysis"] = artifacts.load_analysis(slug, index).model_dump(mode="json")
    elif ch.text_file:
        out["text"] = load_chapter_text(slug, ch).model_dump(mode="json")
    return out


@app.put("/api/books/{slug}/chapters/{index}/analysis")
def put_analysis(slug: str, index: int, analysis: ChapterAnalysis) -> dict:
    _require(slug)
    if analysis.index != index:
        raise HTTPException(400, "index incoherent")
    artifacts.save_analysis(slug, analysis)
    return {"saved": True}


# --- Etapes (enfilees) -------------------------------------------------------

class ChaptersBody(BaseModel):
    chapters: Optional[list[int]] = None


@app.post("/api/books/{slug}/analyze")
def analyze(slug: str, body: ChaptersBody) -> dict:
    _require(slug)
    orchestrator.run_analyze(slug, body.chapters)
    return {"queued": True}


@app.post("/api/books/{slug}/pronounce")
def pronounce(slug: str) -> dict:
    _require(slug)
    orchestrator.run_pronounce(slug)
    return {"queued": True}


@app.post("/api/books/{slug}/cast/auto")
def cast_auto(slug: str) -> dict:
    _require(slug)
    orchestrator.run_cast(slug)
    return {"queued": True}


@app.post("/api/books/{slug}/cast/analyze")
def cast_analyze(slug: str, body: ChaptersBody) -> dict:
    """(Re)analyse le casting d'un/des chapitre(s) avec reconciliation."""
    _require(slug)
    orchestrator.run_cast_analyze(slug, body.chapters)
    return {"queued": True}


@app.post("/api/books/{slug}/cast/dedup")
def cast_dedup(slug: str) -> dict:
    """Deduplique le casting existant (variantes de noms -> aliases)."""
    _require(slug)
    orchestrator.run_dedup_cast(slug)
    return {"queued": True}


class RenderBody(BaseModel):
    chapters: list[int]
    backend: Optional[str] = None
    mono: bool = False


@app.post("/api/books/{slug}/render")
def render(slug: str, body: RenderBody) -> dict:
    _require(slug)
    orchestrator.run_render(slug, body.chapters, backend=body.backend, mono=body.mono)
    return {"queued": True}


# --- Casting / prononciation (lecture-ecriture directe) ----------------------

@app.get("/api/books/{slug}/cast")
def get_cast(slug: str) -> dict:
    from ..casting.voicebank import load_voicebank
    _require(slug)
    return {"cast": artifacts.load_cast(slug).model_dump(mode="json"),
            "voicebank": load_voicebank().model_dump(mode="json")}


@app.put("/api/books/{slug}/cast")
def put_cast(slug: str, cast: Cast) -> dict:
    _require(slug)
    artifacts.save_cast(slug, cast)
    return {"saved": True}


@app.get("/api/books/{slug}/pronunciation")
def get_pron(slug: str) -> dict:
    _require(slug)
    return artifacts.load_pronunciation(slug).model_dump(mode="json")


@app.put("/api/books/{slug}/pronunciation")
def put_pron(slug: str, pron: Pronunciation) -> dict:
    _require(slug)
    artifacts.save_pronunciation(slug, pron)
    return {"saved": True}


# --- Reglages techniques globaux ---------------------------------------------

@app.get("/api/settings")
def read_settings() -> dict:
    return get_settings().model_dump(mode="json")


@app.put("/api/settings")
def write_settings(settings: Settings) -> dict:
    save_settings(settings)
    return {"saved": True}


# --- Voicebank + preview -----------------------------------------------------

@app.get("/api/voicebank")
def get_voicebank() -> dict:
    from ..casting.voicebank import load_voicebank
    return load_voicebank().model_dump(mode="json")


class PreviewBody(BaseModel):
    voice_id: str
    text: str = "Bonjour, voici un aperçu de cette voix pour votre livre audio."


@app.post("/api/voicebank/preview")
async def preview_voice(body: PreviewBody):
    from ..casting.voicebank import load_voicebank
    from ..tts.base import VoiceSpec

    entry = load_voicebank().by_id(body.voice_id)
    if entry is None:
        raise HTTPException(404, "voix inconnue")

    def _synth() -> bytes:
        from ..tts.factory import get_backend
        backend = get_backend("kokoro")
        audio, sr = backend.synthesize(body.text, VoiceSpec(preset=entry.kokoro_voice))
        buf = io.BytesIO()
        sf.write(buf, audio, sr, format="WAV")
        return buf.getvalue()

    data = await asyncio.to_thread(_synth)
    return Response(content=data, media_type="audio/wav")


@app.get("/api/books/{slug}/audio/{index}")
def get_audio(slug: str, index: int):
    state = load_state(slug)
    rs = state.render.get(index)
    if not rs or not rs.mp3:
        raise HTTPException(404, "audio non genere")
    path = book_output_dir(load_book(slug).title) / rs.mp3
    if not path.exists():
        raise HTTPException(404, "fichier introuvable")
    return FileResponse(str(path), media_type="audio/mpeg", filename=rs.mp3)


# --- WebSocket ---------------------------------------------------------------

@app.websocket("/ws/{slug}")
async def ws_endpoint(ws: WebSocket, slug: str) -> None:
    await manager.connect(slug, ws)
    try:
        # Envoi de l'etat courant a la connexion.
        await ws.send_json({"type": "state", "state": load_state(slug).model_dump(mode="json")})
        while True:
            await ws.receive_text()  # garde la connexion ouverte
    except WebSocketDisconnect:
        manager.disconnect(slug, ws)
    except Exception:  # noqa: BLE001
        manager.disconnect(slug, ws)


def _require(slug: str) -> None:
    if not (book_data_dir(slug) / "book.json").exists():
        raise HTTPException(404, "livre inconnu")


# --- Service du frontend build (si present) ----------------------------------
_FRONTEND_DIST = Path(__file__).resolve().parents[2].parent / "frontend" / "dist"
if _FRONTEND_DIST.exists():
    app.mount("/", StaticFiles(directory=str(_FRONTEND_DIST), html=True), name="ui")
