"""
Unified MCP server for Linear local-fast reads + official MCP fallback/writes.
"""

from __future__ import annotations

import asyncio
import atexit
import logging
import os
import signal
import threading
import time
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any

from mcp.server.fastmcp import FastMCP

from .official_session import OfficialMcpSessionManager
from .reader import LinearLocalReader
from .router import ToolRouter

logger = logging.getLogger(__name__)


_DEBUG_DIR = Path("/tmp/oh-my-linear-debug")
_RECONNECT_FLAG = _DEBUG_DIR / "reconnect"
_RECONNECT_MAX_AGE_S = 10.0
def _is_reconnect() -> bool:
    """Check if a recent reconnect flag exists (written by signal handler on previous shutdown).

    Returns True only if the flag file exists AND is less than 10 seconds old.
    This distinguishes "/mcp reconnect" (immediate restart) from
    "Claude Code closed + reopened later" (stale flag).
    """
    try:
        mtime = _RECONNECT_FLAG.stat().st_mtime
        age_s = time.time() - mtime
        _RECONNECT_FLAG.unlink(missing_ok=True)
        return age_s < _RECONNECT_MAX_AGE_S
    except FileNotFoundError:
        return False


_reader: LinearLocalReader | None = None
_official: OfficialMcpSessionManager | None = None
_router: ToolRouter | None = None


def _on_shutdown_signal() -> None:
    """Signal callback for SIGTERM/SIGINT (registered on asyncio loop so it can't be overwritten).

    Always writes a reconnect flag with the current timestamp.
    On next startup, _is_reconnect() checks if the flag is recent (< 10s)
    to distinguish "/mcp reconnect" from a cold session restart.
    """
    _DEBUG_DIR.mkdir(exist_ok=True)
    (_DEBUG_DIR / "sigterm").write_text(f"{time.time()}\n")
    _RECONNECT_FLAG.write_text(f"{time.time()}\n")
    try:
        _shutdown()
    except Exception:
        pass
    os._exit(0)


@asynccontextmanager
async def _lifespan(server: FastMCP) -> AsyncIterator[None]:
    """Create components on startup, clean up on shutdown."""
    global _reader, _official, _router

    loop = asyncio.get_running_loop()
    loop.add_signal_handler(signal.SIGTERM, _on_shutdown_signal)
    loop.add_signal_handler(signal.SIGINT, _on_shutdown_signal)

    _reader = LinearLocalReader()
    _official = OfficialMcpSessionManager()
    _router = ToolRouter(_reader, _official)

    reconnecting = _is_reconnect()
    (_DEBUG_DIR / "startup_reconnect").write_text(f"reconnecting={reconnecting} t={time.time()}\n")
    if reconnecting:
        deleted = OfficialMcpSessionManager.clear_token_cache_for_url()
        logger.info("Reconnect detected, cleared %d token cache file(s)", deleted)
        (_DEBUG_DIR / "tokens_cleared").write_text(f"deleted={deleted} t={time.time()}\n")

    try:
        _reader.refresh_cache()
    except Exception as exc:
        logger.warning("Initial cache load failed, starting degraded: %s", exc)

    # On reconnect, trigger official MCP connect in background (non-blocking).
    # This opens the OAuth browser since tokens were cleared above.
    # Uses a daemon thread so it doesn't block the FastMCP transport handshake.
    if reconnecting and _official is not None:
        threading.Thread(
            target=_connect_official_background,
            daemon=True,
            name="linear-official-reconnect",
        ).start()

    try:
        yield
    finally:
        _shutdown()


mcp = FastMCP(
    "OhMyLinear (Fast + Official)",
    instructions=(
        "OhMyLinear unified server. "
        "Read operations are served from local Linear.app cache first for speed, "
        "and automatically fall back to official Linear MCP when local cache is "
        "unsupported, degraded, or stale-sensitive. "
        "Write operations use official Linear MCP."
    ),
    lifespan=_lifespan,
)


