/**
 * @fileoverview OrdoJS CLI - Hot Module Replacement System
 *
 * Manages hot updates with WebSocket communication and state preservation.
 */

import { OrdoJSCompiler } from '@ordojs/core';
import { EventEmitter } from 'events';
import path from 'path';
import { WebSocket, WebSocketServer } from 'ws';
import { logger } from '../utils/index.js';
import type { FileChangeEvent } from './file-watcher.js';

/**
 * HMR update types
 */
export enum HMRUpdateType {
  COMPONENT_UPDATE = 'component-update',
  STYLE_UPDATE = 'style-update',
  ASSET_UPDATE = 'asset-update',
  FULL_RELOAD = 'full-reload',
  ERROR = 'error'
}

/**
 * HMR update message
 */
export interface HMRUpdate {
  type: HMRUpdateType;
  timestamp: number;
  file: string;
  componentName?: string;
  code?: string;
  css?: string;
  error?: string;
  affectedComponents?: string[];
  preserveState?: boolean;
}

/**
 * Component state snapshot for preservation
 */
export interface ComponentStateSnapshot {
  componentId: string;
  componentName: string;
  state: Record<string, any>;
  props: Record<string, any>;
  timestamp: number;
}

/**
 * HMR client connection
 */
export interface HMRClient {
  id: string;
  socket: WebSocket;
  connectedAt: number;
  lastPing: number;
  userAgent?: string;
}

/**
 * HMR configuration options
 */
export interface HMROptions {
  /** Port for WebSocket server */
  port: number;
  /** Enable state preservation during updates */
  preserveState: boolean;
  /** Debounce delay for updates in milliseconds */
  debounceMs: number;
  /** Maximum number of connected clients */
  maxClients: number;
  /** Enable verbose logging */
  verbose: boolean;
}

/**
 * OrdoJSHMR class for managing hot module replacement
 */
export class OrdoJSHMR extends EventEmitter {
  private options: HMROptions;
  private wsServer: WebSocketServer | null;
  private clients: Map<string, HMRClient>;
  private compiler: OrdoJSCompiler;
  private updateQueue: Map<string, HMRUpdate>;
  private debounceTimers: Map<string, NodeJS.Timeout>;
  private componentStates: Map<string, ComponentStateSnapshot>;
  private isRunning: boolean;

  /**
   * Create a new OrdoJSHMR instance
   */
  constructor(options: Partial<HMROptions> = {}) {
    super();

    this.options = {
      port: 24678, // Default HMR port
      preserveState: true,
      debounceMs: 100,
      maxClients: 50,
      verbose: false,
      ...options
    };

    this.wsServer = null;
    this.clients = new Map();
    this.updateQueue = new Map();
    this.debounceTimers = new Map();
    this.componentStates = new Map();
    this.isRunning = false;

    // Initialize compiler with development settings
    this.compiler = new OrdoJSCompiler({
      target: 'es2022',
      optimize: false,
      sourceMaps: true,
      minify: false
    });
  }

  /**
   * Start the HMR system
   */
  async start(): Promise<void> {
    if (this.isRunning) {
      logger.warn('HMR system is already running');
      return;
    }

    logger.info(`Starting HMR system on port ${this.options.port}...`);

    try {
      // Create WebSocket server
      this.wsServer = new WebSocketServer({
        port: this.options.port,
        perMessageDeflate: false
      });

      // Set up WebSocket event handlers
      this.setupWebSocketHandlers();

      this.isRunning = true;
      logger.success(`HMR system started on ws://localhost:${this.options.port}`);

      // Emit ready event
      this.emit('ready');
    } catch (error) {
      logger.error(`Failed to start HMR system: ${error instanceof Error ? error.message : String(error)}`);
      throw error;
    }
  }

