import { readFile } from "node:fs/promises";
import type { DatabaseAdapter } from "../db.js";
import { hmacHex } from "../privacy.js";
import { isAllowedMcpServer } from "../repo-catalog.js";
import { FileImportError } from "./file-import-contract.js";

interface ImportCounters {
  sessions_upserted: number;
  messages_upserted: number;
  runtime_events_upserted: number;
}

interface WarningEntry {
  code: string;
  message_code: string;
  severity: "info" | "warning" | "error";
  source_file_id?: string;
}

function warn(code: string, sourceFileId: string): WarningEntry {
  return { code, message_code: code, severity: "warning", source_file_id: sourceFileId };
}

function cleanRole(v: unknown): "user" | "assistant" | "unknown" {
  return v === "user" || v === "assistant" ? v : "unknown";
}

const INTERNAL_CLAUDE_TOOLS = new Set(["Read", "Bash", "Glob", "Grep", "Edit", "PowerShell", "Agent", "ToolSearch", "Write", "ExitPlanMode"]);

function parseMcpPrefixedToolName(name: string): { server: string; tool: string } | null {
  const m = /^mcp__([a-z0-9_]+)__([a-zA-Z0-9_]+)$/u.exec(name);
  if (!m) return null;
  return { server: m[1].replaceAll("_", "-"), tool: m[2] };
}

