/**
 * File Watcher Implementation
 *
 * Monitors configuration files for changes and provides debounced
 * notifications to enable hot reloading of credential configurations.
 * Uses Node.js fs.watch for efficient file system monitoring.
 */

import { EventEmitter } from 'events'
import { watch, FSWatcher, stat, access, constants } from 'fs'
import { promisify } from 'util'

import { CredentialManagerError, ErrorType } from '../../types/credential.js'

// Promisify fs functions
const statAsync = promisify(stat)
const accessAsync = promisify(access)

// ============================================================================
// Types and Interfaces
// ============================================================================

export interface FileWatcherOptions {
  /**
   * Debounce delay in milliseconds
   * @default 100
   */
  debounceDelay?: number

  /**
   * Whether to watch for file creation if file doesn't exist
   * @default true
   */
  watchForCreation?: boolean

  /**
   * Whether to emit events for file deletion
   * @default true
   */
  watchForDeletion?: boolean

  /**
   * Maximum number of retries for failed watch operations
   * @default 3
   */
  maxRetries?: number

  /**
   * Delay between retry attempts in milliseconds
   * @default 1000
   */
  retryDelay?: number

  /**
   * Whether to enable detailed logging
   * @default false
   */
  enableLogging?: boolean
}

export interface FileChangeEvent {
  path: string
  type: 'change' | 'create' | 'delete' | 'error'
  timestamp: Date
  error?: Error
}

export interface FileStats {
  size: number
  mtime: Date
  exists: boolean
}

// ============================================================================
// File Watcher Implementation
// ============================================================================

export class FileWatcher extends EventEmitter {
  private watchers = new Map<string, FSWatcher>()
  private debounceTimers = new Map<string, NodeJS.Timeout>()
  private fileStats = new Map<string, FileStats>()
  private options: Required<FileWatcherOptions>
  private retryCounters = new Map<string, number>()

  constructor(options: FileWatcherOptions = {}) {
    super()

    this.options = {
      debounceDelay: options.debounceDelay ?? 100,
      watchForCreation: options.watchForCreation ?? true,
      watchForDeletion: options.watchForDeletion ?? true,
      maxRetries: options.maxRetries ?? 3,
      retryDelay: options.retryDelay ?? 1000,
      enableLogging: options.enableLogging ?? false,
    }
  }

  /**
   * Start watching a file for changes
   */
  async watchFile(filePath: string, callback?: (event: FileChangeEvent) => void): Promise<void> {
    try {
      // Check if already watching this file
      if (this.watchers.has(filePath)) {
        this.log(`Already watching file: ${filePath}`)
        return
      }

      // Initialize file stats
      await this.updateFileStats(filePath)

      // Create debounced change handler
      const debouncedHandler = this.createDebouncedHandler(filePath, callback)

      // Set up file watcher
      const watcher = watch(filePath, { persistent: true }, (eventType, filename) => {
        this.log(`File system event: ${eventType} for ${filename || filePath}`)
        debouncedHandler(eventType)
      })

      // Handle watcher errors
      watcher.on('error', error => {
        this.handleWatcherError(filePath, error, callback)
      })

      this.watchers.set(filePath, watcher)
      this.retryCounters.set(filePath, 0)

      this.log(`Started watching file: ${filePath}`)

      // Emit initial event if callback provided
      if (callback) {
        callback({
          path: filePath,
          type: 'create',
          timestamp: new Date(),
        })
      }
    } catch (error) {
      this.handleWatchError(filePath, error, callback)
    }
  }

  /**
   * Stop watching a specific file
   */
  stopWatchingFile(filePath: string): boolean {
    const watcher = this.watchers.get(filePath)
    if (!watcher) {
      return false
    }

    try {
      watcher.close()
      this.watchers.delete(filePath)

      // Clear debounce timer
      const timer = this.debounceTimers.get(filePath)
      if (timer) {
        clearTimeout(timer)
        this.debounceTimers.delete(filePath)
      }

      // Clean up stats and retry counters
      this.fileStats.delete(filePath)
      this.retryCounters.delete(filePath)

      this.log(`Stopped watching file: ${filePath}`)
      return true
    } catch (error) {
      this.log(`Error stopping file watcher for ${filePath}: ${error.message}`)
      return false
    }
  }

