import * as plugins from '../plugins.js';
import type { IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';

export interface ITunnelManagerConfig {
  tunnelPort?: number;
  targetHost?: string;
  tls?: {
    certPem?: string;
    keyPem?: string;
  };
  performance?: IRemoteIngressPerformanceConfig;
}

/**
 * Manages the RemoteIngressHub instance and tracks connected edge statuses.
 */
export class TunnelManager {
  private hub: InstanceType<typeof plugins.remoteingress.RemoteIngressHub>;
  private manager: RemoteIngressManager;
  private config: ITunnelManagerConfig;
  private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
  private reconcileInterval: ReturnType<typeof setInterval> | null = null;
  private syncChain: Promise<void> = Promise.resolve();
  private reconcileChain: Promise<void> = Promise.resolve();
  private stopped = true;

  constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
    this.manager = manager;
    this.config = config;
    this.hub = new plugins.remoteingress.RemoteIngressHub();

    // Listen for edge connect/disconnect events
    this.hub.on('edgeConnected', (data: { edgeId: string; peerAddr: string }) => {
      this.edgeStatuses.set(data.edgeId, {
        edgeId: data.edgeId,
        connected: true,
        publicIp: data.peerAddr || null,
        activeTunnels: 0,
        lastHeartbeat: Date.now(),
        connectedAt: Date.now(),
      });
    });

    this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
      this.edgeStatuses.delete(data.edgeId);
    });

    this.hub.on('streamSummary', (data: {
      edgeId: string;
      activeStreams: number;
      streamsOpenedTotal: number;
      streamsClosedTotal: number;
    }) => {
      const existing = this.edgeStatuses.get(data.edgeId);
      if (existing) {
        existing.activeTunnels = data.activeStreams;
        existing.lastHeartbeat = Date.now();
        if (existing.traffic) {
          existing.traffic.streamsOpenedTotal = data.streamsOpenedTotal;
          existing.traffic.streamsClosedTotal = data.streamsClosedTotal;
        }
      }
    });
  }

  /**
   * Start the tunnel hub and load allowed edges.
   */
  public async start(): Promise<void> {
    this.stopped = false;
    try {
      await this.hub.start({
        tunnelPort: this.config.tunnelPort ?? 8443,
        targetHost: this.config.targetHost ?? '127.0.0.1',
        tls: this.config.tls,
        ...(this.config.performance ? { performance: this.config.performance } : {}),
        streamEventMode: 'summary',
      } as any);

      if (this.stopped) return;

      // Send allowed edges to the hub
      await this.syncAllowedEdges();

      if (this.stopped) return;

      // Periodically reconcile with authoritative Rust hub status
      this.reconcileInterval = setInterval(() => {
        this.reconcileChain = this.reconcileChain
          .catch(() => {})
          .then(() => this.reconcile());
        this.reconcileChain.catch(() => {});
      }, 15_000);
    } catch (err) {
      await this.stop();
      throw err;
    }
  }

  /**
   * Stop the tunnel hub.
   */
  public async stop(): Promise<void> {
    if (this.stopped) {
      return;
    }
    this.stopped = true;
    if (this.reconcileInterval) {
      clearInterval(this.reconcileInterval);
      this.reconcileInterval = null;
    }
    await Promise.all([
      this.syncChain.catch(() => {}),
      this.reconcileChain.catch(() => {}),
    ]);
    // Remove event listeners before stopping to prevent leaks
    this.hub.removeAllListeners();
    await this.hub.stop();
    this.edgeStatuses.clear();
  }

  /**
   * Reconcile TS-side edge statuses with the authoritative Rust hub status.
   * Overwrites event-derived activeTunnels with the real activeStreams count.
   */
  private async reconcile(): Promise<void> {
    if (this.stopped) return;
    const hubStatus = await this.hub.getStatus();
    if (this.stopped) return;
    if (!hubStatus || !hubStatus.connectedEdges) return;

    const rustEdgeIds = new Set<string>();

    for (const rustEdge of hubStatus.connectedEdges) {
      rustEdgeIds.add(rustEdge.edgeId);
      const existing = this.edgeStatuses.get(rustEdge.edgeId);
      if (existing) {
        existing.activeTunnels = rustEdge.activeStreams;
        existing.lastHeartbeat = Date.now();
        this.applyRustStatus(existing, rustEdge);
        // Update peer address if available from Rust hub
        if (rustEdge.peerAddr) {
          existing.publicIp = rustEdge.peerAddr;
        }
      } else {
        // Missed edgeConnected event — add entry
        const status: IRemoteIngressStatus = {
          edgeId: rustEdge.edgeId,
          connected: true,
          publicIp: rustEdge.peerAddr || null,
          activeTunnels: rustEdge.activeStreams,
          lastHeartbeat: Date.now(),
          connectedAt: rustEdge.connectedAt * 1000,
        };
        this.applyRustStatus(status, rustEdge);
        this.edgeStatuses.set(rustEdge.edgeId, status);
      }
    }

    // Remove entries for edges no longer connected in Rust (missed edgeDisconnected)
    for (const edgeId of this.edgeStatuses.keys()) {
      if (!rustEdgeIds.has(edgeId)) {
        this.edgeStatuses.delete(edgeId);
      }
    }
  }

  /**
   * Sync allowed edges from the manager to the hub.
   * Call this after creating/deleting/updating edges.
   */
  public async syncAllowedEdges(): Promise<void> {
    const run = this.syncChain.catch(() => {}).then(async () => {
      if (this.stopped) return;
      const edges = this.manager.getAllowedEdges();
      if (this.stopped) return;
      await this.hub.updateAllowedEdges(edges as any);
    });
    this.syncChain = run;
    await run;
  }

  private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void {
    status.transportMode = rustEdge.transportMode;
    status.fallbackUsed = rustEdge.fallbackUsed;
    status.performance = rustEdge.performance;
    status.flowControl = rustEdge.flowControl;
    status.queues = rustEdge.queues;
    status.traffic = rustEdge.traffic;
    status.udp = rustEdge.udp;
  }

  /**
   * Get runtime statuses for all known edges.
   */
  public getEdgeStatuses(): IRemoteIngressStatus[] {
    return Array.from(this.edgeStatuses.values());
  }

  /**
   * Get status for a specific edge.
   */
  public getEdgeStatus(edgeId: string): IRemoteIngressStatus | undefined {
    return this.edgeStatuses.get(edgeId);
  }

  /**
   * Get the count of connected edges.
   */
  public getConnectedCount(): number {
    let count = 0;
    for (const status of this.edgeStatuses.values()) {
      if (status.connected) count++;
    }
    return count;
  }

  /**
   * Get the public IPs of all connected edges.
   */
  public getConnectedEdgeIps(): string[] {
    const ips: string[] = [];
    for (const status of this.edgeStatuses.values()) {
      if (status.connected && status.publicIp) {
        ips.push(status.publicIp);
      }
    }
    return ips;
  }

  /**
   * Get the total number of active tunnels across all edges.
   */
  public getTotalActiveTunnels(): number {
    let total = 0;
    for (const status of this.edgeStatuses.values()) {
      total += status.activeTunnels;
    }
    return total;
  }
}
