import * as plugins from '../plugins.js';
import type { IHttpRedirectInfo } from '../../ts_interfaces/data/route-management.js';
import type { IDcRouterRouteConfig, IRouteRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';

const AUTO_REDIRECT_ROUTE_PREFIX = 'dcrouter-auto-http-redirect';
const REDIRECT_STATUS_CODE = 301;
const REDIRECT_PRIORITY = 0;
const REDIRECT_TARGET_TEMPLATE = 'https://{domain}{path}';
const REDIRECT_INITIAL_DATA_TIMEOUT_MS = 10_000;

interface IRedirectCandidate {
  key: string;
  id: string;
  domainPattern: string;
  pathPattern?: string;
  sourceRouteNames: Set<string>;
  sourceRouteIds: Set<string>;
  remoteIngress?: IRouteRemoteIngress;
}

interface IRedirectConflict {
  routeName: string;
  covers: boolean;
}

export interface IHttpRedirectDerivationResult {
  redirects: IHttpRedirectInfo[];
  runtimeRoutes: IDcRouterRouteConfig[];
}

export function deriveHttpRedirectConfiguration(
  routes: plugins.smartproxy.IRouteConfig[],
): IHttpRedirectDerivationResult {
  const candidates = collectRedirectCandidates(routes);
  const httpRoutes = routes.filter((route) => isExplicitHttpRoute(route));
  const redirects: IHttpRedirectInfo[] = [];
  const runtimeRoutes: IDcRouterRouteConfig[] = [];

  for (const candidate of candidates) {
    const conflict = findHttpConflict(candidate, httpRoutes);
    const redirectInfo: IHttpRedirectInfo = {
      id: candidate.id,
      status: conflict ? (conflict.covers ? 'covered' : 'skipped') : 'active',
      domainPattern: candidate.domainPattern,
      pathPattern: candidate.pathPattern,
      fromTemplate: 'http://{domain}{path}',
      toTemplate: REDIRECT_TARGET_TEMPLATE,
      statusCode: REDIRECT_STATUS_CODE,
      priority: REDIRECT_PRIORITY,
      sourceRouteNames: [...candidate.sourceRouteNames].sort(),
      sourceRouteIds: [...candidate.sourceRouteIds].sort(),
      coveredByRouteNames: conflict ? [conflict.routeName] : [],
      remoteIngress: Boolean(candidate.remoteIngress?.enabled),
      notes: conflict
        ? conflict.covers
          ? 'An explicit HTTP route already covers this redirect scope.'
          : 'Skipped because an explicit HTTP route overlaps this redirect scope.'
        : undefined,
    };

    redirects.push(redirectInfo);

    if (redirectInfo.status === 'active') {
      runtimeRoutes.push(buildRuntimeRedirectRoute(candidate));
    }
  }

  return { redirects, runtimeRoutes };
}

export function deriveHttpRedirects(
  routes: plugins.smartproxy.IRouteConfig[],
): IHttpRedirectInfo[] {
  return deriveHttpRedirectConfiguration(routes).redirects;
}

export function buildHttpRedirectRuntimeRoutes(
  routes: plugins.smartproxy.IRouteConfig[],
): IDcRouterRouteConfig[] {
  return deriveHttpRedirectConfiguration(routes).runtimeRoutes;
}

function collectRedirectCandidates(routes: plugins.smartproxy.IRouteConfig[]): IRedirectCandidate[] {
  const candidates = new Map<string, IRedirectCandidate>();

  for (const route of routes) {
    if (!isHttpsRedirectSource(route)) {
      continue;
    }

    for (const domainPattern of getDomainPatterns(route)) {
      const key = createRedirectKey(domainPattern, route.match.path);
      const existing = candidates.get(key);
      if (existing) {
        existing.sourceRouteNames.add(getRouteDisplayName(route));
        if (route.id) existing.sourceRouteIds.add(route.id);
        existing.remoteIngress = mergeRemoteIngress(existing.remoteIngress, (route as IDcRouterRouteConfig).remoteIngress);
        continue;
      }

      const id = createRedirectRouteName(domainPattern, route.match.path);
      candidates.set(key, {
        key,
        id,
        domainPattern,
        pathPattern: route.match.path,
        sourceRouteNames: new Set([getRouteDisplayName(route)]),
        sourceRouteIds: new Set(route.id ? [route.id] : []),
        remoteIngress: mergeRemoteIngress(undefined, (route as IDcRouterRouteConfig).remoteIngress),
      });
    }
  }

  return [...candidates.values()].sort((a, b) => a.id.localeCompare(b.id));
}

function isHttpsRedirectSource(route: plugins.smartproxy.IRouteConfig): boolean {
  if (isGeneratedRedirectRoute(route)) return false;
  if (route.enabled === false) return false;
  if (route.action.type !== 'forward') return false;
  if (!route.match.ports) return false;
  if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 443)) return false;
  if (!route.action.tls) return false;
  if (!route.match.domains) return false;
  if (route.match.transport === 'udp') return false;
  if (route.match.protocol && route.match.protocol !== 'http') return false;
  if (route.match.clientIp || route.match.headers || route.match.tlsVersion) return false;
  return true;
}

