"""Dashboard HTTP handler — extends the health check server with web UI routes."""

from __future__ import annotations

import asyncio
import json
import logging
import mimetypes
from pathlib import Path
from urllib.parse import parse_qs, urlparse

import asyncpg
from jinja2 import Environment, FileSystemLoader
from redis.asyncio import Redis

from loom.bus import channels
from loom.graph.project import get_project_status, list_projects
from loom.graph.task import Task

logger = logging.getLogger(__name__)

_TEMPLATE_DIR = Path(__file__).parent / "templates"
_STATIC_DIR = Path(__file__).parent / "static"

_jinja_env = Environment(
    loader=FileSystemLoader(str(_TEMPLATE_DIR)),
    autoescape=True,
)


def _parse_request(data: bytes) -> tuple[str, str, dict[str, str]]:
    """Parse raw HTTP request into (method, path, query_params)."""
    try:
        request_line = data.decode("utf-8", errors="replace").split("\r\n")[0]
        parts = request_line.split(" ")
        method = parts[0] if parts else "GET"
        raw_path = parts[1] if len(parts) > 1 else "/"
        parsed = urlparse(raw_path)
        query = {k: v[0] for k, v in parse_qs(parsed.query).items()}
        return method, parsed.path, query
    except Exception:
        return "GET", "/", {}


def _http_response(
    status: str, body: str, content_type: str = "text/html; charset=utf-8"
) -> bytes:
    """Build a raw HTTP response."""
    body_bytes = body.encode("utf-8")
    headers = (
        f"HTTP/1.1 {status}\r\n"
        f"Content-Type: {content_type}\r\n"
        f"Content-Length: {len(body_bytes)}\r\n"
        f"Cache-Control: no-cache\r\n"
        "\r\n"
    )
    return headers.encode("utf-8") + body_bytes


def _http_binary_response(
    status: str, body: bytes, content_type: str
) -> bytes:
    """Build a raw HTTP response with binary body."""
    headers = (
        f"HTTP/1.1 {status}\r\n"
        f"Content-Type: {content_type}\r\n"
        f"Content-Length: {len(body)}\r\n"
        f"Cache-Control: public, max-age=3600\r\n"
        "\r\n"
    )
    return headers.encode("utf-8") + body


async def _get_all_tasks(pool: asyncpg.Pool, project_id: str) -> list[dict]:
    """Load all tasks for a project with deps."""
    from loom.graph.store import _batch_load_deps

    async with pool.acquire() as conn:
        rows = await conn.fetch(
            "SELECT * FROM tasks WHERE project_id = $1::uuid ORDER BY created_at",
            project_id,
        )
    task_ids = [r["id"] for r in rows]
    all_deps = await _batch_load_deps(pool, task_ids)
    tasks = []
    for r in rows:
        t = Task.from_record(r, depends_on=all_deps.get(r["id"], []))
        tasks.append(t.model_dump(mode="json"))
    return tasks


async def _get_graph_mermaid(pool: asyncpg.Pool, project_id: str) -> str:
    """Generate Mermaid graph syntax with status-colored nodes."""
    async with pool.acquire() as conn:
        tasks = await conn.fetch(
            "SELECT * FROM tasks WHERE project_id = $1::uuid", project_id
        )
        all_deps = await conn.fetch(
            """
            SELECT td.task_id, td.depends_on FROM task_deps td
            JOIN tasks t ON t.id = td.task_id
            WHERE t.project_id = $1::uuid
            """,
            project_id,
        )

    if not tasks:
        return ""

    dep_map: dict[str, list[str]] = {}
    for d in all_deps:
        dep_map.setdefault(d["task_id"], []).append(d["depends_on"])

    status_styles = {
        "done": "fill:#198754,color:#fff",
        "claimed": "fill:#0d6efd,color:#fff",
        "pending": "fill:#6c757d,color:#fff",
        "failed": "fill:#dc3545,color:#fff",
        "blocked": "fill:#fd7e14,color:#fff",
        "epic": "fill:#6f42c1,color:#fff",
    }

    lines = ["graph TD"]
    for t in tasks:
        safe_title = t["title"].replace('"', "'").replace("[", "(").replace("]", ")")
        tid = t["id"]
        lines.append(f'    {tid}["{safe_title}"]')
        for dep in dep_map.get(tid, []):
            lines.append(f"    {dep} --> {tid}")

    # Add style classes
    for t in tasks:
        style = status_styles.get(t["status"], "")
        if style:
            lines.append(f"    style {t['id']} {style}")

    return "\n".join(lines)


