import * as plugins from '../../plugins.js';
import type {
  IRouteConfig,
  IRouteMatch,
  IRouteAction,
  TPortRange,
  IRouteContext
} from '../../proxies/smart-proxy/models/route-types.js';
import {
  matchRouteDomain,
  calculateRouteSpecificity
} from './route-utils.js';
import { DomainMatcher, PathMatcher, IpMatcher } from './matchers/index.js';

/**
 * Result of route lookup
 */
export interface IRouteLookupResult {
  route: IRouteConfig;
  // Additional match parameters (path, query, etc.)
  params?: Record<string, string>;
}

/**
 * Logger interface for RouteManager
 */
export interface ILogger {
  info: (message: string, ...args: any[]) => void;
  warn: (message: string, ...args: any[]) => void;
  error: (message: string, ...args: any[]) => void;
  debug?: (message: string, ...args: any[]) => void;
}

/**
 * Shared RouteManager used by both SmartProxy and NetworkProxy
 * 
 * This provides a unified implementation for route management,
 * route matching, and port handling.
 */
export class SharedRouteManager extends plugins.EventEmitter {
  private routes: IRouteConfig[] = [];
  private portMap: Map<number, IRouteConfig[]> = new Map();
  private logger: ILogger;
  private enableDetailedLogging: boolean;

  /**
   * Memoization cache for expanded port ranges
   */
  private portRangeCache: Map<string, number[]> = new Map();
  
  constructor(options: {
    logger?: ILogger;
    enableDetailedLogging?: boolean;
    routes?: IRouteConfig[];
  }) {
    super();
    
    // Set up logger (use console if not provided)
    this.logger = options.logger || {
      info: console.log,
      warn: console.warn,
      error: console.error,
      debug: options.enableDetailedLogging ? console.log : undefined
    };
    
    this.enableDetailedLogging = options.enableDetailedLogging || false;
    
    // Initialize routes if provided
    if (options.routes) {
      this.updateRoutes(options.routes);
    }
  }
  
  /**
   * Update routes with new configuration
   */
  public updateRoutes(routes: IRouteConfig[] = []): void {
    // Sort routes by priority (higher first)
    this.routes = [...(routes || [])].sort((a, b) => {
      const priorityA = a.priority ?? 0;
      const priorityB = b.priority ?? 0;
      return priorityB - priorityA;
    });
    
    // Rebuild port mapping for fast lookups
    this.rebuildPortMap();
    
    this.logger.info(`Updated RouteManager with ${this.routes.length} routes`);
  }
  
  /**
   * Get all routes
   */
  public getRoutes(): IRouteConfig[] {
    return [...this.routes];
  }
  
  /**
   * Rebuild the port mapping for fast lookups
   * Also logs information about the ports being listened on
   */
  private rebuildPortMap(): void {
    this.portMap.clear();
    this.portRangeCache.clear(); // Clear cache when rebuilding

    // Track ports for logging
    const portToRoutesMap = new Map<number, string[]>();

    for (const route of this.routes) {
      const ports = this.expandPortRange(route.match.ports);

      // Skip if no ports were found
      if (ports.length === 0) {
        this.logger.warn(`Route ${route.name || 'unnamed'} has no valid ports to listen on`);
        continue;
      }

      for (const port of ports) {
        // Add to portMap for routing
        if (!this.portMap.has(port)) {
          this.portMap.set(port, []);
        }
        this.portMap.get(port)!.push(route);

        // Add to tracking for logging
        if (!portToRoutesMap.has(port)) {
          portToRoutesMap.set(port, []);
        }
        portToRoutesMap.get(port)!.push(route.name || 'unnamed');
      }
    }

    // Log summary of ports and routes
    const totalPorts = this.portMap.size;
    const totalRoutes = this.routes.length;
    this.logger.info(`Route manager configured with ${totalRoutes} routes across ${totalPorts} ports`);

    // Log port details if detailed logging is enabled
    if (this.enableDetailedLogging) {
      for (const [port, routes] of this.portMap.entries()) {
        this.logger.info(`Port ${port}: ${routes.length} routes (${portToRoutesMap.get(port)!.join(', ')})`);
      }
    }
  }
  
