/**
 * Route Utilities
 * 
 * This file provides utility functions for working with route configurations,
 * including merging, finding, and managing route collections.
 */

import type { IRouteConfig, IRouteMatch } from '../models/route-types.js';
import { validateRouteConfig } from './route-validators.js';

/**
 * Merge two route configurations
 * The second route's properties will override the first route's properties where they exist
 * @param baseRoute The base route configuration
 * @param overrideRoute The route configuration with overriding properties
 * @returns A new merged route configuration
 */
export function mergeRouteConfigs(
  baseRoute: IRouteConfig,
  overrideRoute: Partial<IRouteConfig>
): IRouteConfig {
  // Create deep copies to avoid modifying original objects
  const mergedRoute: IRouteConfig = JSON.parse(JSON.stringify(baseRoute));

  // Apply overrides at the top level
  if (overrideRoute.id) mergedRoute.id = overrideRoute.id;
  if (overrideRoute.name) mergedRoute.name = overrideRoute.name;
  if (overrideRoute.enabled !== undefined) mergedRoute.enabled = overrideRoute.enabled;
  if (overrideRoute.priority !== undefined) mergedRoute.priority = overrideRoute.priority;

  // Merge match configuration
  if (overrideRoute.match) {
    mergedRoute.match = { ...mergedRoute.match };
    
    if (overrideRoute.match.ports !== undefined) {
      mergedRoute.match.ports = overrideRoute.match.ports;
    }
    
    if (overrideRoute.match.domains !== undefined) {
      mergedRoute.match.domains = overrideRoute.match.domains;
    }
    
    if (overrideRoute.match.path !== undefined) {
      mergedRoute.match.path = overrideRoute.match.path;
    }
    
    if (overrideRoute.match.headers !== undefined) {
      mergedRoute.match.headers = overrideRoute.match.headers;
    }
  }

  // Merge action configuration
  if (overrideRoute.action) {
    // If action types are different, replace the entire action
    if (overrideRoute.action.type && overrideRoute.action.type !== mergedRoute.action.type) {
      mergedRoute.action = JSON.parse(JSON.stringify(overrideRoute.action));
    } else {
      // Otherwise merge the action properties
      mergedRoute.action = { ...mergedRoute.action };
      
      // Merge target
      if (overrideRoute.action.target) {
        mergedRoute.action.target = {
          ...mergedRoute.action.target,
          ...overrideRoute.action.target
        };
      }
      
      // Merge TLS options
      if (overrideRoute.action.tls) {
        mergedRoute.action.tls = {
          ...mergedRoute.action.tls,
          ...overrideRoute.action.tls
        };
      }
      
      // Merge redirect options
      if (overrideRoute.action.redirect) {
        mergedRoute.action.redirect = {
          ...mergedRoute.action.redirect,
          ...overrideRoute.action.redirect
        };
      }
      
      // Merge static options
      if (overrideRoute.action.static) {
        mergedRoute.action.static = {
          ...mergedRoute.action.static,
          ...overrideRoute.action.static
        };
      }
    }
  }

  return mergedRoute;
}

/**
 * Check if a route matches a domain
 * @param route The route to check
 * @param domain The domain to match against
 * @returns True if the route matches the domain, false otherwise
 */
export function routeMatchesDomain(route: IRouteConfig, domain: string): boolean {
  if (!route.match?.domains) {
    return false;
  }
  
  const domains = Array.isArray(route.match.domains) 
    ? route.match.domains 
    : [route.match.domains];
  
  return domains.some(d => {
    // Handle wildcard domains
    if (d.startsWith('*.')) {
      const suffix = d.substring(2);
      return domain.endsWith(suffix) && domain.split('.').length > suffix.split('.').length;
    }
    return d.toLowerCase() === domain.toLowerCase();
  });
}

/**
 * Check if a route matches a port
 * @param route The route to check
 * @param port The port to match against
 * @returns True if the route matches the port, false otherwise
 */
export function routeMatchesPort(route: IRouteConfig, port: number): boolean {
  if (!route.match?.ports) {
    return false;
  }

  if (typeof route.match.ports === 'number') {
    return route.match.ports === port;
  }

  if (Array.isArray(route.match.ports)) {
    // Simple case - array of numbers
    if (typeof route.match.ports[0] === 'number') {
      return (route.match.ports as number[]).includes(port);
    }

    // Complex case - array of port ranges
    if (typeof route.match.ports[0] === 'object') {
      return (route.match.ports as Array<{ from: number; to: number }>).some(
        range => port >= range.from && port <= range.to
      );
    }
  }

  return false;
}

/**
 * Check if a route matches a path
 * @param route The route to check
 * @param path The path to match against
 * @returns True if the route matches the path, false otherwise
 */
