"""FastMCP server instance, session state, and lifespan management."""

import asyncio
import atexit
import os
import sys
from contextlib import asynccontextmanager
from dataclasses import dataclass, field

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError


def _env_int(name: str, default: int) -> int:
    """Parse an integer env var with fallback on invalid values."""
    raw = os.environ.get(name)
    if raw is None:
        return default
    try:
        return int(raw)
    except ValueError:
        print(
            f"mcvnc: invalid {name}={raw!r}, using default {default}",
            file=sys.stderr,
        )
        return default


DEFAULT_PORT = _env_int("MCVNC_DEFAULT_PORT", 5900)
CONNECTION_TIMEOUT = _env_int("MCVNC_CONNECTION_TIMEOUT", 10)
SCREENSHOT_TIMEOUT = _env_int("MCVNC_SCREENSHOT_TIMEOUT", 30)
SCREENSHOT_MAX_WIDTH = _env_int("MCVNC_SCREENSHOT_MAX_WIDTH", 0)
WAIT_CHANGE_TIMEOUT = _env_int("MCVNC_WAIT_CHANGE_TIMEOUT", 5)

INSTRUCTIONS = """\
VNC remote desktop client -- connect to VNC servers, capture screenshots, \
send keyboard/mouse input, and manage clipboard.

Workflow: vnc_connect -> vnc_screenshot -> interpret UI -> interact -> \
vnc_wait_for_screen_change -> vnc_screenshot again

Use vnc_screenshot_region to capture just a dialog, menu, or area of interest \
instead of the full desktop. Use vnc_wait_for_screen_change after interactions \
to wait until the UI has actually updated before taking the next screenshot.

Session IDs default to "host:port" but can be aliased with session_id param. \
Use vnc_list_sessions() to see active connections.

Screenshots return inline PNG images. Coordinates are absolute pixels \
from the top-left corner of the remote desktop.
"""


@dataclass
class VncSession:
    """A single VNC connection."""

    client: object  # asyncvnc2.Client
    host: str
    port: int
    width: int
    height: int
    lock: asyncio.Lock = field(default_factory=asyncio.Lock)
    _cm: object = field(default=None, repr=False)  # stored context manager


@dataclass
class SessionState:
    """Track all active VNC sessions."""

    sessions: dict[str, VncSession] = field(default_factory=dict)

    def get(self, session_id: str) -> VncSession:
        session = self.sessions.get(session_id)
        if session is None:
            available = list(self.sessions.keys())
            raise ToolError(
                f"No session '{session_id}'. "
                f"Active sessions: {available or 'none -- use vnc_connect first'}"
            )
        return session

    async def disconnect(self, session_id: str) -> str:
        session = self.sessions.pop(session_id, None)
        if session is None:
            raise ToolError(f"No session '{session_id}' to disconnect")
        try:
            await session._cm.__aexit__(None, None, None)
        except Exception as e:
            print(f"mcvnc: disconnect error for {session_id}: {e}", file=sys.stderr)
        return f"Disconnected from {session.host}:{session.port} (session '{session_id}')"

    async def disconnect_all(self):
        for sid in list(self.sessions):
            try:
                await self.disconnect(sid)
            except Exception as e:
                print(f"mcvnc: cleanup error for {sid}: {e}", file=sys.stderr)

    def cleanup_sync(self):
        """Synchronous fallback for atexit -- best-effort close."""
        for sid, session in list(self.sessions.items()):
            try:
                session.client.writer.close()
            except Exception as e:
                print(f"mcvnc: atexit close failed for {sid}: {e}", file=sys.stderr)
        self.sessions.clear()
        print("mcvnc: cleanup complete (atexit)", file=sys.stderr)


_state: SessionState | None = None


def _atexit_cleanup():
    if _state:
        _state.cleanup_sync()


atexit.register(_atexit_cleanup)


@asynccontextmanager
async def lifespan(server: FastMCP):
    """Manage VNC session lifecycle."""
    global _state
    state = SessionState()
    _state = state
    print("mcvnc: VNC MCP server starting", file=sys.stderr)
    try:
        yield state
    finally:
        await state.disconnect_all()
        _state = None
        print("mcvnc: all VNC sessions closed", file=sys.stderr)


def get_state(ctx) -> SessionState:
    """Get SessionState from the request context."""
    state = ctx.request_context.lifespan_context
    if not isinstance(state, SessionState):
        raise RuntimeError("SessionState not available -- server lifespan not running")
    return state


mcp = FastMCP(
    name="mcvnc",
    instructions=INSTRUCTIONS,
    lifespan=lifespan,
)
