// @lifecycle canonical - Watches directories and emits change events for hot reload.
/**
 * File Observer Module
 * Handles file system watching for automatic change detection and hot reload triggers
 *
 * Uses Chokidar for cross-platform file watching with polling support for WSL2/network filesystems.
 */

import { EventEmitter } from 'events';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import chokidar, { type ChokidarOptions, type FSWatcher } from 'chokidar';

import type { ConfigManager, Logger } from '../../shared/types/index.js';

/**
 * File change event types
 */
export type FileChangeType = 'added' | 'modified' | 'removed' | 'renamed';

/**
 * File content types for classification
 */
export type FileContentType = 'prompt' | 'config' | 'methodology' | 'unknown';

/**
 * Framework analysis data for file changes
 */
export interface FrameworkAnalysisData {
  requiresFrameworkUpdate: boolean;
  affectedFrameworks: string[];
  analysisInvalidated: boolean;
  performanceImpact: 'low' | 'medium' | 'high';
}

/**
 * File change event data
 */
export interface FileChangeEvent {
  type: FileChangeType;
  filePath: string;
  filename: string;
  timestamp: number;
  isPromptFile: boolean;
  isConfigFile: boolean;
  isMethodologyFile: boolean;
  /** True when the file is in a registered auxiliary directory (gates, scripts, etc.) */
  isAuxiliaryFile?: boolean;
  /** Extracted methodology ID for methodology file changes */
  methodologyId?: string;
  category?: string;
  frameworkAnalysis?: FrameworkAnalysisData;
}

/**
 * Framework integration capabilities
 */
export interface FrameworkIntegration {
  enabled: boolean;
  analyzeChanges: boolean;
  cacheInvalidation: boolean;
  performanceTracking: boolean;
}

/**
 * File observer configuration
 */
export interface FileObserverConfig {
  enabled: boolean;
  debounceMs: number;
  watchPromptFiles: boolean;
  watchConfigFiles: boolean;
  watchMethodologyFiles: boolean;
  recursive: boolean;
  ignoredPatterns: string[];
  maxRetries: number;
  retryDelayMs: number;
  frameworkIntegration?: FrameworkIntegration;
  /**
   * Use polling for file watching (required for WSL2 and network filesystems).
   * - 'auto': Detect WSL2/network filesystems and enable polling automatically (default)
   * - true: Always use polling
   * - false: Never use polling (uses native fs events)
   */
  usePolling?: boolean | 'auto';
  /**
   * Polling interval in milliseconds when usePolling is enabled.
   * Default: 300ms (balances responsiveness vs CPU usage)
   */
  pollingInterval?: number;
}

/**
 * File observer statistics
 */
export interface FileObserverStats {
  watchersActive: number;
  eventsDetected: number;
  eventsDebounced: number;
  eventsTriggered: number;
  lastEventTime?: number;
  uptime: number;
  retryCount: number;
  frameworkEvents: number;
  frameworkCacheInvalidations: number;
  methodologyFileEvents: number;
}

/**
 * Detect if running in WSL2 environment
 * WSL2's virtualized filesystem doesn't propagate fs.watch events reliably
 */
function isWSL2(): boolean {
  try {
    const release = os.release().toLowerCase();
    return release.includes('wsl2') || release.includes('microsoft');
  } catch {
    return false;
  }
}

/**
 * Default configuration for FileObserver
 */
const DEFAULT_CONFIG: FileObserverConfig = {
  enabled: true,
  debounceMs: 500,
  watchPromptFiles: true,
  watchConfigFiles: true,
  watchMethodologyFiles: true,
  recursive: true,
  ignoredPatterns: [
    '**/.git/**',
    '**/node_modules/**',
    '**/.DS_Store',
    '**/Thumbs.db',
    '**/*.tmp',
    '**/*.temp',
    '**/dist/**',
    '**/*.log',
  ],
  maxRetries: 3,
  retryDelayMs: 1000,
  frameworkIntegration: {
    enabled: false,
    analyzeChanges: false,
    cacheInvalidation: false,
    performanceTracking: false,
  },
  usePolling: 'auto',
  pollingInterval: 300,
};

