"""psyc Operations Cockpit — FastAPI + Jinja."""

from __future__ import annotations

from pathlib import Path
from typing import List

from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware

from psyc import db, log
from psyc.cockpit import adminauth, case_visuals, docker_view, inference, journey as journey_view
from psyc.lines import courier as courier_line
from psyc.lines import ledger as ledger_line
from psyc.lines import lookup as lookup_line
from psyc.lines import news as news_line
from psyc.lines import respond as respond_line
from psyc.lines import route as route_line
from psyc.lines import seal as seal_line
from psyc.lines import train as train_line
from psyc.lines.route import BlockedRoute, Route
from psyc.result import Err


HERE = Path(__file__).resolve().parent
TEMPLATES = Jinja2Templates(directory=str(HERE / "templates"))

log.configure()
_log = log.get(__name__)

app = FastAPI(title="psyc Operations Cockpit", version="0.1.0")
app.add_middleware(SessionMiddleware, secret_key=adminauth.session_secret(), max_age=3600)
app.mount("/static", StaticFiles(directory=str(HERE / "static")), name="static")


def _admin_ok(request: Request) -> bool:
    return bool(request.session.get("admin_ok"))


@app.get("/", response_class=HTMLResponse)
def index(request: Request) -> HTMLResponse:
    items = news_line.recent_items(limit=40)
    featured = news_line.featured_case()
    case_index: dict = {}
    for i in items:
        if i.kind == "case" and i.case_id and i.case_id not in case_index:
            got = db.get_case(i.case_id)
            if not isinstance(got, Err):
                case_index[i.case_id] = case_visuals.case_glyph_svg(got.value)
    buckets = news_line.bucket_items(items)
    return TEMPLATES.TemplateResponse(
        request,
        "home.html",
        {
            "kpis": news_line.kpis(),
            "buckets": buckets,
            "total_items": sum(len(b.items) for b in buckets),
            "feeds": news_line.feed_health(),
            "featured": featured,
            "featured_hero": case_visuals.case_hero_svg(featured) if featured else "",
            "case_glyphs": case_index,
        },
    )


@app.get("/cases", response_class=HTMLResponse)
def cases_list(request: Request) -> HTMLResponse:
    return index(request)


@app.get("/cases/{case_id}", response_class=HTMLResponse)
def case_detail(request: Request, case_id: str) -> HTMLResponse:
    result = db.get_case(case_id)
    if isinstance(result, Err):
        _log.info("cockpit.case.miss", case_id=case_id)
        raise HTTPException(status_code=404, detail=result.reason)
    case = result.value

    routes: List[Route] = []
    blocked: List[BlockedRoute] = []
    if case.classification.incident_type is not None:
        routes, blocked = route_line.plan(case)

    sealed_pkg = None
    if case.evidence.sealed_package_id:
        pkg_result = seal_line.load_package(case.evidence.sealed_package_id)
        if not isinstance(pkg_result, Err):
            sealed_pkg = pkg_result.value

    case_ledger = ledger_line.list_by_case(case_id, limit=50)

    return TEMPLATES.TemplateResponse(
        request,
        "case_detail.html",
        {"case": case, "routes": routes, "blocked": blocked, "sealed_pkg": sealed_pkg, "ledger": case_ledger},
    )


@app.get("/cases/{case_id}/journey", response_class=HTMLResponse)
def case_journey(request: Request, case_id: str) -> HTMLResponse:
    result = db.get_case(case_id)
    if isinstance(result, Err):
        raise HTTPException(status_code=404, detail=result.reason)
    beats = journey_view.build_journey(result.value)
    model_label = inference.adapter_name() or "rules"
    return TEMPLATES.TemplateResponse(
        request,
        "journey.html",
        {"case": result.value, "beats": beats, "model_label": model_label},
    )


@app.get("/ledger", response_class=HTMLResponse)
def ledger_view(request: Request) -> HTMLResponse:
    entries = ledger_line.list_recent(limit=200)
    total = ledger_line.count()
    return TEMPLATES.TemplateResponse(request, "ledger.html", {"entries": entries, "total": total})