  /**
   * Stop the HMR system
   */
  async stop(): Promise<void> {
    if (!this.isRunning) {
      logger.info('HMR system is not running');
      return;
    }

    logger.info('Stopping HMR system...');

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

      // Close all client connections
      for (const client of this.clients.values()) {
        client.socket.close(1000, 'Server shutting down');
      }
      this.clients.clear();

      // Close WebSocket server
      if (this.wsServer) {
        await new Promise<void>((resolve, reject) => {
          this.wsServer!.close((error) => {
            if (error) {
              reject(error);
            } else {
              resolve();
            }
          });
        });
        this.wsServer = null;
      }

      this.isRunning = false;
      logger.success('HMR system stopped');

      // Emit stopped event
      this.emit('stopped');
    } catch (error) {
      logger.error(`Error stopping HMR system: ${error instanceof Error ? error.message : String(error)}`);
      throw error;
    }
  }

  /**
   * Handle file change events from file watcher
   */
  async handleFileChange(event: FileChangeEvent): Promise<void> {
    if (!this.isRunning) {
      return;
    }

    const { filePath, type: changeType } = event;
    const fileExtension = path.extname(filePath);

    if (this.options.verbose) {
      logger.debug(`HMR: Processing file change - ${changeType}: ${path.relative(process.cwd(), filePath)}`);
    }

    // Clear existing debounce timer for this file
    const existingTimer = this.debounceTimers.get(filePath);
    if (existingTimer) {
      clearTimeout(existingTimer);
    }

    // Set new debounce timer
    const timer = setTimeout(async () => {
      this.debounceTimers.delete(filePath);
      await this.processFileUpdate(filePath, changeType, fileExtension);
    }, this.options.debounceMs);

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

  /**
   * Get the number of connected clients
   */
  getClientCount(): number {
    return this.clients.size;
  }

  /**
   * Get HMR statistics
   */
  getStats(): {
    isRunning: boolean;
    clientCount: number;
    port: number;
    updatesSent: number;
    componentsTracked: number;
  } {
    return {
      isRunning: this.isRunning,
      clientCount: this.clients.size,
      port: this.options.port,
      updatesSent: this.updateQueue.size,
      componentsTracked: this.componentStates.size
    };
  }

  /**
   * Process file update and generate HMR patch
   */
  private async processFileUpdate(filePath: string, changeType: string, fileExtension: string): Promise<void> {
    try {
      const relativePath = path.relative(process.cwd(), filePath);

      // Determine update type based on file extension
      let updateType: HMRUpdateType;
      if (fileExtension === '.ordo') {
        updateType = HMRUpdateType.COMPONENT_UPDATE;
      } else if (['.css', '.scss', '.sass', '.less'].includes(fileExtension)) {
        updateType = HMRUpdateType.STYLE_UPDATE;
      } else if (['.js', '.ts', '.json'].includes(fileExtension)) {
        updateType = HMRUpdateType.ASSET_UPDATE;
      } else {
        updateType = HMRUpdateType.FULL_RELOAD;
      }

      // Generate update patch
      const update = await this.generateUpdatePatch(filePath, updateType, changeType);

      if (update) {
        // Store update in queue
        this.updateQueue.set(filePath, update);

        // Send update to all connected clients
        await this.broadcastUpdate(update);

        logger.info(`HMR: Sent ${update.type} for ${relativePath}`);
      }
    } catch (error) {
      logger.error(`HMR: Error processing file update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);

      // Send error update to clients
      const errorUpdate: HMRUpdate = {
        type: HMRUpdateType.ERROR,
        timestamp: Date.now(),
        file: filePath,
        error: error instanceof Error ? error.message : String(error)
      };

      await this.broadcastUpdate(errorUpdate);
    }
  }

  /**
   * Generate HMR update patch for a file
   */
  private async generateUpdatePatch(filePath: string, updateType: HMRUpdateType, changeType: string): Promise<HMRUpdate | null> {
    const timestamp = Date.now();

    switch (updateType) {
      case HMRUpdateType.COMPONENT_UPDATE:
        return await this.generateComponentUpdate(filePath, timestamp, changeType);

      case HMRUpdateType.STYLE_UPDATE:
        return await this.generateStyleUpdate(filePath, timestamp);

      case HMRUpdateType.ASSET_UPDATE:
        return {
          type: HMRUpdateType.ASSET_UPDATE,
          timestamp,
          file: filePath,
          preserveState: false
        };

      case HMRUpdateType.FULL_RELOAD:
        return {
          type: HMRUpdateType.FULL_RELOAD,
          timestamp,
          file: filePath,
          preserveState: false
        };

      default:
        return null;
    }
  }

  /**
   * Generate component update patch
   */
  private async generateComponentUpdate(filePath: string, timestamp: number, changeType: string): Promise<HMRUpdate | null> {
    try {
      // Read and compile the component file
      const fs = await import('fs/promises');
      const source = await fs.readFile(filePath, 'utf-8');

      // Compile the component
      const result = this.compiler.compile(source);

      if (!result.success) {
        throw new Error(`Compilation failed: ${result.errors.join(', ')}`);
      }

      // Extract component name from file path
      const componentName = path.basename(filePath, '.ordo');

      return {
        type: HMRUpdateType.COMPONENT_UPDATE,
        timestamp,
        file: filePath,
        componentName,
        code: result.output,
        preserveState: this.options.preserveState,
        affectedComponents: [componentName]
      };
    } catch (error) {
      logger.error(`Failed to generate component update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
      return null;
    }
  }

  /**
   * Generate style update patch
   */
  private async generateStyleUpdate(filePath: string, timestamp: number): Promise<HMRUpdate | null> {
    try {
      const fs = await import('fs/promises');
      const css = await fs.readFile(filePath, 'utf-8');

      return {
        type: HMRUpdateType.STYLE_UPDATE,
        timestamp,
        file: filePath,
        css,
        preserveState: true
      };
    } catch (error) {
      logger.error(`Failed to generate style update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
      return null;
    }
  }

  /**
   * Broadcast update to all connected clients
   */
  private async broadcastUpdate(update: HMRUpdate): Promise<void> {
    const message = JSON.stringify(update);
    const deadClients: string[] = [];

    for (const [clientId, client] of this.clients.entries()) {
      try {
        if (client.socket.readyState === WebSocket.OPEN) {
          client.socket.send(message);
        } else {
          deadClients.push(clientId);
        }
      } catch (error) {
        logger.debug(`Failed to send update to client ${clientId}: ${error instanceof Error ? error.message : String(error)}`);
        deadClients.push(clientId);
      }
    }

    // Clean up dead clients
    for (const clientId of deadClients) {
      this.clients.delete(clientId);
    }

    if (this.options.verbose && deadClients.length > 0) {
      logger.debug(`Cleaned up ${deadClients.length} dead client connections`);
    }
  }

  /**
   * Set up WebSocket server event handlers
   */
  private setupWebSocketHandlers(): void {
    if (!this.wsServer) return;

    this.wsServer.on('connection', (socket, request) => {
      const clientId = this.generateClientId();
      const userAgent = request.headers['user-agent'];

      // Check client limit
      if (this.clients.size >= this.options.maxClients) {
        socket.close(1013, 'Server at capacity');
        return;
      }

      // Create client record
      const client: HMRClient = {
        id: clientId,
        socket,
        connectedAt: Date.now(),
        lastPing: Date.now(),
        userAgent
      };

      this.clients.set(clientId, client);

      if (this.options.verbose) {
        logger.debug(`HMR: Client connected - ${clientId} (${this.clients.size} total)`);
      }

      // Set up client event handlers
      socket.on('message', (data) => {
        this.handleClientMessage(clientId, data);
      });

      socket.on('close', (code, reason) => {
        this.clients.delete(clientId);
        if (this.options.verbose) {
          logger.debug(`HMR: Client disconnected - ${clientId} (${code}: ${reason})`);
        }
      });

      socket.on('error', (error) => {
        logger.debug(`HMR: Client error - ${clientId}: ${error.message}`);
        this.clients.delete(clientId);
      });

      // Send welcome message
      const welcomeMessage = {
        type: 'welcome',
        timestamp: Date.now(),
        clientId,
        options: {
          preserveState: this.options.preserveState
        }
      };

      socket.send(JSON.stringify(welcomeMessage));
    });

    this.wsServer.on('error', (error) => {
      logger.error(`HMR WebSocket server error: ${error.message}`);
      this.emit('error', error);
    });
  }

  /**
   * Handle messages from HMR clients
   */
  private handleClientMessage(clientId: string, data: Buffer | string): void {
    try {
      const message = JSON.parse(data.toString());
      const client = this.clients.get(clientId);

      if (!client) return;

      switch (message.type) {
        case 'ping':
          client.lastPing = Date.now();
          client.socket.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
          break;

        case 'state-snapshot':
          if (this.options.preserveState && message.snapshot) {
            this.storeComponentState(message.snapshot);
          }
          break;

        case 'error':
          logger.error(`HMR Client error from ${clientId}: ${message.error}`);
          break;

        default:
          if (this.options.verbose) {
            logger.debug(`HMR: Unknown message type from ${clientId}: ${message.type}`);
          }
      }
    } catch (error) {
      logger.debug(`HMR: Invalid message from client ${clientId}: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Store component state snapshot for preservation
   */
  private storeComponentState(snapshot: ComponentStateSnapshot): void {
    const key = `${snapshot.componentName}_${snapshot.componentId}`;
    this.componentStates.set(key, {
      ...snapshot,
      timestamp: Date.now()
    });

    if (this.options.verbose) {
      logger.debug(`HMR: Stored state for ${snapshot.componentName}`);
    }
  }

  /**
   * Generate unique client ID
   */
  private generateClientId(): string {
    return `hmr_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
  }
}
