"""Document final : aperçu HTML fidèle, informations de mise en page, exports PDF/XLSX/ODS/CSV."""
from __future__ import annotations

import hashlib
from typing import Any, Literal
from urllib.parse import quote

from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.responses import HTMLResponse, Response
from sqlalchemy.orm import Session

from ..core import audit, isoweek
from ..core.db import get_db
from ..documents import exports, render
from ..models import ExportLog
from ..services import bilans as bilan_service
from ..services.auth import Principal
from .deps import ApiError, client_ip, require

router = APIRouter(prefix="/api/bilans/{y}/{w}/document", tags=["document final"])

Year = Path(ge=2000, le=2100)
Week = Path(ge=1, le=53)


def doc_options(
    paper: Literal["A4", "A3"] = "A4",
    orientation: Literal["portrait", "paysage"] = "portrait",
    fit: bool = True,
    allow_landscape: bool = False,
    logo: bool = True,
    observations: bool = True,
    details: bool = True,
    chart: bool = False,
    pagination: bool = True,
    generated: bool = True,
    title: str | None = Query(default=None, max_length=120),
    indicators: str | None = Query(default=None, max_length=600, pattern=r"^[0-9,]*$"),
    version: int | None = Query(default=None, ge=1),
) -> render.DocOptions:
    ids = [int(x) for x in indicators.split(",") if x] if indicators else None
    return render.DocOptions(
        paper=paper, orientation=orientation, fit=fit, allow_landscape=allow_landscape, include_logo=logo,
        include_observations=observations, include_details=details, include_chart=chart, pagination=pagination,
        show_generated_at=generated, title=" ".join(title.split()) if title else None, indicator_ids=ids,
        version=version,
    )


def _check(y: int, w: int) -> None:
    try:
        isoweek.validate_week(y, w)
    except ValueError as e:
        raise ApiError(422, str(e), "bad_week") from None


@router.get("", response_class=HTMLResponse)
def preview(y: int = Year, w: int = Week, opts: render.DocOptions = Depends(doc_options),
            db: Session = Depends(get_db), p: Principal = Depends(require("bilan.read"))) -> HTMLResponse:
    _check(y, w)
    try:
        html, layout, nonce = render.preview_html(db, y, w, opts)
    except bilan_service.BilanError as e:
        raise ApiError(e.status, e.message, e.code) from None
    csp = (f"default-src 'none'; style-src 'nonce-{nonce}'; font-src 'self'; img-src 'self' data:; "
           "frame-ancestors 'self'; base-uri 'none'; form-action 'none'; sandbox allow-same-origin")
    return HTMLResponse(html, headers={"Content-Security-Policy": csp, "Cache-Control": "no-store",
                                       "X-EDCF-Pages": str(layout.pages)})


@router.get("/layout")
def layout(y: int = Year, w: int = Week, opts: render.DocOptions = Depends(doc_options),
           db: Session = Depends(get_db), p: Principal = Depends(require("bilan.read"))) -> dict[str, Any]:
    _check(y, w)
    try:
        data = render.document_data(db, y, w, opts)
    except bilan_service.BilanError as e:
        raise ApiError(e.status, e.message, e.code) from None
    lay = render.compute_layout(data, opts)
    return {**render.layout_info(lay), "official": data["official"], "status_line": data["status_line"]}


@router.get("/page/{n}.png")
def pdf_page(n: int = Path(ge=1, le=20), y: int = Year, w: int = Week,
             opts: render.DocOptions = Depends(doc_options), db: Session = Depends(get_db),
             p: Principal = Depends(require("bilan.read"))) -> Response:
    """Aperçu « exact » : la page rastérisée du PDF réellement produit."""
    _check(y, w)
    pdf, lay, _ = render.render_pdf(db, y, w, opts)
    if n > lay.pages:
        raise ApiError(404, "Cette page n'existe pas.", "not_found")
    return Response(render.pdf_page_png(pdf, n), media_type="image/png",
                    headers={"Cache-Control": "no-store", "X-EDCF-Pages": str(lay.pages)})


def _attachment(name: str) -> str:
    return f"attachment; filename=\"{name.encode('ascii', 'ignore').decode()}\"; filename*=UTF-8''{quote(name)}"


@router.get("/export.{fmt}")
def export(request: Request, fmt: Literal["pdf", "xlsx", "ods", "csv"], y: int = Year, w: int = Week,
           opts: render.DocOptions = Depends(doc_options), db: Session = Depends(get_db),
           p: Principal = Depends(require("bilan.read", "export.create"))) -> Response:
    _check(y, w)
    bilan = bilan_service.find_bilan(db, y, w)
    if bilan is None:
        raise ApiError(404, "Aucun bilan n'existe pour cette semaine.", "not_found")
    pages = None
    try:
        if fmt == "pdf":
            body, lay, data = render.render_pdf(db, y, w, opts)
            pages = lay.pages
            mime = "application/pdf"
        else:
            data = render.document_data(db, y, w, opts)
            if fmt == "xlsx":
                body = exports.to_xlsx(data, p.user.display_name)
                mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            elif fmt == "ods":
                body = exports.to_ods(data, p.user.display_name)
                mime = "application/vnd.oasis.opendocument.spreadsheet"
            else:
                body = exports.to_csv(data)
                mime = "text/csv; charset=utf-8"
    except bilan_service.BilanError as e:
        raise ApiError(e.status, e.message, e.code) from None
    digest = hashlib.sha256(body).hexdigest()
    db.add(ExportLog(bilan_id=bilan.id, kind=fmt, options=opts.__dict__, pages=pages, size=len(body),
                     sha256=digest, created_by=p.user.id))
    audit.record(db, "export.created", user_id=p.user.id, username=p.user.username, target_type="bilan",
                 target_id=f"{y}-S{w:02d}", ip=client_ip(request),
                 details={"format": fmt, "pages": pages, "sha256": digest, "officiel": data["official"],
                          "version": data["version_no"], "options": opts.__dict__})
    db.commit()
    headers = {"Content-Disposition": _attachment(f"{exports.base_filename(data)}.{fmt}"),
               "Cache-Control": "no-store", "X-EDCF-SHA256": digest}
    if pages is not None:
        headers["X-EDCF-Pages"] = str(pages)
    return Response(body, media_type=mime, headers=headers)
