/**
 * MCP (Model Context Protocol) server for llm-inspector.
 *
 * Exposes 11 `inspector_*` tools that delegate to existing `/api/*` endpoints
 * via internal HTTP loopback (see `loopback.ts` for the rationale). Mounted
 * at `POST /api/mcp` in the existing TanStack Start process using HTTP Streamable
 * transport. localhost-only, no auth in v1 — see
 * `openspec/changes/add-mcp-server/design.md` decisions D1, D2, D9.
 *
 * The `McpServer` + `WebStandardStreamableHTTPServerTransport` pair is
 * instantiated once per process on first request and reused for all
 * subsequent requests (stateless mode — no `sessionIdGenerator`).
 *
 * Tool implementations live in `./toolHandlers.ts` so they can be unit
 * tested with a mock `callApi`. This file just wires them to the SDK.
 */

import { z } from "zod";
import { McpServer, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
import { setCurrentPort } from "../lib/serverPort";
import { callApi } from "./loopback";
import {
  addProviderImpl,
  getLogChunksImpl,
  getLogImpl,
  getProviderImpl,
  listLogsImpl,
  listModelsImpl,
  listProvidersImpl,
  listSessionsImpl,
  replayLogImpl,
  safeCall,
  testProviderImpl,
  updateProviderImpl,
} from "./toolHandlers";

// ---------------------------------------------------------------------------
// Lazy singleton: build the McpServer + transport on first use.
// ---------------------------------------------------------------------------

type ServerPair = { server: McpServer; transport: WebStandardStreamableHTTPServerTransport };

let initialized: ServerPair | null = null;
let initPromise: Promise<ServerPair> | null = null;

function buildServer(): ServerPair {
  const server = new McpServer(
    { name: "llm-inspector", version: "1.0.0" },
    { capabilities: { tools: {} } },
  );
  registerTools(server);
  const transport = new WebStandardStreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // stateless — see module docstring
  });
  // The connect() call is async but we don't need to await it here; the SDK
  // buffers the first request internally.
  void server.connect(transport);
  return { server, transport };
}

async function getServer(): Promise<ServerPair> {
  if (initialized !== null) return initialized;
  if (initPromise === null) {
    initPromise = Promise.resolve(buildServer()).then((pair) => {
      initialized = pair;
      return pair;
    });
  }
  return initPromise;
}

/** Reset for tests. */
export async function _resetForTests(): Promise<void> {
  if (initialized !== null) {
    try {
      await initialized.server.close();
    } catch {
      // ignore
    }
  }
  initialized = null;
  initPromise = null;
}

// ---------------------------------------------------------------------------
// Public entry point for the route handler.
// ---------------------------------------------------------------------------

/**
 * Handle an incoming MCP HTTP request. Suitable as a TanStack Start route
 * handler — accepts a Web `Request`, returns a `Promise<Response>`.
 */
export async function handleMcpRequest(request: Request): Promise<Response> {
  // Extract port from the incoming request URL so loopback calls work even
  // when PORT env var is unset (e.g. MCP client connected before server init).
  try {
    const url = new URL(request.url);
    const port = Number(url.port);
    if (port > 0) setCurrentPort(port);
  } catch {
    // URL parse failure → fall through to PORT env var in getCurrentPort()
  }
  const { transport } = await getServer();
  return transport.handleRequest(request);
}

// ---------------------------------------------------------------------------
// Tool descriptions (kept here, not in toolHandlers.ts, because they are
// only relevant to the wire-level MCP registration).
// ---------------------------------------------------------------------------