function isExplicitHttpRoute(route: plugins.smartproxy.IRouteConfig): boolean {
  if (isGeneratedRedirectRoute(route)) return false;
  if (route.enabled === false) return false;
  if (!route.match.ports) return false;
  if (!plugins.smartproxy.portRangeIncludes(route.match.ports, 80)) return false;
  if (route.match.transport === 'udp') return false;
  return true;
}

function findHttpConflict(
  candidate: IRedirectCandidate,
  httpRoutes: plugins.smartproxy.IRouteConfig[],
): IRedirectConflict | undefined {
  for (const route of httpRoutes) {
    if (!httpRouteOverlapsCandidate(route, candidate)) {
      continue;
    }

    return {
      routeName: getRouteDisplayName(route),
      covers: httpRouteCoversCandidate(route, candidate),
    };
  }

  return undefined;
}

function httpRouteOverlapsCandidate(
  route: plugins.smartproxy.IRouteConfig,
  candidate: IRedirectCandidate,
): boolean {
  return routeDomainOverlapsCandidate(route, candidate.domainPattern)
    && pathOverlaps(route.match.path, candidate.pathPattern);
}

function httpRouteCoversCandidate(
  route: plugins.smartproxy.IRouteConfig,
  candidate: IRedirectCandidate,
): boolean {
  if (route.match.clientIp || route.match.headers || route.match.tlsVersion) {
    return false;
  }

  return routeDomainCoversCandidate(route, candidate.domainPattern)
    && pathCovers(route.match.path, candidate.pathPattern);
}

function routeDomainOverlapsCandidate(
  route: plugins.smartproxy.IRouteConfig,
  candidatePattern: string,
): boolean {
  const routePatterns = getDomainPatterns(route);
  if (routePatterns.length === 0) {
    return true;
  }

  return routePatterns.some((pattern) => domainPatternsOverlap(pattern, candidatePattern));
}

function routeDomainCoversCandidate(
  route: plugins.smartproxy.IRouteConfig,
  candidatePattern: string,
): boolean {
  const routePatterns = getDomainPatterns(route);
  if (routePatterns.length === 0) {
    return true;
  }

  return routePatterns.some((pattern) => domainPatternCovers(pattern, candidatePattern));
}

function getDomainPatterns(route: plugins.smartproxy.IRouteConfig): string[] {
  if (!route.match.domains) return [];
  return Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
}

function normalizePattern(pattern: string): string {
  return pattern.trim().toLowerCase().replace(/\.$/, '');
}

function domainPatternCovers(coverPattern: string, candidatePattern: string): boolean {
  const cover = normalizePattern(coverPattern);
  const candidate = normalizePattern(candidatePattern);
  if (cover === candidate) return true;
  if (!candidate.includes('*')) return domainPatternMatchesHostname(cover, candidate);

  const coverSuffix = getLeadingWildcardSuffix(cover);
  const candidateSuffix = getLeadingWildcardSuffix(candidate);
  if (coverSuffix && candidateSuffix) {
    return candidateSuffix.endsWith(coverSuffix);
  }

  return false;
}

