import type {
  McpHttpSettings,
  McpServerSettings,
  McpSseSettings,
  McpStdioSettings,
} from "./config";
import { loadMcpJson, saveMcpJson } from "#cli/compat/config";
import { createMcpClient } from "#cli/compat/mcp";
import { toMcpServerConfigs } from "../commands/server-helpers";

type EnvLike = Record<string, string | undefined>;

type McpServerInput = Readonly<Record<string, unknown>>;

let nextMcpId = 0;

function nextId(prefix: string): string {
  return `${prefix}_${Date.now()}_${nextMcpId++}`;
}

let createMcpClientImpl = createMcpClient;

export function setMcpClientFactoryForTests(factory: typeof createMcpClient | undefined): void {
  createMcpClientImpl = factory ?? createMcpClient;
}

export interface McpSettingsStore {
  filePath: string;
  servers: McpServerSettings[];
}

export interface McpServerConnectionTestResult {
  success: boolean;
  message: string;
  latency: number;
  toolCount?: number;
}

function asRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  return value as Record<string, unknown>;
}

function normalizeString(value: unknown): string | undefined {
  if (typeof value === "string") {
    const trimmed = value.trim();
    return trimmed || undefined;
  }
  if (typeof value === "number" || typeof value === "boolean") {
    const trimmed = String(value).trim();
    return trimmed || undefined;
  }
  return undefined;
}

