"""`agnes admin news {show,draft,edit,publish,unpublish,versions,export}`.

Talks to the live server through the `/api/admin/news/*` endpoints
(PAT-authed). Direct-DB access is the wrong contract here because the
running server holds the DuckDB write lock — the CLI must coexist with
it. Same pattern as `agnes admin add-user`, `agnes admin set-password`,
etc.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

import typer

from cli.client import api_get, api_post, api_put

admin_news_app = typer.Typer(help="Admin: edit the /home + /news content")


def _exit_on_error(resp, expected_status: tuple[int, ...] = (200,)) -> dict:
    """Print server-side error detail and exit if `resp.status_code` is
    outside `expected_status`. Returns the parsed JSON body otherwise."""
    if resp.status_code in expected_status:
        try:
            return resp.json()
        except Exception:
            return {}
    detail = ""
    try:
        body = resp.json()
        detail = body.get("detail") if isinstance(body, dict) else body
    except Exception:
        detail = resp.text
    typer.echo(f"server returned {resp.status_code}: {detail}", err=True)
    raise typer.Exit(2)


def _print_row(row: dict | None, label: str = "news") -> None:
    if not row or row.get("published") is False and not row.get("intro") and not row.get("content"):
        # `current` returns {published: False} envelope when nothing's published.
        if row is None or (isinstance(row, dict) and row.get("published") is False and "version" not in row):
            typer.echo(f"({label}: none)")
            return
    typer.echo(f"version    : {row.get('version')}")
    typer.echo(f"status     : {'published' if row.get('published') else 'draft'}")
    typer.echo(f"created_at : {row.get('created_at') or ''}")
    typer.echo(f"updated_at : {row.get('updated_at') or ''}")
    typer.echo(f"created_by : {row.get('created_by') or ''}")
    if row.get("published"):
        typer.echo(f"pub_at     : {row.get('published_at') or ''}")
        typer.echo(f"pub_by     : {row.get('published_by') or ''}")
    typer.echo("-- intro --")
    typer.echo(row.get("intro") or "")
    typer.echo("-- content --")
    typer.echo(row.get("content") or "")


def _load_from_file(path: str) -> tuple[str, str]:
    """Parse `--from FILE` (or `-` for stdin). YAML or JSON object with
    `intro` + `content` keys. Returns (intro, content)."""
    if path == "-":
        text = sys.stdin.read()
    else:
        text = Path(path).read_text(encoding="utf-8")
    # Try YAML first (it is a superset of JSON). Fall back to JSON only
    # for YAMLError to avoid swallowing unrelated exceptions like
    # ImportError or KeyboardInterrupt.
    import yaml
    try:
        data = yaml.safe_load(text)
    except yaml.YAMLError:
        data = json.loads(text)
    if not isinstance(data, dict):
        raise typer.BadParameter("file must contain a mapping with `intro` and `content` keys")
    return str(data.get("intro") or ""), str(data.get("content") or "")


@admin_news_app.command("show")
def show(
    version: int = typer.Option(None, "--version", "-v", help="Show a specific version (default: current published)"),
):
    """Print the current published version, or a specific version."""
    if version is not None:
        resp = api_get(f"/api/admin/news/versions/{version}")
    else:
        resp = api_get("/api/admin/news/current")
    body = _exit_on_error(resp)
    if isinstance(body, dict) and body.get("published") is False and "version" not in body:
        typer.echo("(published: none)")
        return
    _print_row(body, label="published")


@admin_news_app.command("draft")
def draft():
    """Print the active draft (or 'no draft' if none)."""
    resp = api_get("/api/admin/news/draft")
    if resp.status_code == 404:
        typer.echo("(draft: none)")
        return
    body = _exit_on_error(resp)
    _print_row(body, label="draft")


@admin_news_app.command("edit")
def edit(
    intro: str = typer.Option(None, "--intro", help="Intro HTML (used on /home)"),
    content: str = typer.Option(None, "--content", help="Full content HTML (used on /news)"),
    from_file: str = typer.Option(None, "--from", help="Read {intro, content} from YAML/JSON file (`-` for stdin)"),
    expect_version: int = typer.Option(
        None, "--expect-version",
        help="Refuse the edit unless the active draft is at this version "
             "(0 = expect no draft). Guards against overwriting a draft a "
             "concurrent admin started or edited after your last fetch.",
    ),
    force: bool = typer.Option(
        False, "--force",
        help="Skip the local collision check that warns when a draft was "
             "created by another author. --expect-version still applies.",
    ),
):
    """Upsert the active draft. One of --from / (--intro and --content) is required.

    Without --expect-version or --force the CLI checks the active draft
    first and refuses to overwrite a draft authored by someone else,
    printing what's there. Pass --expect-version N (matching the version
    you reviewed) to confirm the overwrite, or --force to commit anyway.
    """
    if from_file is not None:
        intro_v, content_v = _load_from_file(from_file)
    elif intro is None and content is None:
        typer.echo("error: provide --from FILE or --intro / --content", err=True)
        raise typer.Exit(2)
    else:
        intro_v = intro or ""
        content_v = content or ""

    # Local collision check before sending the write. The server-side
    # expected_version guard is the authoritative race-free check; this
    # local pass exists so the CLI can show a friendlier "draft was
    # started by X" hint when the operator forgot --force.
    if not force and expect_version is None:
        resp = api_get("/api/admin/news/draft")
        if resp.status_code == 200:
            existing = resp.json()
            # Caller's identity comes from the PAT they're authenticated
            # with — we don't have that locally, so just show the draft
            # author and let the operator decide.
            typer.echo(
                f"warning: active draft v{existing['version']} was started "
                f"by {existing.get('created_by') or '(unknown)'} "
                f"(updated {existing.get('updated_at') or ''}). "
                f"Saving will overwrite their changes. Re-run with "
                f"`--expect-version {existing['version']}` to confirm or "
                f"`--force` to suppress this check.",
                err=True,
            )
            raise typer.Exit(2)

    qs = ""
    if expect_version is not None:
        qs = f"?expected_version={expect_version}"
    resp = api_put(
        f"/api/admin/news/draft{qs}",
        json={"intro": intro_v, "content": content_v},
    )
    if resp.status_code == 409:
        body = resp.json().get("detail") or {}
        typer.echo(
            f"error: version conflict: expected v{body.get('expected')}, "
            f"active draft is v{body.get('actual')} "
            f"(by {body.get('actual_by') or '?'}). "
            f"Run `agnes admin news draft` to inspect.",
            err=True,
        )
        raise typer.Exit(2)
    body = _exit_on_error(resp)
    typer.echo(f"saved draft v{body['version']}")


@admin_news_app.command("publish")
def publish(
    version: int = typer.Option(
        None, "--version",
        help="Refuse to publish unless the active draft is at this version. "
             "Guards against publishing a draft a concurrent admin replaced "
             "after you reviewed it. Without --version, publishes whatever "
             "the active draft is.",
    ),
):
    """Publish the active draft.

    With `--version N`, the publish only proceeds when the active draft
    is exactly v{N}. If another admin replaced the draft since your
    last review, the call refuses and tells you what's now active.
    """
    qs = "" if version is None else f"?expected_version={version}"
    resp = api_post(f"/api/admin/news/publish{qs}")
    if resp.status_code == 409:
        body = resp.json().get("detail")
        if body == "no_draft":
            typer.echo("no active draft to publish", err=True)
            raise typer.Exit(1)
        if isinstance(body, dict) and body.get("error") == "version_conflict":
            typer.echo(
                f"error: version conflict: expected draft v{body.get('expected')}, "
                f"active draft is v{body.get('actual')} "
                f"(by {body.get('actual_by') or '?'}). "
                f"Inspect with `agnes admin news draft`, then re-run with the "
                f"matching `--version`.",
                err=True,
            )
            raise typer.Exit(2)
    body = _exit_on_error(resp)
    typer.echo(f"published v{body['version']}")


@admin_news_app.command("unpublish")
def unpublish(
    version: int = typer.Argument(..., help="Version number to unpublish (becomes a draft)"),
):
    """Flip a published version back to draft state."""
    resp = api_post(f"/api/admin/news/unpublish/{version}")
    if resp.status_code == 404:
        typer.echo(f"version {version} not found", err=True)
        raise typer.Exit(1)
    if resp.status_code == 409:
        typer.echo(resp.json().get("detail") or "conflict", err=True)
        raise typer.Exit(2)
    body = _exit_on_error(resp)
    typer.echo(f"unpublished v{body['version']}")


@admin_news_app.command("versions")
def versions(limit: int = typer.Option(20, "--limit", help="Max rows to print")):
    """Table of versions: number, status, created_at, by, published_at."""
    resp = api_get(f"/api/admin/news/versions?limit={limit}")
    body = _exit_on_error(resp)
    rows = body.get("versions", [])
    if not rows:
        typer.echo("(no versions)")
        return
    typer.echo(f"{'v':>4}  {'status':<10}  {'created':<19}  {'by':<28}  {'published':<19}  intro")
    typer.echo("-" * 110)
    for r in rows:
        ca = (r.get("created_at") or "")[:19]
        pa = (r.get("published_at") or "")[:19]
        typer.echo(
            f"{r['version']:>4}  {r['status']:<10}  {ca:<19}  {(r.get('created_by') or ''):<28}  "
            f"{pa:<19}  {r.get('intro_preview') or ''}"
        )


@admin_news_app.command("export")
def export(path: str = typer.Argument(..., help="YAML file to write {intro, content} into")):
    """Dump the currently-published version to a YAML file."""
    import yaml
    resp = api_get("/api/admin/news/current")
    body = _exit_on_error(resp)
    if isinstance(body, dict) and body.get("published") is False and "version" not in body:
        typer.echo("no published version to export", err=True)
        raise typer.Exit(1)
    Path(path).write_text(
        yaml.safe_dump(
            {"intro": body.get("intro") or "", "content": body.get("content") or ""},
            allow_unicode=True,
            sort_keys=False,
            width=200,
        ),
        encoding="utf-8",
    )
    typer.echo(f"exported v{body.get('version')} to {path}")
