import * as plugins from '../plugins.js';
import {
  giteaRoutePathClassLabels,
  giteaRoutePathClassPatterns,
  routePathClasses,
} from '../../ts_interfaces/data/route-management.js';
import type {
  IRoutePathPolicyBinding,
  IRouteMetadata,
  IRouteSecurity,
  IRouteSourceBinding,
} from '../../ts_interfaces/data/route-management.js';
import type { ReferenceResolver } from './classes.reference-resolver.js';

const MIN_ROUTE_PRIORITY = 0;
const MAX_ROUTE_PRIORITY = 10000;
const SOURCE_PRIORITY_BAND = 0.0008;
const PATH_PRIORITY_BAND = 0.0001;

export const sourcePolicyLimits = {
  maxBindings: 16,
  maxPathPoliciesPerBinding: 12,
  maxPathPatternsPerPolicy: 64,
  maxPathPatternLength: 256,
  maxPathPatternWildcards: 8,
  maxSourceProfileRefLength: 256,
  maxIdLength: 128,
  maxExceededMessageLength: 512,
  maxCompiledVariantsPerRoute: 512,
} as const;

export class SourcePolicyCompiler {
  public static compileRoute(
    route: plugins.smartproxy.IRouteConfig,
    metadata: IRouteMetadata | undefined,
    referenceResolver: ReferenceResolver | undefined,
    routeId?: string,
  ): plugins.smartproxy.IRouteConfig[] {
    const bindings = metadata?.sourceBindings || [];
    if (bindings.length === 0) {
      return [route];
    }
    if (this.validateSourceBindingsShape(bindings, route)) {
      return [];
    }
    if (!referenceResolver) {
      return [];
    }
    if (this.validateResolvedSourceBindings(bindings, referenceResolver)) {
      return [];
    }

    const compiledRoutes: plugins.smartproxy.IRouteConfig[] = [];
    const basePriority = route.priority ?? 0;
    let hasAllSourcesBinding = false;

    bindings.forEach((binding, index) => {
      const profile = referenceResolver.getProfile(binding.sourceProfileRef);
      const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef);
      if (!profile || !profileSecurity) {
        return;
      }

      const sourceMatches = this.getSourceMatchEntries(profileSecurity);
      if (sourceMatches.length === 0) {
        return;
      }
      if (this.matchesAllSources(sourceMatches)) {
        hasAllSourcesBinding = true;
      }
      const sourcePriority = this.calculateSourcePriority(basePriority, index, bindings.length);
      const sourceMatch = this.matchesAllSources(sourceMatches)
        ? { ...route.match }
        : { ...route.match, clientIp: sourceMatches };
      const pathPolicies = binding.pathPolicies || [];

      if (pathPolicies.length === 0) {
        compiledRoutes.push(this.buildCompiledRoute({
          route,
          sourceMatch,
          profileName: profile.name,
          profileSecurity,
          binding,
          sourcePriority,
          routeId,
          sourceIndex: index,
        }));
        return;
      }

      let hasSourceFallback = false;
      pathPolicies.forEach((pathPolicy, pathIndex) => {
        const pathPatterns = this.getPathPatterns(pathPolicy);
        if (pathPatterns.length === 0) {
          hasSourceFallback = true;
          compiledRoutes.push(this.buildCompiledRoute({
            route,
            sourceMatch,
            profileName: profile.name,
            profileSecurity,
            binding,
            pathPolicy,
            sourcePriority,
            routeId,
            sourceIndex: index,
            pathIndex,
            pathPolicyCount: pathPolicies.length,
          }));
          return;
        }

        pathPatterns.forEach((pathPattern, pathPatternIndex) => {
          compiledRoutes.push(this.buildCompiledRoute({
            route,
            sourceMatch,
            profileName: profile.name,
            profileSecurity,
            binding,
            pathPolicy,
            pathPattern,
            sourcePriority,
            routeId,
            sourceIndex: index,
            pathIndex,
            pathPolicyCount: pathPolicies.length,
            pathPatternIndex,
            pathPatternCount: pathPatterns.length,
          }));
        });
      });

      if (!hasSourceFallback) {
        compiledRoutes.push(this.buildCompiledRoute({
          route,
          sourceMatch,
          profileName: profile.name,
          profileSecurity,
          binding,
          sourcePriority,
          routeId,
          sourceIndex: index,
        }));
      }
    });

