import { existsSync, watch, type FSWatcher } from "node:fs";
import { dirname, resolve, sep } from "node:path";
import { getPilotExtensionPaths } from "../pilot/index.js";

export type ExtensionWatchScope =
  | { kind: "global" }
  | { kind: "project"; projectRoot: string };

export type ExtensionWatchEvent = {
  scope: ExtensionWatchScope;
  changedPaths: string[];
};

export type ExtensionWatchManagerOptions = {
  pilotHome: string;
  debounceMs?: number;
  onChange(event: ExtensionWatchEvent): void;
  onError?(scope: ExtensionWatchScope, error: Error): void;
};

type ScopeWatchRecord = {
  scope: ExtensionWatchScope;
  watchedPaths: string[];
  watchers: FSWatcher[];
  pendingPaths: Set<string>;
  timer?: NodeJS.Timeout;
};

export class ExtensionWatchManager {
  private readonly scopes = new Map<string, ScopeWatchRecord>();
  private started = false;

  constructor(private readonly options: ExtensionWatchManagerOptions) {}

  start(): () => void {
    if (this.started) {
      return () => this.stop();
    }
    this.started = true;
    this.ensureGlobalScope();
    for (const record of this.scopes.values()) {
      if (record.watchers.length === 0) {
        record.watchers.push(...this.createWatchers(record.scope, record.watchedPaths));
      }
    }
    return () => this.stop();
  }

  stop(): void {
    this.started = false;
    for (const record of this.scopes.values()) {
      if (record.timer) {
        clearTimeout(record.timer);
        record.timer = undefined;
      }
      for (const watcher of record.watchers) {
        try {
          watcher.close();
        } catch {
          // Best-effort shutdown.
        }
      }
      record.watchers = [];
      record.pendingPaths.clear();
    }
  }

  watchProject(projectRoot: string): void {
    this.ensureGlobalScope();
    this.ensureScope({ kind: "project", projectRoot: resolve(projectRoot) });
  }

  private ensureGlobalScope(): void {
    this.ensureScope({ kind: "global" });
  }

  private ensureScope(scope: ExtensionWatchScope): void {
    const key = scopeKey(scope);
    if (this.scopes.has(key)) {
      return;
    }
    const watchedPaths = this.getWatchedPaths(scope);
    const record: ScopeWatchRecord = {
      scope,
      watchedPaths,
      watchers: [],
      pendingPaths: new Set<string>(),
    };
    if (this.started) {
      record.watchers.push(...this.createWatchers(scope, watchedPaths));
    }
    this.scopes.set(key, record);
  }

  private getWatchedPaths(scope: ExtensionWatchScope): string[] {
    if (scope.kind === "global") {
      return [
        resolve(this.options.pilotHome, "plugins"),
        resolve(this.options.pilotHome, "skills"),
      ];
    }
    const paths = getPilotExtensionPaths(scope.projectRoot, this.options.pilotHome);
    return [paths.projectPluginsDir, paths.projectSkillsDir];
  }

  private createWatchers(scope: ExtensionWatchScope, watchedPaths: string[]): FSWatcher[] {
    const watchers: FSWatcher[] = [];
    for (const watchedPath of watchedPaths) {
      const watchTarget = resolveExistingWatchTarget(watchedPath);
      const schedule = (filename: string) => {
        if (shouldHandleWatchSignal(watchTarget, watchedPath, filename)) {
          this.schedule(scope, watchedPath);
        }
      };
      const errorTarget = (error: unknown) =>
        this.options.onError?.(scope, error instanceof Error ? error : new Error(String(error)));
      const recursiveWatcher = this.tryWatch(watchTarget, true, schedule, errorTarget);
      if (recursiveWatcher) {
        watchers.push(recursiveWatcher);
        continue;
      }
      const plainWatcher = this.tryWatch(watchTarget, false, schedule, errorTarget);
      if (plainWatcher) {
        watchers.push(plainWatcher);
      }
    }
    return watchers;
  }

  private tryWatch(
    target: string,
    recursive: boolean,
    onSignal: (filename: string) => void,
    onError: (error: unknown) => void,
  ): FSWatcher | undefined {
    try {
      const watcher = watch(target, { recursive }, (_event, filename) => onSignal(toUtf8(filename)));
      watcher.on("error", onError);
      return watcher;
    } catch {
      return undefined;
    }
  }

  private schedule(scope: ExtensionWatchScope, changedPath: string): void {
    const record = this.scopes.get(scopeKey(scope));
    if (!record) {
      return;
    }
    record.pendingPaths.add(changedPath);
    if (record.timer) {
      clearTimeout(record.timer);
    }
    record.timer = setTimeout(() => {
      record.timer = undefined;
      const changedPaths = [...record.pendingPaths].sort();
      record.pendingPaths.clear();
      this.options.onChange({ scope: record.scope, changedPaths });
    }, this.options.debounceMs ?? 250);
  }
}

function scopeKey(scope: ExtensionWatchScope): string {
  return scope.kind === "global" ? "__global__" : scope.projectRoot;
}

function resolveExistingWatchTarget(path: string): string {
  let current = path;
  while (!existsSync(current)) {
    const parent = dirname(current);
    if (parent === current) {
      break;
    }
    current = parent;
  }
  return current;
}

function shouldHandleWatchSignal(watchTarget: string, watchedPath: string, filename: string): boolean {
  if (filename.length === 0) {
    return true;
  }
  const absoluteChanged = resolve(watchTarget, filename);
  return absoluteChanged === watchedPath || absoluteChanged.startsWith(`${watchedPath}${sep}`);
}

function toUtf8(value: string | Buffer | null | undefined): string {
  if (typeof value === "string") {
    return value;
  }
  if (value && typeof value.toString === "function") {
    return value.toString("utf8");
  }
  return "";
}