def _connect_official_background() -> None:
    """Connect to official MCP in background thread after reconnect.

    Triggers OAuth browser flow since tokens were cleared.
    """
    _DEBUG_DIR.mkdir(exist_ok=True)
    (_DEBUG_DIR / "bg_reconnect_started").write_text(f"{time.time()}\n")
    if _official is None:
        (_DEBUG_DIR / "bg_reconnect_error").write_text("_official is None\n")
        return
    try:
        _official._ensure_connected()
        (_DEBUG_DIR / "bg_reconnect_connected").write_text(f"{time.time()}\n")
        logger.info("Official MCP connected after reconnect")
    except Exception as exc:
        (_DEBUG_DIR / "bg_reconnect_error").write_text(f"{exc}\n")
        logger.warning("Background official MCP connect failed: %s", exc)


def _shutdown() -> None:
    if _official is not None:
        _official.close()


atexit.register(_shutdown)


def _read(tool_name: str, **kwargs: Any) -> Any:
    assert _router is not None, "server not initialized"
    return _router.call_read(tool_name, kwargs)


@mcp.tool()
def list_issues(
    assignee: str | None = None,
    team: str | None = None,
    state: str | None = None,
    priority: int | None = None,
    project: str | None = None,
    query: str | None = None,
    orderBy: str = "updatedAt",
    limit: int = 50,
) -> dict[str, Any]:
    """List issues with optional filtering and sorting.

    Args:
        assignee: Filter by user name or email.
        team: Filter by team key or name.
        state: Filter by state name or type (e.g., "Todo", "In Progress", "Done"). Case-insensitive.
        priority: Filter by exact numeric priority level.
        project: Filter by project name or ID.
        query: Case-insensitive substring search in issue titles.
        orderBy: Sort field - "updatedAt" (default) or "createdAt". Descending order.
        limit: Max issues to return (default 50). 0 or negative returns all.

    Returns:
        dict with "issues" (list of {identifier, title, priority, state, stateType,
        assignee, dueDate}) and "totalCount".
    """
    return _read(
        "list_issues",
        assignee=assignee,
        team=team,
        state=state,
        priority=priority,
        project=project,
        query=query,
        orderBy=orderBy,
        limit=limit,
    )


@mcp.tool()
def get_issue(id: str) -> dict[str, Any] | None:
    """Retrieve full details of a specific issue by identifier.

    Args:
        id: Issue identifier (e.g., "ENG-123").

    Returns:
        dict with {identifier, title, description, priority, estimate, state,
        stateType, assignee, project, dueDate, createdAt, updatedAt, comments
        (list of {author, body, createdAt}), url} or None if not found.
    """
    return _read("get_issue", id=id)


@mcp.tool()
def list_teams() -> list[dict[str, Any]]:
    """Retrieve all teams from the workspace.

    Returns:
        List of team dicts sorted by key, each with {key, name, issueCount}.
    """
    return _read("list_teams")


@mcp.tool()
def list_projects(team: str | None = None) -> list[dict[str, Any]]:
    """Retrieve projects, optionally filtered by team.

    Args:
        team: Team name or key to filter by. Returns empty list if not found.

    Returns:
        List of project dicts sorted by name, each with {name, state, issueCount,
        startDate, targetDate}.
    """
    return _read("list_projects", team=team)


@mcp.tool()
def get_team(query: str) -> dict[str, Any] | None:
    """Retrieve a team by name or key.

    Args:
        query: Team key (exact, case-sensitive uppercase) or name (substring, case-insensitive).

    Returns:
        dict with {id, key, name, description, issueCount, issuesByState} or None.
    """
    return _read("get_team", query=query)


@mcp.tool()
def get_project(query: str) -> dict[str, Any] | None:
    """Retrieve a project by name or slug ID.

    Args:
        query: Project name (substring, case-insensitive) or slug ID (exact, case-insensitive).

    Returns:
        dict with {id, name, description, state, startDate, targetDate, issueCount,
        issuesByState} or None.
    """
    return _read("get_project", query=query)


