"""Admin REST endpoints for the news_template entity.

The single news entity (intro + content) is versioned in `news_template`;
this module is the HTTP surface for admin authoring. Web rendering of
the published version happens in `app.web.router.news_page` and the
`/home` handler — those don't go through this API.

Every endpoint is admin-gated; no analyst-side reads here. The /preview
endpoint runs the sanitizer without persisting so the admin UI can show
a live render before saving.
"""

from __future__ import annotations

import duckdb
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

from app.auth.access import require_admin
from app.auth.dependencies import _get_db
from src.repositories.audit import AuditRepository
from src.repositories.news_template import (
    AlreadyDraftError,
    NewsTemplateRepository,
    NoDraftError,
    NotFoundError,
    VersionConflictError,
)
from src.sanitize_news import sanitize


router = APIRouter(prefix="/api/admin/news", tags=["news"])


class NewsBody(BaseModel):
    intro: str = ""
    content: str = ""


def _serialize(row: dict | None) -> dict | None:
    """Convert datetime fields to ISO strings for JSON. Pydantic does this
    automatically when fields are typed; we use a free-form dict here so
    do it by hand."""
    if row is None:
        return None
    out = dict(row)
    for k in ("created_at", "updated_at", "published_at"):
        v = out.get(k)
        if v is not None and hasattr(v, "isoformat"):
            out[k] = v.isoformat()
    return out


# -- read endpoints -----------------------------------------------------


@router.get("/current", dependencies=[Depends(require_admin)])
def get_current(conn: duckdb.DuckDBPyConnection = Depends(_get_db)):
    """Latest published version (or {published: false} if none)."""
    row = NewsTemplateRepository(conn).get_current_published()
    if row is None:
        return {"published": False}
    return _serialize(row)


@router.get("/draft", dependencies=[Depends(require_admin)])
def get_draft(conn: duckdb.DuckDBPyConnection = Depends(_get_db)):
    """Active draft. 404 if none — UI shows 'create new draft' button."""
    row = NewsTemplateRepository(conn).get_active_draft()
    if row is None:
        raise HTTPException(status_code=404, detail="no_draft")
    return _serialize(row)


@router.get("/versions", dependencies=[Depends(require_admin)])
def list_versions(
    limit: int = 50,
    offset: int = 0,
    conn: duckdb.DuckDBPyConnection = Depends(_get_db),
):
    rows = NewsTemplateRepository(conn).list_versions(limit=limit, offset=offset)
    return {"versions": [_serialize(r) for r in rows]}


@router.get("/versions/{version}", dependencies=[Depends(require_admin)])
def get_version(
    version: int,
    conn: duckdb.DuckDBPyConnection = Depends(_get_db),
):
    row = NewsTemplateRepository(conn).get_version(version)
    if row is None:
        raise HTTPException(status_code=404, detail="version_not_found")
    return _serialize(row)


# -- write endpoints ----------------------------------------------------


@router.put("/draft", dependencies=[Depends(require_admin)])
def put_draft(
    body: NewsBody,
    expected_version: int | None = None,
    user: dict = Depends(require_admin),
    conn: duckdb.DuckDBPyConnection = Depends(_get_db),
):
    """Upsert the active draft. Sanitizes both fields BEFORE writing.

    Optimistic-lock: when `expected_version` is supplied (query string),
    the request fails with 409 unless the active draft is at that
    version. Pass `expected_version=0` when you intend to create the
    first draft and want the call to fail if another admin already
    started one.
    """
    repo = NewsTemplateRepository(conn)
    try:
        row = repo.save_draft(
            intro=body.intro,
            content=body.content,
            by=user["email"],
            expected_version=expected_version,
        )
    except VersionConflictError as e:
        raise HTTPException(
            status_code=409,
            detail={
                "error": "version_conflict",
                "expected": e.expected,
                "actual": e.actual,
                "actual_by": e.actual_by,
            },
        ) from e
    AuditRepository(conn).log(
        user_id=user["id"],
        action="news_draft_saved",
        params={"version": row["version"], "by": user["email"]},
        result="ok",
    )
    return _serialize(row)


@router.post("/publish", dependencies=[Depends(require_admin)])
def post_publish(
    expected_version: int | None = None,
    user: dict = Depends(require_admin),
    conn: duckdb.DuckDBPyConnection = Depends(_get_db),
):
    """Publish the active draft.

    When `expected_version` is supplied (query string), the request
    fails with 409 unless the active draft is at that version. Use
    this when reviewing a specific draft before flipping it live so
    a concurrent admin's edit doesn't slip through under your name.
    """
    repo = NewsTemplateRepository(conn)
    try:
        row = repo.publish_draft(by=user["email"], expected_version=expected_version)
    except NoDraftError as e:
        raise HTTPException(status_code=409, detail="no_draft") from e
    except VersionConflictError as e:
        raise HTTPException(
            status_code=409,
            detail={
                "error": "version_conflict",
                "expected": e.expected,
                "actual": e.actual,
                "actual_by": e.actual_by,
            },
        ) from e
    AuditRepository(conn).log(
        user_id=user["id"],
        action="news_published",
        params={"version": row["version"], "by": user["email"]},
        result="ok",
    )
    return _serialize(row)


@router.post("/unpublish/{version}", dependencies=[Depends(require_admin)])
def post_unpublish(
    version: int,
    user: dict = Depends(require_admin),
    conn: duckdb.DuckDBPyConnection = Depends(_get_db),
):
    repo = NewsTemplateRepository(conn)
    try:
        row = repo.unpublish(version=version, by=user["email"])
    except NotFoundError as e:
        raise HTTPException(status_code=404, detail="version_not_found") from e
    except AlreadyDraftError as e:
        raise HTTPException(status_code=409, detail=str(e)) from e
    AuditRepository(conn).log(
        user_id=user["id"],
        action="news_unpublished",
        params={"version": row["version"], "by": user["email"]},
        result="ok",
    )
    return _serialize(row)


@router.post("/preview", dependencies=[Depends(require_admin)])
def post_preview(body: NewsBody):
    """Sanitize candidate intro + content and return — no DB writes."""
    return {"intro": sanitize(body.intro), "content": sanitize(body.content)}
