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

interface ImportContext {
  db: DatabaseAdapter;
  sourceFileId: string;
  machineId: string;
  hmacSalt: string;
}

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;
  details?: Record<string, number | string | boolean>;
}

interface NormalizedMessage {
  role: "user" | "assistant" | "unknown";
  created_at: string;
  model?: string | null;
  tokens?: { input?: number; output?: number; cache_read?: number; cache_write?: number; reasoning?: number };
}

interface NormalizedMcpCall {
  occurred_at: string;
  mcp_server_name: string;
  tool_name: string;
  args: Record<string, unknown>;
}
interface NormalizedLifecycleEvent {
  occurred_at: string;
  event_name: string;
}

function sanitizeWarning(input: WarningEntry): WarningEntry {
  return {
    code: input.code,
    message_code: input.code,
    severity: input.severity,
    ...(input.source_file_id ? { source_file_id: input.source_file_id } : {}),
    ...(input.details ? { details: input.details } : {})
  };
}

function cleanMode(value: unknown): "chat" | "agent" | "edit" | "unknown" {
  if (value === "chat" || value === "agent" || value === "edit") return value;
  return "unknown";
}

function cleanRole(value: unknown): "user" | "assistant" | "unknown" {
  if (value === "user" || value === "assistant") return value;
  return "unknown";
}

function cleanModel(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const v = value.trim();
  if (!v || v.length > 160 || v.includes("\n") || v.includes("\\") || v.startsWith("{")) return null;
  if (v.startsWith("codex/")) return v;
  return `codex/${v}`;
}

function parseSessionLine(line: string): Record<string, unknown> {
  const parsed = JSON.parse(line) as unknown;
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("INVALID_SESSION_OBJECT");
  }
  return parsed as Record<string, unknown>;
}

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

function normalizeCodexEventLines(lines: string[]): { sessions: Record<string, unknown>[]; lifecycleEvents: NormalizedLifecycleEvent[] } {
  const bySession = new Map<
    string,
    { sid: string; created_at: string | null; updated_at: string; mode: string; model: string | null; messages: NormalizedMessage[]; mcp_calls: NormalizedMcpCall[]; lifecycle_events: NormalizedLifecycleEvent[] }
  >();
  let fallbackSid = "line-1";
  const lifecycleEvents: NormalizedLifecycleEvent[] = [];
  for (let i = 0; i < lines.length; i += 1) {
    const row = JSON.parse(lines[i] ?? "") as Record<string, unknown>;
    const ts = typeof row.timestamp === "string" ? row.timestamp : new Date().toISOString();
    const type = typeof row.type === "string" ? row.type : "";
    const payload = row.payload && typeof row.payload === "object" ? (row.payload as Record<string, unknown>) : {};
    const sidRaw =
      (typeof payload.id === "string" && payload.id) ||
      (typeof payload.session_id === "string" && payload.session_id) ||
      (typeof row.session_id === "string" && (row.session_id as string)) ||
      fallbackSid;
    fallbackSid = sidRaw;

    if (!bySession.has(sidRaw)) {
      bySession.set(sidRaw, { sid: sidRaw, created_at: ts, updated_at: ts, mode: "chat", model: null, messages: [], mcp_calls: [], lifecycle_events: [] });
    }
    const agg = bySession.get(sidRaw)!;
    agg.updated_at = ts;
    if (type === "session_meta") {
      if (typeof payload.mode === "string") agg.mode = payload.mode;
      continue;
    }
    if (type === "turn_context") {
      if (typeof payload.model === "string") agg.model = payload.model;
      continue;
    }
    if (type === "event_msg") {
      const pt = typeof payload.type === "string" ? payload.type : "";
      if (pt === "collab_agent_spawn_end" || pt === "collab_close_end") {
        const lifecycleEvent = { occurred_at: ts, event_name: pt };
        lifecycleEvents.push(lifecycleEvent);
        agg.lifecycle_events.push(lifecycleEvent);
      }
      if (pt === "token_count") {
        const info = payload.info && typeof payload.info === "object" ? (payload.info as Record<string, unknown>) : {};
        const last = info.last_token_usage && typeof info.last_token_usage === "object" ? (info.last_token_usage as Record<string, unknown>) : {};
        agg.messages.push({
          role: "assistant",
          created_at: ts,
          tokens: {
            input: typeof last.input_tokens === "number" ? last.input_tokens : 0,
            output: typeof last.output_tokens === "number" ? last.output_tokens : 0,
            cache_read: typeof last.cached_input_tokens === "number" ? last.cached_input_tokens : 0,
            reasoning: typeof last.reasoning_output_tokens === "number" ? last.reasoning_output_tokens : 0
          }
        });
      } else if (pt === "user_message" || pt === "assistant_message" || payload.role === "user" || payload.role === "assistant") {
        const role = payload.role === "user" || pt === "user_message" ? "user" : "assistant";
        agg.messages.push({ role, created_at: ts });
      }
      continue;
    }
    if (type === "response_item") {
      const payloadType = typeof payload.type === "string" ? payload.type : "";
      const name = typeof payload.name === "string" ? payload.name : "";
      if (payloadType === "function_call" && name) {
        const parsed = parseCodexFunctionName(name);
        if (parsed) {
          const args = payload.arguments && typeof payload.arguments === "object" ? (payload.arguments as Record<string, unknown>) : {};
          agg.mcp_calls.push({
            occurred_at: ts,
            mcp_server_name: parsed.server,
            tool_name: parsed.tool,
            args
          });
        }
      }
    }
  }

  return { sessions: [...bySession.values()].map((entry) => ({
    session_id: entry.sid,
    created_at: entry.created_at,
    updated_at: entry.updated_at,
    mode: entry.mode,
    model: entry.model,
    messages: entry.messages,
    mcp_calls: entry.mcp_calls,
    lifecycle_events: entry.lifecycle_events
  })), lifecycleEvents };
}

