import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { MEMORY } from "@/core/constants.js";

/**
 * Ticket-review trigger dedupe: in-memory Set plus JSON persistence under
 * `memory/ticket-review-triggers.json` (or `TICKET_REVIEW_STORE_PATH`). Survives restarts.
 *
 * **Cold start (no persistence file yet):** only comments with `created >=` process session start
 * are eligible; the poller enforces this. After the file exists, eligibility uses stored keys only.
 */

/** Cap persisted keys (FIFO drops oldest) so the JSON file cannot grow without bound. */
const MAX_PERSISTED_KEYS = 8000;

/** Process start time; used only while the persistence file does not exist yet. */
const TICKET_REVIEW_SESSION_START_MS = Date.now();

const membership = new Set<string>();
const insertionOrder: string[] = [];

let loadedFromDisk = false;

export function ticketReviewTriggerKey(issueKey: string, commentId: string): string {
  return `${issueKey}:${commentId}`;
}

function getStorePath(): string {
  const override = process.env.TICKET_REVIEW_STORE_PATH?.trim();
  if (override) return resolve(override);
  return resolve(process.cwd(), MEMORY.DIR, "ticket-review-triggers.json");
}

/** True once `markTicketReviewTriggerProcessed` has created the JSON file (or it was restored on disk). */
export function ticketReviewPersistenceFileExists(): boolean {
  return existsSync(getStorePath());
}

/**
 * While no persistence file exists, only comments at/after process session start may run.
 * After the file exists, the poller does not use this (dedupe is key-based only).
 */
export function isTicketReviewCommentEligibleBySessionStart(commentCreatedIso: string): boolean {
  const t = new Date(commentCreatedIso).getTime();
  if (Number.isNaN(t)) return false;
  return t >= TICKET_REVIEW_SESSION_START_MS;
}

/** @internal Exposed for tests that need to compare against the session cutoff. */
export function ticketReviewSessionStartMsForTests(): number {
  return TICKET_REVIEW_SESSION_START_MS;
}

function loadFromDisk(): void {
  if (loadedFromDisk) return;
  loadedFromDisk = true;
  const path = getStorePath();
  if (!existsSync(path)) return;
  try {
    const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
    if (!parsed || typeof parsed !== "object") return;
    const keys = (parsed as { keys?: unknown }).keys;
    if (!Array.isArray(keys)) return;
    for (const k of keys) {
      if (typeof k !== "string" || membership.has(k)) continue;
      membership.add(k);
      insertionOrder.push(k);
    }
  } catch {
    // Corrupt or empty file — start with in-memory only until next mark.
  }
}

function persistToDisk(): void {
  const path = getStorePath();
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, JSON.stringify({ keys: [...insertionOrder] }, null, 0), "utf-8");
}

export function isTicketReviewTriggerProcessed(issueKey: string, commentId: string): boolean {
  loadFromDisk();
  return membership.has(ticketReviewTriggerKey(issueKey, commentId));
}

export function markTicketReviewTriggerProcessed(issueKey: string, commentId: string): void {
  loadFromDisk();
  const key = ticketReviewTriggerKey(issueKey, commentId);
  if (membership.has(key)) return;
  membership.add(key);
  insertionOrder.push(key);
  while (insertionOrder.length > MAX_PERSISTED_KEYS) {
    const dropped = insertionOrder.shift()!;
    membership.delete(dropped);
  }
  persistToDisk();
}

/**
 * Test-only: clear in-memory state. With `removePersistedFile`, deletes the JSON file.
 * Without it, the next `is*` / `mark*` call reloads from disk (simulates process restart).
 */
export function resetTicketReviewProcessedStoreForTests(options?: {
  removePersistedFile?: boolean;
}): void {
  membership.clear();
  insertionOrder.length = 0;
  loadedFromDisk = false;
  if (options?.removePersistedFile === true) {
    try {
      rmSync(getStorePath(), { force: true });
    } catch {
      /* noop */
    }
  }
}
