import { readFileSync, existsSync, createReadStream } from "node:fs";
import { open } from "node:fs/promises";
import { readdir as readdirCallback } from "node:fs/promises";
import { createInterface } from "node:readline";
import { Buffer } from "node:buffer";
import { join } from "node:path";
import { appendLogEntry, resolveLogDir, logger } from "./logger";
import {
  addToIndex,
  findInIndex,
  getNextLogId,
  getCurrentLogFile,
  saveIndex,
  loadIndex,
} from "./logIndex";
import { writeChunks } from "./chunkStorage";
import type { CapturedLog } from "./schemas";
import { CapturedLogSchema } from "./schemas";
import {
  clearSessionRegistry,
  getLogSessionId,
  getSessionIds,
  markSessionFinished,
  markSessionStarted,
  observeSessionLog,
  rebuildSessionRegistry,
  resolveSessionIdentity,
  type SessionClientInfo,
} from "./sessionSupervisor";

export type { CapturedLog };
export { getNextLogId };
export { getLogSessionId, getSessionSnapshots } from "./sessionSupervisor";

const MAX_MEMORY_CACHE = 100;

// Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
const memoryCache: Map<number, CapturedLog> = new Map();

function evictOldestIfNeeded(): void {
  while (memoryCache.size > MAX_MEMORY_CACHE) {
    // Map preserves insertion order; first key = oldest
    const oldestKey = memoryCache.keys().next().value;
    if (oldestKey === undefined) break;
    memoryCache.delete(oldestKey);
  }
}

function normalizeLogSession(log: CapturedLog): CapturedLog {
  const sessionId = getLogSessionId(log);
  if (sessionId === log.sessionId) return log;
  return { ...log, sessionId };
}

function addToCache(log: CapturedLog): void {
  const cachedLog = normalizeLogSession(log);
  // If updating an existing entry, remove first to reset insertion order
  if (memoryCache.has(cachedLog.id)) {
    memoryCache.delete(cachedLog.id);
  }
  memoryCache.set(cachedLog.id, cachedLog);
  evictOldestIfNeeded();
}

function removeFromCache(id: number): void {
  memoryCache.delete(id);
}

/**
 * Add a test log entry directly to the in-memory store (for dashboard display).
 * This is used by the provider test endpoint alongside appendLogEntry (file logging).
 */
export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
  const id = await getNextLogId();
  // Update the index with the new maxId so subsequent calls get unique IDs
  const index = await loadIndex();
  if (id > index.maxId) {
    index.maxId = id;
    await saveIndex(index);
  }

  // Persist streaming chunks to disk if present
  let streamingChunksPath: string | null = null;
  if (entry.streamingChunks !== undefined && entry.streamingChunks.chunks.length > 0) {
    streamingChunksPath = writeChunks(
      id,
      entry.streamingChunks.chunks,
      entry.streamingChunks.truncated,
    );
  }

  const session = resolveSessionIdentity({
    explicitSessionId: entry.sessionId,
    isTest: entry.isTest,
  });

  const log: CapturedLog = {
    id,
    ...entry,
    sessionId: session.id,
    streamingChunksPath,
  };
  addToCache(log);
  observeSessionLog(log, session.source);
  emitLogUpdate(log);
  return log;
}

type ClientInfo = SessionClientInfo;

