import * as plugins from '../plugins.js';
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import { RemoteIngressEdgeDoc } from '../db/index.js';

interface IRemoteIngressFirewallConfig {
  blockedIps?: string[];
}

function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
  const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
  return [...ports].sort((a, b) => a - b);
}

/**
 * Manages CRUD for remote ingress edge registrations.
 * Persists edge configs via smartdata document classes and provides
 * the allowed edges list for the Rust hub.
 */
export class RemoteIngressManager {
  private edges: Map<string, IRemoteIngress> = new Map();
  private routes: IDcRouterRouteConfig[] = [];
  private firewallConfig?: IRemoteIngressFirewallConfig;

  constructor() {
  }

  /**
   * Load all edge registrations from the database into memory.
   */
  public async initialize(): Promise<void> {
    const docs = await RemoteIngressEdgeDoc.findAll();
    for (const doc of docs) {
      // Migration: old edges without autoDerivePorts default to true
      if ((doc as any).autoDerivePorts === undefined) {
        doc.autoDerivePorts = true;
        await doc.save();
      }
      const edge: IRemoteIngress = {
        id: doc.id,
        name: doc.name,
        secret: doc.secret,
        listenPorts: doc.listenPorts,
        listenPortsUdp: doc.listenPortsUdp,
        enabled: doc.enabled,
        autoDerivePorts: doc.autoDerivePorts,
        performance: doc.performance,
        tags: doc.tags,
        createdAt: doc.createdAt,
        updatedAt: doc.updatedAt,
      };
      this.edges.set(edge.id, edge);
    }
  }

  /**
   * Store the current route configs for port derivation.
   */
  public setRoutes(routes: IDcRouterRouteConfig[]): void {
    this.routes = routes;
  }

  /**
   * Set the full desired firewall snapshot pushed to all edges.
   */
  public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void {
    this.firewallConfig = firewallConfig;
  }

  /**
   * Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
   * When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
   * When edgeFilter is absent, the route applies to all edges.
   */
  public derivePortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
    const ports = new Set<number>();

    for (const route of this.routes) {
      if (!route.remoteIngress?.enabled) continue;

      // Apply edge filter if present
      const filter = route.remoteIngress.edgeFilter;
      if (filter && filter.length > 0) {
        const idMatch = filter.includes(edgeId);
        const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
        if (!idMatch && !tagMatch) continue;
      }

      // Extract ports from the route match
      if (route.match?.ports) {
        for (const p of extractPorts(route.match.ports)) {
          ports.add(p);
        }
      }
    }

