// v2.8 In-memory conflict index — tracks which worktrees/agents are editing
// which files so the collision detector can find overlaps.
//
// Singleton exported as `conflictIndex`; imported by pending_changes.ts to
// register changes at queue time and unregister on worktree teardown.
//
// NOT persisted — survives only as long as the BooCoder process. Postgres
// is the durable record (pending_changes table); this is the hot in-memory
// probe for concurrent edit warnings.

import { findConflicts, type ConflictEntry, type ConflictVerdict } from "./collision-detector.js";

export class ConflictIndex {
  /**
   * filePath → Set of ConflictEntry from various worktrees.
   * A single worktree may have multiple entries for the same file
   * (several pending edits to the same file in one session).
   */
  #map = new Map<string, Set<ConflictEntry>>();

  /**
   * Register that `worktreeId` (agent) is touching `filePath`.
   * Creates an entry in the index so subsequent callers see it as a conflict.
   */
  registerChange(
    filePath: string,
    worktreeId: string,
    agent: string,
    lineRange?: { start: number; end: number },
  ): void {
    let entries = this.#map.get(filePath);
    if (!entries) {
      entries = new Set();
      this.#map.set(filePath, entries);
    }
    entries.add({
      worktreeId,
      agent,
      lineRange,
      status: 'pending' as const,
      timestamp: Date.now(),
    });
  }

  /**
   * Remove all entries for a given worktree. Called on worktree teardown
   * so stale entries don't trigger false warnings.
   */
  removeWorktree(worktreeId: string): void {
    for (const [filePath, entries] of this.#map) {
      const before = entries.size;
      for (const entry of entries) {
        if (entry.worktreeId === worktreeId) {
          entries.delete(entry);
        }
      }
      if (entries.size === 0) {
        this.#map.delete(filePath);
      }
    }
  }

  /**
   * Remove entries older than `maxAgeMs`. Useful as a periodic cleanup
   * when worktree teardown was missed (crash, unclean exit).
   */
  sweepStale(maxAgeMs: number): number {
    const cutoff = Date.now() - maxAgeMs;
    let removed = 0;

    for (const [filePath, entries] of this.#map) {
      for (const entry of entries) {
        if (entry.timestamp < cutoff) {
          entries.delete(entry);
          removed++;
        }
      }
      if (entries.size === 0) {
        this.#map.delete(filePath);
      }
    }

    return removed;
  }

  /**
   * Query the raw ConflictEntry set for a file path. Returns empty set
   * when there are no entries (never mutated the file).
   */
  getEntriesFor(filePath: string): ReadonlySet<ConflictEntry> {
    return this.#map.get(filePath) ?? new Set();
  }

  /**
   * Get all conflict verdicts for a given file path — which other
   * worktrees are touching it. Returns empty when only one worktree
   * has entries (no actual conflict).
   */
  getConflictsFor(filePath: string): ConflictVerdict[] {
    const entries = this.#map.get(filePath);
    if (!entries || entries.size === 0) return [];

    // Determine distinct worktree IDs. If only one, no conflict.
    const worktreeIds = new Set<string>();
    for (const e of entries) worktreeIds.add(e.worktreeId);
    if (worktreeIds.size <= 1) return [];

    // Use the first worktree as the "caller" so findConflicts excludes
    // its entries and returns only entries from OTHER worktrees.
    const caller = [...worktreeIds][0]!;
    return findConflicts(
      [filePath],
      caller,
      new Map(),
      this.#toIndexData(),
    );
  }

  /**
   * Get conflicts for a set of file changes from a specific worktree.
   * Delegates to the pure findConflicts function.
   */
  query(
    changedFiles: string[],
    worktreeId: string,
    changedRanges: Map<string, { start: number; end: number }>,
  ): ConflictVerdict[] {
    return findConflicts(changedFiles, worktreeId, changedRanges, this.#toIndexData());
  }

  /**
   * Snapshot the current map for testing/inspection.
   */
  snapshot(): Map<string, ReadonlySet<ConflictEntry>> {
    return new Map(this.#map);
  }

  #toIndexData(): ReadonlyMap<string, ReadonlySet<ConflictEntry>> {
    return this.#map as ReadonlyMap<string, ReadonlySet<ConflictEntry>>;
  }
}

// Singleton — the whole BooCoder process shares one conflict index.
export const conflictIndex = new ConflictIndex();
