# retoor <retoor@molodetz.nl>

import logging
from pathlib import Path
from typing import Annotated

from fastapi import Depends, APIRouter, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse

from devplacepy.config import SEO_REPORTS_DIR
from devplacepy.models import SeoRunForm
from devplacepy.responses import respond
from devplacepy.schemas import SeoJobOut, SeoReportOut
from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.seo.progress import hub
from devplacepy.services.manager import service_manager
from devplacepy.templating import templates
from devplacepy.utils import get_current_user, not_found, track_action
from devplacepy.dependencies import json_or_form
from ._shared import owner_for as _owner

logger = logging.getLogger(__name__)
router = APIRouter()

ACTIVE_STATES = (queue.PENDING, queue.RUNNING)
TERMINAL_STATES = (queue.DONE, queue.FAILED)

def _status_payload(job: dict) -> dict:
    result = job.get("result", {})
    done = job.get("status") == queue.DONE
    uid = job.get("uid", "")
    payload = job.get("payload", {})
    return {
        "uid": uid,
        "kind": job.get("kind", ""),
        "status": job.get("status", ""),
        "target": result.get("target") or payload.get("url"),
        "mode": result.get("mode") or payload.get("mode"),
        "ws_url": f"/tools/seo/{uid}/ws",
        "report_url": f"/tools/seo/{uid}/report" if done else None,
        "score": result.get("score") if done else None,
        "grade": result.get("grade") if done else None,
        "page_count": int(result.get("page_count") or 0),
        "error": job.get("error") or None,
        "created_at": job.get("created_at"),
        "completed_at": job.get("completed_at") or None,
    }

@router.get("", response_class=HTMLResponse)
async def seo_page(request: Request):
    user = get_current_user(request)
    base = site_url(request)
    description = (
        "Run a deep SEO audit on any URL or sitemap: technical, on-page, structured "
        "data, Core Web Vitals, accessibility and AI-readiness checks with live progress."
    )
    seo_ctx = base_seo_context(
        request,
        title="SEO Diagnostics",
        description=description,
        breadcrumbs=[
            {"name": "Home", "url": "/feed"},
            {"name": "Tools", "url": "/tools"},
            {"name": "SEO Diagnostics", "url": "/tools/seo"},
        ],
        schemas=[
            website_schema(base),
            web_application_schema(
                "SEO Diagnostics", description, "/tools/seo", base
            ),
        ],
    )
    return templates.TemplateResponse(
        request,
        "tools/seo.html",
        {**seo_ctx, "request": request, "user": user},
    )

@router.post("/run")
async def seo_run(request: Request, data: Annotated[SeoRunForm, Depends(json_or_form(SeoRunForm))]):
    owner_kind, owner_id = _owner(request)
    active = [
        job
        for job in queue.list_jobs(kind="seo", owner=(owner_kind, owner_id))
        if job.get("status") in ACTIVE_STATES
    ]
    from devplacepy.services.audit import record as audit

    if active:
        audit.record(
            request,
            "seo.run.request",
            result="denied",
            summary=f"SEO diagnostics denied for {data.url}: audit already running",
            metadata={"target": data.url, "reason": "active_job", "uid": active[0]["uid"]},
            links=[audit.job(active[0]["uid"])],
        )
        return JSONResponse(
            {
                "error": {
                    "status": 429,
                    "message": "You already have an SEO audit running. Wait for it to finish.",
                    "uid": active[0]["uid"],
                }
            },
            status_code=429,
        )
    uid = queue.enqueue(
        "seo",
        {"url": data.url, "mode": data.mode, "max_pages": data.max_pages, "allow_private": False},
        owner_kind,
        owner_id,
        f"SEO: {data.url}"[:64],
    )
    audit.record(
        request,
        "seo.run.request",
        summary=f"requested SEO diagnostics for {data.url}",
        metadata={"target": data.url, "mode": data.mode, "max_pages": data.max_pages},
        links=[audit.job(uid)],
    )
    if owner_kind == "user":
        track_action(owner_id, "seo")
    return JSONResponse(
        {
            "uid": uid,
            "status_url": f"/tools/seo/{uid}",
            "ws_url": f"/tools/seo/{uid}/ws",
        }
    )

@router.get("/{uid}")
async def seo_status(request: Request, uid: str):
    job = queue.get_job(uid)
    if not job or job.get("kind") != "seo":
        raise not_found("Audit not found")
    return JSONResponse(
        SeoJobOut.model_validate(_status_payload(job)).model_dump(mode="json")
    )

@router.get("/{uid}/report")
async def seo_report(request: Request, uid: str):
    job = queue.get_job(uid)
    if not job or job.get("kind") != "seo":
        raise not_found("Audit not found")
    result = job.get("result", {})
    report = result.get("report", {}) if job.get("status") == queue.DONE else {}
    context = {
        "uid": uid,
        "status": job.get("status", ""),
        "target": report.get("target") or job.get("payload", {}).get("url"),
        "mode": report.get("mode") or job.get("payload", {}).get("mode"),
        "score": report.get("score"),
        "grade": report.get("grade"),
        "page_count": report.get("page_count", 0),
        "counts": report.get("counts", {}),
        "categories": report.get("categories", {}),
        "pages": report.get("pages", []),
        "checks": report.get("checks", []),
        "site": report.get("site", {}),
        "generated_at": report.get("generated_at"),
        "request": request,
        "user": get_current_user(request),
        "meta_robots": "noindex,nofollow",
    }
    return respond(request, "tools/seo_report.html", context, model=SeoReportOut)

@router.get("/{uid}/screenshot/{index}")
async def seo_screenshot(request: Request, uid: str, index: int):
    job = queue.get_job(uid)
    if not job or job.get("kind") != "seo":
        raise not_found("Screenshot not available")
    path = (SEO_REPORTS_DIR / uid / f"page-{index}.png").resolve()
    if not path.is_relative_to(SEO_REPORTS_DIR.resolve()) or not path.is_file():
        raise not_found("Screenshot not available")
    return FileResponse(path, media_type="image/png")

@router.websocket("/{uid}/ws")
async def seo_ws(websocket: WebSocket, uid: str):
    await websocket.accept()
    if not service_manager.owns_lock():
        await websocket.close(code=4013)
        return
    job = queue.get_job(uid)
    if not job or job.get("kind") != "seo":
        await websocket.close(code=1008)
        return
    listener = hub.subscribe(uid)
    try:
        for frame in hub.snapshot(uid):
            await websocket.send_json(frame)
        current = queue.get_job(uid)
        if current and current.get("status") in TERMINAL_STATES:
            done = current.get("status") == queue.DONE
            report = current.get("result", {}).get("report", {}) if done else {}
            await websocket.send_json(
                {
                    "type": "done" if done else "failed",
                    "status": current.get("status"),
                    "report_url": f"/tools/seo/{uid}/report",
                    "error": current.get("error") or None,
                    "report": report,
                }
            )
            return
        while True:
            frame = await listener.get()
            await websocket.send_json(frame)
            if frame.get("type") in ("done", "failed"):
                break
    except WebSocketDisconnect:
        pass
    except Exception:  # noqa: BLE001 - never crash the worker on a socket error
        logger.exception("SEO websocket loop failed for %s", uid)
    finally:
        hub.unsubscribe(uid, listener)
