/**
 * Pure (dependency-injected) tool handler implementations for the MCP
 * server. These are split out from `server.ts` so unit tests can pass a
 * mock `callApi` without spinning up a real HTTP loopback. The
 * `registerTool` calls in `server.ts` simply wrap each impl in `safeCall`
 * and bind the real `callApi` from `./loopback`.
 *
 * Each impl returns a `CallToolResult`-shaped object: either
 * `{ content: [{ type: "text", text: string }] }` on success or
 * `{ content: [...], isError: true }` on failure. The wrapping
 * `safeCall` in `server.ts` only catches *thrown* errors (loopback
 * timeouts, etc); semantic errors (404 from /api) are returned as
 * `isError: true` results from inside the impl.
 */

import { z } from "zod";
import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";

/** Minimal callApi contract — same shape as the real `callApi` in loopback.ts. */
export type CallApiFn = (path: string, options?: CallApiOptions) => Promise<Response>;

export type ToolResult = {
  content: { type: "text"; text: string }[];
  isError?: true;
};

// Loose shape returned by `GET /api/logs` — `logs: CapturedLog[]` and a few
// pagination fields. The CapturedLog items are fully validated downstream.
const LogsListResponseSchema = z.object({
  logs: z.array(CapturedLogSchema).optional(),
  total: z.number().optional(),
  offset: z.number().optional(),
  limit: z.number().optional(),
});

const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
export const LIMIT_HARD_CAP = 5;

export function clampLimit(input: number | undefined): number {
  if (input === undefined) return PAGINATION_DEFAULTS.limit;
  if (!Number.isFinite(input) || input < 1) return 1;
  return Math.min(Math.floor(input), LIMIT_HARD_CAP);
}

function textJson(data: unknown): ToolResult {
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}

function toolError(message: string): ToolResult {
  return { content: [{ type: "text", text: message }], isError: true };
}

export function buildLogSummary(log: CapturedLog) {
  const hasError =
    (log.error !== null && log.error !== undefined && log.error.length > 0) ||
    (log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400);
  return {
    id: log.id,
    timestamp: log.timestamp,
    provider: log.providerName ?? null,
    model: log.model,
    apiFormat: log.apiFormat,
    method: log.method,
    path: log.path,
    status: log.responseStatus,
    isStreaming: log.streaming,
    hasError,
    latencyMs: log.elapsedMs,
    tokens: {
      input: log.inputTokens,
      output: log.outputTokens,
      cacheCreate: log.cacheCreationInputTokens,
      cacheRead: log.cacheReadInputTokens,
    },
    sessionId: log.sessionId,
    clientPid: log.clientPid ?? null,
    lastUserMessagePreview: extractLastUserMessagePreview(log),
    responsePreview: extractResponsePreview(log),
    hasChunks:
      (log.streamingChunks !== undefined && log.streamingChunks !== null) ||
      (log.streamingChunksPath !== null && log.streamingChunksPath !== undefined),
    hasRawRequestBody: log.rawRequestBody !== null && log.rawRequestBody !== undefined,
  };
}

// ===========================================================================
// Per-tool impls
// ===========================================================================

export type ListLogsArgs = {
  offset?: number;
  limit?: number;
  sessionId?: string;
  model?: string;
};

export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
  const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
  const limit = clampLimit(args.limit);
  const params = new URLSearchParams({ offset: String(offset), limit: String(limit) });
  if (args.sessionId !== undefined && args.sessionId.length > 0) {
    params.set("sessionId", args.sessionId);
  }
  if (args.model !== undefined && args.model.length > 0) {
    params.set("model", args.model);
  }
  const res = await callApi(`/api/logs?${params.toString()}`);
  if (!res.ok) return toolError(`GET /api/logs returned ${res.status}`);
  const rawBody: unknown = await res.json();
  const parsed = LogsListResponseSchema.safeParse(rawBody);
  if (!parsed.success) return toolError("GET /api/logs returned an unparseable shape");
  const logs = parsed.data.logs ?? [];
  return textJson(logs.map(buildLogSummary));
}

