import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import type { DatabaseAdapter } from "./db.js";
import { hmacHex } from "./privacy.js";

const MAX_THREAD_NAME_LENGTH = 300;
const MAX_INDEX_LINE_BYTES = 1024 * 1024;

export interface CodexSessionIndexWarning {
  code: "CODEX_SESSION_INDEX_LINE_SKIPPED" | "CODEX_SESSION_INDEX_READ_FAILED";
  message_code: "CODEX_SESSION_INDEX_LINE_SKIPPED" | "CODEX_SESSION_INDEX_READ_FAILED";
  severity: "warning";
  details?: { count: number };
}

export interface CodexSessionIndexLine {
  line?: string;
  tooLarge?: true;
}

export interface CodexSessionIndexReader {
  statFile?: (absolutePath: string) => Promise<{ isFile(): boolean }>;
  readLines?: (absolutePath: string) => AsyncIterable<CodexSessionIndexLine>;
}

export interface CodexSessionIndexReadResult {
  // Titles are usable only when the whole append-only index has been read successfully.
  threadNames: Map<string, string>;
  warnings: CodexSessionIndexWarning[];
  complete: boolean;
}

export class CodexSessionIndexSyncError extends Error {
  public readonly code = "CODEX_SESSION_INDEX_SYNC_FAILED";

  public constructor() {
    super("CODEX_SESSION_INDEX_SYNC_FAILED");
    this.name = "CodexSessionIndexSyncError";
  }
}

export function normalizeCodexThreadName(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const normalized = value
    .trim()
    .replace(/[\u0000-\u001F\u007F]+/gu, " ")
    .replace(/\s+/gu, " ")
    .trim()
    .slice(0, MAX_THREAD_NAME_LENGTH)
    .trim();
  return normalized || null;
}

function incrementWarning(warnings: CodexSessionIndexWarning[], code: CodexSessionIndexWarning["code"]): void {
  const existing = warnings.find((warning) => warning.code === code);
  if (existing) {
    existing.details = { count: Number(existing.details?.count ?? 0) + 1 };
    return;
  }
  warnings.push({ code, message_code: code, severity: "warning", ...(code === "CODEX_SESSION_INDEX_LINE_SKIPPED" ? { details: { count: 1 } } : {}) });
}

async function* readIndexLines(absolutePath: string): AsyncGenerator<CodexSessionIndexLine> {
  const stream = createReadStream(absolutePath, { encoding: "utf8", highWaterMark: 64 * 1024 });
  let buffered = "";
  let discardingOversizedLine = false;
  for await (const chunk of stream) {
    let remaining = chunk as string;
    if (discardingOversizedLine) {
      const newlineIndex = remaining.indexOf("\n");
      if (newlineIndex < 0) continue;
      discardingOversizedLine = false;
      remaining = remaining.slice(newlineIndex + 1);
    }
    buffered += remaining;
    let newlineIndex = buffered.indexOf("\n");
    while (newlineIndex >= 0) {
      const line = buffered.slice(0, newlineIndex).trim();
      buffered = buffered.slice(newlineIndex + 1);
      if (Buffer.byteLength(line, "utf8") > MAX_INDEX_LINE_BYTES) yield { tooLarge: true };
      else if (line) yield { line };
      newlineIndex = buffered.indexOf("\n");
    }
    if (Buffer.byteLength(buffered, "utf8") > MAX_INDEX_LINE_BYTES) {
      buffered = "";
      discardingOversizedLine = true;
      yield { tooLarge: true };
    }
  }
  if (!discardingOversizedLine) {
    const line = buffered.trim();
    if (line) yield { line };
  }
}

export async function readCodexSessionIndex(
  absolutePath: string,
  reader: CodexSessionIndexReader = {}
): Promise<CodexSessionIndexReadResult> {
  const threadNames = new Map<string, string>();
  const warnings: CodexSessionIndexWarning[] = [];
  const statFile = reader.statFile ?? stat;
  const readLines = reader.readLines ?? readIndexLines;
  try {
    const fileStats = await statFile(absolutePath);
    if (!fileStats.isFile()) {
      incrementWarning(warnings, "CODEX_SESSION_INDEX_READ_FAILED");
      return { threadNames, warnings, complete: false };
    }
    for await (const entry of readLines(absolutePath)) {
      if (entry.tooLarge || !entry.line) {
        incrementWarning(warnings, "CODEX_SESSION_INDEX_LINE_SKIPPED");
        continue;
      }
      try {
        const parsed: unknown = JSON.parse(entry.line);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
          incrementWarning(warnings, "CODEX_SESSION_INDEX_LINE_SKIPPED");
          continue;
        }
        const record = parsed as Record<string, unknown>;
        const id = typeof record.id === "string" ? record.id.trim() : "";
        const threadName = normalizeCodexThreadName(record.thread_name);
        if (!id || !threadName) {
          incrementWarning(warnings, "CODEX_SESSION_INDEX_LINE_SKIPPED");
          continue;
        }
        threadNames.set(id, threadName);
      } catch {
        incrementWarning(warnings, "CODEX_SESSION_INDEX_LINE_SKIPPED");
      }
    }
  } catch (error) {
    if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
      return { threadNames, warnings, complete: true };
    }
    threadNames.clear();
    incrementWarning(warnings, "CODEX_SESSION_INDEX_READ_FAILED");
    return { threadNames, warnings, complete: false };
  }
  return { threadNames, warnings, complete: true };
}

function parseMetadata(value: string | null): Record<string, unknown> {
  if (!value) return {};
  try {
    const parsed: unknown = JSON.parse(value);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
  } catch {
    return {};
  }
}

export async function synchronizeCodexSessionIndex(
  db: DatabaseAdapter,
  hmacSalt: string,
  absolutePath: string,
  reader?: CodexSessionIndexReader
): Promise<{ updated: number; warnings: CodexSessionIndexWarning[] }> {
  const index = await readCodexSessionIndex(absolutePath, reader);
  if (!index.complete || index.threadNames.size === 0) return { updated: 0, warnings: index.warnings };

  let updated = 0;
  let transactionStarted = false;
  try {
    await db.exec("BEGIN");
    transactionStarted = true;
    for (const [sourceSessionId, threadName] of index.threadNames) {
      const sourceSessionHash = hmacHex(sourceSessionId, hmacSalt);
      const sessions = await db.all<{ id: string; metadata_json: string | null }>(
        "SELECT id, metadata_json FROM sessions WHERE source = 'codex' AND source_session_hash = ?",
        [sourceSessionHash]
      );
      for (const session of sessions) {
        const metadata = parseMetadata(session.metadata_json);
        if (metadata.thread_name === threadName) continue;
        await db.run("UPDATE sessions SET metadata_json = ? WHERE id = ? AND source = 'codex'", [JSON.stringify({ ...metadata, thread_name: threadName }), session.id]);
        updated += 1;
      }
    }
    await db.exec("COMMIT");
    return { updated, warnings: index.warnings };
  } catch {
    if (transactionStarted) {
      try {
        await db.exec("ROLLBACK");
      } catch {
        // The original SQL failure remains the externally visible sync result.
      }
    }
    throw new CodexSessionIndexSyncError();
  }
}
