import { watch } from "chokidar";
import { basename, relative } from "node:path";
import { detectLanguage } from "../types/index.js";
import { createIgnoreFilter } from "../utils/ignore.js";
import { loadSverkloConfig } from "../utils/config-file.js";
import { log } from "../utils/logger.js";
import { toForwardSlashes } from "./file-discovery.js";
import type { Indexer } from "./indexer.js";

/**
 * Finding 5: every relativePath the watcher hands to the indexer must
 * be in the same canonical (forward-slash) form that file-discovery.ts
 * stores. On Windows, native `relative()` returns `src\foo.ts`, while
 * the indexer's primary key is `src/foo.ts`. Without normalization,
 * each Windows file edit creates a duplicate row instead of upserting.
 */
function relForward(rootPath: string, absolutePath: string): string {
  return toForwardSlashes(relative(rootPath, absolutePath));
}

/** File names that, when changed, should trigger a full reindex. */
const CONFIG_FILES = new Set([".sverklo.yaml", ".sverklo.yml"]);

export function startWatcher(indexer: Indexer, rootPath: string): void {
  const ignoreFilter = createIgnoreFilter(rootPath);

  // Debounce map: path -> timeout
  const pending = new Map<string, NodeJS.Timeout>();
  const DEBOUNCE_MS = 500;

  // Serial reindex queue. Without this, a directory rename of N files
  // fires N debounce timers ~simultaneously, each awaiting its own
  // indexer.reindexFile() — N concurrent ONNX embedding calls hit the
  // single embedder session. Architectural review 2026-05-13 (Tier 3.5
  // / Backend B6) flagged the unbounded concurrency as exactly the
  // backpressure gap. We serialize through a single Promise chain so
  // ONNX sees one call at a time but no events are dropped.
  let queueTail: Promise<void> = Promise.resolve();
  function enqueueReindex(work: () => Promise<void>): Promise<void> {
    const next = queueTail.then(work, work);
    queueTail = next.catch(() => {});
    return next;
  }

  const watcher = watch(rootPath, {
    ignored: (path: string) => {
      const rel = relForward(rootPath, path);
      if (!rel) return false;
      // Never ignore config files — we need to watch them for changes
      if (CONFIG_FILES.has(rel)) return false;
      try {
        return ignoreFilter.ignores(rel);
      } catch {
        return false;
      }
    },
    followSymlinks: false,
    persistent: true,
    ignoreInitial: true,
    awaitWriteFinish: { stabilityThreshold: 300 },
  });

  function handleConfigChange(absolutePath: string) {
    const rel = relForward(rootPath, absolutePath);
    log(`Config file changed: ${rel} — triggering full reindex`);
    indexer.invalidateFreshnessCache();

    // Reload the config on the indexer
    indexer.sverkloConfig = loadSverkloConfig(rootPath);

    // Debounce config-triggered reindex
    const existing = pending.get("__config__");
    if (existing) clearTimeout(existing);

    pending.set(
      "__config__",
      setTimeout(() => {
        pending.delete("__config__");
        enqueueReindex(() => indexer.index());
      }, DEBOUNCE_MS)
    );
  }

  function handleChange(absolutePath: string) {
    const rel = relForward(rootPath, absolutePath);

    // Config file changes trigger a full reindex
    if (CONFIG_FILES.has(rel)) {
      handleConfigChange(absolutePath);
      return;
    }

    const lang = detectLanguage(absolutePath);
    if (!lang) return;

    // Any real change invalidates the freshness cache immediately so the
    // next sverklo_status reflects reality without waiting for the TTL.
    indexer.invalidateFreshnessCache();

    // Debounce
    const existing = pending.get(rel);
    if (existing) clearTimeout(existing);

    pending.set(
      rel,
      setTimeout(() => {
        pending.delete(rel);
        log(`File changed: ${rel}`);
        enqueueReindex(() => indexer.reindexFile(rel, absolutePath, lang));
      }, DEBOUNCE_MS)
    );
  }

  watcher.on("add", handleChange);
  watcher.on("change", handleChange);
  watcher.on("unlink", (absolutePath: string) => {
    const rel = relForward(rootPath, absolutePath);

    // Config file deletion also triggers reindex (weights reset to defaults)
    if (CONFIG_FILES.has(rel)) {
      handleConfigChange(absolutePath);
      return;
    }

    log(`File removed: ${rel}`);
    indexer.invalidateFreshnessCache();
    indexer.removeFile(rel);
  });

  log("File watcher started");
}
