"""Server-side Read Aloud library so saved books sync across devices.

Stores each document under the writable config volume:
    <CONFIG_DIR>/reader_library/<id>/
        meta.json              metadata (title, kind, idx, voice, counts, …)
        source.pdf | source.txt  the original document (for re-rendering)
        audio/<idx>.mp3        per-unit synthesised audio (incremental)

Endpoints are deliberately small (file I/O) and mirror the previous IndexedDB
shape so the frontend swap is mechanical.
"""
import json
import shutil
import uuid
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import FileResponse, Response

from core.constants import CONFIG_DIR

router = APIRouter()

_LIB = CONFIG_DIR / "reader_library"


def _lib() -> Path:
    _LIB.mkdir(parents=True, exist_ok=True)
    return _LIB


def _doc_dir(doc_id: str) -> Path:
    # ids are server-generated uuid hex; reject anything else (path safety)
    if not doc_id or not doc_id.isalnum() or len(doc_id) > 40:
        raise HTTPException(400, "Bad document id")
    return _lib() / doc_id


def _read_meta(d: Path) -> dict:
    try:
        return json.loads((d / "meta.json").read_text("utf-8"))
    except Exception:
        return {}


def _audio_indices(d: Path) -> list[int]:
    ad = d / "audio"
    if not ad.is_dir():
        return []
    out = []
    for f in ad.glob("*.mp3"):
        try:
            out.append(int(f.stem))
        except ValueError:
            pass
    return sorted(out)


@router.get("/api/reader/docs")
async def reader_list_docs():
    lib = _lib()
    docs = []
    for d in lib.iterdir():
        if not d.is_dir():
            continue
        meta = _read_meta(d)
        if not meta:
            continue
        meta["id"] = d.name
        meta["synthCount"] = len(_audio_indices(d))
        meta["hasCover"] = (d / "cover.jpg").is_file()
        docs.append(meta)
    docs.sort(key=lambda m: m.get("updated") or "", reverse=True)
    return {"docs": docs}


@router.post("/api/reader/docs")
async def reader_save_doc(request: Request):
    meta = await request.json()
    doc_id = str(meta.get("id") or "").strip()
    if doc_id:
        d = _doc_dir(doc_id)
        if not d.is_dir():
            raise HTTPException(404, "Document not found")
    else:
        doc_id = uuid.uuid4().hex
        d = _lib() / doc_id
        d.mkdir(parents=True, exist_ok=True)
        meta["created"] = meta.get("created")
    meta.pop("id", None)
    (d / "meta.json").write_text(json.dumps(meta, ensure_ascii=False), "utf-8")
    return {"id": doc_id}


@router.get("/api/reader/docs/{doc_id}")
async def reader_get_doc(doc_id: str):
    d = _doc_dir(doc_id)
    meta = _read_meta(d)
    if not meta:
        raise HTTPException(404, "Document not found")
    meta["id"] = doc_id
    meta["audioIdx"] = _audio_indices(d)
    return meta


@router.delete("/api/reader/docs/{doc_id}")
async def reader_delete_doc(doc_id: str):
    d = _doc_dir(doc_id)
    if d.is_dir():
        shutil.rmtree(d, ignore_errors=True)
    return {"ok": True}


@router.patch("/api/reader/docs/{doc_id}/progress")
async def reader_progress(doc_id: str, request: Request):
    d = _doc_dir(doc_id)
    meta = _read_meta(d)
    if not meta:
        raise HTTPException(404, "Document not found")
    body = await request.json()
    if "idx" in body:
        meta["idx"] = body["idx"]
    if "updated" in body:
        meta["updated"] = body["updated"]
    (d / "meta.json").write_text(json.dumps(meta, ensure_ascii=False), "utf-8")
    return {"ok": True}


@router.put("/api/reader/docs/{doc_id}/source")
async def reader_put_source(doc_id: str, request: Request, ext: str = "pdf"):
    d = _doc_dir(doc_id)
    if not d.is_dir():
        raise HTTPException(404, "Document not found")
    ext = "txt" if ext.lower() == "txt" else "pdf"
    body = await request.body()
    # clear any existing source of the other type
    for old in d.glob("source.*"):
        old.unlink()
    (d / f"source.{ext}").write_bytes(body)
    return {"ok": True, "bytes": len(body)}


@router.get("/api/reader/docs/{doc_id}/source")
async def reader_get_source(doc_id: str):
    d = _doc_dir(doc_id)
    for ext, media in (("pdf", "application/pdf"), ("txt", "text/plain")):
        f = d / f"source.{ext}"
        if f.is_file():
            return FileResponse(str(f), media_type=media)
    raise HTTPException(404, "Source not found")


@router.get("/api/reader/docs/{doc_id}/audio")
async def reader_list_audio(doc_id: str):
    return {"idx": _audio_indices(_doc_dir(doc_id))}


@router.put("/api/reader/docs/{doc_id}/audio/{idx}")
async def reader_put_audio(doc_id: str, idx: int, request: Request):
    d = _doc_dir(doc_id)
    if not d.is_dir():
        raise HTTPException(404, "Document not found")
    (d / "audio").mkdir(exist_ok=True)
    (d / "audio" / f"{idx}.mp3").write_bytes(await request.body())
    return {"ok": True}


@router.get("/api/reader/docs/{doc_id}/audio/{idx}")
async def reader_get_audio(doc_id: str, idx: int):
    f = _doc_dir(doc_id) / "audio" / f"{idx}.mp3"
    if not f.is_file():
        raise HTTPException(404, "Audio not found")
    return FileResponse(str(f), media_type="audio/mpeg")


@router.put("/api/reader/docs/{doc_id}/cover")
async def reader_put_cover(doc_id: str, request: Request):
    d = _doc_dir(doc_id)
    if not d.is_dir():
        raise HTTPException(404, "Document not found")
    body = await request.body()
    (d / "cover.jpg").write_bytes(body)
    return {"ok": True}


@router.get("/api/reader/docs/{doc_id}/cover")
async def reader_get_cover(doc_id: str):
    f = _doc_dir(doc_id) / "cover.jpg"
    if not f.is_file():
        raise HTTPException(404, "Cover not found")
    return FileResponse(str(f), media_type="image/jpeg", headers={"Cache-Control": "max-age=31536000"})
