import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
import type { Email } from '../core/classes.email.js';

/**
 * Email router that evaluates routes and determines actions
 */
export class EmailRouter extends EventEmitter {
  private routes: IEmailRoute[];
  private patternCache: Map<string, boolean> = new Map();
  private storageManager?: any; // StorageManager instance
  private persistChanges: boolean;
  
  /**
   * Create a new email router
   * @param routes Array of email routes
   * @param options Router options
   */
  constructor(routes: IEmailRoute[], options?: {
    storageManager?: any;
    persistChanges?: boolean;
  }) {
    super();
    this.routes = this.sortRoutesByPriority(routes);
    this.storageManager = options?.storageManager;
    this.persistChanges = options?.persistChanges ?? !!this.storageManager;
    
    // If storage manager is provided, try to load persisted routes
    if (this.storageManager) {
      this.loadRoutes({ merge: true }).catch(error => {
        console.error(`Failed to load persisted routes: ${error.message}`);
      });
    }
  }
  
  /**
   * Sort routes by priority (higher priority first)
   * @param routes Routes to sort
   * @returns Sorted routes
   */
  private sortRoutesByPriority(routes: IEmailRoute[]): IEmailRoute[] {
    return [...routes].sort((a, b) => {
      const priorityA = a.priority ?? 0;
      const priorityB = b.priority ?? 0;
      return priorityB - priorityA; // Higher priority first
    });
  }
  
  /**
   * Get all configured routes
   * @returns Array of routes
   */
  public getRoutes(): IEmailRoute[] {
    return [...this.routes];
  }
  
  /**
   * Update routes
   * @param routes New routes
   * @param persist Whether to persist changes (defaults to persistChanges setting)
   */
  public async updateRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> {
    this.routes = this.sortRoutesByPriority(routes);
    this.clearCache();
    this.emit('routesUpdated', this.routes);
    
    // Persist if requested or if persistChanges is enabled
    if (persist ?? this.persistChanges) {
      await this.saveRoutes();
    }
  }
  
  /**
   * Set routes (alias for updateRoutes)
   * @param routes New routes
   * @param persist Whether to persist changes
   */
  public async setRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> {
    await this.updateRoutes(routes, persist);
  }
  
  /**
   * Clear the pattern cache
   */
  public clearCache(): void {
    this.patternCache.clear();
    this.emit('cacheCleared');
  }
  
  /**
   * Evaluate routes and find the first match
   * @param context Email context
   * @returns Matched route or null
   */
  public async evaluateRoutes(context: IEmailContext): Promise<IEmailRoute | null> {
    for (const route of this.routes) {
      if (await this.matchesRoute(route, context)) {
        this.emit('routeMatched', route, context);
        return route;
      }
    }
    return null;
  }
  
  /**
   * Check if a route matches the context
   * @param route Route to check
   * @param context Email context
   * @returns True if route matches
   */
  private async matchesRoute(route: IEmailRoute, context: IEmailContext): Promise<boolean> {
    const match = route.match;
    
    // Check recipients
    if (match.recipients && !this.matchesRecipients(context.email, match.recipients)) {
      return false;
    }
    
    // Check senders
    if (match.senders && !this.matchesSenders(context.email, match.senders)) {
      return false;
    }
    
    // Check client IP
    if (match.clientIp && !this.matchesClientIp(context, match.clientIp)) {
      return false;
    }
    
    // Check authentication
    if (match.authenticated !== undefined && 
        context.session.authenticated !== match.authenticated) {
      return false;
    }
    
    // Check headers
    if (match.headers && !this.matchesHeaders(context.email, match.headers)) {
      return false;
    }
    
    // Check size
    if (match.sizeRange && !this.matchesSize(context.email, match.sizeRange)) {
      return false;
    }
    
    // Check subject
    if (match.subject && !this.matchesSubject(context.email, match.subject)) {
      return false;
    }
    
    // Check attachments
    if (match.hasAttachments !== undefined && 
        (context.email.attachments.length > 0) !== match.hasAttachments) {
      return false;
    }
    
    // All checks passed
    return true;
  }
  
  /**
   * Check if email recipients match patterns
   * @param email Email to check
   * @param patterns Patterns to match
   * @returns True if any recipient matches
   */
  private matchesRecipients(email: Email, patterns: string | string[]): boolean {
    const patternArray = Array.isArray(patterns) ? patterns : [patterns];
    const recipients = email.getAllRecipients();
    
    for (const recipient of recipients) {
      for (const pattern of patternArray) {
        if (this.matchesPattern(recipient, pattern)) {
          return true;
        }
      }
    }
    return false;
  }
  
