import * as http from 'http';
import * as url from 'url';
import * as tsclass from '@tsclass/tsclass';

/**
 * Optional path pattern configuration that can be added to proxy configs
 */
export interface IPathPatternConfig {
  pathPattern?: string;
}

/**
 * Interface for router result with additional metadata
 */
export interface IRouterResult {
  config: tsclass.network.IReverseProxyConfig;
  pathMatch?: string;
  pathParams?: Record<string, string>;
  pathRemainder?: string;
}

export class ProxyRouter {
  // Store original configs for reference
  private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
  // Default config to use when no match is found (optional)
  private defaultConfig?: tsclass.network.IReverseProxyConfig;
  // Store path patterns separately since they're not in the original interface
  private pathPatterns: Map<tsclass.network.IReverseProxyConfig, string> = new Map();
  // Logger interface
  private logger: { 
    error: (message: string, data?: any) => void;
    warn: (message: string, data?: any) => void;
    info: (message: string, data?: any) => void;
    debug: (message: string, data?: any) => void;
  };

  constructor(
    configs?: tsclass.network.IReverseProxyConfig[],
    logger?: { 
      error: (message: string, data?: any) => void;
      warn: (message: string, data?: any) => void;
      info: (message: string, data?: any) => void;
      debug: (message: string, data?: any) => void;
    }
  ) {
    this.logger = logger || console;
    if (configs) {
      this.setNewProxyConfigs(configs);
    }
  }

