/**
 * MCP HTTP adapter (Option B).
 * Connects to a running Engram HTTP server for evaluation.
 * Activated via --mcp flag.
 */

import { randomUUID } from "node:crypto";
import type {
  MemorySystem,
  Message,
  SearchResult,
  MemoryStats,
} from "./types.js";

export interface McpAdapterOptions {
  baseUrl: string;
  authToken?: string;
  timeoutMs?: number;
}

async function mcpRequest(
  baseUrl: string,
  method: string,
  params: Record<string, unknown>,
  options: { authToken?: string; timeoutMs?: number },
): Promise<unknown> {
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };
  if (options.authToken) {
    headers["Authorization"] = `Bearer ${options.authToken}`;
  }

  const body = JSON.stringify({
    jsonrpc: "2.0",
    id: Date.now(),
    method,
    params,
  });

  const controller = new AbortController();
  const timeout = setTimeout(
    () => controller.abort(),
    options.timeoutMs ?? 30_000,
  );

  try {
    const res = await fetch(`${baseUrl}/rpc`, {
      method: "POST",
      headers,
      body,
      signal: controller.signal,
    });

    if (!res.ok) {
      throw new Error(`MCP HTTP ${res.status}: ${await res.text()}`);
    }

    const json = (await res.json()) as { result?: unknown; error?: { message: string } };
    if (json.error) {
      throw new Error(`MCP RPC error: ${json.error.message}`);
    }
    return json.result;
  } finally {
    clearTimeout(timeout);
  }
}

export async function createMcpAdapter(
  options: McpAdapterOptions,
): Promise<MemorySystem> {
  const { baseUrl, authToken, timeoutMs } = options;
  const rpcOpts = { authToken, timeoutMs };
  let runPrefix = createRunPrefix();
  let runSessionIds = new Set<string>();
  const qualifySessionId = (sessionId: string, prefix = runPrefix): string =>
    `${prefix}:${sessionId}`;
  const stripRunPrefix = (sessionId: string, prefix = runPrefix): string =>
    sessionId.startsWith(`${prefix}:`)
      ? sessionId.slice(prefix.length + 1)
      : sessionId;
  const isCurrentRunSession = (sessionId: string, prefix = runPrefix): boolean =>
    sessionId.startsWith(`${prefix}:`);

  // Health check
  try {
    const res = await fetch(`${baseUrl}/health`, {
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) {
      throw new Error(`Health check failed: ${res.status}`);
    }
  } catch (err) {
    throw new Error(
      `Cannot connect to Engram MCP server at ${baseUrl}: ${err instanceof Error ? err.message : err}`,
    );
  }

  return {
    async store(sessionId: string, messages: Message[]): Promise<void> {
      const qualifiedSessionId = qualifySessionId(sessionId);
      await mcpRequest(
        baseUrl,
        "engram.lcm.observe",
        { sessionId: qualifiedSessionId, messages },
        rpcOpts,
      );
      runSessionIds.add(sessionId);
    },

    async recall(sessionId: string, query: string, budgetChars?: number): Promise<string> {
      const result = await mcpRequest(
        baseUrl,
        "engram.lcm.recall",
        { sessionId: qualifySessionId(sessionId), query, budgetChars: budgetChars ?? 32000 },
        rpcOpts,
      );
      return typeof result === "string" ? result : JSON.stringify(result);
    },

    async search(
      query: string,
      limit: number,
      sessionId?: string,
    ): Promise<SearchResult[]> {
      const requestedLimit = normalizeSearchLimit(limit);
      if (requestedLimit <= 0) return [];

      const searchRunPrefix = runPrefix;
      const qualifiedSessionId =
        typeof sessionId === "string" && sessionId.length > 0
          ? qualifySessionId(sessionId, searchRunPrefix)
          : undefined;
      const result = await mcpRequest(
        baseUrl,
        "engram.lcm.search",
        {
          query,
          limit: requestedLimit,
          ...(qualifiedSessionId
            ? { sessionId: qualifiedSessionId }
            : { sessionPrefix: `${searchRunPrefix}:` }),
        },
        rpcOpts,
      );
      if (!Array.isArray(result)) return [];
      return (result as Array<Record<string, unknown>>)
        .map((r) => ({
          turnIndex: typeof r.turn_index === "number" ? r.turn_index : 0,
          role: typeof r.role === "string" ? r.role : "unknown",
          snippet: typeof r.snippet === "string" ? r.snippet : "",
          sessionId: typeof r.session_id === "string" ? r.session_id : "",
        }))
        .filter((entry) => isCurrentRunSession(entry.sessionId, searchRunPrefix))
        .slice(0, requestedLimit)
        .map((entry) => ({
          ...entry,
          sessionId: stripRunPrefix(entry.sessionId, searchRunPrefix),
        }));
    },

    async reset(): Promise<void> {
      runPrefix = createRunPrefix();
      runSessionIds = new Set<string>();
    },

    async getStats(sessionId?: string): Promise<MemoryStats> {
      const statsRunPrefix = runPrefix;
      const qualifiedSessionId =
        typeof sessionId === "string" && sessionId.length > 0
          ? qualifySessionId(sessionId, statsRunPrefix)
          : undefined;
      const readStats = async (params: Record<string, unknown>): Promise<MemoryStats> => {
        const result = await mcpRequest(
          baseUrl,
          "engram.lcm.stats",
          params,
          rpcOpts,
        );
        const r = result as Record<string, unknown> | null;
        return {
          totalMessages: typeof r?.totalMessages === "number" ? r.totalMessages : 0,
          totalSummaryNodes: typeof r?.totalSummaryNodes === "number" ? r.totalSummaryNodes : 0,
          maxDepth: typeof r?.maxDepth === "number" ? r.maxDepth : -1,
        };
      };
      if (!qualifiedSessionId) {
        const sessionIds = [...runSessionIds];
        if (sessionIds.length === 0) {
          return { totalMessages: 0, totalSummaryNodes: 0, maxDepth: -1 };
        }
        const stats = await Promise.all(
          sessionIds.map((storedSessionId) =>
            readStats({ sessionId: qualifySessionId(storedSessionId, statsRunPrefix) }),
          ),
        );
        return stats.reduce<MemoryStats>(
          (combined, next) => ({
            totalMessages: combined.totalMessages + next.totalMessages,
            totalSummaryNodes: combined.totalSummaryNodes + next.totalSummaryNodes,
            maxDepth: Math.max(combined.maxDepth, next.maxDepth),
          }),
          { totalMessages: 0, totalSummaryNodes: 0, maxDepth: -1 },
        );
      }
      return readStats({ sessionId: qualifiedSessionId });
    },

    async destroy(): Promise<void> {
      // Nothing to clean up for HTTP adapter
    },
  };
}

function createRunPrefix(): string {
  return `eval-${randomUUID()}`;
}

function normalizeSearchLimit(limit: number): number {
  if (!Number.isFinite(limit)) return 0;
  return Math.max(0, Math.floor(limit));
}