/**
 * FileObserver class
 * Provides robust file system watching with event-driven architecture
 */
export class FileObserver extends EventEmitter {
  protected logger: Logger;
  private config: FileObserverConfig;
  private watchers: Map<string, FSWatcher> = new Map();
  private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
  private stats: FileObserverStats;
  private isStarted: boolean = false;
  private startTime: number = 0;
  private retryCount: number = 0;
  private configManager: ConfigManager | undefined;
  private auxiliaryDirectories: string[] = [];
  private sigintHandler: (() => void) | undefined;
  private sigtermHandler: (() => void) | undefined;
  private shouldUsePolling: boolean = false;

  constructor(logger: Logger, config?: Partial<FileObserverConfig>, configManager?: ConfigManager) {
    super();
    this.logger = logger;
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.configManager = configManager;
    this.stats = {
      watchersActive: 0,
      eventsDetected: 0,
      eventsDebounced: 0,
      eventsTriggered: 0,
      uptime: 0,
      retryCount: 0,
      frameworkEvents: 0,
      frameworkCacheInvalidations: 0,
      methodologyFileEvents: 0,
    };

    // Determine if polling should be used
    if (this.config.usePolling === 'auto') {
      this.shouldUsePolling = isWSL2();
    } else {
      this.shouldUsePolling = this.config.usePolling ?? false;
    }

    // Set max listeners to prevent warning for multiple prompt directories
    this.setMaxListeners(50);
  }

  /**
   * Register directories used by auxiliary reload handlers (gates, scripts, etc.).
   * Files in these directories bypass prompt/config/methodology classification
   * so they can reach auxiliary reload handlers downstream.
   */
  registerAuxiliaryDirectories(directories: string[]): void {
    this.auxiliaryDirectories = directories.map((d) => path.normalize(d));
  }

  /**
   * Start file watching
   */
  async start(): Promise<void> {
    if (this.isStarted) {
      this.logger.warn('FileObserver is already started');
      return;
    }

    if (!this.config.enabled) {
      this.logger.info('FileObserver is disabled in configuration');
      return;
    }

    this.logger.info('📁 FileObserver: Starting file system watching...');
    this.startTime = Date.now();
    this.isStarted = true;

    // Listen for process termination to clean up watchers
    if (!this.sigintHandler) {
      this.sigintHandler = () => {
        void this.stop();
      };
      process.on('SIGINT', this.sigintHandler);
    }
    if (!this.sigtermHandler) {
      this.sigtermHandler = () => {
        void this.stop();
      };
      process.on('SIGTERM', this.sigtermHandler);
    }

    const pollingStatus = this.shouldUsePolling
      ? `polling enabled (interval: ${this.config.pollingInterval}ms)`
      : 'native fs events';
    this.logger.info(
      `✅ FileObserver started with debounce: ${this.config.debounceMs}ms, ${pollingStatus}`
    );
  }

  /**
   * Stop file watching and clean up resources
   */
  async stop(): Promise<void> {
    if (!this.isStarted) {
      return;
    }

    this.logger.info('🛑 FileObserver: Stopping file system watching...');

    // Clear all debounce timers
    for (const timer of this.debounceTimers.values()) {
      clearTimeout(timer);
    }
    this.debounceTimers.clear();

    // Close all watchers (chokidar close() returns a Promise)
    const closePromises = Array.from(this.watchers.entries()).map(async ([watchPath, watcher]) => {
      try {
        await watcher.close();
        this.logger.debug(`Closed watcher for: ${watchPath}`);
      } catch (error) {
        this.logger.warn(`Failed to close watcher for ${watchPath}:`, error);
      }
    });
    await Promise.all(closePromises);
    this.watchers.clear();

    this.isStarted = false;
    this.stats.watchersActive = 0;

    this.logger.info('✅ FileObserver stopped and resources cleaned up');

    if (this.sigintHandler) {
      process.off('SIGINT', this.sigintHandler);
      this.sigintHandler = undefined;
    }
    if (this.sigtermHandler) {
      process.off('SIGTERM', this.sigtermHandler);
      this.sigtermHandler = undefined;
    }
  }