async def handle_dashboard_request(
    writer: asyncio.StreamWriter,
    pool: asyncpg.Pool,
    redis_client: Redis,
    project_id: str,
    config,
    path: str,
) -> None:
    """Route dashboard HTTP requests. Path is pre-parsed by the caller."""
    try:

        if path == "/dashboard":
            response = await _handle_board(pool, project_id)
        elif path == "/dashboard/graph":
            response = await _handle_graph_page(pool, project_id)
        elif path == "/dashboard/projects":
            response = await _handle_projects_page(pool)
        elif path == "/dashboard/api/tasks":
            response = await _handle_api_tasks(pool, project_id)
        elif path.startswith("/dashboard/api/task/"):
            task_id = path.split("/")[-1]
            response = await _handle_api_task_detail(pool, project_id, task_id)
        elif path == "/dashboard/api/graph":
            response = await _handle_api_graph(pool, project_id)
        elif path == "/dashboard/api/projects":
            response = await _handle_api_projects(pool)
        elif path == "/dashboard/api/events":
            await _handle_sse(writer, redis_client, project_id)
            return  # SSE handles its own connection lifecycle
        elif path.startswith("/static/"):
            response = _handle_static(path)
        else:
            response = _http_response("404 Not Found", "Not Found")

        writer.write(response)
        await writer.drain()
    except Exception as e:
        logger.debug("Dashboard request error: %s", e)


async def _handle_board(pool: asyncpg.Pool, project_id: str) -> bytes:
    """Render the kanban board page."""
    try:
        status = await get_project_status(pool, project_id)
        stats = {
            "total": status.total, "pending": status.pending,
            "claimed": status.claimed, "done": status.done,
            "failed": status.failed, "blocked": status.blocked,
        }
        project_name = status.project_name
    except Exception:
        stats = {"total": 0, "pending": 0, "claimed": 0, "done": 0, "failed": 0, "blocked": 0}
        project_name = "Unknown"

    tmpl = _jinja_env.get_template("board.html")
    html = tmpl.render(
        title="Task Board", page="board",
        project_name=project_name, stats=stats,
    )
    return _http_response("200 OK", html)


async def _handle_graph_page(pool: asyncpg.Pool, project_id: str) -> bytes:
    """Render the graph visualization page."""
    try:
        status = await get_project_status(pool, project_id)
        project_name = status.project_name
    except Exception:
        project_name = "Unknown"

    tmpl = _jinja_env.get_template("graph.html")
    html = tmpl.render(title="Graph", page="graph", project_name=project_name)
    return _http_response("200 OK", html)


async def _handle_projects_page(pool: asyncpg.Pool) -> bytes:
    """Render the projects overview page."""
    tmpl = _jinja_env.get_template("projects.html")
    html = tmpl.render(title="Projects", page="projects")
    return _http_response("200 OK", html)


async def _handle_api_tasks(pool: asyncpg.Pool, project_id: str) -> bytes:
    """Return HTMX fragment of task board columns."""
    tasks = await _get_all_tasks(pool, project_id)
    tasks_by_status: dict[str, list[dict]] = {}
    for t in tasks:
        tasks_by_status.setdefault(t["status"], []).append(t)

    tmpl = _jinja_env.get_template("tasks_fragment.html")
    html = tmpl.render(tasks_by_status=tasks_by_status)
    return _http_response("200 OK", html)


