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

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

interface WarningEntry {
  code: string;
  message_code: string;
  severity: "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 counters: ImportCounters = { sessions_upserted: 0, messages_upserted: 0, runtime_events_upserted: 0 };
  const warnings: WarningEntry[] = [];
  await context.db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    let eventSeq = 0;
    const looksLikeJsonlEvents = lines.length > 0 && (() => {
      try {
        const first = JSON.parse(lines[0] ?? "") as Record<string, unknown>;
        return typeof first.type === "string" && typeof first.sessionId === "string";
      } catch {
        return false;
      }
    })();

    if (looksLikeJsonlEvents) {
      const bySession = new Map<
        string,
        {
          sid: string;
          createdAt: string | null;
          updatedAt: string;
          model: 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 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 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, 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;
        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);
        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', ?, ?, ?, ?, ?, ?, ?, 'native', 'code', ?, 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(agg.sid, context.hmacSalt),
            context.sourceFileId,
            context.machineId,
            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"
          ]
        );
        counters.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
            ]
          );
          counters.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]
          );
          counters.runtime_events_upserted += 1;
        }
      }
    } else {
      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 sidRaw = typeof parsed.session_id === "string" ? parsed.session_id : null;
        if (!sidRaw) {
          warnings.push(warn("UNSUPPORTED_FORMAT", 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 : [];

      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
        ]
      );
      counters.sessions_upserted += 1;

      for (let i = 0; i < messages.length; i += 1) {
        const msg = messages[i] as Record<string, unknown>;
        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
          ]
        );
        counters.messages_upserted += 1;
      }

        const mcpCalls = Array.isArray(parsed.mcp_calls) ? parsed.mcp_calls : [];
        for (const c of mcpCalls) {
        const call = c 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)) continue;
        eventSeq += 1;
        const occurredAt = typeof call.occurred_at === "string" ? call.occurred_at : updatedAt;
        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]
        );
        counters.runtime_events_upserted += 1;
        }
      }
    }
    await context.db.exec("COMMIT");
  } catch {
    await context.db.exec("ROLLBACK");
    warnings.push(warn("UNSUPPORTED_FORMAT", context.sourceFileId));
  }
  return { counters, warnings };
}

export async function importClaudeDesktopFile(
  context: { sourceFileId: string }
): Promise<{ counters: ImportCounters; warnings: WarningEntry[] }> {
  return {
    counters: { sessions_upserted: 0, messages_upserted: 0, runtime_events_upserted: 0 },
    warnings: [warn("UNSUPPORTED_FORMAT", context.sourceFileId)]
  };
}