export async function getLogImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
  const res = await callApi(`/api/logs/${id}`);
  if (!res.ok) return toolError(`GET /api/logs/${id} returned ${res.status}`);
  const rawBody: unknown = await res.json();
  const parsed = CapturedLogSchema.safeParse(rawBody);
  if (!parsed.success) {
    return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
  }
  // Strip streamingChunks per design D6 — the agent asks for chunks
  // explicitly via inspector_get_log_chunks.
  const { streamingChunks: _chunks, ...rest } = parsed.data;
  return textJson(rest);
}

export async function getLogChunksImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
  const res = await callApi(`/api/logs/${id}/chunks`);
  if (!res.ok) return toolError(`GET /api/logs/${id}/chunks returned ${res.status}`);
  return textJson(await res.json());
}

export async function listSessionsImpl(callApi: CallApiFn): Promise<ToolResult> {
  const res = await callApi("/api/sessions");
  if (!res.ok) return toolError(`GET /api/sessions returned ${res.status}`);
  return textJson(await res.json());
}

export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
  const res = await callApi("/api/models");
  if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
  return textJson(await res.json());
}

export async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
  const res = await callApi("/api/providers");
  if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
  return textJson(await res.json());
}

export async function getProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
  const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
  if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
  return textJson(await res.json());
}

export type ReplayLogArgs = { id: number };

export async function replayLogImpl(callApi: CallApiFn, args: ReplayLogArgs): Promise<ToolResult> {
  const logRes = await callApi(`/api/logs/${args.id}`);
  if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
  const logRaw: unknown = await logRes.json();
  const logParsed = CapturedLogSchema.safeParse(logRaw);
  if (!logParsed.success) {
    return toolError(`GET /api/logs/${args.id} returned an unparseable CapturedLog`);
  }
  const log = logParsed.data;
  if (log.rawRequestBody === null) return toolError("Log has no rawRequestBody to replay");
  const replayRes = await callApi(`/api/logs/${args.id}/replay`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ modifiedBody: log.rawRequestBody }),
  });
  if (!replayRes.ok) {
    return toolError(`POST /api/logs/${args.id}/replay returned ${replayRes.status}`);
  }
  return textJson(await replayRes.json());
}

export type AddProviderInput = {
  name: string;
  apiKey: string;
  format: "anthropic" | "openai";
  model: string;
  anthropicBaseUrl?: string;
  openaiBaseUrl?: string;
  authHeader?: "bearer" | "x-api-key";
  apiDocsUrl?: string;
};

export async function addProviderImpl(
  callApi: CallApiFn,
  provider: AddProviderInput,
): Promise<ToolResult> {
  const res = await callApi("/api/providers", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(provider),
  });
  if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
  return textJson(await res.json());
}

export type UpdateProviderInput = {
  id: string;
  name?: string;
  apiKey?: string;
  format?: "anthropic" | "openai";
  model?: string;
  baseUrl?: string;
  authHeader?: "bearer" | "x-api-key";
  anthropicBaseUrl?: string;
  openaiBaseUrl?: string;
  apiDocsUrl?: string;
};

export async function updateProviderImpl(
  callApi: CallApiFn,
  input: UpdateProviderInput,
): Promise<ToolResult> {
  const { id, ...patch } = input;
  const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(patch),
  });
  if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
  return textJson(await res.json());
}

export async function testProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
  const res = await callApi(`/api/providers/${encodeURIComponent(id)}/test`, {
    method: "POST",
  });
  if (!res.ok) return toolError(`POST /api/providers/${id}/test returned ${res.status}`);
  return textJson(await res.json());
}

/**
 * Wrap an impl so any thrown error becomes a well-formed `isError: true`
 * CallToolResult (instead of a JSON-RPC protocol error, which would abort
 * the whole session). Loopback timeouts get a clear message.
 */
export async function safeCall(fn: () => Promise<ToolResult>): Promise<ToolResult> {
  try {
    return await fn();
  } catch (err) {
    if (err instanceof LoopbackTimeoutError) {
      return toolError(`Internal loopback call timed out after ${err.timeoutMs}ms: ${err.path}`);
    }
    const message = err instanceof Error ? err.message : String(err);
    return toolError(`Tool execution failed: ${message}`);
  }
}
