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: "info" | "warning" | "error";
  source_file_id?: string;
  details?: Record<string, number | string | boolean>;
}

export interface HookImportLine {
  lineNo: number;
  rawLine: 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 toIsoOrNow(value: unknown): string {
  if (typeof value === "string" && Number.isFinite(Date.parse(value))) return new Date(value).toISOString();
  return new Date().toISOString();
}

function safeName(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  if (!trimmed || trimmed.length > 180) return null;
  return /^[A-Za-z0-9_.:-]+$/u.test(trimmed) ? trimmed : null;
}

type OptionalField<T> =
  | { state: "absent"; value: null }
  | { state: "valid"; value: T }
  | { state: "invalid"; value: null };

function hasOwnField(record: Record<string, unknown>, field: string): boolean {
  return Object.prototype.hasOwnProperty.call(record, field);
}

function optionalEnum<T extends string>(record: Record<string, unknown>, field: string, allowed: readonly T[]): OptionalField<T> {
  if (!hasOwnField(record, field)) return { state: "absent", value: null };
  const normalized = typeof record[field] === "string" ? record[field].trim().toLowerCase() : "";
  return (allowed as readonly string[]).includes(normalized)
    ? { state: "valid", value: normalized as T }
    : { state: "invalid", value: null };
}

function optionalNonNegativeInteger(record: Record<string, unknown>, field: string): OptionalField<number> {
  if (!hasOwnField(record, field)) return { state: "absent", value: null };
  const value = record[field];
  return typeof value === "number" && Number.isInteger(value) && value >= 0
    ? { state: "valid", value }
    : { state: "invalid", value: null };
}

function hasConsistentSuggestionMetadata(
  outcome: "none" | "mcp" | "skill" | "mcp_and_skill",
  mcpSuggestionCount: number,
  skillSuggestionCount: number
): boolean {
  switch (outcome) {
    case "none":
      return mcpSuggestionCount === 0 && skillSuggestionCount === 0;
    case "mcp":
      return mcpSuggestionCount > 0 && skillSuggestionCount === 0;
    case "skill":
      return mcpSuggestionCount === 0 && skillSuggestionCount > 0;
    case "mcp_and_skill":
      return mcpSuggestionCount > 0 && skillSuggestionCount > 0;
  }
}

function inferredHookPhase(event: string): "lifecycle" | "prompt" | "pretool" | null {
  if (event === "SessionStart") return "lifecycle";
  if (event === "UserPromptSubmit") return "prompt";
  if (event === "PreToolUse") return "pretool";
  return null;
}

function hotwordFromPrompt(parsed: Record<string, unknown>): "quick" | "agents" | "quick_agents" | null {
  if (!Array.isArray(parsed.detectedHotwords)) return null;
  const hotwords = new Set(parsed.detectedHotwords.filter((value): value is string => typeof value === "string").map((value) => value.toLowerCase()));
  const quick = hotwords.has("!quick");
  const agents = hotwords.has("!agents");
  return quick && agents ? "quick_agents" : quick ? "quick" : agents ? "agents" : null;
}

export async function importHookLogLines(
  context: ImportContext,
  entries: readonly HookImportLine[]
): Promise<{ counters: ImportCounters; warnings: WarningEntry[] }> {
  const counters: ImportCounters = { sessions_upserted: 0, messages_upserted: 0, runtime_events_upserted: 0 };
  const warnings: WarningEntry[] = [];

  await context.db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    for (const entry of entries) {
      const line = entry.rawLine.trim();
      if (!line) continue;
      let parsed: Record<string, unknown>;
      try {
        parsed = JSON.parse(line) as Record<string, unknown>;
      } catch {
        warnings.push(
          sanitizeWarning({
            code: "INVALID_JSON_LINE_SKIPPED",
            message_code: "INVALID_JSON_LINE_SKIPPED",
            severity: "warning",
            source_file_id: context.sourceFileId,
            details: { line_no: entry.lineNo }
          })
        );
        continue;
      }

      const event = typeof parsed.event === "string" ? parsed.event : "Unknown";
      if (event === "SkillUsed") {
        continue;
      }
      const occurredAt = toIsoOrNow(parsed.ts);

      let eventType: "mcp" | "skill" | "hook" | "agent_tool" | "agent_lifecycle" | "hotword" = "hook";
      let eventOrigin: "observed_structured" | "hook_hint" = "hook_hint";
      let eventName = typeof parsed.hook === "string" ? parsed.hook : event;
      let mcpServerName: string | null = null;
      let toolName: string | null = null;
      let skillName: string | null = null;
      const hookName: string | null = typeof parsed.hook === "string" ? parsed.hook : null;
      const hookEvent: string | null = event;
      let metadata: Record<string, unknown> | null = null;
      let isSelfEvent = 0;
      const parsedHookPhase = optionalEnum(parsed, "hook_phase", ["lifecycle", "prompt", "pretool"] as const);
      const parsedSuggestionOutcome = optionalEnum(parsed, "suggestion_outcome", ["none", "mcp", "skill", "mcp_and_skill"] as const);
      const parsedMcpSuggestionCount = optionalNonNegativeInteger(parsed, "mcp_suggestion_count");
      const parsedSkillSuggestionCount = optionalNonNegativeInteger(parsed, "skill_suggestion_count");
      const parsedContextMode = optionalEnum(parsed, "contextMode", ["light", "full"] as const);
      const parsedSubagentMode = optionalEnum(parsed, "subagentMode", ["off", "requested", "required"] as const);
      for (const [field, parsedField, category] of [
        ["hook_phase", parsedHookPhase, "invalid_enum"],
        ["suggestion_outcome", parsedSuggestionOutcome, "invalid_enum"],
        ["mcp_suggestion_count", parsedMcpSuggestionCount, "invalid_non_negative_integer"],
        ["skill_suggestion_count", parsedSkillSuggestionCount, "invalid_non_negative_integer"],
        ["contextMode", parsedContextMode, "invalid_enum"],
        ["subagentMode", parsedSubagentMode, "invalid_enum"]
      ] as const) {
        if (parsedField.state !== "invalid") continue;
        warnings.push(sanitizeWarning({
          code: "HOOK_FIELD_INVALID",
          message_code: "HOOK_FIELD_INVALID",
          severity: "warning",
          source_file_id: context.sourceFileId,
          details: { line_no: entry.lineNo, field, category }
        }));
      }
      let suggestionMetadataIsInconsistent = false;
      if (
        parsedSuggestionOutcome.state === "valid" &&
        parsedMcpSuggestionCount.state === "valid" &&
        parsedSkillSuggestionCount.state === "valid"
      ) {
        suggestionMetadataIsInconsistent = !hasConsistentSuggestionMetadata(
          parsedSuggestionOutcome.value,
          parsedMcpSuggestionCount.value,
          parsedSkillSuggestionCount.value
        );
      }
      if (suggestionMetadataIsInconsistent) {
        warnings.push(sanitizeWarning({
          code: "HOOK_SUGGESTION_METADATA_INCONSISTENT",
          message_code: "HOOK_SUGGESTION_METADATA_INCONSISTENT",
          severity: "warning",
          source_file_id: context.sourceFileId,
          details: { line_no: entry.lineNo, field: "suggestion_outcome", category: "outcome_count_mismatch" }
        }));
      }
      const suggestionOutcome = parsedMcpSuggestionCount.state === "invalid" || parsedSkillSuggestionCount.state === "invalid" || suggestionMetadataIsInconsistent
        ? null
        : parsedSuggestionOutcome.value;
      const hookPhase = parsedHookPhase.state === "valid" ? parsedHookPhase.value : inferredHookPhase(event);
      const mcpSuggestionCount = parsedMcpSuggestionCount.state === "valid" ? parsedMcpSuggestionCount.value : 0;
      const skillSuggestionCount = parsedSkillSuggestionCount.state === "valid" ? parsedSkillSuggestionCount.value : 0;

      if (event === "McpHint") {
        eventType = "hook";
        eventOrigin = "hook_hint";
        const rawServer = typeof parsed.mcp_server === "string" ? parsed.mcp_server : "";
        const rawToolName = safeName(parsed.tool_name);
        if (isAllowedMcpServer(rawServer)) {
          mcpServerName = rawServer;
          toolName = rawToolName;
          eventName = toolName ? `${mcpServerName}.${toolName}` : `mcp_hint.${mcpServerName}`;
          metadata = {
            kind: "mcp_hint",
            mcp_server_name: rawServer,
            ...(toolName ? { tool_name: toolName } : {}),
            ...(safeName(parsed.raw_tool_name) ? { raw_tool_name: safeName(parsed.raw_tool_name) } : {}),
            timestamp_source: "native"
          };
          if (rawServer === "analytics-mcp-server") isSelfEvent = 1;
        } else {
          metadata = { kind: "mcp_hint", timestamp_source: "native" };
        }
      } else if (event === "SkillHint") {
        skillName = safeName(parsed.skill);
        if (!skillName) continue;
        eventType = "skill";
        eventOrigin = "hook_hint";
        eventName = `skill.${skillName}`;
        metadata = { kind: "skill_hint", timestamp_source: "native" };
      } else if (event === "SkillDeclared") {
        skillName = safeName(parsed.skill);
        if (!skillName) continue;
        eventType = "skill";
        eventOrigin = "observed_structured";
        eventName = `skill.${skillName}`;
        metadata = { kind: "skill_declared", timestamp_source: "native" };
      } else if (event === "HookCall" || event === "SessionStart" || event === "UserPromptSubmit" || event === "PreToolUse") {
        eventType = "hook";
        eventOrigin = "hook_hint";
        metadata = { kind: "hook_call", timestamp_source: "native" };
      }

      const argsObj =
        parsed.args && typeof parsed.args === "object" && !Array.isArray(parsed.args)
          ? (parsed.args as Record<string, unknown>)
          : {};
      const argsKeys = Object.keys(argsObj).filter((k) => /^[a-zA-Z0-9_]{1,80}$/u.test(k)).sort();
      const argsKeysJson = JSON.stringify(argsKeys);
      const argsStr = JSON.stringify(argsObj);
      const argsHash = hmacHex(argsStr, context.hmacSalt);
      const argsBytes = Buffer.byteLength(argsStr, "utf8");
      const eventId = hmacHex(
        `${context.machineId}:hook_log:${context.sourceFileId}:${entry.lineNo}:${eventType}:${eventName}:${occurredAt}`,
        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, hook_phase, suggestion_outcome, mcp_suggestion_count, skill_suggestion_count,
            source_line_no, event_seq, args_keys_json, args_hash, args_bytes,
            occurred_at, timestamp_source, is_self_event, metadata_json
          ) VALUES (?, 'hook_log', ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'native', ?, ?)
        `,
        [
          eventId,
          context.sourceFileId,
          eventType,
          eventOrigin,
          eventName,
          mcpServerName,
          toolName,
          skillName,
          hookName,
          hookEvent,
          hookPhase,
          suggestionOutcome,
          mcpSuggestionCount,
          skillSuggestionCount,
          entry.lineNo,
          entry.lineNo,
          argsKeysJson,
          argsHash,
          argsBytes,
          occurredAt,
          isSelfEvent,
          metadata ? JSON.stringify(metadata) : null
        ]
      );
      const observationKind =
        event === "SkillHint" ? "skill_hint" : event === "McpHint" ? "hook_hint" : "hook_log";
      const observationId = hmacHex(`${eventId}:hook_log:${context.sourceFileId}:${entry.lineNo}:${observationKind}`, 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 (?, ?, 'hook_log', ?, NULL, ?, ?, ?, ?)`,
        [observationId, eventId, context.sourceFileId, entry.lineNo, entry.lineNo, observationKind, occurredAt]
      );
      counters.runtime_events_upserted += 1;

      const hotwordMode = event === "UserPromptSubmit" ? hotwordFromPrompt(parsed) : null;
      if (hotwordMode) {
        const contextMode = parsedContextMode.value;
        const subagentMode = parsedSubagentMode.value;
        const quickOverridden = (hotwordMode === "quick" || hotwordMode === "quick_agents")
          ? contextMode === "full" ? 1 : contextMode === "light" ? 0 : null
          : null;
        const hotwordEventId = hmacHex(`${eventId}:hotword`, 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, hook_name, hook_event,
            hotword_mode, context_mode, subagent_mode, quick_overridden, source_line_no, event_seq, args_keys_json,
            args_hash, args_bytes, occurred_at, timestamp_source, is_self_event, metadata_json
          ) VALUES (?, 'hook_log', ?, NULL, 'hotword', 'hook_hint', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'native', 0, ?)`,
          [
            hotwordEventId,
            context.sourceFileId,
            `hotword.${hotwordMode}`,
            hookName,
            hookEvent,
            hotwordMode,
            contextMode,
            subagentMode,
            quickOverridden,
            entry.lineNo,
            entry.lineNo,
            argsKeysJson,
            argsHash,
            argsBytes,
            occurredAt,
            JSON.stringify({ kind: "hotword", timestamp_source: "native" })
          ]
        );
        const hotwordObservationId = hmacHex(`${hotwordEventId}:hook_log:${context.sourceFileId}:${entry.lineNo}:hook_log`, 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 (?, ?, 'hook_log', ?, NULL, ?, ?, 'hook_log', ?)`,
          [hotwordObservationId, hotwordEventId, context.sourceFileId, entry.lineNo, entry.lineNo, occurredAt]
        );
        counters.runtime_events_upserted += 1;
      }
    }
    await context.db.exec("COMMIT");
  } catch (error) {
    await context.db.exec("ROLLBACK");
    throw error;
  }

  return { counters, warnings };
}