export async function createLog(
  method: string,
  path: string,
  requestBody: string | null,
  headers: Headers,
  clientInfo?: ClientInfo,
  rawHeaders?: Record<string, string>,
  upstreamHeaders?: Record<string, string>,
  apiFormat: "anthropic" | "openai" | "unknown" = "unknown",
  model: string | null = null,
  sessionId: string | null = null,
  /** Pre-acquired log id; if omitted, `getNextLogId` is awaited internally. */
  preAcquiredId?: number,
): Promise<CapturedLog> {
  const userAgent = headers.get("user-agent");
  const origin = headers.get("origin");

  const id = preAcquiredId ?? (await getNextLogId());
  const session = resolveSessionIdentity({
    explicitSessionId: sessionId,
    clientInfo,
  });

  const log: CapturedLog = {
    id,
    timestamp: new Date().toISOString(),
    method,
    path,
    model,
    sessionId: session.id,
    rawRequestBody: requestBody,
    responseStatus: null,
    responseText: null,
    inputTokens: null,
    outputTokens: null,
    cacheCreationInputTokens: null,
    cacheReadInputTokens: null,
    elapsedMs: null,
    streaming: false,
    userAgent,
    origin,
    rawHeaders: rawHeaders,
    headers: upstreamHeaders,
    apiFormat,
    isTest: false,
    providerName: null,
    clientPort: clientInfo?.port ?? null,
    clientPid: clientInfo?.pid ?? null,
    clientCwd: clientInfo?.cwd ?? null,
    clientProjectFolder: clientInfo?.projectFolder ?? null,
    streamingChunksPath: null,
  };

  // Write to disk and update index
  const logFile = getCurrentLogFile();
  appendLogEntry(log);
  await addToIndex(id, logFile, -1, -1); // line numbers not tracked precisely

  // Add to memory cache
  addToCache(log);
  markSessionStarted(log, session.source);
  emitLogUpdate(log);

  return log;
}

export function finalizeLogUpdate(log: CapturedLog): void {
  addToCache(log);
  markSessionFinished(log);
  emitLogUpdate(log);
}

/**
 * Get a log by ID, checking memory cache first then disk.
 * Uses byte-offset index for efficient single-line reads.
 */
export async function getLogById(id: number): Promise<CapturedLog | null> {
  // Check memory cache first
  const cached = memoryCache.get(id);
  if (cached !== undefined) {
    return cached;
  }

  // Look up in index and load from disk
  const entry = await findInIndex(id);
  if (entry === null) {
    return null;
  }

  // Load from disk using byte-offset index
  try {
    const filePath = join(resolveLogDir(), entry.file);
    if (!existsSync(filePath)) {
      return null;
    }

    // Use byte offset for direct read when available
    if (entry.byteOffset >= 0 && entry.byteLength > 0) {
      const fh = await open(filePath, "r");
      try {
        const buffer = Buffer.alloc(entry.byteLength);
        const { bytesRead } = await fh.read(buffer, 0, entry.byteLength, entry.byteOffset);
        if (bytesRead === 0) return null;
        const line = buffer.toString("utf-8", 0, bytesRead).trimEnd();
        if (line === "") return null;
        const parsed: unknown = JSON.parse(line);
        const result = CapturedLogSchema.safeParse(parsed);
        if (result.success) {
          const log = normalizeLogSession(result.data);
          addToCache(log);
          observeSessionLog(log);
          return log;
        }
      } finally {
        await fh.close();
      }
      return null;
    }

    // Fallback: scan file for matching ID (for entries written before index rebuild)
    const content = readFileSync(filePath, "utf-8");
    const lines = content.split("\n");

    let lastMatch: CapturedLog | null = null;
    for (const line of lines) {
      if (line.trim() === "") continue;
      try {
        const parsed: unknown = JSON.parse(line);
        if (typeof parsed === "object" && parsed !== null) {
          const desc = Object.getOwnPropertyDescriptor(parsed, "id");
          if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
            const result = CapturedLogSchema.safeParse(parsed);
            if (result.success) {
              lastMatch = normalizeLogSession(result.data);
              if (result.data.responseStatus !== null) {
                addToCache(lastMatch);
                observeSessionLog(lastMatch);
                return lastMatch;
              }
            }
          }
        }
      } catch {
        // Skip malformed lines
      }
    }
    if (lastMatch !== null) {
      addToCache(lastMatch);
      observeSessionLog(lastMatch);
      return lastMatch;
    }
  } catch (err) {
    logger.error("[store] Failed to read log from disk:", String(err));
  }

  return null;
}

export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog[] {
  // Cache maintains insertion order (sorted by ID since logs are added in ID order)
  // Use spread instead of Array.from for slightly better performance
  return [...memoryCache.values()].filter((l) => {
    if (sessionId !== undefined && getLogSessionId(l) !== sessionId) return false;
    if (model !== undefined && l.model !== model) return false;
    return true;
  });
}

export function getSessions(): string[] {
  return getSessionIds();
}

