"""OpenCode-compatible FastAPI server.

This server implements the OpenCode API endpoints to allow OpenCode SDK clients
to interact with AgentPool agents.
"""

from __future__ import annotations

from contextlib import asynccontextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any

from fastapi import FastAPI, Request  # noqa: TC002
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse

from agentpool import AgentPool
from agentpool_server.opencode_server.routes import (
    agent_router,
    app_router,
    config_router,
    file_router,
    global_router,
    lsp_router,
    message_router,
    pty_router,
    session_router,
    tui_router,
)
from agentpool_server.opencode_server.state import ServerState


class OpenCodeJSONResponse(JSONResponse):
    """Custom JSON response that excludes None values (like OpenCode does)."""

    def render(self, content: Any) -> bytes:
        return super().render(jsonable_encoder(content, exclude_none=True))


if TYPE_CHECKING:
    from collections.abc import AsyncIterator, Set as AbstractSet


VERSION = "0.1.0"


async def check_pypi_version(package: str = "agentpool") -> str | None:
    """Check PyPI for the latest version of a package.

    Args:
        package: Package name to check

    Returns:
        Latest version string, or None if check fails
    """
    import httpx

    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            response = await client.get(f"https://pypi.org/pypi/{package}/json")
            if response.status_code == 200:  # noqa: PLR2004
                data: dict[str, Any] = response.json()
                info: dict[str, Any] = data.get("info", {})
                version: str | None = info.get("version")
                return version
    except Exception:  # noqa: BLE001
        pass
    return None


def compare_versions(current: str, latest: str) -> bool:
    """Check if latest version is newer than current.

    Args:
        current: Current version string
        latest: Latest version string

    Returns:
        True if latest is newer than current
    """
    from packaging.version import Version

    try:
        return Version(latest) > Version(current)
    except Exception:  # noqa: BLE001
        return False


def create_app(  # noqa: PLR0915
    *,
    pool: AgentPool[Any],
    agent_name: str | None = None,
    working_dir: str | None = None,
) -> FastAPI:
    """Create the FastAPI application.

    Args:
        pool: AgentPool for session persistence and agent access.
        agent_name: Name of the agent to use for handling messages.
                   If None, uses the first agent in the pool.
        working_dir: Working directory for file operations. Defaults to cwd.

    Returns:
        Configured FastAPI application.

    Raises:
        ValueError: If specified agent_name not found or pool has no agents.
    """
    # Resolve the agent from the pool
    if agent_name:
        agent = pool.all_agents.get(agent_name)
        if agent is None:
            msg = f"Agent '{agent_name}' not found in pool"
            raise ValueError(msg)
    else:
        # Use first agent as default
        agent = next(iter(pool.all_agents.values()), None)
        if agent is None:
            msg = "Pool has no agents"
            raise ValueError(msg)

    state = ServerState(
        working_dir=working_dir or str(Path.cwd()),
        pool=pool,
        agent=agent,
    )

    # Set up todo change callback to broadcast events
    async def on_todo_change(tracker: Any) -> None:
        """Broadcast todo updates to all active sessions."""
        from agentpool_server.opencode_server.models.events import Todo, TodoUpdatedEvent

        # Convert tracker entries to OpenCode Todo models
        todos = [
            Todo(id=e.id, content=e.content, status=e.status, priority=e.priority)
            for e in tracker.entries
        ]
        # Broadcast to all active sessions
        for session_id in state.sessions:
            event = TodoUpdatedEvent.create(session_id=session_id, todos=todos)
            await state.broadcast_event(event)

    pool.todos.on_change = on_todo_change

    # Watchers for VCS and file events
    git_branch_watcher: Any = None
    project_file_watcher: Any = None

    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
        nonlocal git_branch_watcher, project_file_watcher
        import logging

        from watchfiles import Change

        from agentpool.utils.file_watcher import FileWatcher, GitBranchWatcher
        from agentpool_server.opencode_server.models.events import (
            FileWatcherUpdatedEvent,
            VcsBranchUpdatedEvent,
        )

        log = logging.getLogger(__name__)

        # --- Git branch watcher ---
        async def on_branch_change(branch: str | None) -> None:
            """Broadcast branch change to all subscribers."""
            log.info("Broadcasting vcs.branch.updated event: %s", branch)
            event = VcsBranchUpdatedEvent.create(branch=branch)
            await state.broadcast_event(event)

        log.info("Setting up GitBranchWatcher for: %s", state.working_dir)
        git_branch_watcher = GitBranchWatcher(
            repo_path=state.working_dir,
            callback=on_branch_change,
        )
        await git_branch_watcher.start()
        log.info("GitBranchWatcher started, current branch: %s", git_branch_watcher.current_branch)

        # --- Project file watcher ---
        # Map watchfiles Change types to OpenCode event types
        change_type_map: dict[Change, str] = {
            Change.added: "add",
            Change.modified: "change",
            Change.deleted: "unlink",
        }

        async def on_file_change(changes: AbstractSet[tuple[Change, str]]) -> None:
            """Broadcast file changes to all subscribers."""
            for change_type, file_path in changes:
                # Skip .git directory changes
                if "/.git/" in file_path or file_path.endswith("/.git"):
                    continue
                event_type = change_type_map.get(change_type, "change")
                log.info("Broadcasting file.watcher.updated: %s %s", event_type, file_path)
                event = FileWatcherUpdatedEvent.create(file=file_path, event=event_type)  # type: ignore[arg-type]
                await state.broadcast_event(event)

        log.info("Setting up project FileWatcher for: %s", state.working_dir)
        project_file_watcher = FileWatcher(
            paths=[state.working_dir],
            callback=on_file_change,
            debounce=500,  # 500ms debounce to batch rapid changes
        )
        await project_file_watcher.start()
        log.info("Project FileWatcher started")

        # --- Version update check (triggered when first client connects) ---
        async def check_for_updates() -> None:
            """Check PyPI for updates and notify via toast."""
            from agentpool import __version__ as current_version
            from agentpool_server.opencode_server.models.events import TuiToastShowEvent

            latest = await check_pypi_version("agentpool")
            if latest and compare_versions(current_version, latest):
                log.info("Update available: %s -> %s", current_version, latest)
                event = TuiToastShowEvent.create(
                    title="Update Available",
                    message=f"agentpool {latest} is available (current: {current_version})",
                    variant="info",
                    duration=10000,
                )
                await state.broadcast_event(event)

        # Register callback to run when first SSE client connects
        state.on_first_subscriber = check_for_updates

        yield

        # Shutdown - clean up
        pool.todos.on_change = None
        if git_branch_watcher:
            await git_branch_watcher.stop()
        if project_file_watcher:
            await project_file_watcher.stop()
        # Clean up LSP servers
        if state.lsp_manager is not None:
            await state.lsp_manager.stop_all()

    app = FastAPI(
        title="OpenCode-Compatible API",
        description="AgentPool server with OpenCode API compatibility",
        version=VERSION,
        lifespan=lifespan,
        default_response_class=OpenCodeJSONResponse,
    )

    # Add CORS middleware (required for OpenCode TUI)
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # Store state on app for access in routes
    app.state.server_state = state

    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(
        request: Request, exc: RequestValidationError
    ) -> JSONResponse:
        body = await request.body()
        print(f"Validation error for {request.url}")
        print(f"Body: {body.decode()}")
        print(f"Errors: {exc.errors()}")
        return JSONResponse(
            status_code=422,
            content={"detail": exc.errors(), "body": body.decode()},
        )

    # Register routers
    app.include_router(global_router)
    app.include_router(app_router)
    app.include_router(config_router)
    app.include_router(session_router)
    app.include_router(message_router)
    app.include_router(file_router)
    app.include_router(agent_router)
    app.include_router(pty_router)
    app.include_router(tui_router)
    app.include_router(lsp_router)

    # OpenAPI doc redirect
    @app.get("/doc")
    async def get_doc() -> RedirectResponse:
        """Redirect to OpenAPI docs."""
        return RedirectResponse(url="/docs")

    return app