const TOOL_LIST_LOGS_DESC = `List recent captured LLM proxy logs in reverse-chronological order with parsed previews. Useful for "what did I just send?" discovery.

REFLEXIVE-LOOP WARNING: this list includes the agent's own recent /proxy calls. If results appear to be the agent's own tool calls (e.g., the agent called inspector_list_logs and now sees itself in the results), filter by clientPid (your own PID) or by timestamp on the client side.

Parameters:
  - offset (number, default 0): skip this many entries from the start of the filtered list
  - limit (number, default 3, hard-clamped to 5): how many summaries to return
  - sessionId (string, optional): filter by session/affinity id
  - model (string, optional): filter by model name

Returns: array of "thick summary" objects, each containing:
  - id, timestamp, provider, model, apiFormat, method, path, status, isStreaming,
    hasError, latencyMs, tokens { input, output, cacheCreate, cacheRead },
    sessionId, clientPid,
  - lastUserMessagePreview (string|null, ≤500 chars) — first text block of the last user message, parsed via the format-specific schema (NOT a raw body slice)
  - responsePreview (string|null, ≤500 chars) — first text block of the assistant response
  - hasChunks, hasRawRequestBody (booleans)

Either preview field is null when the body is unknown format, unparseable, or the relevant content is non-text (e.g., image-only).`;

const PROVIDER_WRITE_WARNING =
  "⚠ This tool mutates provider configuration. Confirm with the user before invoking.";

// ---------------------------------------------------------------------------
// Tool registration (thin wiring of impls to the SDK)
// ---------------------------------------------------------------------------