  /**
   * Stop watching all files
   */
  stopWatchingAll(): void {
    const watchedFiles = Array.from(this.watchers.keys())

    for (const filePath of watchedFiles) {
      this.stopWatchingFile(filePath)
    }

    this.log('Stopped watching all files')
  }

  /**
   * Get list of currently watched files
   */
  getWatchedFiles(): string[] {
    return Array.from(this.watchers.keys())
  }

  /**
   * Check if a file is currently being watched
   */
  isWatching(filePath: string): boolean {
    return this.watchers.has(filePath)
  }

  /**
   * Get current file statistics
   */
  async getFileStats(filePath: string): Promise<FileStats | null> {
    try {
      await this.updateFileStats(filePath)
      return this.fileStats.get(filePath) || null
    } catch (error) {
      return null
    }
  }

  /**
   * Force check for file changes
   */
  async checkForChanges(filePath: string): Promise<boolean> {
    try {
      const oldStats = this.fileStats.get(filePath)
      await this.updateFileStats(filePath)
      const newStats = this.fileStats.get(filePath)

      if (!oldStats || !newStats) {
        return false
      }

      // Check if file has been modified
      return newStats.mtime.getTime() !== oldStats.mtime.getTime() || newStats.size !== oldStats.size
    } catch (error) {
      this.log(`Error checking file changes for ${filePath}: ${error.message}`)
      return false
    }
  }

  // ============================================================================
  // Private Helper Methods
  // ============================================================================

  /**
   * Create debounced change handler for a file
   */
  private createDebouncedHandler(
    filePath: string,
    callback?: (event: FileChangeEvent) => void,
  ): (eventType: string) => void {
    return (eventType: string) => {
      // Clear existing timer
      const existingTimer = this.debounceTimers.get(filePath)
      if (existingTimer) {
        clearTimeout(existingTimer)
      }

      // Set new debounced timer
      const timer = setTimeout(async () => {
        try {
          await this.handleFileChange(filePath, eventType, callback)
        } catch (error) {
          this.handleWatchError(filePath, error, callback)
        } finally {
          this.debounceTimers.delete(filePath)
        }
      }, this.options.debounceDelay)

      this.debounceTimers.set(filePath, timer)
    }
  }

  /**
   * Handle file change events
   */
  private async handleFileChange(
    filePath: string,
    eventType: string,
    callback?: (event: FileChangeEvent) => void,
  ): Promise<void> {
    try {
      const oldStats = this.fileStats.get(filePath)
      const currentlyExists = await this.fileExists(filePath)

      let changeType: 'change' | 'create' | 'delete'

      if (!oldStats?.exists && currentlyExists) {
        // File was created
        changeType = 'create'
        if (!this.options.watchForCreation) {
          return
        }
      } else if (oldStats?.exists && !currentlyExists) {
        // File was deleted
        changeType = 'delete'
        if (!this.options.watchForDeletion) {
          return
        }
      } else if (currentlyExists) {
        // File was modified
        changeType = 'change'

        // Check if file actually changed
        const hasChanged = await this.checkForChanges(filePath)
        if (!hasChanged) {
          this.log(`File ${filePath} event fired but no actual changes detected`)
          return
        }
      } else {
        // No significant change
        return
      }

      // Update file stats
      if (currentlyExists) {
        await this.updateFileStats(filePath)
      } else {
        this.fileStats.set(filePath, {
          size: 0,
          mtime: new Date(),
          exists: false,
        })
      }

      // Reset retry counter on successful operation
      this.retryCounters.set(filePath, 0)

      const event: FileChangeEvent = {
        path: filePath,
        type: changeType,
        timestamp: new Date(),
      }

      this.log(`File ${changeType}: ${filePath}`)

      // Emit event
      this.emit('change', event)

      // Call callback if provided
      if (callback) {
        callback(event)
      }
    } catch (error) {
      this.handleWatchError(filePath, error, callback)
    }
  }