async def _handle_api_task_detail(
    pool: asyncpg.Pool, project_id: str, task_id: str
) -> bytes:
    """Return HTMX fragment of task detail."""
    from loom.graph.store import get_task

    try:
        task = await get_task(pool, task_id)
        task_data = task.model_dump(mode="json")
    except Exception:
        return _http_response("404 Not Found", "<p>Task not found</p>")

    tmpl = _jinja_env.get_template("task_detail.html")
    html = tmpl.render(task=task_data)
    return _http_response("200 OK", html)


async def _handle_api_graph(pool: asyncpg.Pool, project_id: str) -> bytes:
    """Return HTMX fragment with Mermaid graph."""
    graph_data = await _get_graph_mermaid(pool, project_id)
    tmpl = _jinja_env.get_template("graph_fragment.html")
    html = tmpl.render(graph_data=graph_data)
    return _http_response("200 OK", html)


async def _handle_api_projects(pool: asyncpg.Pool) -> bytes:
    """Return HTMX fragment of projects list."""
    projects_raw = await list_projects(pool)
    projects = []
    for p in projects_raw:
        pid = str(p["id"])
        try:
            status = await get_project_status(pool, pid)
            pct = round(status.done / status.total * 100) if status.total > 0 else 0
            projects.append({
                "id": pid, "name": p["name"],
                "description": p.get("description", ""),
                "total": status.total, "done": status.done, "pct": pct,
            })
        except Exception:
            projects.append({
                "id": pid, "name": p["name"],
                "description": p.get("description", ""),
                "total": 0, "done": 0, "pct": 0,
            })

    tmpl = _jinja_env.get_template("projects_fragment.html")
    html = tmpl.render(projects=projects)
    return _http_response("200 OK", html)


async def _handle_sse(
    writer: asyncio.StreamWriter,
    redis_client: Redis,
    project_id: str,
) -> None:
    """Server-Sent Events endpoint streaming Redis pub/sub updates."""
    headers = (
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/event-stream\r\n"
        "Cache-Control: no-cache\r\n"
        "Connection: keep-alive\r\n"
        "\r\n"
    )
    writer.write(headers.encode("utf-8"))
    await writer.drain()

    pubsub = redis_client.pubsub()
    channel = channels.updates_channel(project_id)
    try:
        await pubsub.subscribe(channel)
        while True:
            msg = await asyncio.wait_for(pubsub.get_message(timeout=30), timeout=35)
            if msg and msg["type"] == "message":
                data = msg["data"] if isinstance(msg["data"], str) else msg["data"].decode()
                event = f"event: task_update\ndata: {data}\n\n"
                writer.write(event.encode("utf-8"))
                await writer.drain()
            else:
                # Send heartbeat to keep connection alive
                writer.write(b": heartbeat\n\n")
                await writer.drain()
    except (asyncio.TimeoutError, ConnectionError, Exception):
        pass
    finally:
        try:
            await pubsub.unsubscribe(channel)
            await pubsub.close()
        except Exception:
            pass
        try:
            writer.close()
        except Exception:
            pass


def _handle_static(path: str) -> bytes:
    """Serve static files from the static directory."""
    filename = path.replace("/static/", "", 1)
    filepath = _STATIC_DIR / filename

    if not filepath.exists() or not filepath.is_file():
        return _http_response("404 Not Found", "Not Found")

    # Prevent path traversal
    try:
        filepath.resolve().relative_to(_STATIC_DIR.resolve())
    except ValueError:
        return _http_response("403 Forbidden", "Forbidden")

    content_type = mimetypes.guess_type(str(filepath))[0] or "application/octet-stream"
    body = filepath.read_bytes()
    return _http_binary_response("200 OK", body, content_type)