function domainPatternsOverlap(firstPattern: string, secondPattern: string): boolean {
  const first = normalizePattern(firstPattern);
  const second = normalizePattern(secondPattern);
  if (first === second) return true;
  if (!first.includes('*')) return domainPatternMatchesHostname(second, first);
  if (!second.includes('*')) return domainPatternMatchesHostname(first, second);

  const firstSuffix = getLeadingWildcardSuffix(first);
  const secondSuffix = getLeadingWildcardSuffix(second);
  if (firstSuffix && secondSuffix) {
    return firstSuffix.endsWith(secondSuffix) || secondSuffix.endsWith(firstSuffix);
  }

  return false;
}

function domainPatternMatchesHostname(pattern: string, hostname: string): boolean {
  const regex = wildcardPatternToRegex(normalizePattern(pattern));
  return regex.test(normalizePattern(hostname));
}

function wildcardPatternToRegex(pattern: string): RegExp {
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
  return new RegExp(`^${escaped.replace(/\*/g, '.*')}$`, 'i');
}

function getLeadingWildcardSuffix(pattern: string): string | undefined {
  if (!pattern.startsWith('*')) return undefined;
  if (pattern.slice(1).includes('*')) return undefined;
  return pattern.slice(1);
}

function pathCovers(coverPath: string | undefined, candidatePath: string | undefined): boolean {
  if (!coverPath) return true;
  if (!candidatePath) return false;
  if (coverPath === candidatePath) return true;
  if (!coverPath.includes('*')) return false;
  const coverPrefix = coverPath.split('*')[0];
  if (!candidatePath.includes('*')) return candidatePath.startsWith(coverPrefix);
  const candidatePrefix = candidatePath.split('*')[0];
  return candidatePrefix.startsWith(coverPrefix);
}

function pathOverlaps(firstPath: string | undefined, secondPath: string | undefined): boolean {
  if (!firstPath || !secondPath) return true;
  if (firstPath === secondPath) return true;
  const firstPrefix = firstPath.split('*')[0];
  const secondPrefix = secondPath.split('*')[0];
  return firstPrefix.startsWith(secondPrefix) || secondPrefix.startsWith(firstPrefix);
}

function buildRuntimeRedirectRoute(candidate: IRedirectCandidate): IDcRouterRouteConfig {
  return {
    id: candidate.id,
    name: candidate.id,
    description: 'Generated HTTP to HTTPS redirect',
    priority: REDIRECT_PRIORITY,
    tags: ['system', 'redirect', 'auto'],
    match: {
      ports: 80,
      domains: candidate.domainPattern,
      ...(candidate.pathPattern ? { path: candidate.pathPattern } : {}),
    },
    action: {
      type: 'socket-handler',
      socketHandler: createHttpRedirectHandler(REDIRECT_TARGET_TEMPLATE, REDIRECT_STATUS_CODE),
    },
    ...(candidate.remoteIngress ? { remoteIngress: candidate.remoteIngress } : {}),
  };
}

function mergeRemoteIngress(
  current: IRouteRemoteIngress | undefined,
  next: IRouteRemoteIngress | undefined,
): IRouteRemoteIngress | undefined {
  if (!next?.enabled) return current;
  if (!current?.enabled) {
    return {
      enabled: true,
      ...(next.edgeFilter?.length ? { edgeFilter: [...next.edgeFilter] } : {}),
    };
  }

  const currentFilter = current.edgeFilter || [];
  const nextFilter = next.edgeFilter || [];
  if (currentFilter.length === 0 || nextFilter.length === 0) {
    return { enabled: true };
  }

  return {
    enabled: true,
    edgeFilter: [...new Set([...currentFilter, ...nextFilter])].sort(),
  };
}

function createRedirectKey(domainPattern: string, pathPattern?: string): string {
  return `${normalizePattern(domainPattern)}|${pathPattern || ''}`;
}