@app.get("/train", response_class=HTMLResponse)
def train_view(request: Request) -> HTMLResponse:
    datasets = train_line.list_datasets()
    adapters = train_line.list_adapters()
    return TEMPLATES.TemplateResponse(request, "train.html", {"datasets": datasets, "adapters": adapters})


@app.get("/healthz")
def healthz() -> dict:
    return {"status": "ok"}


# PWA service worker — must live at the root so its scope is the whole site.
# Static file is on disk under /static/sw.js; this route just serves it from /.
@app.get("/sw.js", include_in_schema=False)
def service_worker() -> FileResponse:
    return FileResponse(
        HERE / "static" / "sw.js",
        media_type="application/javascript",
        headers={"Service-Worker-Allowed": "/", "Cache-Control": "no-cache"},
    )


@app.get("/api/inference-status")
def inference_status() -> dict:
    adapter = inference.server_adapter()
    return {"online": adapter is not None, "adapter": adapter}


@app.get("/lookup", response_class=HTMLResponse)
def lookup_view(request: Request, q: str = "") -> HTMLResponse:
    query = q.strip()
    matches = lookup_line.lookup(query) if query else []
    counts = {t: len(lookup_line.export_blocklist(t)) for t in lookup_line.IOC_TYPES}
    return TEMPLATES.TemplateResponse(
        request,
        "lookup.html",
        {
            "query": query,
            "matches": matches,
            "searched": bool(query),
            "total_iocs": db.ioc_count(),
            "counts": counts,
        },
    )


@app.get("/export/blocklist", response_class=PlainTextResponse)
def export_blocklist(type: str = "ip", min_severity: str = "") -> PlainTextResponse:
    if type not in lookup_line.IOC_TYPES:
        raise HTTPException(status_code=400, detail=f"unknown type: {type}")
    values = lookup_line.export_blocklist(type, min_severity or None)
    header = f"# psyc blocklist — type={type} min_severity={min_severity or 'any'} count={len(values)}\n"
    return PlainTextResponse(header + "\n".join(values) + "\n")


@app.get("/response", response_class=HTMLResponse)
def response_view(request: Request, status: str = "proposed", fired: int = 0, kind: str = "") -> HTMLResponse:
    from psyc.models import ActionStatus
    sf = None if status == "all" else ActionStatus(status)
    actions = respond_line.list_actions(status=sf, limit=200)
    counts = {
        "proposed": respond_line.action_count(ActionStatus.PROPOSED),
        "executed": respond_line.action_count(ActionStatus.EXECUTED),
        "rejected": respond_line.action_count(ActionStatus.REJECTED),
        "failed": respond_line.action_count(ActionStatus.FAILED),
    }
    return TEMPLATES.TemplateResponse(
        request,
        "response.html",
        {"actions": actions, "counts": counts, "current_status": status, "fired": fired, "fired_kind": kind},
    )


@app.post("/response/approve/{action_id}")
def response_approve(action_id: int, approver: str = Form("operator")) -> RedirectResponse:
    result = respond_line.execute_action(action_id, approver=approver)
    if isinstance(result, Err):
        _log.warning("cockpit.response.approve.error", action_id=action_id, reason=result.reason)
        return RedirectResponse("/response", status_code=303)
    # Carry the fired action id + type so the page can set off the disco.
    kind = result.value.action_type.value
    return RedirectResponse(f"/response?fired={action_id}&kind={kind}", status_code=303)


@app.post("/response/reject/{action_id}")
def response_reject(action_id: int, approver: str = Form("operator"), reason: str = Form("")) -> RedirectResponse:
    respond_line.reject_action(action_id, approver=approver, reason=reason)
    return RedirectResponse("/response", status_code=303)


# ---------- hidden admin zone (TOTP-gated) -------------------------------