export function getModels(): string[] {
  const set = new Set<string>();
  for (const l of memoryCache.values()) {
    if (l.model !== null && l.model !== "") set.add(l.model);
  }
  return [...set];
}

/**
 * Load recent completed log entries from all log files into the memory cache.
 * Called on server startup so the frontend shows existing logs.
 *
 * Strategy: stream files forward, keep the LAST completed entry per ID,
 * then only retain the newest MAX_MEMORY_CACHE entries in cache.
 */
export async function loadLogsIntoMemory(): Promise<void> {
  const logDir = resolveLogDir();
  if (!existsSync(logDir)) return;

  // lastCompletedById: id -> CapturedLog (overwrite on each occurrence)
  const lastCompletedById = new Map<number, CapturedLog>();

  try {
    const entries = await readdirCallback(logDir);
    const filesWithExt = entries.filter((f: string) => f.endsWith(".jsonl")).sort();
    for (const file of filesWithExt) {
      const filePath = join(logDir, file);
      await loadLogFile(filePath, lastCompletedById);
    }
  } catch (err) {
    logger.error("[store] Failed to read log directory:", String(err));
  }

  // Sort by id descending, keep only MAX_MEMORY_CACHE newest completed entries
  const sorted = [...lastCompletedById.values()].sort((a, b) => b.id - a.id);
  for (let i = 0; i < Math.min(sorted.length, MAX_MEMORY_CACHE); i++) {
    const entry = sorted[i];
    if (entry !== undefined) addToCache(entry);
  }

  rebuildSessionRegistry(memoryCache.values());
}

async function loadLogFile(
  filePath: string,
  lastCompletedById: Map<number, CapturedLog>,
): Promise<void> {
  if (!existsSync(filePath)) return;

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

    for await (const line of fileStream) {
      if (line.trim() === "") continue;
      try {
        const parsed: unknown = JSON.parse(line);
        if (typeof parsed === "object" && parsed !== null) {
          const desc = Object.getOwnPropertyDescriptor(parsed, "id");
          if (desc !== undefined && typeof desc.value === "number") {
            const statusDesc = Object.getOwnPropertyDescriptor(parsed, "responseStatus");
            if (statusDesc !== undefined && statusDesc.value !== null) {
              const result = CapturedLogSchema.safeParse(parsed);
              if (result.success) {
                lastCompletedById.set(desc.value, result.data);
              }
            }
          }
        }
      } catch {
        // Skip malformed lines
      }
    }
  } catch (err) {
    logger.error("[store] Failed to load logs into memory:", String(err));
  }
}

export function clearAllLogs(): { cleared: number } {
  const count = memoryCache.size;
  memoryCache.clear();
  clearSessionRegistry();
  return { cleared: count };
}

/**
 * Remove a specific set of log IDs from the in-memory cache. Returns the
 * number of logs actually removed. IDs not present in the cache are ignored.
 * This is used by the per-conversation-group "Clear" button in the UI.
 */
export function clearLogsByIds(ids: readonly number[]): { cleared: number } {
  const unique = new Set<number>();
  for (const id of ids) {
    if (memoryCache.has(id)) unique.add(id);
  }
  for (const id of unique) {
    removeFromCache(id);
  }
  rebuildSessionRegistry(memoryCache.values());
  return { cleared: unique.size };
}

// SSE event system for real-time log updates
type LogUpdateHandler = (log: CapturedLog) => void;
const sseHandlers: Set<LogUpdateHandler> = new Set();

export function onLogUpdate(handler: LogUpdateHandler): () => void {
  sseHandlers.add(handler);
  return () => {
    sseHandlers.delete(handler);
  };
}

export function emitLogUpdate(log: CapturedLog): void {
  const failedHandlers: LogUpdateHandler[] = [];
  for (const handler of sseHandlers) {
    try {
      handler(log);
    } catch {
      // Collect failed handlers to remove after iteration
      failedHandlers.push(handler);
    }
  }
  // Remove failed handlers after iteration to avoid Set mutation during for...of
  for (const handler of failedHandlers) {
    sseHandlers.delete(handler);
  }
}
