# src/plotsrv/app.py
from __future__ import annotations

import base64
import time
from pathlib import Path
from typing import Any

import pandas as pd
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import Response, HTMLResponse
from fastapi.staticfiles import StaticFiles

from . import store, config
from . import html as html_mod
from .backends import df_to_rich_sample
from .ui_config import get_ui_settings
from .renderers import register_default_renderers
from .renderers.registry import render_any
from .storage.worker import enqueue_snapshot
from .storage.backend import list_snapshots, load_snapshot

app = FastAPI()
register_default_renderers()

# Static files shipped inside plotsrv package (logo, etc.)
STATIC_DIR = Path(__file__).resolve().parent / "static"
if STATIC_DIR.exists():
    app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")


def _ensure_assets_mount() -> None:
    """
    Mount /assets based on current runtime UI settings.

    Lazy- so CLI args (--config/--name) are already
    applied before we resolve paths.
    """
    ui = get_ui_settings()
    assets_dir = ui.assets_dir
    if assets_dir is None or not assets_dir.exists():
        return

    existing = app.router.routes
    for route in existing:
        if getattr(route, "path", None) == "/assets":
            current_dir = getattr(getattr(route, "app", None), "directory", None)
            if current_dir == str(assets_dir):
                return

    app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")


def _storage_root() -> Path:
    return config.get_storage_root_dir()


def _snapshot_summary_dict(snap: Any) -> dict[str, Any]:
    return {
        "snapshot_id": snap.snapshot_id,
        "view_id": snap.view_id,
        "section": snap.section,
        "label": snap.label,
        "kind": snap.kind,
        "created_at": snap.created_at,
        "payload_filename": snap.payload_filename,
        "payload_format": snap.payload_format,
        "size_bytes": snap.size_bytes,
        "payload_exists": snap.payload_exists,
        "extra": snap.extra or {},
    }


def _load_snapshot_or_404(*, view_id: str, snapshot_id: str):
    try:
        return load_snapshot(
            root_dir=_storage_root(),
            view_id=view_id,
            snapshot_id=snapshot_id,
        )
    except LookupError as e:
        raise HTTPException(status_code=404, detail=str(e))


def _render_plot_snapshot_html(*, view_id: str, snapshot_id: str) -> dict[str, Any]:
    src = f"/plot?view={view_id}&snapshot={snapshot_id}"
    html = f"""
    <div class="plot-frame">
      <img id="plot" src="{src}" alt="Plot snapshot" />
    </div>
    """.strip()

    return {
        "view_id": view_id,
        "snapshot_id": snapshot_id,
        "kind": "plot",
        "html": html,
        "mime": "text/html",
        "truncation": None,
        "meta": {
            "src": src,
            "snapshot": True,
        },
    }


def _render_table_snapshot_html(*, view_id: str, snapshot_id: str) -> dict[str, Any]:
    data_src = f"/table/data?view={view_id}&snapshot={snapshot_id}"
    html = """
    <div class="plot-frame">
      <div id="table-grid" class="table-grid"></div>
    </div>
    """.strip()

    return {
        "view_id": view_id,
        "snapshot_id": snapshot_id,
        "kind": "table",
        "html": html,
        "mime": "text/html",
        "truncation": None,
        "meta": {
            "data_src": data_src,
            "snapshot": True,
        },
    }


@app.get("/status")
def status(view: str | None = None) -> dict[str, object]:
    """
    Status for a single view (default: active view).
    """
    vid = view or store.get_active_view_id()
    s = store.get_status(view_id=vid)
    s.update(store.get_service_info())
    s["view_id"] = vid
    s["freshness"] = store.get_freshness(view_id=vid)
    return s


@app.get("/history")
def get_history(view: str | None = None) -> dict[str, Any]:
    """
    List stored snapshots for a view, newest first.
    """
    vid = view or store.get_active_view_id()
    snaps = list_snapshots(root_dir=_storage_root(), view_id=vid)

    return {
        "view_id": vid,
        "count": len(snaps),
        "snapshots": [_snapshot_summary_dict(s) for s in snaps],
    }