  /**
   * Sets a new set of reverse configs to be routed to
   * @param reverseCandidatesArg Array of reverse proxy configurations
   */
  public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void {
    this.reverseProxyConfigs = [...reverseCandidatesArg];
    
    // Find default config if any (config with "*" as hostname)
    this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*');
    
    this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`);
  }

  /**
   * Routes a request based on hostname and path
   * @param req The incoming HTTP request
   * @returns The matching proxy config or undefined if no match found
   */
  public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig {
    const result = this.routeReqWithDetails(req);
    return result ? result.config : undefined;
  }
  
  /**
   * Routes a request with detailed matching information
   * @param req The incoming HTTP request
   * @returns Detailed routing result including matched config and path information
   */
  public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined {
    // Extract and validate host header
    const originalHost = req.headers.host;
    if (!originalHost) {
      this.logger.error('No host header found in request');
      return this.defaultConfig ? { config: this.defaultConfig } : undefined;
    }
    
    // Parse URL for path matching
    const parsedUrl = url.parse(req.url || '/');
    const urlPath = parsedUrl.pathname || '/';
    
    // Extract hostname without port
    const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
    
    // First try exact hostname match
    const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath);
    if (exactConfig) {
      return exactConfig;
    }
    
    // Try wildcard subdomain
    if (hostWithoutPort.includes('.')) {
      const domainParts = hostWithoutPort.split('.');
      if (domainParts.length > 2) {
        const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
        const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
        if (wildcardConfig) {
          return wildcardConfig;
        }
      }
    }
    
    // Fall back to default config if available
    if (this.defaultConfig) {
      this.logger.warn(`No specific config found for host: ${hostWithoutPort}, using default`);
      return { config: this.defaultConfig };
    }
    
    this.logger.error(`No config found for host: ${hostWithoutPort}`);
    return undefined;
  }
  
  /**
   * Find a config for a specific host and path
   */
  private findConfigForHost(hostname: string, path: string): IRouterResult | undefined {
    // Find all configs for this hostname
    const configs = this.reverseProxyConfigs.filter(
      config => config.hostName.toLowerCase() === hostname.toLowerCase()
    );
    
    if (configs.length === 0) {
      return undefined;
    }
    
    // First try configs with path patterns
    const configsWithPaths = configs.filter(config => this.pathPatterns.has(config));
    
    // Sort by path pattern specificity - more specific first
    configsWithPaths.sort((a, b) => {
      const aPattern = this.pathPatterns.get(a) || '';
      const bPattern = this.pathPatterns.get(b) || '';
      
      // Exact patterns come before wildcard patterns
      const aHasWildcard = aPattern.includes('*');
      const bHasWildcard = bPattern.includes('*');
      
      if (aHasWildcard && !bHasWildcard) return 1;
      if (!aHasWildcard && bHasWildcard) return -1;
      
      // Longer patterns are considered more specific
      return bPattern.length - aPattern.length;
    });
    
    // Check each config with path pattern
    for (const config of configsWithPaths) {
      const pathPattern = this.pathPatterns.get(config);
      if (pathPattern) {
        const pathMatch = this.matchPath(path, pathPattern);
        if (pathMatch) {
          return {
            config,
            pathMatch: pathMatch.matched,
            pathParams: pathMatch.params,
            pathRemainder: pathMatch.remainder
          };
        }
      }
    }
    
    // If no path pattern matched, use the first config without a path pattern
    const configWithoutPath = configs.find(config => !this.pathPatterns.has(config));
    if (configWithoutPath) {
      return { config: configWithoutPath };
    }
    
    return undefined;
  }
  
  /**
   * Matches a URL path against a pattern
   * Supports:
   * - Exact matches: /users/profile
   * - Wildcards: /api/* (matches any path starting with /api/)
   * - Path parameters: /users/:id (captures id as a parameter)
   * 
   * @param path The URL path to match
   * @param pattern The pattern to match against
   * @returns Match result with params and remainder, or null if no match
   */
  private matchPath(path: string, pattern: string): { 
    matched: string; 
    params: Record<string, string>; 
    remainder: string;
  } | null {
    // Handle exact match
    if (path === pattern) {
      return {
        matched: pattern,
        params: {},
        remainder: ''
      };
    }
    
    // Handle wildcard match
    if (pattern.endsWith('/*')) {
      const prefix = pattern.slice(0, -2);
      if (path === prefix || path.startsWith(`${prefix}/`)) {
        return {
          matched: prefix,
          params: {},
          remainder: path.slice(prefix.length)
        };
      }
      return null;
    }
    
    // Handle path parameters
    const patternParts = pattern.split('/').filter(p => p);
    const pathParts = path.split('/').filter(p => p);
    
    // Too few path parts to match
    if (pathParts.length < patternParts.length) {
      return null;
    }
    
    const params: Record<string, string> = {};
    
    // Compare each part
    for (let i = 0; i < patternParts.length; i++) {
      const patternPart = patternParts[i];
      const pathPart = pathParts[i];
      
      // Handle parameter
      if (patternPart.startsWith(':')) {
        const paramName = patternPart.slice(1);
        params[paramName] = pathPart;
        continue;
      }
      
      // Handle wildcard at the end
      if (patternPart === '*' && i === patternParts.length - 1) {
        break;
      }
      
      // Handle exact match for this part
      if (patternPart !== pathPart) {
        return null;
      }
    }
    
    // Calculate the remainder - the unmatched path parts
    const remainderParts = pathParts.slice(patternParts.length);
    const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
    
    // Calculate the matched path
    const matchedParts = patternParts.map((part, i) => {
      return part.startsWith(':') ? pathParts[i] : part;
    });
    const matched = '/' + matchedParts.join('/');
    
    return {
      matched,
      params,
      remainder
    };
  }
  
  /**
   * Gets all currently active proxy configurations
   * @returns Array of all active configurations
   */
  public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] {
    return [...this.reverseProxyConfigs];
  }
  
  /**
   * Gets all hostnames that this router is configured to handle
   * @returns Array of hostnames
   */
  public getHostnames(): string[] {
    const hostnames = new Set<string>();
    for (const config of this.reverseProxyConfigs) {
      if (config.hostName !== '*') {
        hostnames.add(config.hostName.toLowerCase());
      }
    }
    return Array.from(hostnames);
  }
  
  /**
   * Adds a single new proxy configuration
   * @param config The configuration to add
   * @param pathPattern Optional path pattern for route matching
   */
  public addProxyConfig(
    config: tsclass.network.IReverseProxyConfig, 
    pathPattern?: string
  ): void {
    this.reverseProxyConfigs.push(config);
    
    // Store path pattern if provided
    if (pathPattern) {
      this.pathPatterns.set(config, pathPattern);
    }
  }
  
  /**
   * Sets a path pattern for an existing config
   * @param config The existing configuration
   * @param pathPattern The path pattern to set
   * @returns Boolean indicating if the config was found and updated
   */
  public setPathPattern(
    config: tsclass.network.IReverseProxyConfig, 
    pathPattern: string
  ): boolean {
    const exists = this.reverseProxyConfigs.includes(config);
    if (exists) {
      this.pathPatterns.set(config, pathPattern);
      return true;
    }
    return false;
  }
  
  /**
   * Removes a proxy configuration by hostname
   * @param hostname The hostname to remove
   * @returns Boolean indicating whether any configs were removed
   */
  public removeProxyConfig(hostname: string): boolean {
    const initialCount = this.reverseProxyConfigs.length;
    
    // Find configs to remove
    const configsToRemove = this.reverseProxyConfigs.filter(
      config => config.hostName === hostname
    );
    
    // Remove them from the patterns map
    for (const config of configsToRemove) {
      this.pathPatterns.delete(config);
    }
    
    // Filter them out of the configs array
    this.reverseProxyConfigs = this.reverseProxyConfigs.filter(
      config => config.hostName !== hostname
    );
    
    return this.reverseProxyConfigs.length !== initialCount;
  }
}