export async function importClaudeCodeFile(
  context: { db: DatabaseAdapter; sourceFileId: string; machineId: string; hmacSalt: string },
  absolutePath: string
): Promise<{ counters: ImportCounters; warnings: WarningEntry[] }> {
  const raw = await readFile(absolutePath, "utf8");
  const lines = raw.split(/\r?\n/u).map((v) => v.trim()).filter(Boolean);
  const committedCounters: ImportCounters = { sessions_upserted: 0, messages_upserted: 0, runtime_events_upserted: 0 };
  const warnings: WarningEntry[] = [];
  let detectedFormat: "code" | "legacy" | null = null;
  const codeRecords: Record<string, unknown>[] = [];
  const legacyRecords: Record<string, unknown>[] = [];
  for (const line of lines) {
    let parsed: Record<string, unknown>;
    try {
      parsed = JSON.parse(line) as Record<string, unknown>;
    } catch {
      warnings.push(warn("INVALID_JSON_LINE_SKIPPED", context.sourceFileId));
      continue;
    }
    const isCodeRecord = typeof parsed.type === "string" && typeof parsed.sessionId === "string";
    const isLegacyRecord = typeof parsed.session_id === "string";
    if (detectedFormat === null && isCodeRecord) detectedFormat = "code";
    if (detectedFormat === null && isLegacyRecord) detectedFormat = "legacy";
    if (detectedFormat === "code" && isCodeRecord) codeRecords.push(parsed);
    if (detectedFormat === "legacy" && isLegacyRecord) legacyRecords.push(parsed);
  }
  if (detectedFormat === null) throw new FileImportError("SOURCE_SCHEMA_UNSUPPORTED");
  await context.db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    let eventSeq = 0;
    if (detectedFormat === "code") {
      const bySession = new Map<
        string,
        {
          sid: string;
          createdAt: string | null;
          updatedAt: string;
          model: string | null;
          cwd: string | null;
          messages: Array<{ role: "user" | "assistant" | "unknown"; created_at: string; model: string | null; tokens: { input: number; output: number; cache_read: number; cache_write: number; reasoning: number; token_available: boolean } }>;
          mcpCalls: Array<{ occurred_at: string; mcp_server_name: string; tool_name: string; args_hash: string }>;
        }
      >();

      for (const parsed of codeRecords) {
        const sid = typeof parsed.sessionId === "string" ? parsed.sessionId : null;
        if (!sid) continue;
        const isSidechain = parsed.isSidechain === true;
        const agentId = typeof parsed.agentId === "string" ? parsed.agentId : "unknown";
        const sidRaw = isSidechain ? `${sid}:subagent:${agentId}` : sid;
        const ts = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
        if (!bySession.has(sidRaw)) {
          bySession.set(sidRaw, { sid: sidRaw, createdAt: ts, updatedAt: ts, model: null, cwd: null, messages: [], mcpCalls: [] });
        }
        const agg = bySession.get(sidRaw)!;
        agg.updatedAt = ts;
        const msg = parsed.message && typeof parsed.message === "object" ? (parsed.message as Record<string, unknown>) : {};
        const usage = msg.usage && typeof msg.usage === "object" ? (msg.usage as Record<string, unknown>) : {};
        const model = typeof msg.model === "string" ? msg.model.slice(0, 160) : null;
        if (model) agg.model = model;
        // `cwd` is the workspace path Claude Code was run from -- hashed into `project_id` below
        // (never stored/displayed raw), the same privacy-preserving convention vscode-copilot.ts
        // already uses for `workspace_uri`.
        if (typeof parsed.cwd === "string") agg.cwd = parsed.cwd;
        if (parsed.type === "user" || parsed.type === "assistant") {
          const inTok = typeof usage.input_tokens === "number" ? Math.max(0, Math.floor(usage.input_tokens)) : 0;
          const outTok = typeof usage.output_tokens === "number" ? Math.max(0, Math.floor(usage.output_tokens)) : 0;
          const cacheWrite =
            typeof usage.cache_creation_input_tokens === "number" ? Math.max(0, Math.floor(usage.cache_creation_input_tokens)) : 0;
          const cacheRead = typeof usage.cache_read_input_tokens === "number" ? Math.max(0, Math.floor(usage.cache_read_input_tokens)) : 0;
          const reasoningTok = typeof usage.thinking_tokens === "number" ? Math.max(0, Math.floor(usage.thinking_tokens)) : 0;
          agg.messages.push({
            role: cleanRole(parsed.type),
            created_at: ts,
            model,
            tokens: { input: inTok, output: outTok, cache_read: cacheRead, cache_write: cacheWrite, reasoning: reasoningTok, token_available: inTok + outTok + cacheRead + cacheWrite + reasoningTok > 0 }
          });
        }
        const mcpServer = typeof parsed.attributionMcpServer === "string" ? parsed.attributionMcpServer : "";
        const mcpTool = typeof parsed.attributionMcpTool === "string" ? parsed.attributionMcpTool : "";
        if (mcpServer && mcpTool && !INTERNAL_CLAUDE_TOOLS.has(mcpServer) && isAllowedMcpServer(mcpServer)) {
          agg.mcpCalls.push({ occurred_at: ts, mcp_server_name: mcpServer, tool_name: mcpTool, args_hash: "noargs" });
        } else {
          const toolNameOnly =
            (typeof parsed.attributionMcpTool === "string" && parsed.attributionMcpTool) ||
            (typeof parsed.tool_name === "string" && parsed.tool_name) ||
            (typeof parsed.toolName === "string" && parsed.toolName) ||
            "";
          const parsedPrefixed = toolNameOnly ? parseMcpPrefixedToolName(toolNameOnly) : null;
          if (parsedPrefixed && isAllowedMcpServer(parsedPrefixed.server)) {
            agg.mcpCalls.push({ occurred_at: ts, mcp_server_name: parsedPrefixed.server, tool_name: parsedPrefixed.tool, args_hash: "noargs" });
          }
        }
      }

      for (const agg of bySession.values()) {
        const sessionId = hmacHex(`${context.machineId}:claude:code:${agg.sid}`, context.hmacSalt);
        if (agg.messages.length === 0 && agg.mcpCalls.length === 0) {
          await context.db.run("DELETE FROM sessions WHERE id = ? AND source = 'claude' AND source_file_id = ?", [sessionId, context.sourceFileId]);
          continue;
        }
        const projectId = agg.cwd ? `workspace:${hmacHex(agg.cwd, context.hmacSalt).slice(0, 24)}` : null;
        await context.db.run("DELETE FROM message_metrics WHERE session_id = ?", [sessionId]);
        await context.db.run("DELETE FROM runtime_events WHERE session_id = ?", [sessionId]);
        await context.db.run(
          `INSERT INTO sessions(
            id, source, source_session_hash, source_file_id, machine_id, project_id, mode, model_primary, token_available,
            message_count, user_message_count, assistant_message_count, created_at, updated_at, created_at_source, client_surface, session_kind, metadata_json
          ) VALUES (?, 'claude', ?, ?, ?, ?, 'chat', ?, ?, ?, ?, ?, ?, ?, 'native', 'code', ?, NULL)
          ON CONFLICT(id) DO UPDATE SET
            source=excluded.source, source_file_id=excluded.source_file_id, project_id=excluded.project_id, model_primary=excluded.model_primary,
            token_available=excluded.token_available, message_count=excluded.message_count,
            user_message_count=excluded.user_message_count, assistant_message_count=excluded.assistant_message_count,
            created_at=excluded.created_at, updated_at=excluded.updated_at, client_surface=excluded.client_surface, session_kind=excluded.session_kind`,
          [
            sessionId,
            hmacHex(agg.sid, context.hmacSalt),
            context.sourceFileId,
            context.machineId,
            projectId,
            agg.model,
            agg.messages.some((m) => m.tokens.token_available) ? 1 : 0,
            agg.messages.length,
            agg.messages.filter((m) => m.role === "user").length,
            agg.messages.filter((m) => m.role === "assistant").length,
            agg.createdAt,
            agg.updatedAt,
            agg.sid.includes(":subagent:") ? "subagent" : "main"
          ]
        );
        committedCounters.sessions_upserted += 1;

        for (let i = 0; i < agg.messages.length; i += 1) {
          const msg = agg.messages[i];
          await context.db.run(
            `INSERT INTO message_metrics(
               id, session_id, source_file_id, seq, role, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
               reasoning_tokens, token_available, partial_token_data, created_at, timestamp_source, metadata_json
             ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 'native', NULL)`,
            [
              hmacHex(`${context.machineId}:${sessionId}:${i + 1}`, context.hmacSalt),
              sessionId,
              context.sourceFileId,
              i + 1,
              msg.role,
              msg.model,
              msg.tokens.input,
              msg.tokens.output,
              msg.tokens.cache_read,
              msg.tokens.cache_write,
              msg.tokens.reasoning,
              msg.tokens.token_available ? 1 : 0,
              msg.created_at
            ]
          );
          committedCounters.messages_upserted += 1;
        }

        for (const call of agg.mcpCalls) {
          eventSeq += 1;
          const eventName = `${call.mcp_server_name}.${call.tool_name}`;
          const eventId = hmacHex(
            `${context.machineId}:canon:mcp:${eventName}:${call.occurred_at}:${sessionId}:${call.args_hash || "noargs"}`,
            context.hmacSalt
          );
          await context.db.run(
            `INSERT OR IGNORE INTO runtime_events(
              id, source, source_file_id, session_id, event_type, event_origin, event_name, mcp_server_name, tool_name,
              skill_name, hook_name, hook_event, source_line_no, event_seq, args_keys_json, args_hash, args_bytes,
              occurred_at, timestamp_source, is_self_event, metadata_json
            ) VALUES (?, 'claude', ?, ?, 'mcp', 'observed_structured', ?, ?, ?, NULL, NULL, NULL, NULL, ?, '[]', NULL, 0, ?, 'native', ?, NULL)`,
            [
              eventId,
              context.sourceFileId,
              sessionId,
              eventName,
              call.mcp_server_name,
              call.tool_name,
              eventSeq,
              call.occurred_at,
              call.mcp_server_name === "analytics-mcp-server" ? 1 : 0
            ]
          );
          const observationId = hmacHex(`${eventId}:claude:${context.sourceFileId}:${eventSeq}:chat_structured`, context.hmacSalt);
          await context.db.run(
            `INSERT OR IGNORE INTO runtime_event_observations(
              id, runtime_event_id, observed_source, source_file_id, session_id, source_line_no, event_seq, observation_kind, observed_at
            ) VALUES (?, ?, 'claude', ?, ?, NULL, ?, 'chat_structured', ?)`,
            [observationId, eventId, context.sourceFileId, sessionId, eventSeq, call.occurred_at]
          );
          committedCounters.runtime_events_upserted += 1;
        }
      }
    } else {
      for (const parsed of legacyRecords) {
        const sidRaw = typeof parsed.session_id === "string" ? parsed.session_id : null;
        if (!sidRaw) {
          warnings.push(warn("RECORD_SCHEMA_UNSUPPORTED", context.sourceFileId));
          continue;
        }
        const sessionId = hmacHex(`${context.machineId}:claude:code:${sidRaw}`, context.hmacSalt);
        const createdAt = typeof parsed.created_at === "string" ? parsed.created_at : null;
        const updatedAt = typeof parsed.updated_at === "string" ? parsed.updated_at : new Date().toISOString();
        const model = typeof parsed.model === "string" ? parsed.model.slice(0, 160) : null;
        const messages = (Array.isArray(parsed.messages) ? parsed.messages : []).filter((value): value is Record<string, unknown> => {
          if (!value || typeof value !== "object" || Array.isArray(value)) return false;
          const role = (value as Record<string, unknown>).role;
          return role === "user" || role === "assistant";
        });
        const mcpCalls = (Array.isArray(parsed.mcp_calls) ? parsed.mcp_calls : []).flatMap((value) => {
          if (!value || typeof value !== "object" || Array.isArray(value)) return [];
          const call = value as Record<string, unknown>;
          const server = typeof call.mcp_server_name === "string" ? call.mcp_server_name : "";
          const tool = typeof call.tool_name === "string" ? call.tool_name : "";
          if (!server || !tool || !isAllowedMcpServer(server)) return [];
          return [{ server, tool, occurredAt: typeof call.occurred_at === "string" ? call.occurred_at : updatedAt }];
        });
        if (messages.length === 0 && mcpCalls.length === 0) {
          await context.db.run("DELETE FROM sessions WHERE id = ? AND source = 'claude' AND source_file_id = ?", [sessionId, context.sourceFileId]);
          continue;
        }

      await context.db.run("DELETE FROM message_metrics WHERE session_id = ?", [sessionId]);
      await context.db.run("DELETE FROM runtime_events WHERE session_id = ?", [sessionId]);

      await context.db.run(
        `INSERT INTO sessions(
          id, source, source_session_hash, source_file_id, machine_id, project_id, mode, model_primary, token_available,
          message_count, user_message_count, assistant_message_count, created_at, updated_at, created_at_source, client_surface, session_kind, metadata_json
        ) VALUES (?, 'claude', ?, ?, ?, NULL, 'chat', ?, 0, ?, ?, ?, ?, ?, 'native', 'code', 'main', NULL)
        ON CONFLICT(id) DO UPDATE SET
          source=excluded.source, source_file_id=excluded.source_file_id, model_primary=excluded.model_primary,
          token_available=excluded.token_available, message_count=excluded.message_count,
          user_message_count=excluded.user_message_count, assistant_message_count=excluded.assistant_message_count,
          created_at=excluded.created_at, updated_at=excluded.updated_at, client_surface=excluded.client_surface, session_kind=excluded.session_kind`,
        [
          sessionId,
          hmacHex(sidRaw, context.hmacSalt),
          context.sourceFileId,
          context.machineId,
          model,
          messages.length,
          messages.filter((m) => (m as { role?: unknown }).role === "user").length,
          messages.filter((m) => (m as { role?: unknown }).role === "assistant").length,
          createdAt,
          updatedAt
        ]
      );
      committedCounters.sessions_upserted += 1;

      for (let i = 0; i < messages.length; i += 1) {
        const msg = messages[i];
        await context.db.run(
          `INSERT INTO message_metrics(
             id, session_id, source_file_id, seq, role, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
             reasoning_tokens, token_available, partial_token_data, created_at, timestamp_source, metadata_json
           ) VALUES (?, ?, ?, ?, ?, NULL, 0, 0, 0, 0, 0, 0, 0, ?, 'native', NULL)`,
          [
            hmacHex(`${context.machineId}:${sessionId}:${i + 1}`, context.hmacSalt),
            sessionId,
            context.sourceFileId,
            i + 1,
            cleanRole(msg.role),
            typeof msg.created_at === "string" ? msg.created_at : updatedAt
          ]
        );
        committedCounters.messages_upserted += 1;
      }

        for (const call of mcpCalls) {
        const server = call.server;
        const tool = call.tool;
        eventSeq += 1;
        const occurredAt = call.occurredAt;
        const eventName = `${server}.${tool}`;
        const eventId = hmacHex(`${context.machineId}:canon:mcp:${eventName}:${occurredAt}:${sessionId}:noargs`, context.hmacSalt);
        await context.db.run(
          `INSERT OR IGNORE INTO runtime_events(
            id, source, source_file_id, session_id, event_type, event_origin, event_name, mcp_server_name, tool_name,
            skill_name, hook_name, hook_event, source_line_no, event_seq, args_keys_json, args_hash, args_bytes,
            occurred_at, timestamp_source, is_self_event, metadata_json
          ) VALUES (?, 'claude', ?, ?, 'mcp', 'observed_structured', ?, ?, ?, NULL, NULL, NULL, NULL, ?, '[]', NULL, 0, ?, 'native', ?, NULL)`,
          [
            eventId,
            context.sourceFileId,
            sessionId,
            eventName,
            server,
            tool,
            eventSeq,
            occurredAt,
            server === "analytics-mcp-server" ? 1 : 0
          ]
        );
        const observationId = hmacHex(`${eventId}:claude:${context.sourceFileId}:${eventSeq}:chat_structured`, context.hmacSalt);
        await context.db.run(
          `INSERT OR IGNORE INTO runtime_event_observations(
            id, runtime_event_id, observed_source, source_file_id, session_id, source_line_no, event_seq, observation_kind, observed_at
          ) VALUES (?, ?, 'claude', ?, ?, NULL, ?, 'chat_structured', ?)`,
          [observationId, eventId, context.sourceFileId, sessionId, eventSeq, occurredAt]
        );
        committedCounters.runtime_events_upserted += 1;
        }
      }
    }
    await context.db.exec("COMMIT");
  } catch (error) {
    try {
      await context.db.exec("ROLLBACK");
    } catch {
      // Preserve the original SQL/import error.
    }
    throw error;
  }
  return { counters: committedCounters, warnings };
}

export async function importClaudeDesktopFile(
  context: { sourceFileId: string }
): Promise<{ counters: ImportCounters; warnings: WarningEntry[] }> {
  throw new FileImportError("SOURCE_SCHEMA_UNSUPPORTED");
}