    return [...ports].sort((a, b) => a - b);
  }

  /**
   * Derive UDP listen ports for an edge from routes with transport 'udp' or 'all'.
   * These ports need UDP listeners on the edge (e.g. for QUIC/HTTP3).
   */
  public deriveUdpPortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
    const ports = new Set<number>();

    for (const route of this.routes) {
      if (!route.remoteIngress?.enabled) continue;

      // Apply edge filter if present
      const filter = route.remoteIngress.edgeFilter;
      if (filter && filter.length > 0) {
        const idMatch = filter.includes(edgeId);
        const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
        if (!idMatch && !tagMatch) continue;
      }

      // Only include ports from routes that listen on UDP
      const transport = route.match?.transport;
      if (transport === 'udp' || transport === 'all') {
        if (route.match?.ports) {
          for (const p of extractPorts(route.match.ports)) {
            ports.add(p);
          }
        }
      }
    }

    return [...ports].sort((a, b) => a - b);
  }

  /**
   * Get the effective listen ports for an edge.
   * Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
   */
  public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
    const manualPorts = edge.listenPorts || [];
    const shouldDerive = edge.autoDerivePorts !== false;
    if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
    const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags);
    return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
  }

  /**
   * Get the effective UDP listen ports for an edge.
   * Manual UDP ports are always included. Auto-derived UDP ports are added when autoDerivePorts is true.
   */
  public getEffectiveListenPortsUdp(edge: IRemoteIngress): number[] {
    const manualPorts = edge.listenPortsUdp || [];
    const shouldDerive = edge.autoDerivePorts !== false;
    if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
    const derivedPorts = this.deriveUdpPortsForEdge(edge.id, edge.tags);
    return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
  }

  /**
   * Get manual and derived port breakdown for an edge (used in API responses).
   * Derived ports exclude any ports already present in the manual list.
   */
  public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } {
    const manual = edge.listenPorts || [];
    const shouldDerive = edge.autoDerivePorts !== false;
    if (!shouldDerive) return { manual, derived: [] };
    const manualSet = new Set(manual);
    const allDerived = this.derivePortsForEdge(edge.id, edge.tags);
    const derived = allDerived.filter((p) => !manualSet.has(p));
    return { manual, derived };
  }

  /**
   * Create a new edge registration.
   */
  public async createEdge(
    name: string,
    listenPorts: number[] = [],
    tags?: string[],
    autoDerivePorts: boolean = true,
    performance?: IRemoteIngressPerformanceConfig,
  ): Promise<IRemoteIngress> {
    const id = plugins.uuid.v4();
    const secret = plugins.crypto.randomBytes(32).toString('hex');
    const now = Date.now();

    const edge: IRemoteIngress = {
      id,
      name,
      secret,
      listenPorts,
      enabled: true,
      autoDerivePorts,
      performance,
      tags: tags || [],
      createdAt: now,
      updatedAt: now,
    };

    const doc = new RemoteIngressEdgeDoc();
    Object.assign(doc, edge);
    await doc.save();
    this.edges.set(id, edge);
    return edge;
  }

  /**
   * Get an edge by ID.
   */
  public getEdge(id: string): IRemoteIngress | undefined {
    return this.edges.get(id);
  }

  /**
   * Get all edge registrations.
   */
  public getAllEdges(): IRemoteIngress[] {
    return Array.from(this.edges.values());
  }

  /**
   * Update an edge registration.
   */
  public async updateEdge(
    id: string,
    updates: {
      name?: string;
      listenPorts?: number[];
      autoDerivePorts?: boolean;
      enabled?: boolean;
      performance?: IRemoteIngressPerformanceConfig;
      tags?: string[];
    },
  ): Promise<IRemoteIngress | null> {
    const edge = this.edges.get(id);
    if (!edge) {
      return null;
    }

    if (updates.name !== undefined) edge.name = updates.name;
    if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
    if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
    if (updates.enabled !== undefined) edge.enabled = updates.enabled;
    if (updates.performance !== undefined) edge.performance = updates.performance;
    if (updates.tags !== undefined) edge.tags = updates.tags;
    edge.updatedAt = Date.now();

    const doc = await RemoteIngressEdgeDoc.findById(id);
    if (doc) {
      Object.assign(doc, edge);
      await doc.save();
    }
    this.edges.set(id, edge);
    return edge;
  }

  /**
   * Delete an edge registration.
   */
  public async deleteEdge(id: string): Promise<boolean> {
    if (!this.edges.has(id)) {
      return false;
    }
    const doc = await RemoteIngressEdgeDoc.findById(id);
    if (doc) {
      await doc.delete();
    }
    this.edges.delete(id);
    return true;
  }

  /**
   * Regenerate the secret for an edge.
   */
  public async regenerateSecret(id: string): Promise<string | null> {
    const edge = this.edges.get(id);
    if (!edge) {
      return null;
    }

    edge.secret = plugins.crypto.randomBytes(32).toString('hex');
    edge.updatedAt = Date.now();

    const doc = await RemoteIngressEdgeDoc.findById(id);
    if (doc) {
      Object.assign(doc, edge);
      await doc.save();
    }
    this.edges.set(id, edge);
    return edge.secret;
  }

  /**
   * Verify an edge's secret using constant-time comparison.
   */
  public verifySecret(id: string, secret: string): boolean {
    const edge = this.edges.get(id);
    if (!edge) {
      return false;
    }
    const expected = Buffer.from(edge.secret);
    const provided = Buffer.from(secret);
    if (expected.length !== provided.length) {
      return false;
    }
    return plugins.crypto.timingSafeEqual(expected, provided);
  }

  /**
   * Get the list of allowed edges (enabled only) for the Rust hub.
   * Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
   */
  public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> {
    const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> = [];
    for (const edge of this.edges.values()) {
      if (edge.enabled) {
        const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
        const performance = edge.performance && Object.keys(edge.performance).length > 0 ? edge.performance : undefined;
        result.push({
          id: edge.id,
          secret: edge.secret,
          listenPorts: this.getEffectiveListenPorts(edge),
          ...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
          ...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
          ...(performance ? { performance } : {}),
        });
      }
    }
    return result;
  }
}