  /**
   * Add a directory to watch
   */
  async watchDirectory(directoryPath: string, category?: string): Promise<void> {
    if (!this.isStarted) {
      throw new Error('FileObserver must be started before adding watchers');
    }

    if (this.watchers.has(directoryPath)) {
      this.logger.debug(`Directory already being watched: ${directoryPath}`);
      return;
    }

    try {
      // Verify directory exists
      const stats = await fs.promises.stat(directoryPath);
      if (!stats.isDirectory()) {
        throw new Error(`Path is not a directory: ${directoryPath}`);
      }

      // Configure chokidar options
      const watchOptions: ChokidarOptions = {
        persistent: true,
        ignoreInitial: true,
        followSymlinks: true,
        depth: this.config.recursive ? undefined : 0,
        ignored: this.config.ignoredPatterns,
        usePolling: this.shouldUsePolling,
        interval: this.config.pollingInterval,
        binaryInterval: this.config.pollingInterval,
        awaitWriteFinish: {
          stabilityThreshold: 100,
          pollInterval: 100,
        },
      };

      const watcher = chokidar.watch(directoryPath, watchOptions);

      // Handle chokidar events
      watcher
        .on('add', (filePath: string) => {
          this.handleChokidarEvent('add', directoryPath, filePath, category);
        })
        .on('change', (filePath: string) => {
          this.handleChokidarEvent('change', directoryPath, filePath, category);
        })
        .on('unlink', (filePath: string) => {
          this.handleChokidarEvent('unlink', directoryPath, filePath, category);
        })
        .on('error', (error: unknown) => {
          this.handleWatcherError(directoryPath, error);
        });

      this.watchers.set(directoryPath, watcher);
      this.stats.watchersActive = this.watchers.size;

      this.logger.info(
        `👁️ FileObserver: Watching directory: ${directoryPath}${
          category ? ` (category: ${category})` : ''
        }${this.shouldUsePolling ? ' (polling)' : ''}`
      );
    } catch (error) {
      this.logger.error(`Failed to watch directory ${directoryPath}:`, error);
      if (this.retryCount < this.config.maxRetries) {
        this.retryCount++;
        this.stats.retryCount++;
        this.logger.info(
          `Retrying in ${this.config.retryDelayMs}ms (attempt ${this.retryCount}/${this.config.maxRetries})`
        );
        setTimeout(() => this.watchDirectory(directoryPath, category), this.config.retryDelayMs);
      } else {
        throw error;
      }
    }
  }

  /**
   * Handle chokidar file events
   * Translates chokidar event types to our internal event format
   */
  private handleChokidarEvent(
    eventType: 'add' | 'change' | 'unlink',
    directoryPath: string,
    filePath: string,
    category?: string
  ): void {
    const filename = path.relative(directoryPath, filePath);
    const chokidarToInternal: Record<string, string> = {
      add: 'rename',
      change: 'change',
      unlink: 'rename',
    };
    this.handleFileEvent(
      chokidarToInternal[eventType] ?? 'change',
      directoryPath,
      filename,
      category
    );
  }

  /**
   * Remove a directory from watching
   */
  async unwatchDirectory(directoryPath: string): Promise<void> {
    const watcher = this.watchers.get(directoryPath);
    if (!watcher) {
      this.logger.debug(`Directory not being watched: ${directoryPath}`);
      return;
    }

    try {
      await watcher.close();
      this.watchers.delete(directoryPath);
      this.stats.watchersActive = this.watchers.size;

      // Clear any pending debounce timers for this directory
      const timersToRemove: string[] = [];
      for (const [key, timer] of this.debounceTimers.entries()) {
        if (key.startsWith(directoryPath)) {
          clearTimeout(timer);
          timersToRemove.push(key);
        }
      }
      timersToRemove.forEach((key) => this.debounceTimers.delete(key));

      this.logger.info(`🚫 FileObserver: Stopped watching directory: ${directoryPath}`);
    } catch (error) {
      this.logger.error(`Failed to stop watching directory ${directoryPath}:`, error);
      throw error;
    }
  }