  /**
   * Handle watcher errors with retry logic
   */
  private handleWatcherError(filePath: string, error: Error, callback?: (event: FileChangeEvent) => void): void {
    this.log(`Watcher error for ${filePath}: ${error.message}`)

    const retryCount = this.retryCounters.get(filePath) || 0

    if (retryCount < this.options.maxRetries) {
      this.log(`Retrying watch setup for ${filePath} (attempt ${retryCount + 1})`)
      this.retryCounters.set(filePath, retryCount + 1)

      setTimeout(() => {
        this.restartWatcher(filePath, callback)
      }, this.options.retryDelay)
    } else {
      this.log(`Max retries exceeded for ${filePath}, stopping watcher`)
      this.stopWatchingFile(filePath)
      this.handleWatchError(filePath, error, callback)
    }
  }

  /**
   * Handle general watch errors
   */
  private handleWatchError(filePath: string, error: any, callback?: (event: FileChangeEvent) => void): void {
    const watchError = new CredentialManagerError(
      `File watch error for ${filePath}: ${error.message}`,
      ErrorType.CONFIG_LOAD_ERROR,
    )

    const event: FileChangeEvent = {
      path: filePath,
      type: 'error',
      timestamp: new Date(),
      error: watchError,
    }

    this.emit('error', event)

    if (callback) {
      callback(event)
    }
  }

  /**
   * Restart a watcher after error
   */
  private async restartWatcher(filePath: string, callback?: (event: FileChangeEvent) => void): Promise<void> {
    // Stop existing watcher
    this.stopWatchingFile(filePath)

    // Wait a bit before restarting
    await new Promise(resolve => setTimeout(resolve, 100))

    // Restart watching
    try {
      await this.watchFile(filePath, callback)
    } catch (error) {
      this.handleWatchError(filePath, error, callback)
    }
  }

  /**
   * Update file statistics
   */
  private async updateFileStats(filePath: string): Promise<void> {
    try {
      const stats = await statAsync(filePath)
      this.fileStats.set(filePath, {
        size: stats.size,
        mtime: stats.mtime,
        exists: true,
      })
    } catch (error) {
      // File doesn't exist or is inaccessible
      this.fileStats.set(filePath, {
        size: 0,
        mtime: new Date(),
        exists: false,
      })
    }
  }

  /**
   * Check if file exists and is accessible
   */
  private async fileExists(filePath: string): Promise<boolean> {
    try {
      await accessAsync(filePath, constants.F_OK)
      return true
    } catch (error) {
      return false
    }
  }

  /**
   * Log messages if logging is enabled
   */
  private log(message: string): void {
    if (this.options.enableLogging) {
      console.log(`[FileWatcher] ${message}`)
    }
  }
}

// ============================================================================
// Utility Functions
// ============================================================================

/**
 * Create a file watcher with default options
 */
export function createFileWatcher(options?: FileWatcherOptions): FileWatcher {
  return new FileWatcher(options)
}

/**
 * Create a file watcher optimized for configuration files
 */
export function createConfigFileWatcher(): FileWatcher {
  return new FileWatcher({
    debounceDelay: 100,
    watchForCreation: true,
    watchForDeletion: true,
    maxRetries: 5,
    retryDelay: 1000,
    enableLogging: false,
  })
}

/**
 * Create a file watcher for development with logging
 */
export function createDevFileWatcher(): FileWatcher {
  return new FileWatcher({
    debounceDelay: 50,
    watchForCreation: true,
    watchForDeletion: true,
    maxRetries: 3,
    retryDelay: 500,
    enableLogging: true,
  })
}

/**
 * Watch a single file with a simple callback
 */
export async function watchSingleFile(
  filePath: string,
  callback: (event: FileChangeEvent) => void,
  options?: FileWatcherOptions,
): Promise<FileWatcher> {
  const watcher = new FileWatcher(options)
  await watcher.watchFile(filePath, callback)
  return watcher
}

// ============================================================================
// Export
// ============================================================================

export default FileWatcher