export async function importCodexFile(
  context: ImportContext,
  absolutePath: string
): Promise<{ counters: ImportCounters; warnings: WarningEntry[] }> {
  const raw = await readFile(absolutePath, "utf8");
  const lines = raw.split(/\r?\n/u).map((entry) => entry.trim()).filter(Boolean);
  const counters: ImportCounters = { sessions_upserted: 0, messages_upserted: 0, runtime_events_upserted: 0 };
  const warnings: WarningEntry[] = [];
  let eventSeqGlobal = 0;

  await context.db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    const firstLine = lines[0] ? JSON.parse(lines[0]) as Record<string, unknown> : {};
    const eventStyle = !!firstLine && typeof firstLine === "object" && typeof firstLine.type === "string" && "payload" in firstLine;
    const normalized = eventStyle ? normalizeCodexEventLines(lines) : { sessions: lines.map((line) => parseSessionLine(line)), lifecycleEvents: [] };
    const sessions = normalized.sessions;
    if (normalized.lifecycleEvents.length > 0) {
      warnings.push(
        sanitizeWarning({
          code: "COLLAB_AGENT_LIFECYCLE_DETECTED",
          message_code: "COLLAB_AGENT_LIFECYCLE_DETECTED",
          severity: "warning",
          source_file_id: context.sourceFileId,
          details: { count: normalized.lifecycleEvents.length }
        })
      );
    }

    for (let lineIndex = 0; lineIndex < sessions.length; lineIndex += 1) {
      const sessionObj = sessions[lineIndex] ?? {};
      const sourceSessionId = typeof sessionObj.session_id === "string" ? sessionObj.session_id : `line-${lineIndex + 1}`;
      const sessionId = hmacHex(`${context.machineId}:codex:${sourceSessionId}`, context.hmacSalt);
      const updatedAt = typeof sessionObj.updated_at === "string" ? sessionObj.updated_at : new Date().toISOString();
      const createdAt = typeof sessionObj.created_at === "string" ? sessionObj.created_at : null;
      const modelPrimary = cleanModel(sessionObj.model);
      const messages = Array.isArray(sessionObj.messages) ? sessionObj.messages : [];
      const sourceSessionHash = hmacHex(sourceSessionId, 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]);

      const userCount = messages.filter((msg) => (msg as { role?: unknown }).role === "user").length;
      const assistantCount = messages.filter((msg) => (msg as { role?: unknown }).role === "assistant").length;
      const tokenAvailable = messages.some((msg) => {
        const t = (msg as { tokens?: Record<string, unknown> }).tokens;
        return !!t && typeof t === "object" && (typeof t.input === "number" || typeof t.output === "number");
      });

      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 (?, 'codex', ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, 'native', 'cli', 'main', NULL)
          ON CONFLICT(id) DO UPDATE SET
            source_file_id = excluded.source_file_id,
            mode = excluded.mode,
            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,
            created_at_source = excluded.created_at_source,
            client_surface = excluded.client_surface,
            session_kind = excluded.session_kind
        `,
        [
          sessionId,
          sourceSessionHash,
          context.sourceFileId,
          context.machineId,
          cleanMode(sessionObj.mode),
          modelPrimary,
          tokenAvailable ? 1 : 0,
          messages.length,
          userCount,
          assistantCount,
          createdAt,
          updatedAt
        ]
      );
      counters.sessions_upserted += 1;

      for (let idx = 0; idx < messages.length; idx += 1) {
        const msg = messages[idx] as Record<string, unknown>;
        const tokens = (msg.tokens && typeof msg.tokens === "object" ? msg.tokens : {}) as Record<string, unknown>;
        const msgId = hmacHex(`${context.machineId}:${sessionId}:${idx + 1}`, context.hmacSalt);
        const model = cleanModel(msg.model);
        const inTok = typeof tokens.input === "number" ? Math.max(0, Math.floor(tokens.input)) : 0;
        const outTok = typeof tokens.output === "number" ? Math.max(0, Math.floor(tokens.output)) : 0;
        const cacheRead = typeof tokens.cache_read === "number" ? Math.max(0, Math.floor(tokens.cache_read)) : 0;
        const cacheWrite = typeof tokens.cache_write === "number" ? Math.max(0, Math.floor(tokens.cache_write)) : 0;
        const reasoningTok = typeof tokens.reasoning === "number" ? Math.max(0, Math.floor(tokens.reasoning)) : 0;
        const msgTokenAvailable = inTok > 0 || outTok > 0 || cacheRead > 0 || cacheWrite > 0 || reasoningTok > 0;

        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)
          `,
          [
            msgId,
            sessionId,
            context.sourceFileId,
            idx + 1,
            cleanRole(msg.role),
            model,
            inTok,
            outTok,
            cacheRead,
            cacheWrite,
            reasoningTok,
            msgTokenAvailable ? 1 : 0,
            typeof msg.created_at === "string" ? msg.created_at : updatedAt
          ]
        );
        counters.messages_upserted += 1;
      }

      const mcpCalls = Array.isArray(sessionObj.mcp_calls) ? sessionObj.mcp_calls : [];
      for (const call of mcpCalls) {
        const c = call as Record<string, unknown>;
        const mcpServerName = typeof c.mcp_server_name === "string" ? c.mcp_server_name : "";
        const toolName = typeof c.tool_name === "string" ? c.tool_name : "";
        if (!mcpServerName || !toolName || !isAllowedMcpServer(mcpServerName)) {
          warnings.push(
            sanitizeWarning({
              code: "UNSUPPORTED_FORMAT",
              message_code: "UNSUPPORTED_FORMAT",
              severity: "warning",
              source_file_id: context.sourceFileId
            })
          );
          continue;
        }
        eventSeqGlobal += 1;
        const occurredAt = typeof c.occurred_at === "string" ? c.occurred_at : updatedAt;
        const eventName = `${mcpServerName}.${toolName}`;
        const args = c.args && typeof c.args === "object" ? (c.args as Record<string, unknown>) : {};
        const keys = Object.keys(args).filter((k) => /^[a-zA-Z0-9_]{1,80}$/u.test(k)).sort();
        const argsKeysJson = JSON.stringify(keys);
        const argsBytes = Buffer.byteLength(JSON.stringify(args), "utf8");
        const argsHash = hmacHex(JSON.stringify(args), context.hmacSalt);
        const argsMarker = argsHash || "noargs";
        const eventId = hmacHex(`${context.machineId}:canon:mcp:${eventName}:${occurredAt}:${sessionId}:${argsMarker}`, 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 (?, 'codex', ?, ?, 'mcp', 'observed_structured', ?, ?, ?, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, 'native', ?, NULL)
          `,
          [
            eventId,
            context.sourceFileId,
            sessionId,
            eventName,
            mcpServerName,
            toolName,
            eventSeqGlobal,
            argsKeysJson,
            argsHash,
            argsBytes,
            occurredAt,
            mcpServerName === "analytics-mcp-server" ? 1 : 0
          ]
        );
        const observationId = hmacHex(`${eventId}:codex:${context.sourceFileId}:${eventSeqGlobal}: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 (?, ?, 'codex', ?, ?, NULL, ?, 'chat_structured', ?)`,
          [observationId, eventId, context.sourceFileId, sessionId, eventSeqGlobal, occurredAt]
        );
        counters.runtime_events_upserted += 1;
      }

      const lifecycleEvents = Array.isArray(sessionObj.lifecycle_events) ? sessionObj.lifecycle_events : [];
      for (const lifecycle of lifecycleEvents) {
        const lc = lifecycle as Record<string, unknown>;
        const eventName = typeof lc.event_name === "string" ? lc.event_name : "collab_unknown";
        const occurredAt = typeof lc.occurred_at === "string" ? lc.occurred_at : updatedAt;
        eventSeqGlobal += 1;
        const eventId = hmacHex(`${context.machineId}:canon:agent_lifecycle:${eventName}:${occurredAt}:${sessionId}`, 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 (?, 'codex', ?, ?, 'agent_lifecycle', 'observed_structured', ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, '[]', NULL, 0, ?, 'native', 0, NULL)`,
          [eventId, context.sourceFileId, sessionId, eventName, eventSeqGlobal, occurredAt]
        );
        const observationId = hmacHex(`${eventId}:codex:${context.sourceFileId}:${eventSeqGlobal}: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 (?, ?, 'codex', ?, ?, NULL, ?, 'chat_structured', ?)`,
          [observationId, eventId, context.sourceFileId, sessionId, eventSeqGlobal, occurredAt]
        );
        counters.runtime_events_upserted += 1;
      }
    }

    await context.db.exec("COMMIT");
  } catch (error) {
    await context.db.exec("ROLLBACK");
    if (error instanceof SyntaxError) {
      warnings.push(
        sanitizeWarning({
          code: "INVALID_JSON_LINE_SKIPPED",
          message_code: "INVALID_JSON_LINE_SKIPPED",
          severity: "warning",
          source_file_id: context.sourceFileId
        })
      );
    }
    throw error;
  }

  return { counters, warnings };
}

export function codexStorageValidationNote(): string {
  return "Codex Desktop storage candidate roots validated via fixture-driven import; real-machine Windows/Ubuntu verification documented in README.";
}