class OpenCodeServer:
    """OpenCode-compatible server wrapper.

    Provides a convenient interface for running the server.
    """

    def __init__(
        self,
        pool: AgentPool[Any],
        *,
        host: str = "127.0.0.1",
        port: int = 4096,
        agent_name: str | None = None,
        working_dir: str | None = None,
    ) -> None:
        """Initialize the server.

        Args:
            pool: AgentPool for session persistence and agent access.
            host: Host to bind to.
            port: Port to listen on.
            agent_name: Name of the agent to use for handling messages.
            working_dir: Working directory for file operations.
        """
        self.host = host
        self.port = port
        self.pool = pool
        self.agent_name = agent_name
        self.working_dir = working_dir
        self._app: FastAPI | None = None

    @property
    def app(self) -> FastAPI:
        """Get or create the FastAPI application."""
        if self._app is None:
            self._app = create_app(
                pool=self.pool,
                agent_name=self.agent_name,
                working_dir=self.working_dir,
            )
        return self._app

    def run(self) -> None:
        """Run the server (blocking)."""
        import uvicorn

        uvicorn.run(self.app, host=self.host, port=self.port)

    async def run_async(self) -> None:
        """Run the server asynchronously."""
        import uvicorn

        config = uvicorn.Config(self.app, host=self.host, port=self.port, ws="websockets-sansio")
        server = uvicorn.Server(config)
        await server.serve()


def run_server(
    pool: AgentPool[Any],
    *,
    host: str = "127.0.0.1",
    port: int = 4096,
    agent_name: str | None = None,
    working_dir: str | None = None,
) -> None:
    """Run the OpenCode-compatible server.

    Args:
        pool: AgentPool for session persistence and agent access.
        host: Host to bind to.
        port: Port to listen on.
        agent_name: Name of the agent to use for handling messages.
        working_dir: Working directory for file operations.
    """
    server = OpenCodeServer(
        pool,
        host=host,
        port=port,
        agent_name=agent_name,
        working_dir=working_dir,
    )
    server.run()


if __name__ == "__main__":
    from agentpool import config_resources

    pool = AgentPool(config_resources.CLAUDE_CODE_ASSISTANT)
    run_server(pool)