@app.get("/plot")
def get_plot(
    download: bool = False,
    view: str | None = None,
    snapshot: str | None = None,
) -> Response:
    """
    Return the current plot PNG for a view, or a historical snapshot if requested.
    """
    vid = view or store.get_active_view_id()

    if snapshot:
        loaded = _load_snapshot_or_404(view_id=vid, snapshot_id=snapshot)
        if str(loaded.meta.kind).strip().lower() != "plot":
            raise HTTPException(
                status_code=400,
                detail=f"Snapshot {snapshot!r} is not a plot snapshot.",
            )
        png = loaded.obj
        if not isinstance(png, (bytes, bytearray)):
            raise HTTPException(
                status_code=500,
                detail="Stored plot snapshot payload was not valid PNG bytes.",
            )
    else:
        try:
            png = store.get_plot(view_id=vid)
        except LookupError:
            raise HTTPException(
                status_code=404, detail="No plot has been published yet."
            )

    headers: dict[str, str] = {
        "Cache-Control": "no-store, max-age=0",
        "Pragma": "no-cache",
    }
    if download:
        filename = f"plotsrv_plot_{snapshot}.png" if snapshot else "plotsrv_plot.png"
        headers["Content-Disposition"] = f'attachment; filename="{filename}"'

    return Response(bytes(png), media_type="image/png", headers=headers)


@app.get("/table/data")
def get_table_data(
    limit: int = Query(default=config.get_max_table_rows_rich(), ge=1),
    view: str | None = None,
    snapshot: str | None = None,
) -> dict[str, Any]:
    vid = view or store.get_active_view_id()

    if snapshot:
        loaded = _load_snapshot_or_404(view_id=vid, snapshot_id=snapshot)
        if str(loaded.meta.kind).strip().lower() != "table":
            raise HTTPException(
                status_code=400,
                detail=f"Snapshot {snapshot!r} is not a table snapshot.",
            )
        df = loaded.obj
        if not isinstance(df, pd.DataFrame):
            raise HTTPException(
                status_code=500,
                detail="Stored table snapshot payload was not a DataFrame.",
            )

        max_rows = min(limit, config.get_max_table_rows_rich())
        rows_df = df.head(max_rows)
        columns = list(rows_df.columns)
        rows = rows_df.to_dict(orient="records")

        total_rows = None
        returned_rows = None
        if isinstance(loaded.meta.extra, dict):
            raw_total = loaded.meta.extra.get("total_rows")
            raw_returned = loaded.meta.extra.get("returned_rows")
            total_rows = raw_total if isinstance(raw_total, int) else None
            returned_rows = raw_returned if isinstance(raw_returned, int) else None

        total_rows = total_rows if total_rows is not None else len(df)
        returned_rows = returned_rows if returned_rows is not None else len(rows)

        return {
            "columns": columns,
            "rows": rows,
            "total_rows": total_rows,
            "returned_rows": returned_rows,
            "snapshot_id": snapshot,
        }

    if not store.has_table(view_id=vid):
        raise HTTPException(status_code=404, detail="No table has been published yet.")

    df = store.get_table_df(view_id=vid)
    max_rows = min(limit, config.get_max_table_rows_rich())

    rows_df = df.head(max_rows)
    columns = list(rows_df.columns)
    rows = rows_df.to_dict(orient="records")

    total_rows, returned_rows = store.get_table_counts(view_id=vid)

    total_rows = total_rows if total_rows is not None else len(df)
    returned_rows = returned_rows if returned_rows is not None else len(rows)

    return {
        "columns": columns,
        "rows": rows,
        "total_rows": total_rows,
        "returned_rows": returned_rows,
    }


@app.get("/table/export")
def export_table(
    format: str = "csv",
    view: str | None = None,
    snapshot: str | None = None,
) -> Response:
    """
    Export the current table (CSV for now), or a historical table snapshot.
    """
    vid = view or store.get_active_view_id()

    fmt = (format or "csv").lower().strip()
    if fmt != "csv":
        raise HTTPException(
            status_code=400, detail="Only format=csv is supported right now."
        )

    if snapshot:
        loaded = _load_snapshot_or_404(view_id=vid, snapshot_id=snapshot)
        if str(loaded.meta.kind).strip().lower() != "table":
            raise HTTPException(
                status_code=400,
                detail=f"Snapshot {snapshot!r} is not a table snapshot.",
            )
        df = loaded.obj
        if not isinstance(df, pd.DataFrame):
            raise HTTPException(
                status_code=500,
                detail="Stored table snapshot payload was not a DataFrame.",
            )

        csv_bytes = df.to_csv(index=False).encode("utf-8")
        headers = {
            "Content-Disposition": f'attachment; filename="plotsrv_table_{snapshot}.csv"',
        }
        return Response(csv_bytes, media_type="text/csv", headers=headers)

    if not store.has_table(view_id=vid):
        raise HTTPException(status_code=404, detail="No table has been published yet.")

    df = store.get_table_df(view_id=vid)
    csv_bytes = df.to_csv(index=False).encode("utf-8")
    headers = {
        "Content-Disposition": 'attachment; filename="plotsrv_table.csv"',
    }
    return Response(csv_bytes, media_type="text/csv", headers=headers)


