import json
import os
from pathlib import Path

import aiosqlite
import uvicorn
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import FileResponse, JSONResponse
from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles

from peon_mcp.db import SCHEMA, get_db_path

STATIC_DIR = Path(__file__).parent / "static"

# ---------------------------------------------------------------------------
# Database helper
# ---------------------------------------------------------------------------


async def get_db() -> aiosqlite.Connection:
    project = os.environ.get("PEON_PROJECT")
    db_path = get_db_path()
    db = await aiosqlite.connect(db_path)
    db.row_factory = aiosqlite.Row
    await db.execute("PRAGMA journal_mode=WAL")
    await db.execute("PRAGMA foreign_keys=ON")
    await db.executescript(SCHEMA)
    await db.commit()
    return db


def row_to_dict(row: aiosqlite.Row) -> dict:
    return dict(row)


# ---------------------------------------------------------------------------
# Project endpoints
# ---------------------------------------------------------------------------


async def list_projects(request: Request) -> JSONResponse:
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM projects ORDER BY created_at DESC"
        )
        return JSONResponse([row_to_dict(r) for r in rows])
    finally:
        await db.close()


async def create_project(request: Request) -> JSONResponse:
    body = await request.json()
    project_id = body.get("id", "").strip()
    path = body.get("path", "").strip()
    if not project_id or not path:
        return JSONResponse({"error": "id and path are required"}, status_code=400)
    db = await get_db()
    try:
        await db.execute(
            "INSERT INTO projects (id, path) VALUES (?, ?)", (project_id, path)
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM projects WHERE id = ?", (project_id,)
        )
        return JSONResponse(row_to_dict(rows[0]), status_code=201)
    except aiosqlite.IntegrityError:
        return JSONResponse(
            {"error": f"Project '{project_id}' already exists"}, status_code=409
        )
    finally:
        await db.close()


async def get_project(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM projects WHERE id = ?", (project_id,)
        )
        if not rows:
            return JSONResponse({"error": "Project not found"}, status_code=404)
        return JSONResponse(row_to_dict(rows[0]))
    finally:
        await db.close()