function registerTools(server: McpServer): void {
  // ----- Read tools -----

  server.registerTool(
    "inspector_list_logs",
    {
      title: "List captured LLM logs",
      description: TOOL_LIST_LOGS_DESC,
      inputSchema: z.object({
        offset: z.number().int().nonnegative().optional().describe("Skip this many entries."),
        limit: z
          .number()
          .int()
          .positive()
          .optional()
          .describe("How many summaries to return. Hard-capped at 5."),
        sessionId: z.string().optional().describe("Filter by session/affinity id."),
        model: z.string().optional().describe("Filter by model name."),
      }),
    },
    (args) => safeCall(() => listLogsImpl(callApi, args)),
  );

  server.registerTool(
    "inspector_get_log",
    {
      title: "Get a single captured log by id",
      description:
        "Returns the full CapturedLog for the given id, including rawRequestBody and responseText with no truncation. SSE streaming chunks are NOT included — use inspector_get_log_chunks for those. Returns an error if the id does not exist.",
      inputSchema: z.object({
        id: z.number().int().positive().describe("The log id."),
      }),
    },
    ({ id }) => safeCall(() => getLogImpl(callApi, id)),
  );

  server.registerTool(
    "inspector_get_log_chunks",
    {
      title: "Get SSE streaming chunks for a captured log",
      description:
        "Returns the SSE chunks array for a streaming log, in the order they were received. Returns an error if the log has no chunks (i.e. was non-streaming or the log id is unknown).",
      inputSchema: z.object({
        id: z.number().int().positive().describe("The log id."),
      }),
    },
    ({ id }) => safeCall(() => getLogChunksImpl(callApi, id)),
  );

  server.registerTool(
    "inspector_list_sessions",
    {
      title: "List known session ids",
      description:
        "Returns the array of session ids that have been observed in captured logs. Useful for discovering what sessionId values to pass to inspector_list_logs.",
      inputSchema: z.object({}),
    },
    () => safeCall(() => listSessionsImpl(callApi)),
  );

  server.registerTool(
    "inspector_list_models",
    {
      title: "List distinct model names seen in captured logs",
      description: "Returns the array of distinct model names observed across all captured logs.",
      inputSchema: z.object({}),
    },
    () => safeCall(() => listModelsImpl(callApi)),
  );

  server.registerTool(
    "inspector_list_providers",
    {
      title: "List configured LLM providers",
      description:
        "Returns the full ProviderConfig array, including apiKey in PLAINTEXT. MCP is localhost-only — any process that can call /api/mcp can also read <dataDir>/providers.json directly, so the apiKey is not redacted. Do not expose this MCP server to non-trusted local processes.",
      inputSchema: z.object({}),
    },
    () => safeCall(() => listProvidersImpl(callApi)),
  );

  // ----- Action tools -----

  server.registerTool(
    "inspector_get_provider",
    {
      title: "Get a single provider by id",
      description:
        "Returns the full ProviderConfig for the given id, including apiKey in PLAINTEXT (same posture as inspector_list_providers).",
      inputSchema: z.object({
        id: z.string().describe("The provider id."),
      }),
    },
    ({ id }) => safeCall(() => getProviderImpl(callApi, id)),
  );

  server.registerTool(
    "inspector_replay_log",
    {
      title: "Replay a captured log against its provider",
      description:
        "Re-sends the captured request body to the upstream LLM and returns the response summary: success flag, status, responseText, input/output token counts, elapsedMs, and whether the response was streaming. Useful for re-running a request after a fix or a transient failure. Returns an error if the log id is unknown or the upstream call fails.",
      inputSchema: z.object({
        id: z.number().int().positive().describe("The log id to replay."),
      }),
    },
    (args) => safeCall(() => replayLogImpl(callApi, args)),
  );

  // ----- Provider write tools -----

  server.registerTool(
    "inspector_add_provider",
    {
      title: "Add a new LLM provider",
      description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, format (anthropic|openai), model. Optional: baseUrl, authHeader (default: bearer), apiDocsUrl.`,
      inputSchema: z.object({
        name: z.string().min(1),
        apiKey: z.string().min(1),
        format: z.enum(["anthropic", "openai"]),
        model: z.string().min(1),
        anthropicBaseUrl: z.string().optional(),
        openaiBaseUrl: z.string().optional(),
        authHeader: z.enum(["bearer", "x-api-key"]).optional(),
        apiDocsUrl: z.string().optional(),
      }),
    },
    (provider) => safeCall(() => addProviderImpl(callApi, provider)),
  );

  server.registerTool(
    "inspector_update_provider",
    {
      title: "Update an existing provider",
      description: `${PROVIDER_WRITE_WARNING} Updating with nonsense values effectively soft-deletes a provider. Confirm with the user before invoking.

PATCH-style update: only the fields you supply are changed. Returns the updated ProviderConfig.`,
      inputSchema: z.object({
        id: z.string().describe("The provider id to update."),
        name: z.string().min(1).optional(),
        apiKey: z.string().min(1).optional(),
        format: z.enum(["anthropic", "openai"]).optional(),
        model: z.string().min(1).optional(),
        baseUrl: z.string().min(1).optional(),
        authHeader: z.enum(["bearer", "x-api-key"]).optional(),
        anthropicBaseUrl: z.string().optional(),
        openaiBaseUrl: z.string().optional(),
        apiDocsUrl: z.string().optional(),
      }),
    },
    (input) => safeCall(() => updateProviderImpl(callApi, input)),
  );

  server.registerTool(
    "inspector_test_provider",
    {
      title: "Test a provider's connectivity",
      description:
        "Runs the same connectivity test the UI's 'Test' button does, against both Anthropic and OpenAI endpoints if configured. Returns the per-endpoint success/failure result. Returns an error if the provider id is unknown.",
      inputSchema: z.object({
        id: z.string().describe("The provider id to test."),
      }),
    },
    ({ id }) => safeCall(() => testProviderImpl(callApi, id)),
  );
}

// Surface the tool catalog for tests that assert on the 11-tool name set.
export const TOOL_NAMES = [
  "inspector_list_logs",
  "inspector_get_log",
  "inspector_get_log_chunks",
  "inspector_list_sessions",
  "inspector_list_models",
  "inspector_list_providers",
  "inspector_get_provider",
  "inspector_replay_log",
  "inspector_add_provider",
  "inspector_update_provider",
  "inspector_test_provider",
] as const;
export type ToolName = (typeof TOOL_NAMES)[number];