@app.post("/publish")
def publish(payload: dict[str, Any]) -> dict[str, Any]:
    """
    Publish a plot or table into a specific view.

    Expected payload (flexible):
      {
        "view_id": "etl-1:import",   # optional
        "section": "etl-1",          # optional
        "label": "import",           # optional
        "kind": "plot"|"table",
        "plot_png_b64": "...",       # if plot
        "table": {                   # if table
          "columns": [...],
          "rows": [...],
          "total_rows": 123,
          "returned_rows": 100
        },
        "table_html_simple": "<table>...</table>",  # optional
        "update_limit_s": 600,        # optional throttling
        "force": false                # optional bypass throttling
      }
    """
    kind = str(payload.get("kind") or "").strip().lower()
    if kind not in ("plot", "table", "artifact"):
        raise HTTPException(
            status_code=422,
            detail="publish: kind must be 'plot', 'table', or 'artifact'",
        )

    section = payload.get("section")
    label = payload.get("label")
    view_id = store.normalize_view_id(
        payload.get("view_id"), section=section, label=label
    )

    publish_source_raw = payload.get("publish_source")
    publish_source = (
        str(publish_source_raw).strip().lower()
        if isinstance(publish_source_raw, str) and publish_source_raw.strip()
        else None
    )

    store.register_view(
        view_id=view_id,
        section=section,
        label=label,
        kind="none",
        icon_key="unknown",
        activate_if_first=False,
    )
    current_active = store.get_active_view_id()
    known_view_ids = {v.view_id for v in store.list_views()}
    if current_active not in known_view_ids:
        store.set_active_view(view_id)

    update_limit_s = payload.get("update_limit_s")
    force = bool(payload.get("force") or False)

    now_s = time.time()
    if not force:
        if not store.should_accept_publish(
            view_id=view_id, update_limit_s=update_limit_s, now_s=now_s
        ):
            return {
                "ok": True,
                "ignored": True,
                "reason": "throttled",
                "view_id": view_id,
            }

    if kind == "plot":
        b64 = payload.get("plot_png_b64")
        if not b64:
            raise HTTPException(
                status_code=422,
                detail="publish: plot_png_b64 is required for kind='plot'",
            )

        try:
            png_bytes = base64.b64decode(b64.encode("utf-8"))
        except Exception:
            raise HTTPException(
                status_code=422, detail="publish: plot_png_b64 was not valid base64"
            )

        store.set_plot(png_bytes, view_id=view_id)
        store.mark_success(duration_s=None, view_id=view_id)
        store.note_publish(view_id, now_s=now_s)

        enqueue_snapshot(
            view_id=view_id,
            kind="plot",
            obj=png_bytes,
            section=section if isinstance(section, str) else None,
            label=label if isinstance(label, str) else None,
            source=publish_source,
        )

        return {"ok": True, "ignored": False, "view_id": view_id}

    elif kind == "artifact":
        artifact_kind = str(payload.get("artifact_kind") or "python").strip().lower()
        artifact_obj = payload.get("artifact")

        store.set_artifact(
            obj=artifact_obj,
            kind=artifact_kind,  # type: ignore[arg-type]
            label=label,
            section=section,
            view_id=view_id,
        )
        store.mark_success(duration_s=None, view_id=view_id)
        store.note_publish(view_id, now_s=now_s)

        enqueue_snapshot(
            view_id=view_id,
            kind=artifact_kind,
            obj=artifact_obj,
            section=section if isinstance(section, str) else None,
            label=label if isinstance(label, str) else None,
            source=publish_source,
        )

        return {"ok": True, "ignored": False, "view_id": view_id}

    elif kind == "table":
        table = payload.get("table")
        if not isinstance(table, dict):
            raise HTTPException(
                status_code=422,
                detail="publish: table dict is required for kind='table'",
            )

        cols = table.get("columns")
        rows = table.get("rows")
        if not isinstance(cols, list) or not isinstance(rows, list):
            raise HTTPException(
                status_code=422,
                detail="publish: table must include columns(list) and rows(list)",
            )

        total_rows = table.get("total_rows")
        returned_rows = table.get("returned_rows")

        if total_rows is not None and not isinstance(total_rows, int):
            total_rows = None
        if returned_rows is not None and not isinstance(returned_rows, int):
            returned_rows = None

        df = pd.DataFrame(rows, columns=cols)

        html_simple = payload.get("table_html_simple")
        if html_simple is not None and not isinstance(html_simple, str):
            html_simple = None

        store.set_table(
            df,
            html_simple=html_simple,
            view_id=view_id,
            total_rows=total_rows,
            returned_rows=returned_rows,
        )
        store.mark_success(duration_s=None, view_id=view_id)
        store.note_publish(view_id, now_s=now_s)

        enqueue_snapshot(
            view_id=view_id,
            kind="table",
            obj=df,
            section=section if isinstance(section, str) else None,
            label=label if isinstance(label, str) else None,
            extra={
                "total_rows": total_rows,
                "returned_rows": returned_rows,
            },
            source=publish_source,
        )

        return {"ok": True, "ignored": False, "view_id": view_id}


