import { realpathSync } from "node:fs";
import path from "node:path";
import { DependencyWatcher, type DependencyWatcherOptions } from "./dependency-watcher.ts";

export interface BunImportTrackerOptions extends DependencyWatcherOptions {
  /**
   * Directory whose modules belong to the tracked graph. Files outside it and
   * anything under a `node_modules` directory load untouched.
   */
  readonly root: string;
}

const loaders: Record<string, "js" | "jsx" | "ts" | "tsx"> = {
  ".js": "js",
  ".mjs": "js",
  ".cjs": "js",
  ".jsx": "jsx",
  ".ts": "ts",
  ".mts": "ts",
  ".cts": "ts",
  ".tsx": "tsx",
};

const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

/**
 * Records every project-local module Bun loads after registration and watches
 * those files for changes.
 *
 * Bun has no loader hooks that can evict or re-namespace an evaluated module,
 * so unlike Node's `ImportWatcher` (./import-watcher.ts) this cannot import
 * a fresh generation in-process. A runtime `Bun.plugin` `onLoad` hook is used purely
 * as a dependency probe: it hands the source back unchanged with the loader
 * Bun would have picked itself. It stays registered after `close()`, only
 * echoing sources. Callers react to a change by exiting so a
 * supervisor can start a fresh process.
 */
export class BunImportTracker extends DependencyWatcher {
  readonly #loaded = new Set<string>();

  constructor(options: BunImportTrackerOptions) {
    if (process.versions.bun === undefined) {
      throw new Error("BunImportTracker requires Bun; Node callers should use watchImport.");
    }
    super(options);
    // Bun reports real paths (`/private/tmp/...` for `/tmp/...` on macOS);
    // match them against the root's real path too.
    const root = realpathSync.native(path.resolve(options.root)) + path.sep;
    const nodeModules = `node_modules${path.sep}`;
    // `root` already ends in a separator, so the segment right after it has no
    // leading one: a `.*${sep}node_modules${sep}` lookahead alone would miss
    // `<root>node_modules/...` and intercept every dependency. Bun cannot
    // return CommonJS source from `onLoad` (oven-sh/bun#19279), so a CJS
    // dependency that reaches this probe loses its exports.
    const filter = new RegExp(
      `^${escapeRegExp(root)}(?!(?:.*${escapeRegExp(path.sep)})?${escapeRegExp(nodeModules)}).*\\.[cm]?[jt]sx?$`,
    );
    Bun.plugin({
      name: "@alchemy.run/node-utils/watch-import-bun",
      setup: (build) => {
        build.onLoad({ filter }, async (args) => {
          this.#loaded.add(args.path);
          this.set(new Set(this.#loaded));
          return {
            contents: await Bun.file(args.path).text(),
            loader: loaders[path.extname(args.path)] ?? "js",
          };
        });
      },
    });
  }
}

export const trackBunImports = (options: BunImportTrackerOptions) => new BunImportTracker(options);