@mcp.tool()
def list_users() -> list[dict[str, Any]]:
    """List all workspace users with assigned issue counts.

    Returns:
        List of user dicts, each with {id, name, email, displayName, assignedIssueCount}.
    """
    return _read("list_users")


@mcp.tool()
def get_user(query: str) -> dict[str, Any] | None:
    """Retrieve a user by name or email.

    Args:
        query: User name or email to search for.

    Returns:
        dict with {id, name, email, displayName, assignedIssueCount, issuesByState} or None.
    """
    return _read("get_user", query=query)


@mcp.tool()
def list_issue_statuses(team: str) -> list[dict[str, Any]]:
    """List all issue statuses (workflow states) for a team.

    Args:
        team: Team key, name, or ID.

    Returns:
        List of status dicts, each with {id, name, type, color, position}.
    """
    return _read("list_issue_statuses", team=team)


@mcp.tool()
def get_issue_status(
    team: str,
    name: str | None = None,
    id: str | None = None,
) -> dict[str, Any] | None:
    """Get a single issue status by name or ID within a team.

    Args:
        team: Team key, name, or ID.
        name: Status name to search for. Optional if id is provided.
        id: Status ID to look up. Optional if name is provided.

    Returns:
        dict with {id, name, type, color, position, team} or None.
    """
    return _read("get_issue_status", team=team, name=name, id=id)


@mcp.tool()
def list_comments(issueId: str) -> list[dict[str, Any]]:
    """List all comments for a specific issue.

    Args:
        issueId: Issue identifier (e.g., "ENG-123").

    Returns:
        List of comment dicts, each with {id, author, body, createdAt, updatedAt}.
    """
    return _read("list_comments", issueId=issueId)


@mcp.tool()
def list_issue_labels(team: str | None = None) -> list[dict[str, Any]]:
    """List all issue labels, optionally filtered by team.

    Args:
        team: Team key or name to filter by. Returns all labels if None.

    Returns:
        List of label dicts sorted by name, each with {id, name, color, isGroup}.
    """
    return _read("list_issue_labels", team=team)


@mcp.tool()
def list_initiatives() -> list[dict[str, Any]]:
    """Retrieve all initiatives sorted alphabetically by name.

    Returns:
        List of initiative dicts, each with {id, name, slugId, color, status, owner}.
    """
    return _read("list_initiatives")


@mcp.tool()
def get_initiative(query: str) -> dict[str, Any] | None:
    """Retrieve a single initiative by name or identifier.

    Args:
        query: Initiative name or identifier to search for.

    Returns:
        dict with {id, name, slugId, color, status, owner, teamIds, createdAt,
        updatedAt} or None.
    """
    return _read("get_initiative", query=query)


@mcp.tool()
def list_cycles(teamId: str) -> list[dict[str, Any]]:
    """Retrieve cycles for a team.

    Args:
        teamId: Team ID or name.

    Returns:
        List of cycle dicts, each with {id, number, startsAt, endsAt, completedAt,
        progress ({completed, started, unstarted, total} or None)}.
    """
    return _read("list_cycles", teamId=teamId)


@mcp.tool()
def list_documents(project: str | None = None) -> list[dict[str, Any]]:
    """Retrieve documents, optionally filtered by project.

    Args:
        project: Project ID or name to filter by. Returns all documents if None.

    Returns:
        List of document dicts sorted by updatedAt desc, each with {id, title,
        slugId, project, createdAt, updatedAt}.
    """
    return _read("list_documents", project=project)


@mcp.tool()
def get_document(id: str) -> dict[str, Any] | None:
    """Retrieve a document by ID.

    Args:
        id: Document identifier.

    Returns:
        dict with {id, title, slugId, project, creator, createdAt, updatedAt, url} or None.
    """
    return _read("get_document", id=id)


