import { readFile, writeFile, stat, readdir, mkdir } from "node:fs/promises";
import { createReadStream, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { createInterface } from "node:readline";
import { Buffer } from "node:buffer";
import { resolveLogDir, logger } from "./logger";

type LogIndexEntry = {
  id: number;
  file: string;
  byteOffset: number;
  byteLength: number;
};

type LogIndex = {
  version: number;
  entries: Record<number, LogIndexEntry>;
  maxId: number;
};

const INDEX_VERSION = 1;
const INDEX_FILE = "logs.idx";

export { resolveLogDir as getLogDir };

function getIndexPath(): string {
  return join(resolveLogDir(), INDEX_FILE);
}

let cachedIndex: LogIndex | null = null;

function createEmptyIndex(): LogIndex {
  return {
    version: INDEX_VERSION,
    entries: {},
    maxId: 0,
  };
}

function isLogIndex(obj: unknown): obj is LogIndex {
  if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
  const versionDesc = Object.getOwnPropertyDescriptor(obj, "version");
  const entriesDesc = Object.getOwnPropertyDescriptor(obj, "entries");
  const maxIdDesc = Object.getOwnPropertyDescriptor(obj, "maxId");
  return (
    versionDesc !== undefined &&
    typeof versionDesc.value === "number" &&
    entriesDesc !== undefined &&
    typeof entriesDesc.value === "object" &&
    entriesDesc.value !== null &&
    maxIdDesc !== undefined &&
    typeof maxIdDesc.value === "number"
  );
}

export async function loadIndex(): Promise<LogIndex> {
  if (cachedIndex !== null) return cachedIndex;

  const indexPath = getIndexPath();
  if (!existsSync(indexPath)) {
    cachedIndex = createEmptyIndex();
    return cachedIndex;
  }

  try {
    const content = await readFile(indexPath, "utf-8");
    const parsed: unknown = JSON.parse(content);
    if (isLogIndex(parsed)) {
      cachedIndex = parsed;
    } else {
      cachedIndex = createEmptyIndex();
    }
    return cachedIndex;
  } catch (err) {
    logger.error("[logIndex] Failed to load index:", String(err));
    cachedIndex = createEmptyIndex();
    return cachedIndex;
  }
}

export async function saveIndex(index: LogIndex): Promise<void> {
  const indexPath = getIndexPath();
  const dir = dirname(indexPath);

  try {
    await mkdir(dir, { recursive: true });
  } catch {
    // Ignore
  }

  try {
    await writeFile(indexPath, JSON.stringify(index), "utf-8");
  } catch (err) {
    logger.error("[logIndex] Failed to save index:", String(err));
  }
}

export async function addToIndex(
  id: number,
  file: string,
  byteOffset: number,
  byteLength: number,
): Promise<void> {
  const index = await loadIndex();
  index.entries[id] = { id, file, byteOffset, byteLength };
  if (id > index.maxId) {
    index.maxId = id;
  }
  // Defer disk writes to reduce I/O - flush after a batch of updates
  scheduleIndexFlush();
}

// Batch writes: collect pending flushes and write once
let indexFlushScheduled = false;
let indexFlushTimeout: ReturnType<typeof setTimeout> | null = null;

async function flushIndexAsync(): Promise<void> {
  indexFlushScheduled = false;
  indexFlushTimeout = null;
  const index = await loadIndex();
  await saveIndex(index);
}

function scheduleIndexFlush(): void {
  if (indexFlushScheduled) return;
  indexFlushScheduled = true;
  indexFlushTimeout = setTimeout(() => {
    void flushIndexAsync();
  }, 1000); // Flush after 1 second of inactivity
}

export async function flushIndex(): Promise<void> {
  if (indexFlushTimeout !== null) {
    clearTimeout(indexFlushTimeout);
    indexFlushTimeout = null;
  }
  indexFlushScheduled = false;
  const index = await loadIndex();
  await saveIndex(index);
}

export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
  const index = await loadIndex();
  return index.entries[id] ?? null;
}

type FileIndexResult = {
  entries: Record<number, LogIndexEntry>;
  maxId: number;
};

async function indexFile(filePath: string, file: string): Promise<FileIndexResult> {
  const entries: Record<number, LogIndexEntry> = {};
  let maxId = 0;
  let byteOffset = 0;

  const fileStream = createInterface({
    input: createReadStream(filePath),
    crlfDelay: Infinity,
  });

  for await (const line of fileStream) {
    const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
    if (line.trim() !== "") {
      try {
        const entry: unknown = JSON.parse(line);
        if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
          const idDesc = Object.getOwnPropertyDescriptor(entry, "id");
          if (idDesc !== undefined && typeof idDesc.value === "number") {
            const entryId = idDesc.value;
            maxId = Math.max(maxId, entryId);
            entries[entryId] = {
              id: entryId,
              file,
              byteOffset,
              byteLength: lineBytes,
            };
          }
        }
      } catch {
        // Skip malformed lines
      }
    }
    byteOffset += lineBytes;
  }

  return { entries, maxId };
}

export async function rebuildIndex(): Promise<LogIndex> {
  const logDir = resolveLogDir();
  const newIndex = createEmptyIndex();

  if (!existsSync(logDir)) {
    cachedIndex = newIndex;
    await saveIndex(newIndex);
    return newIndex;
  }

  const files = (await readdir(logDir)).filter((f) => f.endsWith(".jsonl")).sort();
  const CONCURRENCY = 4;

  // Process files in bounded-concurrency batches
  for (let i = 0; i < files.length; i += CONCURRENCY) {
    const batch = files.slice(i, i + CONCURRENCY);
    const results = await Promise.all(batch.map((file) => indexFile(join(logDir, file), file)));

    for (const result of results) {
      Object.assign(newIndex.entries, result.entries);
      newIndex.maxId = Math.max(newIndex.maxId, result.maxId);
    }
  }

  cachedIndex = newIndex;
  await saveIndex(newIndex);
  return newIndex;
}

// Async mutex for atomic ID generation to prevent race conditions
// Uses a promise queue instead of busy-waiting
let idGenerationPromise: Promise<void> | null = null;
let releaseLock: (() => void) | null = null;

async function acquireLock(): Promise<void> {
  if (releaseLock === null) {
    idGenerationPromise = new Promise<void>((resolve) => {
      releaseLock = resolve;
    });
  } else {
    // Wait for the previous lock to be released
    await idGenerationPromise;
    // After waiting, we need to create a new promise for the next waiter
    idGenerationPromise = new Promise<void>((resolve) => {
      releaseLock = resolve;
    });
  }
}

function releaseLockFn(): void {
  if (releaseLock) {
    const resolve = releaseLock;
    releaseLock = null;
    idGenerationPromise = null;
    resolve();
  }
}

export async function getNextLogId(): Promise<number> {
  await acquireLock();
  try {
    const index = await loadIndex();
    const nextId = index.maxId + 1;
    index.maxId = nextId;
    // Synchronously update the index in memory (disk write is deferred via batching)
    cachedIndex = index;
    return nextId;
  } finally {
    releaseLockFn();
  }
}

export function getCurrentLogFile(): string {
  const now = new Date();
  const yyyy = now.getUTCFullYear();
  const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(now.getUTCDate()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd}.jsonl`;
}