  /**
   * Handle file system events
   */
  private handleFileEvent(
    eventType: string,
    directoryPath: string,
    filename: string | null,
    category?: string
  ): void {
    if (!filename) {
      return;
    }

    const filePath = path.join(directoryPath, filename);
    this.stats.eventsDetected++;

    // Check if file should be ignored
    if (this.shouldIgnoreFile(filePath, filename)) {
      return;
    }

    // Determine file types
    const isPromptFile = this.isPromptFile(filename);
    const isConfigFile = this.isConfigFile(filename, filePath);
    const methodologyInfo = this.isMethodologyFile(filename, filePath);
    const isMethodologyFile = methodologyInfo.isMethodology;

    // Check if file is in a registered auxiliary directory (gates, scripts, etc.)
    const isAuxiliaryFile = this.isInAuxiliaryDirectory(filePath);

    // Skip if we're not watching any applicable type
    if (!isPromptFile && !isConfigFile && !isMethodologyFile && !isAuxiliaryFile) {
      return;
    }

    if (!this.config.watchPromptFiles && isPromptFile) {
      return;
    }

    if (!this.config.watchConfigFiles && isConfigFile) {
      return;
    }

    if (!this.config.watchMethodologyFiles && isMethodologyFile) {
      return;
    }

    const changeType = this.mapEventType(eventType);
    const event: FileChangeEvent = {
      type: changeType,
      filePath,
      filename,
      timestamp: Date.now(),
      isPromptFile,
      isConfigFile,
      isMethodologyFile,
      ...(isAuxiliaryFile ? { isAuxiliaryFile } : {}),
    };

    if (methodologyInfo.methodologyId) {
      event.methodologyId = methodologyInfo.methodologyId;
    }

    if (category) {
      event.category = category;
    }

    // Add framework analysis if enabled
    if (this.config.frameworkIntegration?.enabled) {
      event.frameworkAnalysis = this.analyzeFrameworkImpact(event);
      if (event.frameworkAnalysis.requiresFrameworkUpdate) {
        this.stats.frameworkEvents++;
      }
    }

    // Track methodology events separately
    if (isMethodologyFile) {
      this.stats.methodologyFileEvents++;
    }

    this.logger.debug(`File event detected: ${changeType} ${filename} in ${directoryPath}`);

    // Apply debouncing
    this.debounceEvent(event);
  }

  /**
   * Apply debouncing to prevent excessive event firing
   */
  private debounceEvent(event: FileChangeEvent): void {
    const debounceKey = `${event.filePath}_${event.type}`;

    // Clear existing timer for this file+type
    const existingTimer = this.debounceTimers.get(debounceKey);
    if (existingTimer) {
      clearTimeout(existingTimer);
      this.stats.eventsDebounced++;
    }

    // Set new timer
    const timer = setTimeout(() => {
      this.debounceTimers.delete(debounceKey);
      void this.emitFileChangeEvent(event);
    }, this.config.debounceMs);

    this.debounceTimers.set(debounceKey, timer);
  }

  /**
   * Emit the file change event
   * For 'renamed' events, checks file existence to detect deletions
   */
  private async emitFileChangeEvent(event: FileChangeEvent): Promise<void> {
    // For 'renamed' events, check if file still exists to detect deletions
    // Node.js fs.watch emits 'rename' for both renames AND deletions
    if (event.type === 'renamed') {
      const finalType = await this.classifyRenameEvent(event.filePath);
      event.type = finalType;
    }

    this.stats.eventsTriggered++;
    this.stats.lastEventTime = event.timestamp;

    this.logger.info(`🔄 FileObserver: File ${event.type}: ${event.filename}`);

    // Emit specific event types
    this.emit('fileChange', event);
    this.emit(`file:${event.type}`, event);

    if (event.isPromptFile) {
      this.emit('promptFileChange', event);
    }

    if (event.isConfigFile) {
      this.emit('configFileChange', event);
    }

    if (event.isMethodologyFile) {
      this.emit('methodologyFileChange', event);
    }
  }