export function routeMatchesPath(route: IRouteConfig, path: string): boolean {
  if (!route.match?.path) {
    return true; // No path specified means it matches any path
  }
  
  // Handle exact path
  if (route.match.path === path) {
    return true;
  }
  
  // Handle path prefix with trailing slash (e.g., /api/)
  if (route.match.path.endsWith('/') && path.startsWith(route.match.path)) {
    return true;
  }
  
  // Handle exact path match without trailing slash
  if (!route.match.path.endsWith('/') && path === route.match.path) {
    return true;
  }
  
  // Handle wildcard paths (e.g., /api/*)
  if (route.match.path.endsWith('*')) {
    const prefix = route.match.path.slice(0, -1);
    return path.startsWith(prefix);
  }
  
  return false;
}

/**
 * Check if a route matches headers
 * @param route The route to check
 * @param headers The headers to match against
 * @returns True if the route matches the headers, false otherwise
 */
export function routeMatchesHeaders(
  route: IRouteConfig, 
  headers: Record<string, string>
): boolean {
  if (!route.match?.headers || Object.keys(route.match.headers).length === 0) {
    return true; // No headers specified means it matches any headers
  }
  
  // Check each header in the route's match criteria
  return Object.entries(route.match.headers).every(([key, value]) => {
    // If the header isn't present in the request, it doesn't match
    if (!headers[key]) {
      return false;
    }
    
    // Handle exact match
    if (typeof value === 'string') {
      return headers[key] === value;
    }
    
    // Handle regex match
    if (value instanceof RegExp) {
      return value.test(headers[key]);
    }
    
    return false;
  });
}

/**
 * Find all routes that match the given criteria
 * @param routes Array of routes to search
 * @param criteria Matching criteria
 * @returns Array of matching routes sorted by priority
 */
export function findMatchingRoutes(
  routes: IRouteConfig[],
  criteria: {
    domain?: string;
    port?: number;
    path?: string;
    headers?: Record<string, string>;
  }
): IRouteConfig[] {
  // Filter routes that are enabled and match all provided criteria
  const matchingRoutes = routes.filter(route => {
    // Skip disabled routes
    if (route.enabled === false) {
      return false;
    }
    
    // Check domain match if specified
    if (criteria.domain && !routeMatchesDomain(route, criteria.domain)) {
      return false;
    }
    
    // Check port match if specified
    if (criteria.port !== undefined && !routeMatchesPort(route, criteria.port)) {
      return false;
    }
    
    // Check path match if specified
    if (criteria.path && !routeMatchesPath(route, criteria.path)) {
      return false;
    }
    
    // Check headers match if specified
    if (criteria.headers && !routeMatchesHeaders(route, criteria.headers)) {
      return false;
    }
    
    return true;
  });
  
  // Sort matching routes by priority (higher priority first)
  return matchingRoutes.sort((a, b) => {
    const priorityA = a.priority || 0;
    const priorityB = b.priority || 0;
    return priorityB - priorityA; // Higher priority first
  });
}

/**
 * Find the best matching route for the given criteria
 * @param routes Array of routes to search
 * @param criteria Matching criteria
 * @returns The best matching route or undefined if no match
 */
export function findBestMatchingRoute(
  routes: IRouteConfig[],
  criteria: {
    domain?: string;
    port?: number;
    path?: string;
    headers?: Record<string, string>;
  }
): IRouteConfig | undefined {
  const matchingRoutes = findMatchingRoutes(routes, criteria);
  return matchingRoutes.length > 0 ? matchingRoutes[0] : undefined;
}

/**
 * Create a route ID based on route properties
 * @param route Route configuration
 * @returns Generated route ID
 */
export function generateRouteId(route: IRouteConfig): string {
  // Create a deterministic ID based on route properties
  const domains = Array.isArray(route.match?.domains) 
    ? route.match.domains.join('-') 
    : route.match?.domains || 'any';
    
  let portsStr = 'any';
  if (route.match?.ports) {
    if (Array.isArray(route.match.ports)) {
      portsStr = route.match.ports.join('-');
    } else if (typeof route.match.ports === 'number') {
      portsStr = route.match.ports.toString();
    }
  }
    
  const path = route.match?.path || 'any';
  const action = route.action?.type || 'unknown';
  
  return `route-${domains}-${portsStr}-${path}-${action}`.replace(/[^a-zA-Z0-9-]/g, '-');
}

/**
 * Clone a route configuration
 * @param route Route to clone
 * @returns Deep copy of the route
 */
export function cloneRoute(route: IRouteConfig): IRouteConfig {
  return JSON.parse(JSON.stringify(route));
}