/**
 * In-process file watcher (dev). Classifies each change and chooses the
 * cheapest update that still shows the latest page:
 *
 *   *.css / styles/   -> invalidate CSS cache, push { type: "css" }  (instant swap)
 *   anything else      -> invalidate changed modules, rescan routes, and ask
 *                         the connected browser to sync fresh HTML. The server
 *                         process and HMR socket stay alive.
 */

import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
import { isAbsolute, join, relative, resolve } from "node:path";
import type { HmrHub } from "./hmr.ts";
import type { DevAssetServer } from "./assets.ts";
import type { DevToolbarCollector } from "@wrnexus/dev-toolbar/server";

export interface WatchOptions {
  appDir: string;
  /** Additional package component/runtime/style directories watched for HMR. */
  extraDirs?: string[];
  hub: HmrHub;
  assets: DevAssetServer;
  devToolbarCollector?: DevToolbarCollector;
  /** Called with changed app-relative files that need an in-process hot update. */
  onHotChange: (files: string[]) => void | Promise<void>;
}

function isIgnored(rel: string): boolean {
  return (
    rel.includes("node_modules/") ||
    rel.includes(".wrnexus/") ||
    rel.includes(".wrnexus-hmr-") ||
    rel.startsWith("dist/") ||
    rel.includes("/dist/")
  );
}

type Kind = "css" | "server";

function classify(rel: string): Kind {
  if (rel.endsWith(".css") || rel.startsWith("styles/") || rel.includes("/styles/")) return "css";
  return "server";
}

export interface WatchHandle {
  close(): void;
}

/** Returns a composite watcher so the running server can close every source root. */
export function startWatcher(opts: WatchOptions): WatchHandle | undefined {
  const { appDir, hub, assets } = opts;
  const pending = new Set<Kind>();
  const pendingFiles = new Set<string>();
  let timer: ReturnType<typeof setTimeout> | null = null;

  const flush = (): void => {
    timer = null;
    if (pending.has("server")) {
      pending.clear();
      const files = [...pendingFiles];
      pendingFiles.clear();
      for (const file of files) opts.devToolbarCollector?.clear(file);
      void Promise.resolve(opts.onHotChange(files)).catch((error) => {
        console.error("[wrnexus] hot update failed", error);
      });
      return;
    }
    if (pending.has("css")) {
      assets.invalidateCss();
      hub.css();
      hub.broadcastJson({
        channel: "toolbar",
        type: "toolbar:scan",
        reason: "styles-change",
        files: [...pendingFiles],
      });
    }
    pending.clear();
    pendingFiles.clear();
  };

  const appRoot = resolve(appDir);
  const candidates = [appRoot, ...(opts.extraDirs ?? []).map((dir) => resolve(dir))];
  const roots = [...new Set(candidates)]
    .filter((dir) => existsSync(dir) && statSync(dir).isDirectory())
    .filter(
      (dir, index, values) =>
        !values.some((other, otherIndex) => {
          if (otherIndex >= index) return false;
          const nested = relative(other, dir);
          return nested === "" || (!nested.startsWith("..") && !isAbsolute(nested));
        }),
    );
  const watchers: FSWatcher[] = [];

  for (const root of roots) {
    try {
      const external = root !== appRoot;
      watchers.push(
        watch(root, { recursive: true }, (_event, filename) => {
          if (!filename) return;
          const relativeFile = filename.toString().replace(/\\/g, "/");
          if (isIgnored(relativeFile)) return;
          const file = external ? join(root, relativeFile).replace(/\\/g, "/") : relativeFile;
          pendingFiles.add(file);
          pending.add(classify(file));
          if (timer) clearTimeout(timer);
          timer = setTimeout(flush, 200); // debounce editor write bursts
        }),
      );
    } catch (error) {
      console.warn(`[wrnexus] file watching unavailable for ${root}`, error);
    }
  }

  if (!watchers.length) return undefined;
  return {
    close() {
      if (timer) clearTimeout(timer);
      for (const watcher of watchers) watcher.close();
    },
  };
}