@app.get("/admin", response_class=HTMLResponse)
def admin_home(request: Request) -> HTMLResponse:
    if _admin_ok(request):
        enrolled = request.query_params.get("enrolled", "")
        new_qr = adminauth.member_qr(enrolled) if enrolled else None
        return TEMPLATES.TemplateResponse(
            request, "admin.html",
            {"members": adminauth.members(), "who": request.session.get("admin_who", ""),
             "new_qr": new_qr, "new_label": request.query_params.get("label", "")},
        )
    # Not authenticated — show the gate. Bootstrap QR only until first member exists.
    ctx = {"provisioned": adminauth.is_bootstrapped(), "error": request.query_params.get("error", "")}
    if not ctx["provisioned"]:
        ctx["qr"] = adminauth.bootstrap_qr()
    return TEMPLATES.TemplateResponse(request, "admin_gate.html", ctx)


@app.post("/admin/verify")
def admin_verify(request: Request, code: str = Form(...)) -> RedirectResponse:
    who = adminauth.verify(code)
    if who:
        request.session["admin_ok"] = True
        request.session["admin_who"] = who
        return RedirectResponse("/admin", status_code=303)
    return RedirectResponse("/admin?error=1", status_code=303)


@app.post("/admin/members")
def admin_add_member(request: Request, label: str = Form("member")) -> RedirectResponse:
    if not _admin_ok(request):
        raise HTTPException(status_code=403, detail="admin session required")
    member_id, _ = adminauth.add_member(label)
    return RedirectResponse(f"/admin?enrolled={member_id}&label={label}", status_code=303)


@app.post("/admin/members/{member_id}/revoke")
def admin_revoke_member(request: Request, member_id: str) -> RedirectResponse:
    if not _admin_ok(request):
        raise HTTPException(status_code=403, detail="admin session required")
    adminauth.revoke_member(member_id)
    return RedirectResponse("/admin", status_code=303)


@app.get("/admin/logout")
def admin_logout(request: Request) -> RedirectResponse:
    request.session.pop("admin_ok", None)
    request.session.pop("admin_who", None)
    return RedirectResponse("/admin", status_code=303)


@app.get("/admin/docker", response_class=HTMLResponse)
def admin_docker(request: Request) -> HTMLResponse:
    if not _admin_ok(request):
        return RedirectResponse("/admin", status_code=303)
    topo = docker_view.topology()
    return TEMPLATES.TemplateResponse(request, "admin_docker.html", {"topo": topo})


@app.get("/queue", response_class=HTMLResponse)
def queue_view(request: Request, status: str = "pending") -> HTMLResponse:
    from psyc.models import ApprovalStatus
    status_filter = None if status == "all" else ApprovalStatus(status)
    rows = courier_line.list_pending(status=status_filter, limit=200)
    counts = {
        "pending": courier_line.pending_count(ApprovalStatus.PENDING),
        "approved": courier_line.pending_count(ApprovalStatus.APPROVED),
        "rejected": courier_line.pending_count(ApprovalStatus.REJECTED),
    }
    return TEMPLATES.TemplateResponse(
        request,
        "queue.html",
        {"rows": rows, "counts": counts, "current_status": status},
    )


@app.post("/queue/approve/{pid}")
def queue_approve(pid: int, reviewer: str = Form("operator")) -> RedirectResponse:
    result = courier_line.dispatch_pending(pid, reviewer=reviewer)
    if isinstance(result, Err):
        _log.warning("cockpit.queue.approve.error", pending_id=pid, reason=result.reason)
    return RedirectResponse("/queue", status_code=303)


@app.post("/queue/reject/{pid}")
def queue_reject(pid: int, reviewer: str = Form("operator"), reason: str = Form("")) -> RedirectResponse:
    result = courier_line.reject_pending(pid, reviewer=reviewer, reason=reason)
    if isinstance(result, Err):
        _log.warning("cockpit.queue.reject.error", pending_id=pid, reason=result.reason)
    return RedirectResponse("/queue", status_code=303)