async def project_stats(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    db = await get_db()
    try:
        # Feature counts by status
        rows = await db.execute_fetchall(
            "SELECT status, COUNT(*) as count FROM features WHERE project_id = ? GROUP BY status",
            (project_id,),
        )
        feature_counts = {r["status"]: r["count"] for r in rows}

        # Task counts by status
        rows = await db.execute_fetchall(
            "SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status",
            (project_id,),
        )
        task_counts = {r["status"]: r["count"] for r in rows}

        # Task counts by priority
        rows = await db.execute_fetchall(
            "SELECT priority, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY priority",
            (project_id,),
        )
        task_priority_counts = {r["priority"]: r["count"] for r in rows}

        # Work log stats
        rows = await db.execute_fetchall(
            "SELECT COUNT(*) as total FROM work_logs WHERE project_id = ?",
            (project_id,),
        )
        total_logs = rows[0]["total"]

        rows = await db.execute_fetchall(
            "SELECT test_result, COUNT(*) as count FROM work_logs WHERE project_id = ? AND test_result != '' GROUP BY test_result",
            (project_id,),
        )
        test_counts = {r["test_result"]: r["count"] for r in rows}

        # Recent logs
        rows = await db.execute_fetchall(
            "SELECT * FROM work_logs WHERE project_id = ? ORDER BY created_at DESC LIMIT 10",
            (project_id,),
        )
        recent_logs = [row_to_dict(r) for r in rows]

        return JSONResponse(
            {
                "feature_counts": feature_counts,
                "task_counts": task_counts,
                "task_priority_counts": task_priority_counts,
                "total_logs": total_logs,
                "test_counts": test_counts,
                "recent_logs": recent_logs,
            }
        )
    finally:
        await db.close()


# ---------------------------------------------------------------------------
# Feature endpoints
# ---------------------------------------------------------------------------


async def list_features(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    db = await get_db()
    try:
        query = "SELECT * FROM features WHERE project_id = ?"
        params: list = [project_id]
        status = request.query_params.get("status")
        if status:
            query += " AND status = ?"
            params.append(status)
        query += " ORDER BY created_at DESC"
        rows = await db.execute_fetchall(query, params)
        return JSONResponse([row_to_dict(r) for r in rows])
    finally:
        await db.close()


async def create_feature(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    body = await request.json()
    name = body.get("name", "").strip()
    if not name:
        return JSONResponse({"error": "name is required"}, status_code=400)
    description = body.get("description", "")
    status = body.get("status", "planned")
    db = await get_db()
    try:
        cursor = await db.execute(
            "INSERT INTO features (project_id, name, description, status) VALUES (?, ?, ?, ?)",
            (project_id, name, description, status),
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM features WHERE id = ?", (cursor.lastrowid,)
        )
        return JSONResponse(row_to_dict(rows[0]), status_code=201)
    finally:
        await db.close()


async def get_feature(request: Request) -> JSONResponse:
    feature_id = request.path_params["id"]
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM features WHERE id = ?", (feature_id,)
        )
        if not rows:
            return JSONResponse({"error": "Feature not found"}, status_code=404)
        return JSONResponse(row_to_dict(rows[0]))
    finally:
        await db.close()


async def update_feature(request: Request) -> JSONResponse:
    feature_id = request.path_params["id"]
    body = await request.json()
    db = await get_db()
    try:
        fields: list[str] = []
        params: list = []
        for key in ("name", "description", "status"):
            if key in body:
                fields.append(f"{key} = ?")
                params.append(body[key])
        if not fields:
            return JSONResponse({"error": "No fields to update"}, status_code=400)
        fields.append("updated_at = CURRENT_TIMESTAMP")
        params.append(feature_id)
        await db.execute(
            f"UPDATE features SET {', '.join(fields)} WHERE id = ?", params
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM features WHERE id = ?", (feature_id,)
        )
        if not rows:
            return JSONResponse({"error": "Feature not found"}, status_code=404)
        return JSONResponse(row_to_dict(rows[0]))
    finally:
        await db.close()


async def delete_feature(request: Request) -> JSONResponse:
    feature_id = request.path_params["id"]
    db = await get_db()
    try:
        cursor = await db.execute(
            "DELETE FROM features WHERE id = ?", (feature_id,)
        )
        await db.commit()
        if cursor.rowcount == 0:
            return JSONResponse({"error": "Feature not found"}, status_code=404)
        return JSONResponse({"ok": True})
    finally:
        await db.close()


# ---------------------------------------------------------------------------
# Task endpoints
# ---------------------------------------------------------------------------


async def list_tasks(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    db = await get_db()
    try:
        query = "SELECT * FROM tasks WHERE project_id = ?"
        params: list = [project_id]
        status = request.query_params.get("status")
        priority = request.query_params.get("priority")
        if status:
            query += " AND status = ?"
            params.append(status)
        if priority:
            query += " AND priority = ?"
            params.append(priority)
        query += (
            " ORDER BY CASE priority"
            " WHEN 'critical' THEN 0"
            " WHEN 'high' THEN 1"
            " WHEN 'medium' THEN 2"
            " WHEN 'low' THEN 3"
            " END, created_at DESC"
        )
        rows = await db.execute_fetchall(query, params)
        return JSONResponse([row_to_dict(r) for r in rows])
    finally:
        await db.close()


async def create_task(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    body = await request.json()
    title = body.get("title", "").strip()
    if not title:
        return JSONResponse({"error": "title is required"}, status_code=400)
    description = body.get("description", "")
    priority = body.get("priority", "medium")
    status = body.get("status", "todo")
    commit_base = body.get("commit_base", "")
    commit_head = body.get("commit_head", "")
    db = await get_db()
    try:
        cursor = await db.execute(
            "INSERT INTO tasks (project_id, title, description, priority, status, commit_base, commit_head) VALUES (?, ?, ?, ?, ?, ?, ?)",
            (project_id, title, description, priority, status, commit_base, commit_head),
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM tasks WHERE id = ?", (cursor.lastrowid,)
        )
        return JSONResponse(row_to_dict(rows[0]), status_code=201)
    finally:
        await db.close()


async def get_task(request: Request) -> JSONResponse:
    task_id = request.path_params["id"]
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM tasks WHERE id = ?", (task_id,)
        )
        if not rows:
            return JSONResponse({"error": "Task not found"}, status_code=404)
        return JSONResponse(row_to_dict(rows[0]))
    finally:
        await db.close()


async def update_task(request: Request) -> JSONResponse:
    task_id = request.path_params["id"]
    body = await request.json()
    db = await get_db()
    try:
        fields: list[str] = []
        params: list = []
        for key in ("title", "description", "priority", "status", "commit_base", "commit_head"):
            if key in body:
                fields.append(f"{key} = ?")
                params.append(body[key])
        if not fields:
            return JSONResponse({"error": "No fields to update"}, status_code=400)
        fields.append("updated_at = CURRENT_TIMESTAMP")
        params.append(task_id)
        await db.execute(
            f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?", params
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM tasks WHERE id = ?", (task_id,)
        )
        if not rows:
            return JSONResponse({"error": "Task not found"}, status_code=404)
        return JSONResponse(row_to_dict(rows[0]))
    finally:
        await db.close()


async def delete_task(request: Request) -> JSONResponse:
    task_id = request.path_params["id"]
    db = await get_db()
    try:
        cursor = await db.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
        await db.commit()
        if cursor.rowcount == 0:
            return JSONResponse({"error": "Task not found"}, status_code=404)
        return JSONResponse({"ok": True})
    finally:
        await db.close()


# ---------------------------------------------------------------------------
# Work log endpoints
# ---------------------------------------------------------------------------


async def list_work_logs(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT * FROM work_logs WHERE project_id = ? ORDER BY created_at DESC",
            (project_id,),
        )
        return JSONResponse([row_to_dict(r) for r in rows])
    finally:
        await db.close()


async def create_work_log(request: Request) -> JSONResponse:
    project_id = request.path_params["id"]
    body = await request.json()
    task_id = body.get("task_id")
    summary = body.get("summary", "").strip()
    if task_id is None or not summary:
        return JSONResponse(
            {"error": "task_id and summary are required"}, status_code=400
        )
    files_changed = body.get("files_changed", "")
    test_result = body.get("test_result", "")
    db = await get_db()
    try:
        cursor = await db.execute(
            "INSERT INTO work_logs (task_id, project_id, summary, files_changed, test_result) VALUES (?, ?, ?, ?, ?)",
            (task_id, project_id, summary, files_changed, test_result),
        )
        await db.commit()
        rows = await db.execute_fetchall(
            "SELECT * FROM work_logs WHERE id = ?", (cursor.lastrowid,)
        )
        return JSONResponse(row_to_dict(rows[0]), status_code=201)
    finally:
        await db.close()


# ---------------------------------------------------------------------------
# System / loop endpoints
# ---------------------------------------------------------------------------


async def system_info(request: Request) -> JSONResponse:
    try:
        import importlib.metadata
        version = importlib.metadata.version("peon-mcp")
    except Exception:
        version = "dev"

    return JSONResponse({
        "db_path": get_db_path(),
        "version": version,
    })


async def list_loops(request: Request) -> JSONResponse:
    db = await get_db()
    try:
        query = (
            "SELECT * FROM loop_heartbeats"
            " WHERE last_heartbeat >= datetime('now', '-90 seconds')"
        )
        params: list = []
        project_id = request.query_params.get("project_id")
        if project_id:
            query += " AND project_id = ?"
            params.append(project_id)
        query += " ORDER BY started_at DESC"
        rows = await db.execute_fetchall(query, params)
        loops = [row_to_dict(r) for r in rows]
        return JSONResponse({
            "active_count": len(loops),
            "loops": loops,
        })
    finally:
        await db.close()


# ---------------------------------------------------------------------------
# SPA fallback
# ---------------------------------------------------------------------------


async def spa_fallback(request: Request) -> FileResponse:
    # Serve actual static files if they exist (images, etc.)
    req_path = request.path_params.get("path", "")
    if req_path:
        static_file = STATIC_DIR / req_path
        if static_file.exists() and static_file.is_file():
            return FileResponse(str(static_file))
    # Otherwise serve index.html for SPA routing
    index = STATIC_DIR / "index.html"
    if index.exists():
        return FileResponse(str(index))
    return JSONResponse(
        {"error": "UI not built. Run: cd ui && npm install && npm run build"},
        status_code=404,
    )


# ---------------------------------------------------------------------------
# App factory
# ---------------------------------------------------------------------------


def create_app() -> Starlette:
    routes = [
        # System
        Route("/api/system-info", system_info, methods=["GET"]),
        Route("/api/loops", list_loops, methods=["GET"]),
        # Projects
        Route("/api/projects", list_projects, methods=["GET"]),
        Route("/api/projects", create_project, methods=["POST"]),
        # Features (by feature id)
        Route("/api/features/{id:int}", get_feature, methods=["GET"]),
        Route("/api/features/{id:int}", update_feature, methods=["PUT"]),
        Route("/api/features/{id:int}", delete_feature, methods=["DELETE"]),
        # Tasks (by task id)
        Route("/api/tasks/{id:int}", get_task, methods=["GET"]),
        Route("/api/tasks/{id:int}", update_task, methods=["PUT"]),
        Route("/api/tasks/{id:int}", delete_task, methods=["DELETE"]),
        # Project sub-resources (must come before generic project GET)
        Route("/api/projects/{id:path}/stats", project_stats, methods=["GET"]),
        Route("/api/projects/{id:path}/features", list_features, methods=["GET"]),
        Route("/api/projects/{id:path}/features", create_feature, methods=["POST"]),
        Route("/api/projects/{id:path}/tasks", list_tasks, methods=["GET"]),
        Route("/api/projects/{id:path}/tasks", create_task, methods=["POST"]),
        Route("/api/projects/{id:path}/logs", list_work_logs, methods=["GET"]),
        Route("/api/projects/{id:path}/logs", create_work_log, methods=["POST"]),
        # Generic project GET (last among project routes)
        Route("/api/projects/{id:path}", get_project, methods=["GET"]),
    ]

    # Serve static files (assets, images, etc.) with SPA fallback
    if STATIC_DIR.exists():
        routes.append(Mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets"))))
    routes.append(Route("/{path:path}", spa_fallback))

    middleware = [
        Middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_methods=["*"],
            allow_headers=["*"],
        )
    ]

    return Starlette(routes=routes, middleware=middleware)


app = create_app()


def find_open_port(host: str, preferred: int) -> int:
    """Return *preferred* if available, otherwise ask the OS for a free port."""
    import socket

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.bind((host, preferred))
        sock.close()
        return preferred
    except OSError:
        pass

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((host, 0))
    port = sock.getsockname()[1]
    sock.close()
    return port


def main():
    host = os.environ.get("PEON_UI_HOST", "127.0.0.1")
    preferred_port = int(os.environ.get("PEON_UI_PORT", "8420"))
    port = find_open_port(host, preferred_port)
    if port != preferred_port:
        print(f"Port {preferred_port} in use, using {port} instead")
    url = f"http://{host}:{port}"
    link = f"\033]8;;{url}\033\\{url}\033]8;;\033\\"
    print(f"peon-ui starting on {link}")
    uvicorn.run(app, host=host, port=port)


if __name__ == "__main__":
    main()