@app.get("/", response_class=HTMLResponse)
def index(view: str | None = None) -> HTMLResponse:
    """
    Main HTML viewer.

    Supports: /?view=<view_id>
    """
    if view:
        store.set_active_view(view)

    active_view = store.get_active_view_id()
    kind = store.get_kind(active_view)

    if store.has_artifact(view_id=active_view):
        art = store.get_artifact(view_id=active_view)
        if art.kind not in ("plot", "table"):
            kind = "artifact"

    table_html_simple = None
    if (
        kind == "table"
        and config.get_table_view_mode() == "simple"
        and store.has_table(view_id=active_view)
    ):
        try:
            table_html_simple = store.get_table_html_simple(view_id=active_view)
        except LookupError:
            table_html_simple = None

    ui = get_ui_settings()
    _ensure_assets_mount()

    html_str = html_mod.render_index(
        kind=kind,
        table_view_mode=config.get_table_view_mode(),
        table_html_simple=table_html_simple,
        max_table_rows_simple=config.get_max_table_rows_simple(),
        max_table_rows_rich=config.get_max_table_rows_rich(),
        ui_settings=ui,
        views=store.list_views(),
        active_view_id=active_view,
    )
    return HTMLResponse(content=html_str)


@app.get("/artifact")
def get_artifact(
    view: str | None = None,
    snapshot: str | None = None,
) -> dict[str, Any]:
    """
    Render the latest artifact for the view, or a historical snapshot if requested.
    """
    vid = view or store.get_active_view_id()

    if snapshot:
        loaded = _load_snapshot_or_404(view_id=vid, snapshot_id=snapshot)
        kind_hint = str(loaded.meta.kind).strip().lower()

        if kind_hint == "plot":
            return _render_plot_snapshot_html(view_id=vid, snapshot_id=snapshot)

        if kind_hint == "table":
            return _render_table_snapshot_html(view_id=vid, snapshot_id=snapshot)

        rr = render_any(loaded.obj, view_id=vid, kind_hint=kind_hint)
        return {
            "view_id": vid,
            "snapshot_id": snapshot,
            "kind": rr.kind,
            "html": rr.html,
            "mime": rr.mime,
            "truncation": (
                None
                if rr.truncation is None
                else {
                    "truncated": rr.truncation.truncated,
                    "reason": rr.truncation.reason,
                    "details": rr.truncation.details,
                }
            ),
            "meta": {
                **(rr.meta or {}),
                "snapshot": True,
                "snapshot_meta": _snapshot_summary_dict(loaded.meta),
            },
        }

    if not store.has_artifact(view_id=vid):
        raise HTTPException(
            status_code=404, detail="No artifact has been published yet."
        )

    art = store.get_artifact(view_id=vid)
    rr = render_any(art.obj, view_id=vid, kind_hint=art.kind)

    return {
        "view_id": vid,
        "kind": rr.kind,
        "html": rr.html,
        "mime": rr.mime,
        "truncation": (
            None
            if rr.truncation is None
            else {
                "truncated": rr.truncation.truncated,
                "reason": rr.truncation.reason,
                "details": rr.truncation.details,
            }
        ),
        "meta": rr.meta or {},
    }


@app.get("/views")
def get_views() -> list[dict[str, Any]]:
    return [
        {
            "view_id": v.view_id,
            "section": v.section,
            "label": v.label,
            "kind": v.kind,
            "icon_key": v.icon_key,
        }
        for v in store.list_views()
    ]