    if (compiledRoutes.length > 0 && !hasAllSourcesBinding) {
      compiledRoutes.push(this.buildDenyFallbackRoute(route, basePriority, routeId));
    }

    return this.applyIntegerPriorities(compiledRoutes, basePriority);
  }

  public static validateSourceBindingsPayload(sourceBindings?: Partial<IRouteSourceBinding>[]): string | undefined {
    if (sourceBindings === undefined) {
      return undefined;
    }
    if (!Array.isArray(sourceBindings)) {
      return 'Source bindings must be an array';
    }
    if (sourceBindings.length === 0) {
      return undefined;
    }
    if (sourceBindings.length > sourcePolicyLimits.maxBindings) {
      return `Source policy exceeds ${sourcePolicyLimits.maxBindings} bindings`;
    }

    const validClasses = new Set<string>(routePathClasses);
    for (const binding of sourceBindings) {
      if (!binding || typeof binding !== 'object') {
        return 'Source binding must be an object';
      }
      if (typeof binding.sourceProfileRef !== 'string') {
        return 'Source binding requires a source profile';
      }
      if (binding.sourceProfileRef.length > sourcePolicyLimits.maxSourceProfileRefLength) {
        return `Source binding source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`;
      }
      if (binding.sourceProfileRef.trim().length === 0) {
        return 'Source binding requires a source profile';
      }
      if (typeof binding.id === 'string' && binding.id.length > sourcePolicyLimits.maxIdLength) {
        return `Source binding id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
      }
      if (typeof binding.maxConnections === 'number' && binding.maxConnections < 0) {
        return 'Source policy maxConnections must be non-negative';
      }
      const bindingRateLimitError = this.validateRateLimitPayload(binding.rateLimit);
      if (bindingRateLimitError) {
        return bindingRateLimitError;
      }
      const bindingChallengeError = this.validateChallengePayload(binding.challenge);
      if (bindingChallengeError) {
        return bindingChallengeError;
      }
      const bindingMessage = binding.onExceeded?.errorMessage;
      if (typeof bindingMessage === 'string' && bindingMessage.length > sourcePolicyLimits.maxExceededMessageLength) {
        return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
      }

      const pathPolicies = binding.pathPolicies;
      if (pathPolicies === undefined) {
        continue;
      }
      if (!Array.isArray(pathPolicies)) {
        return 'Source policy path policies must be an array';
      }
      if (pathPolicies.length > sourcePolicyLimits.maxPathPoliciesPerBinding) {
        return `Source policy binding exceeds ${sourcePolicyLimits.maxPathPoliciesPerBinding} path policies`;
      }

      for (const pathPolicy of pathPolicies) {
        if (!pathPolicy || typeof pathPolicy !== 'object') {
          return 'Source policy path policy must be an object';
        }
        if (!validClasses.has(pathPolicy.pathClass)) {
          return 'Source policy path policy uses an unsupported path class';
        }
        if (typeof pathPolicy.id === 'string' && pathPolicy.id.length > sourcePolicyLimits.maxIdLength) {
          return `Source policy path policy id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
        }
        if (typeof pathPolicy.maxConnections === 'number' && pathPolicy.maxConnections < 0) {
          return 'Source policy path policy maxConnections must be non-negative';
        }
        const pathRateLimitError = this.validateRateLimitPayload(pathPolicy.rateLimit);
        if (pathRateLimitError) {
          return pathRateLimitError;
        }
        const pathChallengeError = this.validateChallengePayload(pathPolicy.challenge);
        if (pathChallengeError) {
          return pathChallengeError;
        }
        const pathMessage = pathPolicy.onExceeded?.errorMessage;
        if (typeof pathMessage === 'string' && pathMessage.length > sourcePolicyLimits.maxExceededMessageLength) {
          return `Source policy exceeded message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
        }

        const pathPatterns = pathPolicy.pathPatterns;
        if (pathPatterns === undefined) {
          continue;
        }
        if (!Array.isArray(pathPatterns)) {
          return 'Source policy path patterns must be an array';
        }
        if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) {
          return `Source policy path class exceeds ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`;
        }
        for (const pattern of pathPatterns) {
          if (typeof pattern !== 'string') {
            return 'Source policy path pattern must be a string';
          }
          if (pattern.length > sourcePolicyLimits.maxPathPatternLength) {
            return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternLength} characters`;
          }
          const wildcardCount = pattern.split('*').length - 1;
          if (wildcardCount > sourcePolicyLimits.maxPathPatternWildcards) {
            return `Source policy path pattern exceeds ${sourcePolicyLimits.maxPathPatternWildcards} wildcards`;
          }
        }
      }
    }

    return undefined;
  }

  private static validateRateLimitPayload(rateLimit: IRouteSecurity['rateLimit'] | undefined): string | undefined {
    if (rateLimit === null || rateLimit === undefined) {
      return undefined;
    }
    if (typeof rateLimit !== 'object') {
      return 'Source policy rate limit must be an object, null, or omitted';
    }
    const rawRateLimit = rateLimit as unknown as Record<string, unknown>;
    for (const key of ['maxRequests', 'window'] as const) {
      const value = rawRateLimit[key];
      if (typeof value === 'string' && value.length > 32) {
        return `Source policy rate limit ${key} exceeds 32 characters`;
      }
    }
    if (
      typeof rateLimit.errorMessage === 'string'
      && rateLimit.errorMessage.length > sourcePolicyLimits.maxExceededMessageLength
    ) {
      return `Source policy rate limit error message exceeds ${sourcePolicyLimits.maxExceededMessageLength} characters`;
    }
    const rawOnExceeded = rawRateLimit.onExceeded;
    if (rawOnExceeded !== undefined) {
      if (!rawOnExceeded || typeof rawOnExceeded !== 'object') {
        return 'Source policy rate limit onExceeded must be an object or omitted';
      }
      const onExceeded = rawOnExceeded as NonNullable<IRouteSecurity['rateLimit']>['onExceeded'];
      if (!onExceeded || !['429', 'challenge'].includes(onExceeded.type)) {
        return 'Source policy rate limit onExceeded.type must be 429 or challenge';
      }
      if (
        onExceeded.clearanceEffect !== undefined
        && !['bypass-rate-limit', 'none'].includes(onExceeded.clearanceEffect)
      ) {
        return 'Source policy rate limit onExceeded.clearanceEffect must be bypass-rate-limit or none';
      }
      if (onExceeded.type === 'challenge') {
        if (!onExceeded.challenge) {
          return 'Source policy rate limit challenge requires challenge config';
        }
        const challengeError = this.validateChallengePayload(onExceeded.challenge);
        if (challengeError) {
          return challengeError;
        }
      }
    }
    return undefined;
  }

  private static validateChallengePayload(challenge: IRouteSecurity['challenge'] | undefined): string | undefined {
    if (challenge === null || challenge === undefined) {
      return undefined;
    }
    if (typeof challenge !== 'object') {
      return 'Source policy challenge must be an object, null, or omitted';
    }
    if (typeof challenge.providerId !== 'string' || challenge.providerId.trim().length === 0) {
      return 'Source policy challenge requires providerId';
    }
    if (typeof challenge.challengeType !== 'string' || challenge.challengeType.trim().length === 0) {
      return 'Source policy challenge requires challengeType';
    }
    if (challenge.providerId.length > sourcePolicyLimits.maxIdLength) {
      return `Source policy challenge providerId exceeds ${sourcePolicyLimits.maxIdLength} characters`;
    }
    if (challenge.challengeType.length > sourcePolicyLimits.maxIdLength) {
      return `Source policy challenge challengeType exceeds ${sourcePolicyLimits.maxIdLength} characters`;
    }
    return undefined;
  }

  public static validateSourcePolicyShape(
    sourceBindings?: IRouteSourceBinding[],
    route?: plugins.smartproxy.IRouteConfig,
  ): string | undefined {
    return this.validateSourceBindingsShape(sourceBindings, route);
  }

  public static validateSourceBindingsShape(
    sourceBindings?: IRouteSourceBinding[],
    route?: plugins.smartproxy.IRouteConfig,
  ): string | undefined {
    const payloadError = this.validateSourceBindingsPayload(sourceBindings);
    if (payloadError) {
      return payloadError;
    }
    const bindings = sourceBindings || [];
    if (bindings.length === 0) {
      return undefined;
    }

    let estimatedCompiledRoutes = 0;
    for (const binding of bindings) {
      const pathPolicies = binding.pathPolicies || [];

      if (pathPolicies.length === 0) {
        estimatedCompiledRoutes++;
      } else {
        let hasSourceFallback = false;
        for (const pathPolicy of pathPolicies) {
          const pathPatterns = this.getPathPatterns(pathPolicy);
          if (pathPatterns.length > sourcePolicyLimits.maxPathPatternsPerPolicy) {
            return `Source policy path class expands beyond ${sourcePolicyLimits.maxPathPatternsPerPolicy} path patterns`;
          }
          if (pathPatterns.length === 0) {
            hasSourceFallback = true;
            estimatedCompiledRoutes++;
          } else {
            estimatedCompiledRoutes += pathPatterns.length;
          }
        }
        if (!hasSourceFallback) {
          estimatedCompiledRoutes++;
        }
      }

      if (estimatedCompiledRoutes > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
        return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route variants`;
      }
    }

    // Private-only source bindings add one terminal deny route to prevent fall-through
    // to broader routes with the same host/path/port scope.
    estimatedCompiledRoutes++;

    const expandedPortCount = route ? this.getExpandedPortCount(route.match?.ports) : 1;
    if (estimatedCompiledRoutes * expandedPortCount > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
      return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route-port variants`;
    }
    if (route && typeof route.priority === 'number' && Number.isFinite(route.priority)) {
      const integerBasePriority = Math.trunc(this.clampPriority(route.priority));
      if (integerBasePriority + estimatedCompiledRoutes > MAX_ROUTE_PRIORITY) {
        return `Source policy route priority leaves no priority headroom for ${estimatedCompiledRoutes} compiled variants`;
      }
    }

    return undefined;
  }

  public static validateResolvedSourcePolicy(
    sourceBindings: IRouteSourceBinding[] | undefined,
    referenceResolver: ReferenceResolver | undefined,
  ): string | undefined {
    return this.validateResolvedSourceBindings(sourceBindings, referenceResolver);
  }

  public static validateResolvedSourceBindings(
    sourceBindings: IRouteSourceBinding[] | undefined,
    referenceResolver: ReferenceResolver | undefined,
  ): string | undefined {
    const bindings = sourceBindings || [];
    if (bindings.length === 0) {
      return undefined;
    }
    if (!referenceResolver) {
      return 'Source policy requires source profile resolution';
    }

    for (let index = 0; index < bindings.length; index++) {
      const binding = bindings[index];
      const profile = referenceResolver.getProfile(binding.sourceProfileRef);
      if (!profile) {
        return `Source profile '${binding.sourceProfileRef}' not found`;
      }
      const profileSecurity = referenceResolver.resolveSourceProfileSecurity(binding.sourceProfileRef);
      if (!profileSecurity) {
        return `Source profile '${profile.name}' could not be resolved`;
      }
      const sourceMatches = this.getSourceMatchEntries(profileSecurity);
      if (sourceMatches.length === 0) {
        return `Source profile '${profile.name}' has no source matches`;
      }
      const matchesAllSources = this.matchesAllSources(sourceMatches);
      if (matchesAllSources && index < bindings.length - 1) {
        return 'Wildcard source profile bindings must be last in source bindings';
      }
    }

    return undefined;
  }

  private static buildCompiledRoute(options: {
    route: plugins.smartproxy.IRouteConfig;
    sourceMatch: plugins.smartproxy.IRouteConfig['match'];
    profileName: string;
    profileSecurity: IRouteSecurity;
    binding: IRouteSourceBinding;
    pathPolicy?: IRoutePathPolicyBinding;
    pathPattern?: string;
    sourcePriority: number;
    routeId?: string;
    sourceIndex: number;
    pathIndex?: number;
    pathPolicyCount?: number;
    pathPatternIndex?: number;
    pathPatternCount?: number;
  }): plugins.smartproxy.IRouteConfig {
    const routeKey = options.route.id || options.routeId || options.route.name || 'route';
    const bindingKey = options.binding.id || options.binding.sourceProfileRef || String(options.sourceIndex + 1);
    const pathPolicyKey = options.pathPolicy
      ? options.pathPolicy.id || options.pathPolicy.pathClass
      : undefined;
    const pathLabel = options.pathPolicy
      ? giteaRoutePathClassLabels[options.pathPolicy.pathClass]
      : undefined;
    const pathPatternSuffix = options.pathPatternCount && options.pathPatternCount > 1
      ? `:${(options.pathPatternIndex || 0) + 1}`
      : '';
    const pathPriority = options.pathPolicy
      ? this.calculatePathPriorityOffset(
          options.pathPattern,
          options.pathIndex || 0,
          options.pathPolicyCount || 1,
          options.pathPatternIndex || 0,
          options.pathPatternCount || 1,
        )
      : 0;

    const security = this.buildBindingSecurity(
      options.route.security,
      options.profileSecurity,
      options.binding,
      options.pathPolicy,
    );
    const match: plugins.smartproxy.IRouteConfig['match'] = options.pathPattern
      ? { ...options.sourceMatch, path: options.pathPattern }
      : { ...options.sourceMatch };
    if (this.requiresHttpProtocol(security)) {
      match.protocol = 'http';
    }

    return {
      ...options.route,
      id: pathPolicyKey
        ? `${routeKey}:source:${bindingKey}:path:${pathPolicyKey}${pathPatternSuffix}`
        : `${routeKey}:source:${bindingKey}`,
      name: pathLabel
        ? `${options.route.name || routeKey}:source:${options.profileName}:path:${pathLabel}${pathPatternSuffix}`
        : `${options.route.name || routeKey}:source:${options.profileName}`,
      match,
      priority: this.clampPriority(options.sourcePriority + pathPriority),
      security,
    };
  }

  private static buildDenyFallbackRoute(
    route: plugins.smartproxy.IRouteConfig,
    basePriority: number,
    routeId?: string,
  ): plugins.smartproxy.IRouteConfig {
    const routeKey = route.id || routeId || route.name || 'route';
    return {
      ...route,
      id: `${routeKey}:source:deny-fallback`,
      name: `${route.name || routeKey}:source:deny-fallback`,
      match: { ...route.match },
      priority: this.clampPriority(basePriority - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND),
      action: {
        type: 'socket-handler',
        socketHandler: (socket) => this.denySocket(socket),
      },
      security: undefined,
    };
  }

  private static denySocket(socket: plugins.net.Socket): void {
    let timeout: ReturnType<typeof setTimeout> & { unref?: () => void };
    const cleanup = () => {
      clearTimeout(timeout);
      socket.removeListener('data', handleData);
      socket.removeListener('error', cleanup);
      socket.removeListener('close', cleanup);
    };

    const handleData = (chunk: string | Uint8Array) => {
      cleanup();
      if (this.looksLikeHttpRequest(chunk)) {
        socket.end('HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nForbidden');
        return;
      }
      socket.destroy();
    };

    timeout = setTimeout(() => {
      cleanup();
      socket.destroy();
    }, 2000) as ReturnType<typeof setTimeout> & { unref?: () => void };
    timeout.unref?.();

    socket.once('data', handleData);
    socket.once('error', cleanup);
    socket.once('close', cleanup);
  }

  private static looksLikeHttpRequest(chunk: string | Uint8Array): boolean {
    const prefix = typeof chunk === 'string'
      ? chunk.slice(0, 16)
      : String.fromCharCode(...chunk.subarray(0, 16));
    return /^(GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS|TRACE|CONNECT)\s/.test(prefix)
      || prefix.startsWith('PRI * HTTP/2.0');
  }

  private static getPathPatterns(pathPolicy: IRoutePathPolicyBinding): string[] {
    const patterns: string[] = pathPolicy.pathPatterns?.length
      ? pathPolicy.pathPatterns
      : giteaRoutePathClassPatterns[pathPolicy.pathClass];
    return [...new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean))];
  }

  private static calculatePathPriorityOffset(
    pathPattern: string | undefined,
    pathIndex: number,
    pathPolicyCount: number,
    pathPatternIndex: number,
    pathPatternCount: number,
  ): number {
    if (!pathPattern) {
      return 0;
    }
    const pathPolicyOffset = ((pathPolicyCount - pathIndex) / (pathPolicyCount + 1))
      * (PATH_PRIORITY_BAND * 0.9);
    const pathPatternOffset = ((pathPatternCount - pathPatternIndex) / (pathPatternCount + 1))
      * (PATH_PRIORITY_BAND * 0.1 / (pathPolicyCount + 1));
    return pathPolicyOffset + pathPatternOffset;
  }

  private static calculateSourcePriority(
    basePriority: number,
    sourceIndex: number,
    sourceCount: number,
  ): number {
    const safeBasePriority = this.clampPriority(
      basePriority,
      MIN_ROUTE_PRIORITY,
      MAX_ROUTE_PRIORITY - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND,
    );
    const sourceStep = SOURCE_PRIORITY_BAND / (sourceCount + 1);
    return safeBasePriority + ((sourceCount - sourceIndex) * sourceStep);
  }

  private static applyIntegerPriorities(
    routes: plugins.smartproxy.IRouteConfig[],
    basePriority: number,
  ): plugins.smartproxy.IRouteConfig[] {
    if (routes.length === 0) {
      return routes;
    }

    const priorityOrder = routes
      .map((route, originalIndex) => ({
        originalIndex,
        priority: typeof route.priority === 'number' && Number.isFinite(route.priority)
          ? route.priority
          : basePriority,
      }))
      .sort((a, b) => (b.priority - a.priority) || (a.originalIndex - b.originalIndex));
    const topPriority = Math.trunc(this.clampPriority(
      basePriority + routes.length,
      MIN_ROUTE_PRIORITY + routes.length,
      MAX_ROUTE_PRIORITY,
    ));
    const integerPriorities = new Map<number, number>();
    priorityOrder.forEach((entry, index) => {
      integerPriorities.set(entry.originalIndex, topPriority - index);
    });

    return routes.map((route, index) => ({
      ...route,
      priority: integerPriorities.get(index) ?? MIN_ROUTE_PRIORITY,
    }));
  }

  private static clampPriority(
    priority: number,
    min = MIN_ROUTE_PRIORITY,
    max = MAX_ROUTE_PRIORITY,
  ): number {
    if (!Number.isFinite(priority)) {
      return min;
    }
    return Math.min(max, Math.max(min, priority));
  }

  private static getExpandedPortCount(portRange: plugins.smartproxy.IRouteConfig['match']['ports'] | undefined): number {
    if (portRange === undefined) {
      return 1;
    }
    if (typeof portRange === 'number') {
      return Number.isFinite(portRange) ? 1 : sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
    }
    if (!Array.isArray(portRange)) {
      return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
    }

    let count = 0;
    for (const portEntry of portRange) {
      if (typeof portEntry === 'number') {
        if (!Number.isFinite(portEntry)) {
          return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
        }
        count++;
      } else if (
        portEntry
        && typeof portEntry === 'object'
        && Number.isFinite(portEntry.from)
        && Number.isFinite(portEntry.to)
        && portEntry.from <= portEntry.to
      ) {
        count += Math.floor(portEntry.to) - Math.floor(portEntry.from) + 1;
      } else {
        return sourcePolicyLimits.maxCompiledVariantsPerRoute + 1;
      }
      if (count > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
        return count;
      }
    }

    return Math.max(1, count);
  }

  private static normalizeMaxConnections(value: IRouteSecurity['maxConnections']): number | undefined {
    return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
  }

  private static forceIpRateLimit(
    rateLimit: IRouteSecurity['rateLimit'] | undefined,
  ): IRouteSecurity['rateLimit'] | undefined {
    if (rateLimit === null) {
      return null;
    }
    if (!rateLimit || rateLimit.enabled === false) {
      return undefined;
    }
    const { headerName: _headerName, ...rest } = structuredClone(rateLimit);
    if (Number(rest.maxRequests) <= 0 || Number(rest.window) <= 0) {
      return undefined;
    }
    return {
      ...rest,
      enabled: true,
      keyBy: 'ip',
    } as IRouteSecurity['rateLimit'];
  }

  private static sanitizeSourcePolicySecurity(
    security: IRouteSecurity,
    options: { preserveClears?: boolean } = {},
  ): IRouteSecurity {
    const sanitized = structuredClone(security);
    const maxConnections = this.normalizeMaxConnections(sanitized.maxConnections);
    if (maxConnections === undefined) {
      delete sanitized.maxConnections;
    } else {
      sanitized.maxConnections = maxConnections;
    }
    if (sanitized.rateLimit !== undefined && sanitized.rateLimit !== null) {
      const rateLimit = this.forceIpRateLimit(sanitized.rateLimit);
      if (rateLimit === undefined) {
        delete sanitized.rateLimit;
      } else {
        sanitized.rateLimit = rateLimit;
      }
    }
    if (sanitized.rateLimit === null && !options.preserveClears) {
      delete sanitized.rateLimit;
    }
    if (sanitized.challenge === null && !options.preserveClears) {
      delete sanitized.challenge;
    }
    return sanitized;
  }

  private static isEmptySecurity(security: IRouteSecurity): boolean {
    return Object.keys(security).length === 0;
  }

  private static getSourceMatchEntries(security: IRouteSecurity): string[] {
    const entries = security.ipAllowList || [];
    const normalizedEntries: string[] = [];
    for (const entry of entries) {
      const rawEntry = typeof entry === 'string' ? entry : entry.ip;
      if (typeof rawEntry !== 'string') continue;
      const normalizedEntry = rawEntry.trim();
      if (normalizedEntry) {
        normalizedEntries.push(normalizedEntry);
      }
    }
    return [...new Set(normalizedEntries)];
  }

  private static matchesAllSources(sourceMatches: string[]): boolean {
    return sourceMatches.includes('*')
      || (sourceMatches.includes('0.0.0.0/0') && sourceMatches.includes('::/0'));
  }

  private static buildBindingSecurity(
    routeSecurity: IRouteSecurity | undefined,
    profileSecurity: IRouteSecurity,
    binding: IRouteSourceBinding,
    pathPolicy?: IRoutePathPolicyBinding,
  ): plugins.smartproxy.IRouteConfig['security'] {
    const baseSecurity = this.omitSourceMatchFields(routeSecurity || {});
    delete baseSecurity.rateLimit;
    delete baseSecurity.challenge;
    const sourceSecurity = this.omitSourceMatchFields(profileSecurity, { preserveClears: true });

    if (binding.rateLimit !== undefined) {
      const rateLimit = this.forceIpRateLimit(binding.rateLimit);
      if (rateLimit !== undefined) {
        sourceSecurity.rateLimit = rateLimit;
      }
    }
    if (binding.challenge !== undefined) {
      sourceSecurity.challenge = binding.challenge;
    }
    if (binding.maxConnections !== undefined) {
      const maxConnections = this.normalizeMaxConnections(binding.maxConnections);
      if (maxConnections === undefined) {
        delete sourceSecurity.maxConnections;
      } else {
        sourceSecurity.maxConnections = maxConnections;
      }
    }
    if (binding.onExceeded?.errorMessage && sourceSecurity.rateLimit) {
      sourceSecurity.rateLimit = {
        ...sourceSecurity.rateLimit,
        errorMessage: binding.onExceeded.errorMessage,
      };
    }

    if (pathPolicy?.rateLimit !== undefined) {
      const rateLimit = this.forceIpRateLimit(pathPolicy.rateLimit);
      if (rateLimit !== undefined) {
        sourceSecurity.rateLimit = rateLimit;
      }
    }
    if (pathPolicy?.challenge !== undefined) {
      sourceSecurity.challenge = pathPolicy.challenge;
    }
    if (pathPolicy?.maxConnections !== undefined) {
      const maxConnections = this.normalizeMaxConnections(pathPolicy.maxConnections);
      if (maxConnections === undefined) {
        delete sourceSecurity.maxConnections;
      } else {
        sourceSecurity.maxConnections = maxConnections;
      }
    }
    if (pathPolicy?.onExceeded?.errorMessage && sourceSecurity.rateLimit) {
      sourceSecurity.rateLimit = {
        ...sourceSecurity.rateLimit,
        errorMessage: pathPolicy.onExceeded.errorMessage,
      };
    }

    const mergedSecurity = this.sanitizeSourcePolicySecurity({
      ...baseSecurity,
      ...sourceSecurity,
    });

    if (this.isEmptySecurity(mergedSecurity)) {
      return undefined;
    }

    const { rateLimit, challenge, ...rest } = mergedSecurity;
    return {
      ...rest,
      ...(rateLimit ? { rateLimit } : {}),
      ...(challenge ? { challenge } : {}),
    };
  }

  private static requiresHttpProtocol(security: IRouteSecurity | undefined): boolean {
    return Boolean(
      security?.challenge
      || (security?.rateLimit && security.rateLimit !== null && security.rateLimit.onExceeded?.type === 'challenge'),
    );
  }

  private static omitSourceMatchFields(
    security: IRouteSecurity,
    options: { preserveClears?: boolean } = {},
  ): IRouteSecurity {
    const { ipAllowList: _ipAllowList, ...controls } = security;
    return this.sanitizeSourcePolicySecurity(controls, options);
  }
}