  /**
   * Classify a 'rename' event as either 'removed' or 'added' based on file existence
   * Node.js fs.watch emits 'rename' for file creation, deletion, and actual renames
   */
  private async classifyRenameEvent(filePath: string): Promise<FileChangeType> {
    try {
      await fs.promises.access(filePath);
      // File exists - this is either a new file or an actual rename target
      return 'added';
    } catch {
      // File doesn't exist - it was deleted
      return 'removed';
    }
  }

  /**
   * Handle watcher errors
   */
  private handleWatcherError(directoryPath: string, error: unknown): void {
    const normalizedError =
      error instanceof Error ? error : new Error(`Unknown watcher error: ${String(error)}`);

    this.logger.error(`FileObserver: Watcher error for ${directoryPath}:`, normalizedError);

    // Remove failed watcher
    this.watchers.delete(directoryPath);
    this.stats.watchersActive = this.watchers.size;

    // Emit error event
    this.emit('watcherError', { directoryPath, error: normalizedError });

    // Attempt to restart watcher
    if (this.retryCount < this.config.maxRetries) {
      this.retryCount++;
      this.stats.retryCount++;
      setTimeout(() => {
        this.logger.info(`Attempting to restart watcher for: ${directoryPath}`);
        this.watchDirectory(directoryPath).catch((retryError) => {
          this.logger.error(`Failed to restart watcher for ${directoryPath}:`, retryError);
        });
      }, this.config.retryDelayMs);
    }
  }

  /**
   * Check if file should be ignored
   */
  private shouldIgnoreFile(filePath: string, filename: string): boolean {
    const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');

    return this.config.ignoredPatterns.some((pattern) => {
      // Simple glob pattern matching
      const regexPattern = pattern
        .replace(/\*\*/g, '.*')
        .replace(/\*/g, '[^/]*')
        .replace(/\?/g, '[^/]');

      const regex = new RegExp(`^${regexPattern}$`);
      return regex.test(normalizedPath) || regex.test(filename);
    });
  }

  /**
   * Check if file is in a registered auxiliary directory
   */
  private isInAuxiliaryDirectory(filePath: string): boolean {
    if (this.auxiliaryDirectories.length === 0) return false;
    const normalized = path.normalize(filePath);
    return this.auxiliaryDirectories.some((dir) => normalized.startsWith(dir));
  }

  /**
   * Check if file is a prompt file (Markdown or YAML prompt format)
   */
  private isPromptFile(filename: string): boolean {
    const ext = path.extname(filename).toLowerCase();
    return ext === '.md' || ext === '.markdown' || ext === '.yaml' || ext === '.yml';
  }

  /**
   * Check if file is a configuration file
   */
  private isConfigFile(filename: string, fullPath?: string): boolean {
    const basename = path.basename(filename);

    // Standard config files
    if (basename === 'config.json') {
      return true;
    }

    // Main prompts config - get filename from ConfigManager if available
    if (this.configManager && fullPath) {
      try {
        const mainConfigPath = this.configManager.getPromptsDirectory();
        const mainConfigFilename = path.basename(mainConfigPath);
        return basename === mainConfigFilename;
      } catch (error) {
        // Fallback to default behavior if ConfigManager fails
        this.logger.debug(`Could not get prompts config path from ConfigManager: ${error}`);
      }
    }

    // Fallback for backward compatibility
    return basename === 'promptsConfig.json';
  }