function createRedirectRouteName(domainPattern: string, pathPattern?: string): string {
  const key = createRedirectKey(domainPattern, pathPattern);
  const slug = key
    .replace(/\*/g, 'wildcard')
    .replace(/[^a-zA-Z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 48) || 'route';
  const hash = plugins.crypto.createHash('sha1').update(key).digest('hex').slice(0, 8);
  return `${AUTO_REDIRECT_ROUTE_PREFIX}-${slug}-${hash}`;
}

function getRouteDisplayName(route: plugins.smartproxy.IRouteConfig): string {
  return route.name || route.id || 'unnamed-route';
}

function isGeneratedRedirectRoute(route: plugins.smartproxy.IRouteConfig): boolean {
  return Boolean(route.name?.startsWith(AUTO_REDIRECT_ROUTE_PREFIX) || route.id?.startsWith(AUTO_REDIRECT_ROUTE_PREFIX));
}

function createHttpRedirectHandler(
  locationTemplate: string,
  statusCode: number,
): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
  return (socket, context) => {
    const cleanup = () => {
      clearTimeout(timeout);
      socket.removeListener('data', handleData);
      socket.removeListener('error', cleanup);
      socket.removeListener('close', cleanup);
    };

    const handleData = (data: string | Uint8Array) => {
      cleanup();
      const request = parseHttpRequest(data);
      if (!request) {
        socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
        return;
      }

      const domain = normalizeHostHeader(request.headers.host) || context.domain || 'localhost';
      const finalLocation = locationTemplate
        .replace('{domain}', domain)
        .replace('{port}', String(context.port))
        .replace('{path}', request.path || '/')
        .replace('{clientIp}', context.clientIp);
      const message = `Redirecting to ${finalLocation}`;
      const response = [
        `HTTP/1.1 ${statusCode} ${getHttpStatusText(statusCode)}`,
        `Location: ${finalLocation}`,
        'Content-Type: text/plain',
        `Content-Length: ${message.length}`,
        'Connection: close',
        '',
        message,
      ].join('\r\n');

      socket.end(response);
    };

    const timeout = setTimeout(() => {
      cleanup();
      socket.end('HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n');
    }, REDIRECT_INITIAL_DATA_TIMEOUT_MS) as ReturnType<typeof setTimeout> & { unref?: () => void };
    timeout.unref?.();

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

function parseHttpRequest(data: string | Uint8Array): {
  method: string;
  path: string;
  headers: Record<string, string>;
} | undefined {
  const requestText = typeof data === 'string' ? data : new TextDecoder().decode(data);
  const headerEnd = requestText.indexOf('\r\n\r\n');
  const headerText = headerEnd >= 0 ? requestText.slice(0, headerEnd) : requestText;
  const lines = headerText.split('\r\n');
  const [method, rawPath] = (lines[0] || '').split(' ');
  if (!method || !rawPath) return undefined;

  const headers: Record<string, string> = {};
  for (const line of lines.slice(1)) {
    const colonIndex = line.indexOf(':');
    if (colonIndex <= 0) continue;
    const key = line.slice(0, colonIndex).trim().toLowerCase();
    const value = line.slice(colonIndex + 1).trim();
    headers[key] = value;
  }

  return {
    method,
    path: normalizeRequestPath(rawPath),
    headers,
  };
}

function normalizeRequestPath(rawPath: string): string {
  if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) {
    try {
      const url = new URL(rawPath);
      return `${url.pathname}${url.search}` || '/';
    } catch {
      return '/';
    }
  }

  return rawPath.startsWith('/') ? rawPath : '/';
}

function normalizeHostHeader(hostHeader: string | undefined): string | undefined {
  if (!hostHeader) return undefined;
  const host = hostHeader.split(',')[0].trim();
  if (!host || /[\s\x00-\x1f\x7f]/.test(host)) return undefined;
  if (host.startsWith('[')) {
    const bracketIndex = host.indexOf(']');
    return bracketIndex > 0 ? host.slice(0, bracketIndex + 1) : undefined;
  }

  return host.replace(/:(80|443)$/, '');
}

function getHttpStatusText(statusCode: number): string {
  switch (statusCode) {
    case 301:
      return 'Moved Permanently';
    case 302:
      return 'Found';
    case 307:
      return 'Temporary Redirect';
    case 308:
      return 'Permanent Redirect';
    default:
      return 'Redirect';
  }
}