@mcp.tool()
def list_milestones(project: str) -> list[dict[str, Any]]:
    """List all milestones for a project.

    Args:
        project: Project name or identifier.

    Returns:
        List of milestone dicts, each with {id, name, targetDate, progress
        ({completed, started, unstarted, total})}. Empty list if project not found.
    """
    return _read("list_milestones", project=project)


@mcp.tool()
def get_milestone(project: str, query: str) -> dict[str, Any] | None:
    """Retrieve a specific milestone in a project by name or ID.

    Args:
        project: Project name or identifier.
        query: Milestone name or query string to match.

    Returns:
        dict with {id, name, project, targetDate, sortOrder, progress
        ({completed, started, unstarted, total})} or None.
    """
    return _read("get_milestone", project=project, query=query)


@mcp.tool()
def get_status_updates(
    type: str,
    id: str | None = None,
    project: str | None = None,
    initiative: str | None = None,
    user: str | None = None,
    includeArchived: bool | None = None,
    orderBy: str = "createdAt",
    limit: int = 50,
    cursor: str | None = None,
    createdAt: str | None = None,
    updatedAt: str | None = None,
) -> dict[str, Any] | None:
    """Retrieve status updates with filtering. Local cache supports type='project' only.

    Args:
        type: Update type. Local cache only supports "project"; others fall back to official MCP.
        id: Specific status update ID.
        project: Project name or identifier to filter by.
        initiative: Initiative filter (not supported locally, triggers fallback).
        user: User name or identifier to filter by.
        includeArchived: Include archived (not supported locally).
        orderBy: Sort by "createdAt" (default) or "updatedAt". Descending.
        limit: Max results (default 50). 0 returns all.
        cursor: Pagination cursor (not supported locally).
        createdAt: Filter by creation date (not supported locally).
        updatedAt: Filter by update date (not supported locally).

    Returns:
        dict with "statusUpdates" (list of {id, body, health, author, project,
        createdAt, updatedAt}) and "totalCount", or None.
    """
    return _read(
        "get_status_updates",
        type=type,
        id=id,
        project=project,
        initiative=initiative,
        user=user,
        includeArchived=includeArchived,
        orderBy=orderBy,
        limit=limit,
        cursor=cursor,
        createdAt=createdAt,
        updatedAt=updatedAt,
    )


@mcp.tool()
def list_project_updates(project: str) -> list[dict[str, Any]]:
    """List all status updates for a project.

    Args:
        project: Project name or identifier.

    Returns:
        List of update dicts, each with {id, body, health, author, project,
        createdAt, updatedAt}. Empty list if project not found.
    """
    return _read("list_project_updates", project=project)


@mcp.tool()
def official_call_tool(name: str, args: dict[str, Any] | None = None) -> Any:
    """
    Call any official Linear MCP tool by name.

    Use this for write operations and any official-only tools.
    """
    assert _router is not None, "server not initialized"
    return _router.call_official(name, args or {})


@mcp.tool()
def list_official_tools() -> list[str]:
    """List tool names currently available from official Linear MCP."""
    assert _official is not None, "server not initialized"
    return _official.list_tools()


@mcp.tool()
def refresh_cache() -> dict[str, Any]:
    """Force reload of local cache and return health state."""
    assert _router is not None, "server not initialized"
    return _router.refresh_local_cache()


@mcp.tool()
def get_cache_health() -> dict[str, Any]:
    """Return local+official health and coherence-window state."""
    assert _router is not None, "server not initialized"
    return _router.get_health()


@mcp.tool()
def reauth() -> dict[str, Any]:
    """Re-authenticate with official Linear MCP. Use when switching accounts.

    Clears cached OAuth tokens, disconnects, and reconnects (opens browser for login).
    """
    assert _official is not None, "server not initialized"
    return _official.reauth()


def main() -> None:
    mcp.run()
