"""FastAPI application for Hangar."""

import asyncio
import shutil
from pathlib import Path

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from hangar.dependencies import get_project_path, register_on_project_changed
from hangar.routers import gitdiff, project, specs, stats, sync, tasks, terminal, timeline, watch, wizard
from hangar.services.watcher import WatcherService

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

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Start/stop the filesystem watcher with the app."""
    watcher = WatcherService(project_path=get_project_path())
    watch.set_watcher_service(watcher)
    await watcher.start()

    # Restart the watcher when the project path changes (e.g., via /api/project/open)
    import asyncio
    loop = asyncio.get_event_loop()

    def _on_project_changed(new_path):
        loop.create_task(watcher.restart(new_path))

    register_on_project_changed(_on_project_changed)

    yield
    register_on_project_changed(None)
    await watcher.stop()


app = FastAPI(
    title="Hangar",
    description="Orchestration-first IDE for spec-driven AI development",
    version="0.1.3",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(tasks.router)
app.include_router(specs.router)
app.include_router(gitdiff.router)
app.include_router(sync.router)
app.include_router(wizard.router)
app.include_router(timeline.router)
app.include_router(stats.router)
app.include_router(project.router)
app.include_router(terminal.router)
app.include_router(watch.router)


@app.get("/api/health")
async def health() -> dict:
    """Health check endpoint."""
    return {"status": "ok", "service": "hangar"}


class ToolStatus(BaseModel):
    """Status of an external tool (Beads or OpenSpec)."""
    available: bool
    cli_found: bool
    initialized: bool
    version: str | None = None
    error: str | None = None


class StatusResponse(BaseModel):
    """Combined status of all external tools."""
    beads: ToolStatus
    openspec: ToolStatus


async def _check_cli(cmd: str, version_flag: str = "--version") -> tuple[bool, str | None]:
    """Check if a CLI tool is available and get its version."""
    if not shutil.which(cmd):
        return False, None
    try:
        proc = await asyncio.create_subprocess_exec(
            cmd, version_flag,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()
        if proc.returncode == 0:
            version = stdout.decode().strip() or stderr.decode().strip()
            # Take just the first line
            return True, version.split("\n")[0][:80]
        return True, None  # CLI found but version flag failed
    except Exception:
        return False, None


async def _probe_tool(cmd: str, probe_args: list[str], cwd: Path) -> str | None:
    """Run a lightweight probe command and return error message if it fails."""
    try:
        proc = await asyncio.create_subprocess_exec(
            cmd, *probe_args,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=cwd,
        )
        stdout, stderr = await proc.communicate()
        if proc.returncode != 0:
            return stderr.decode().strip() or f"{cmd} exited with code {proc.returncode}"
        return None
    except Exception as e:
        return str(e)


@app.get("/api/status")
async def status() -> StatusResponse:
    """Check availability of Beads and OpenSpec tools."""
    project_path = get_project_path()

    # Run all checks in parallel
    beads_cli_task = _check_cli("bd")
    openspec_cli_task = _check_cli("openspec")
    beads_probe_task = _probe_tool("bd", ["list", "--all", "--json"], project_path)
    openspec_probe_task = _probe_tool("openspec", ["list", "--json"], project_path)

    (beads_cli_found, beads_version), (openspec_cli_found, openspec_version), beads_error, openspec_error = (
        await asyncio.gather(beads_cli_task, openspec_cli_task, beads_probe_task, openspec_probe_task)
    )

    beads_initialized = (project_path / ".beads").is_dir()
    openspec_initialized = (project_path / "openspec").is_dir()

    return StatusResponse(
        beads=ToolStatus(
            available=beads_cli_found and beads_error is None,
            cli_found=beads_cli_found,
            initialized=beads_initialized,
            version=beads_version,
            error=beads_error,
        ),
        openspec=ToolStatus(
            available=openspec_cli_found and openspec_error is None,
            cli_found=openspec_cli_found,
            initialized=openspec_initialized,
            version=openspec_version,
            error=openspec_error,
        ),
    )


# SPA fallback: non-API 404s serve the SPA shell
@app.exception_handler(404)
async def spa_fallback(request, exc):
    if request.url.path.startswith("/api/"):
        detail = getattr(exc, "detail", "Not Found")
        return JSONResponse(status_code=404, content={"detail": detail})
    fallback = STATIC_DIR / "200.html"
    if fallback.is_file():
        return FileResponse(fallback)
    return JSONResponse(status_code=404, content={"detail": "Not Found"})


# Mount static files if the build directory exists (skip in dev mode)
if STATIC_DIR.is_dir():
    app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="spa")


if __name__ == "__main__":
    from hangar.cli import main
    main()