  /**
   * Expand a port range specification into an array of individual ports
   * Uses caching to improve performance for frequently used port ranges
   *
   * @public - Made public to allow external code to interpret port ranges
   */
  public expandPortRange(portRange: TPortRange): number[] {
    // For simple number, return immediately
    if (typeof portRange === 'number') {
      return [portRange];
    }

    // Create a cache key for this port range
    const cacheKey = JSON.stringify(portRange);

    // Check if we have a cached result
    if (this.portRangeCache.has(cacheKey)) {
      return this.portRangeCache.get(cacheKey)!;
    }

    // Process the port range
    let result: number[] = [];

    if (Array.isArray(portRange)) {
      // Handle array of port objects or numbers
      result = portRange.flatMap(item => {
        if (typeof item === 'number') {
          return [item];
        } else if (typeof item === 'object' && 'from' in item && 'to' in item) {
          // Handle port range object - check valid range
          if (item.from > item.to) {
            this.logger.warn(`Invalid port range: from (${item.from}) > to (${item.to})`);
            return [];
          }

          // Handle port range object
          const ports: number[] = [];
          for (let p = item.from; p <= item.to; p++) {
            ports.push(p);
          }
          return ports;
        }
        return [];
      });
    }

    // Cache the result
    this.portRangeCache.set(cacheKey, result);

    return result;
  }

  /**
   * Get all ports that should be listened on
   * This method automatically infers all required ports from route configurations
   */
  public getListeningPorts(): number[] {
    // Return the unique set of ports from all routes
    return Array.from(this.portMap.keys());
  }
  
  /**
   * Get all routes for a given port
   */
  public getRoutesForPort(port: number): IRouteConfig[] {
    return this.portMap.get(port) || [];
  }
  
  /**
   * Find the matching route for a connection
   */
  public findMatchingRoute(context: IRouteContext): IRouteLookupResult | null {
    // Get routes for this port if using port-based filtering
    const routesToCheck = context.port 
      ? (this.portMap.get(context.port) || []) 
      : this.routes;
    
    // Find the first matching route based on priority order
    for (const route of routesToCheck) {
      if (this.matchesRoute(route, context)) {
        return { route };
      }
    }
    
    return null;
  }
  
  /**
   * Check if a route matches the given context
   */
  private matchesRoute(route: IRouteConfig, context: IRouteContext): boolean {
    // Skip disabled routes
    if (route.enabled === false) {
      return false;
    }
    
    // Check port match if provided in context
    if (context.port !== undefined) {
      const ports = this.expandPortRange(route.match.ports);
      if (!ports.includes(context.port)) {
        return false;
      }
    }

    // Check domain match if specified
    if (route.match.domains && context.domain) {
      const domains = Array.isArray(route.match.domains)
        ? route.match.domains
        : [route.match.domains];

      if (!domains.some(domainPattern => DomainMatcher.match(domainPattern, context.domain!))) {
        return false;
      }
    }

    // Check path match if specified
    if (route.match.path && context.path) {
      if (!PathMatcher.match(route.match.path, context.path).matches) {
        return false;
      }
    }

    // Check client IP match if specified
    if (route.match.clientIp && context.clientIp) {
      if (!route.match.clientIp.some(ip => IpMatcher.match(ip, context.clientIp))) {
        return false;
      }
    }

    // Check TLS version match if specified
    if (route.match.tlsVersion && context.tlsVersion) {
      if (!route.match.tlsVersion.includes(context.tlsVersion)) {
        return false;
      }
    }
    
    // Check header match if specified
    if (route.match.headers && context.headers) {
      for (const [headerName, expectedValue] of Object.entries(route.match.headers)) {
        const actualValue = context.headers[headerName.toLowerCase()];
        
        // If header doesn't exist, no match
        if (actualValue === undefined) {
          return false;
        }
        
        // Match against string or regex
        if (typeof expectedValue === 'string') {
          if (actualValue !== expectedValue) {
            return false;
          }
        } else if (expectedValue instanceof RegExp) {
          if (!expectedValue.test(actualValue)) {
            return false;
          }
        }
      }
    }

    // All criteria matched
    return true;
  }
  
  
  