  /**
   * Check if email sender matches patterns
   * @param email Email to check
   * @param patterns Patterns to match
   * @returns True if sender matches
   */
  private matchesSenders(email: Email, patterns: string | string[]): boolean {
    const patternArray = Array.isArray(patterns) ? patterns : [patterns];
    const sender = email.from;
    
    for (const pattern of patternArray) {
      if (this.matchesPattern(sender, pattern)) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Check if client IP matches patterns
   * @param context Email context
   * @param patterns IP patterns to match
   * @returns True if IP matches
   */
  private matchesClientIp(context: IEmailContext, patterns: string | string[]): boolean {
    const patternArray = Array.isArray(patterns) ? patterns : [patterns];
    const clientIp = context.session.remoteAddress;
    
    if (!clientIp) {
      return false;
    }
    
    for (const pattern of patternArray) {
      // Check for CIDR notation
      if (pattern.includes('/')) {
        if (this.ipInCidr(clientIp, pattern)) {
          return true;
        }
      } else {
        // Exact match
        if (clientIp === pattern) {
          return true;
        }
      }
    }
    return false;
  }
  
  /**
   * Check if email headers match patterns
   * @param email Email to check
   * @param headerPatterns Header patterns to match
   * @returns True if headers match
   */
  private matchesHeaders(email: Email, headerPatterns: Record<string, string | RegExp>): boolean {
    for (const [header, pattern] of Object.entries(headerPatterns)) {
      const value = email.headers[header];
      if (!value) {
        return false;
      }
      
      if (pattern instanceof RegExp) {
        if (!pattern.test(value)) {
          return false;
        }
      } else {
        if (value !== pattern) {
          return false;
        }
      }
    }
    return true;
  }
  
  /**
   * Check if email size matches range
   * @param email Email to check
   * @param sizeRange Size range to match
   * @returns True if size is in range
   */
  private matchesSize(email: Email, sizeRange: { min?: number; max?: number }): boolean {
    // Calculate approximate email size
    const size = this.calculateEmailSize(email);
    
    if (sizeRange.min !== undefined && size < sizeRange.min) {
      return false;
    }
    if (sizeRange.max !== undefined && size > sizeRange.max) {
      return false;
    }
    return true;
  }
  
  /**
   * Check if email subject matches pattern
   * @param email Email to check
   * @param pattern Pattern to match
   * @returns True if subject matches
   */
  private matchesSubject(email: Email, pattern: string | RegExp): boolean {
    const subject = email.subject || '';
    
    if (pattern instanceof RegExp) {
      return pattern.test(subject);
    } else {
      return this.matchesPattern(subject, pattern);
    }
  }
  
  /**
   * Check if a string matches a glob pattern
   * @param str String to check
   * @param pattern Glob pattern
   * @returns True if matches
   */
  private matchesPattern(str: string, pattern: string): boolean {
    // Check cache
    const cacheKey = `${str}:${pattern}`;
    const cached = this.patternCache.get(cacheKey);
    if (cached !== undefined) {
      return cached;
    }
    
    // Convert glob to regex
    const regexPattern = this.globToRegExp(pattern);
    const matches = regexPattern.test(str);
    
    // Cache result
    this.patternCache.set(cacheKey, matches);
    
    return matches;
  }
  
  /**
   * Convert glob pattern to RegExp
   * @param pattern Glob pattern
   * @returns Regular expression
   */
  private globToRegExp(pattern: string): RegExp {
    // Escape special regex characters except * and ?
    let regexString = pattern
      .replace(/[.+^${}()|[\]\\]/g, '\\$&')
      .replace(/\*/g, '.*')
      .replace(/\?/g, '.');
    
    return new RegExp(`^${regexString}$`, 'i');
  }
  
  /**
   * Check if IP is in CIDR range
   * @param ip IP address to check
   * @param cidr CIDR notation (e.g., '192.168.0.0/16')
   * @returns True if IP is in range
   */
  private ipInCidr(ip: string, cidr: string): boolean {
    try {
      const [range, bits] = cidr.split('/');
      const mask = parseInt(bits, 10);
      
      // Convert IPs to numbers
      const ipNum = this.ipToNumber(ip);
      const rangeNum = this.ipToNumber(range);
      
      // Calculate mask
      const maskBits = 0xffffffff << (32 - mask);
      
      // Check if in range
      return (ipNum & maskBits) === (rangeNum & maskBits);
    } catch {
      return false;
    }
  }
  
  /**
   * Convert IP address to number
   * @param ip IP address
   * @returns Number representation
   */
  private ipToNumber(ip: string): number {
    const parts = ip.split('.');
    return parts.reduce((acc, part, index) => {
      return acc + (parseInt(part, 10) << (8 * (3 - index)));
    }, 0);
  }
  
  /**
   * Calculate approximate email size in bytes
   * @param email Email to measure
   * @returns Size in bytes
   */
  private calculateEmailSize(email: Email): number {
    let size = 0;
    
    // Headers
    for (const [key, value] of Object.entries(email.headers)) {
      size += key.length + value.length + 4; // ": " + "\r\n"
    }
    
    // Body
    size += (email.text || '').length;
    size += (email.html || '').length;
    
    // Attachments
    for (const attachment of email.attachments) {
      if (attachment.content) {
        size += attachment.content.length;
      }
    }
    
    return size;
  }
  
  /**
   * Save current routes to storage
   */
  public async saveRoutes(): Promise<void> {
    if (!this.storageManager) {
      this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
      return;
    }
    
    try {
      // Validate all routes before saving
      for (const route of this.routes) {
        if (!route.name || !route.match || !route.action) {
          throw new Error(`Invalid route: ${JSON.stringify(route)}`);
        }
      }
      
      const routesData = JSON.stringify(this.routes, null, 2);
      await this.storageManager.set('/email/routes/config.json', routesData);
      
      this.emit('routesPersisted', this.routes.length);
    } catch (error) {
      console.error(`Failed to save routes: ${error.message}`);
      throw error;
    }
  }
  
  /**
   * Load routes from storage
   * @param options Load options
   */
  public async loadRoutes(options?: {
    merge?: boolean; // Merge with existing routes
    replace?: boolean; // Replace existing routes
  }): Promise<IEmailRoute[]> {
    if (!this.storageManager) {
      this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
      return [];
    }
    
    try {
      const routesData = await this.storageManager.get('/email/routes/config.json');
      
      if (!routesData) {
        return [];
      }
      
      const loadedRoutes = JSON.parse(routesData) as IEmailRoute[];
      
      // Validate loaded routes
      for (const route of loadedRoutes) {
        if (!route.name || !route.match || !route.action) {
          console.warn(`Skipping invalid route: ${JSON.stringify(route)}`);
          continue;
        }
      }
      
      if (options?.replace) {
        // Replace all routes
        this.routes = this.sortRoutesByPriority(loadedRoutes);
      } else if (options?.merge) {
        // Merge with existing routes (loaded routes take precedence)
        const routeMap = new Map<string, IEmailRoute>();
        
        // Add existing routes
        for (const route of this.routes) {
          routeMap.set(route.name, route);
        }
        
        // Override with loaded routes
        for (const route of loadedRoutes) {
          routeMap.set(route.name, route);
        }
        
        this.routes = this.sortRoutesByPriority(Array.from(routeMap.values()));
      }
      
      this.clearCache();
      this.emit('routesLoaded', loadedRoutes.length);
      
      return loadedRoutes;
    } catch (error) {
      console.error(`Failed to load routes: ${error.message}`);
      throw error;
    }
  }
  
  /**
   * Add a route
   * @param route Route to add
   * @param persist Whether to persist changes
   */
  public async addRoute(route: IEmailRoute, persist?: boolean): Promise<void> {
    // Validate route
    if (!route.name || !route.match || !route.action) {
      throw new Error('Invalid route: missing required fields');
    }
    
    // Check if route already exists
    const existingIndex = this.routes.findIndex(r => r.name === route.name);
    if (existingIndex >= 0) {
      throw new Error(`Route '${route.name}' already exists`);
    }
    
    // Add route
    this.routes.push(route);
    this.routes = this.sortRoutesByPriority(this.routes);
    this.clearCache();
    
    this.emit('routeAdded', route);
    this.emit('routesUpdated', this.routes);
    
    // Persist if requested
    if (persist ?? this.persistChanges) {
      await this.saveRoutes();
    }
  }
  
  /**
   * Remove a route by name
   * @param name Route name
   * @param persist Whether to persist changes
   */
  public async removeRoute(name: string, persist?: boolean): Promise<void> {
    const index = this.routes.findIndex(r => r.name === name);
    
    if (index < 0) {
      throw new Error(`Route '${name}' not found`);
    }
    
    const removedRoute = this.routes.splice(index, 1)[0];
    this.clearCache();
    
    this.emit('routeRemoved', removedRoute);
    this.emit('routesUpdated', this.routes);
    
    // Persist if requested
    if (persist ?? this.persistChanges) {
      await this.saveRoutes();
    }
  }
  
  /**
   * Update a route
   * @param name Route name
   * @param route Updated route data
   * @param persist Whether to persist changes
   */
  public async updateRoute(name: string, route: IEmailRoute, persist?: boolean): Promise<void> {
    // Validate route
    if (!route.name || !route.match || !route.action) {
      throw new Error('Invalid route: missing required fields');
    }
    
    const index = this.routes.findIndex(r => r.name === name);
    
    if (index < 0) {
      throw new Error(`Route '${name}' not found`);
    }
    
    // Update route
    this.routes[index] = route;
    this.routes = this.sortRoutesByPriority(this.routes);
    this.clearCache();
    
    this.emit('routeUpdated', route);
    this.emit('routesUpdated', this.routes);
    
    // Persist if requested
    if (persist ?? this.persistChanges) {
      await this.saveRoutes();
    }
  }
  
  /**
   * Get a route by name
   * @param name Route name
   * @returns Route or undefined
   */
  public getRoute(name: string): IEmailRoute | undefined {
    return this.routes.find(r => r.name === name);
  }
}