function normalizeNumber(value: unknown): number | undefined {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  const normalized = normalizeString(value);
  if (!normalized) return undefined;
  const parsed = Number(normalized);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function normalizeBoolean(value: unknown): boolean | undefined {
  if (typeof value === "boolean") return value;
  const normalized = normalizeString(value)?.toLowerCase();
  if (normalized === "true" || normalized === "1") return true;
  if (normalized === "false" || normalized === "0") return false;
  return undefined;
}

function cloneServers(servers: readonly McpServerSettings[]): McpServerSettings[] {
  return JSON.parse(JSON.stringify(servers)) as McpServerSettings[];
}

function extractStringRecord(value: unknown): Record<string, string> | undefined {
  const record = asRecord(value);
  if (!record) return undefined;
  const out: Record<string, string> = {};
  for (const [key, entry] of Object.entries(record)) {
    const normalized = normalizeString(entry);
    if (normalized) out[key] = normalized;
  }
  return Object.keys(out).length > 0 ? out : undefined;
}

function mergeStringRecord(
  base: Record<string, string> | undefined,
  patch: unknown,
): Record<string, string> | undefined {
  if (patch === null) return undefined;
  const next = extractStringRecord(patch);
  if (!next) return base ? { ...base } : undefined;
  return {
    ...base,
    ...next,
  };
}

function normalizeArgs(value: unknown): string[] | undefined {
  if (Array.isArray(value)) return value.map((arg) => String(arg));
  if (typeof value === "string") {
    return value
      .split(/\s+/)
      .map((part) => part.trim())
      .filter(Boolean);
  }
  return undefined;
}

function normalizeTransport(value: unknown): McpServerSettings["transport"] | undefined {
  const normalized = normalizeString(value)?.toLowerCase();
  if (normalized === "http" || normalized === "sse" || normalized === "stdio") return normalized;
  return undefined;
}

function inferTransport(input: McpServerInput): McpServerSettings["transport"] {
  const explicit = normalizeTransport(input.transport ?? input.type);
  if (explicit) return explicit;
  if (normalizeString(input.url)) return "http";
  return "stdio";
}

function resolveBearerAuth(
  input: McpServerInput,
  existing?: McpHttpSettings | McpSseSettings,
): McpHttpSettings["auth"] | McpSseSettings["auth"] | undefined {
  if ("auth" in input && input.auth === null) return undefined;
  const authRecord = asRecord(input.auth);
  if (authRecord && normalizeString(authRecord.type)?.toLowerCase() === "bearer") {
    const token = normalizeString(authRecord.token);
    return token ? { type: "bearer", token } : undefined;
  }
  const token = normalizeString(input.authToken ?? input.bearerToken);
  if (token) return { type: "bearer", token };
  return existing?.auth ? { ...existing.auth } : undefined;
}

function toPublicServer(server: McpServerSettings): Record<string, unknown> {
  if (server.transport === "http" || server.transport === "sse") {
    return {
      id: server.id,
      name: server.name,
      transport: server.transport,
      url: server.url,
      auth: server.auth ? { ...server.auth } : undefined,
      headers: server.headers ? { ...server.headers } : undefined,
      requestTimeoutMs: server.requestTimeoutMs,
      enabled: server.enabled !== false,
    };
  }

  return {
    id: server.id,
    name: server.name,
    transport: "stdio",
    command: server.command,
    args: [...server.args],
    env: server.env ? { ...server.env } : undefined,
    cwd: server.cwd,
    autoRestart: server.autoRestart !== false,
    requestTimeoutMs: server.requestTimeoutMs,
    restartDelayMs: server.restartDelayMs,
    maxRestarts: server.maxRestarts,
    restartWindowMs: server.restartWindowMs,
    enabled: server.enabled !== false,
  };
}

function findServerIndexByName(servers: readonly McpServerSettings[], name: string): number {
  const normalized = name.trim();
  return servers.findIndex((server) => server.name.trim() === normalized);
}

function buildHttpLikeServer(
  input: McpServerInput,
  existing: McpHttpSettings | McpSseSettings | undefined,
  transport: "http" | "sse",
): McpHttpSettings | McpSseSettings {
  const url = normalizeString(input.url) ?? existing?.url;
  if (!url) {
    throw new Error(
      `MCP server "${normalizeString(input.name) ?? existing?.name ?? "unknown"}" requires a url.`,
    );
  }

  const nextHeaders =
    "headers" in input
      ? mergeStringRecord(existing?.headers, input.headers)
      : existing?.headers
        ? { ...existing.headers }
        : undefined;

  return {
    id: normalizeString(input.id) ?? existing?.id ?? nextId("mcp"),
    name: normalizeString(input.name) ?? existing?.name ?? "mcp-server",
    transport,
    url,
    auth: resolveBearerAuth(input, existing),
    headers: nextHeaders,
    requestTimeoutMs: normalizeNumber(input.requestTimeoutMs) ?? existing?.requestTimeoutMs,
    enabled: normalizeBoolean(input.enabled) ?? existing?.enabled ?? true,
  };
}

function buildStdioServer(input: McpServerInput, existing?: McpStdioSettings): McpStdioSettings {
  const command = normalizeString(input.command) ?? existing?.command;
  if (!command) {
    throw new Error(
      `MCP server "${normalizeString(input.name) ?? existing?.name ?? "unknown"}" requires a command.`,
    );
  }

  const args =
    "args" in input ? (normalizeArgs(input.args) ?? []) : existing ? [...existing.args] : [];

  const env =
    "env" in input
      ? mergeStringRecord(existing?.env, input.env)
      : existing?.env
        ? { ...existing.env }
        : undefined;

  return {
    id: normalizeString(input.id) ?? existing?.id ?? nextId("mcp"),
    name: normalizeString(input.name) ?? existing?.name ?? "mcp-server",
    transport: "stdio",
    command,
    args,
    env,
    cwd: "cwd" in input ? normalizeString(input.cwd) : existing?.cwd,
    autoRestart:
      "autoRestart" in input
        ? (normalizeBoolean(input.autoRestart) ?? false)
        : (existing?.autoRestart ?? true),
    requestTimeoutMs: normalizeNumber(input.requestTimeoutMs) ?? existing?.requestTimeoutMs,
    restartDelayMs: normalizeNumber(input.restartDelayMs) ?? existing?.restartDelayMs,
    maxRestarts: normalizeNumber(input.maxRestarts) ?? existing?.maxRestarts,
    restartWindowMs: normalizeNumber(input.restartWindowMs) ?? existing?.restartWindowMs,
    enabled: normalizeBoolean(input.enabled) ?? existing?.enabled ?? true,
  };
}

function buildServerFromInput(
  input: McpServerInput,
  existing?: McpServerSettings,
): McpServerSettings {
  const transport =
    normalizeTransport(input.transport ?? input.type) ??
    existing?.transport ??
    inferTransport(input);
  const name = normalizeString(input.name) ?? existing?.name;
  if (!name) throw new Error("MCP server name is required.");

  const withName = {
    ...input,
    name,
  };

  if (transport === "http" || transport === "sse") {
    const existingHttpLike =
      existing?.transport === "http" || existing?.transport === "sse" ? existing : undefined;
    return buildHttpLikeServer(withName, existingHttpLike, transport);
  }

  const existingStdio = existing?.transport === "stdio" ? existing : undefined;
  return buildStdioServer(withName, existingStdio);
}

async function saveStore(
  filePath: string,
  servers: readonly McpServerSettings[],
): Promise<McpSettingsStore> {
  const nextServers = cloneServers(servers);
  await saveMcpJson(filePath, nextServers);
  return {
    filePath,
    servers: nextServers,
  };
}

export async function loadMcpSettingsStore(env: EnvLike): Promise<McpSettingsStore> {
  const { filePath, servers } = await loadMcpJson(env);
  return { filePath, servers: cloneServers(servers) };
}

export async function listMcpServers(env: EnvLike): Promise<Record<string, unknown>[]> {
  const store = await loadMcpSettingsStore(env);
  return store.servers.map((server) => toPublicServer(server));
}

export async function getMcpServer(env: EnvLike, name: string): Promise<Record<string, unknown>> {
  const store = await loadMcpSettingsStore(env);
  const index = findServerIndexByName(store.servers, name);
  if (index < 0) throw new Error(`Unknown MCP server: ${name}`);
  return toPublicServer(store.servers[index]!);
}

export async function createMcpServer(
  env: EnvLike,
  input: McpServerInput,
): Promise<McpSettingsStore> {
  const store = await loadMcpSettingsStore(env);
  const name = normalizeString(input.name);
  if (!name) throw new Error("MCP server name is required.");
  if (findServerIndexByName(store.servers, name) >= 0)
    throw new Error(`MCP server "${name}" already exists.`);
  const server = buildServerFromInput(input);
  return await saveStore(store.filePath, [...store.servers, server]);
}

export async function updateMcpServer(
  env: EnvLike,
  name: string,
  input: McpServerInput,
): Promise<McpSettingsStore> {
  const store = await loadMcpSettingsStore(env);
  const index = findServerIndexByName(store.servers, name);
  if (index < 0) throw new Error(`Unknown MCP server: ${name}`);
  const current = store.servers[index]!;
  const updated = buildServerFromInput(
    {
      ...input,
      name: current.name,
      id: current.id,
    },
    current,
  );
  const nextServers = [...store.servers];
  nextServers[index] = updated;
  return await saveStore(store.filePath, nextServers);
}

export async function deleteMcpServer(env: EnvLike, name: string): Promise<McpSettingsStore> {
  const store = await loadMcpSettingsStore(env);
  const index = findServerIndexByName(store.servers, name);
  if (index < 0) throw new Error(`Unknown MCP server: ${name}`);
  const nextServers = store.servers.filter((_, currentIndex) => currentIndex !== index);
  return await saveStore(store.filePath, nextServers);
}

export async function toggleMcpServer(
  env: EnvLike,
  name: string,
  enabled: boolean,
): Promise<McpSettingsStore> {
  return await updateMcpServer(env, name, { enabled });
}

export async function testMcpServerSettingsConnection(
  server: McpServerSettings,
): Promise<McpServerConnectionTestResult> {
  const config = toMcpServerConfigs([server])[0];
  if (!config) {
    return {
      success: false,
      message: "Invalid MCP server configuration",
      latency: 0,
    };
  }

  const startedAt = Date.now();
  const client = createMcpClientImpl(config);
  try {
    await client.initialize();
    const tools = await client.listTools();
    return {
      success: true,
      message: "OK",
      latency: Date.now() - startedAt,
      toolCount: tools.length,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return {
      success: false,
      message,
      latency: Date.now() - startedAt,
    };
  } finally {
    await client.disconnect().catch(() => {});
  }
}

export async function testMcpServer(
  env: EnvLike,
  name: string,
): Promise<McpServerConnectionTestResult> {
  const store = await loadMcpSettingsStore(env);
  const index = findServerIndexByName(store.servers, name);
  if (index < 0) throw new Error(`Unknown MCP server: ${name}`);
  return await testMcpServerSettingsConnection(store.servers[index]!);
}