  /**
   * Validate the route configuration and return any warnings
   */
  public validateConfiguration(): string[] {
    const warnings: string[] = [];
    const duplicatePorts = new Map<number, number>();
    
    // Check for routes with the same exact match criteria
    for (let i = 0; i < this.routes.length; i++) {
      for (let j = i + 1; j < this.routes.length; j++) {
        const route1 = this.routes[i];
        const route2 = this.routes[j];
        
        // Check if route match criteria are the same
        if (this.areMatchesSimilar(route1.match, route2.match)) {
          warnings.push(
            `Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` +
            `The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.`
          );
        }
      }
    }
    
    // Check for routes that may never be matched due to priority
    for (let i = 0; i < this.routes.length; i++) {
      const route = this.routes[i];
      const higherPriorityRoutes = this.routes.filter(r => 
        (r.priority || 0) > (route.priority || 0));
      
      for (const higherRoute of higherPriorityRoutes) {
        if (this.isRouteShadowed(route, higherRoute)) {
          warnings.push(
            `Route "${route.name || i}" may never be matched because it is shadowed by ` +
            `higher priority route "${higherRoute.name || 'unnamed'}"`
          );
          break;
        }
      }
    }
    
    return warnings;
  }
  
  /**
   * Check if two route matches are similar (potential conflict)
   */
  private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean {
    // Check port overlap
    const ports1 = new Set(this.expandPortRange(match1.ports));
    const ports2 = new Set(this.expandPortRange(match2.ports));
    
    let havePortOverlap = false;
    for (const port of ports1) {
      if (ports2.has(port)) {
        havePortOverlap = true;
        break;
      }
    }
    
    if (!havePortOverlap) {
      return false;
    }
    
    // Check domain overlap
    if (match1.domains && match2.domains) {
      const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains];
      const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains];
      
      // Check if any domain pattern from match1 could match any from match2
      let haveDomainOverlap = false;
      for (const domain1 of domains1) {
        for (const domain2 of domains2) {
          if (domain1 === domain2 || 
              (domain1.includes('*') || domain2.includes('*'))) {
            haveDomainOverlap = true;
            break;
          }
        }
        if (haveDomainOverlap) break;
      }
      
      if (!haveDomainOverlap) {
        return false;
      }
    } else if (match1.domains || match2.domains) {
      // One has domains, the other doesn't - they could overlap
      // The one with domains is more specific, so it's not exactly a conflict
      return false;
    }
    
    // Check path overlap
    if (match1.path && match2.path) {
      // This is a simplified check - in a real implementation,
      // you'd need to check if the path patterns could match the same paths
      return match1.path === match2.path || 
             match1.path.includes('*') || 
             match2.path.includes('*');
    } else if (match1.path || match2.path) {
      // One has a path, the other doesn't
      return false;
    }
    
    // If we get here, the matches have significant overlap
    return true;
  }
  
  /**
   * Check if a route is completely shadowed by a higher priority route
   */
  private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean {
    // If they don't have similar match criteria, no shadowing occurs
    if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) {
      return false;
    }
    
    // If higher priority route has more specific criteria, no shadowing
    const routeSpecificity = calculateRouteSpecificity(route.match);
    const higherRouteSpecificity = calculateRouteSpecificity(higherPriorityRoute.match);
    
    if (higherRouteSpecificity > routeSpecificity) {
      return false;
    }
    
    // If higher priority route is equally or less specific but has higher priority,
    // it shadows the lower priority route
    return true;
  }
  
}