  /**
   * Check if file is a methodology YAML file
   * Methodology files live in resources/methodologies/{id}/ directories and are YAML files
   *
   * @returns Object with isMethodology flag and extracted methodologyId
   */
  private isMethodologyFile(
    filename: string,
    fullPath?: string
  ): { isMethodology: boolean; methodologyId?: string } {
    const ext = path.extname(filename).toLowerCase();

    // Must be a YAML file
    if (ext !== '.yaml' && ext !== '.yml') {
      return { isMethodology: false };
    }

    // Check if path contains /methodologies/ directory (matches both resources/methodologies/ and legacy methodologies/)
    if (!fullPath) {
      return { isMethodology: false };
    }

    const normalizedPath = fullPath.replace(/\\/g, '/');
    const methodologyMatch = normalizedPath.match(/\/methodologies\/([^/]+)\//);

    if (!methodologyMatch) {
      return { isMethodology: false };
    }

    // Extract methodology ID from path (e.g., resources/methodologies/cageerf/methodology.yaml -> cageerf)
    const methodologyId = methodologyMatch[1]?.toLowerCase();

    if (!methodologyId) {
      return { isMethodology: false };
    }

    return {
      isMethodology: true,
      methodologyId,
    };
  }

  /**
   * Map fs.watch event types to our event types
   */
  private mapEventType(eventType: string): FileChangeType {
    switch (eventType) {
      case 'rename':
        return 'renamed';
      case 'change':
        return 'modified';
      default:
        return 'modified';
    }
  }

  /**
   * Get current statistics
   */
  getStats(): FileObserverStats {
    return {
      ...this.stats,
      uptime: this.isStarted ? Date.now() - this.startTime : 0,
    };
  }

  /**
   * Get current configuration
   */
  getConfig(): FileObserverConfig {
    return { ...this.config };
  }

  /**
   * Update configuration
   */
  updateConfig(newConfig: Partial<FileObserverConfig>): void {
    this.config = { ...this.config, ...newConfig };
    this.logger.info('FileObserver configuration updated');
  }

  /**
   * Get list of watched directories
   */
  getWatchedDirectories(): string[] {
    return Array.from(this.watchers.keys());
  }

  /**
   * Check if FileObserver is running
   */
  isRunning(): boolean {
    return this.isStarted;
  }

  /**
   * Analyze framework impact of file changes
   *  Basic analysis without complex framework dependencies
   */
  private analyzeFrameworkImpact(event: FileChangeEvent): FrameworkAnalysisData {
    // Basic framework analysis for  compatibility
    const requiresFrameworkUpdate = event.isPromptFile || event.isConfigFile;
    const affectedFrameworks = requiresFrameworkUpdate ? ['basic'] : [];
    const analysisInvalidated =
      event.isPromptFile && (event.type === 'modified' || event.type === 'added');
    const performanceImpact: 'low' | 'medium' | 'high' = event.isConfigFile ? 'high' : 'low';

    if (requiresFrameworkUpdate && this.config.frameworkIntegration?.cacheInvalidation) {
      this.stats.frameworkCacheInvalidations++;
      this.logger.debug(`Framework cache invalidation triggered by ${event.filename}`);
    }

    return {
      requiresFrameworkUpdate,
      affectedFrameworks,
      analysisInvalidated,
      performanceImpact,
    };
  }

  /**
   * Enable framework integration
   */
  enableFrameworkIntegration(options: Partial<FrameworkIntegration> = {}): void {
    this.config.frameworkIntegration = {
      enabled: true,
      analyzeChanges: true,
      cacheInvalidation: true,
      performanceTracking: true,
      ...options,
    };
    this.logger.info('Framework integration enabled for FileObserver');
  }

  /**
   * Disable framework integration
   */
  disableFrameworkIntegration(): void {
    this.config.frameworkIntegration = {
      enabled: false,
      analyzeChanges: false,
      cacheInvalidation: false,
      performanceTracking: false,
    };
    this.logger.info('Framework integration disabled for FileObserver');
  }

  /**
   * Check if framework integration is enabled
   */
  isFrameworkIntegrationEnabled(): boolean {
    return this.config.frameworkIntegration?.enabled ?? false;
  }

  /**
   * Get debug information
   */
  getDebugInfo(): {
    isRunning: boolean;
    config: FileObserverConfig;
    stats: FileObserverStats;
    watchedDirectories: string[];
    activeDebounceTimers: number;
    frameworkIntegration: FrameworkIntegration | undefined;
  } {
    return {
      isRunning: this.isRunning(),
      config: this.getConfig(),
      stats: this.getStats(),
      watchedDirectories: this.getWatchedDirectories(),
      activeDebounceTimers: this.debounceTimers.size,
      frameworkIntegration: this.config.frameworkIntegration,
    };
  }
}

/**
 * Factory function to create a FileObserver instance
 */
export function createFileObserver(
  logger: Logger,
  config?: Partial<FileObserverConfig>,
  configManager?: ConfigManager
): FileObserver {
  return new FileObserver(logger, config, configManager);
}
