import * as plugins from './plugins.js';
import { EventEmitter } from 'events';
import { decodeConnectionToken } from './classes.token.js';
import type { IFirewallConfig } from './classes.remoteingresshub.js';

// Command map for the edge side of remoteingress-bin
type TEdgeCommands = {
  ping: {
    params: Record<string, never>;
    result: { pong: boolean };
  };
  startEdge: {
    params: {
      hubHost: string;
      hubPort: number;
      edgeId: string;
      secret: string;
      bindAddress?: string;
      transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback';
    };
    result: { started: boolean };
  };
  stopEdge: {
    params: Record<string, never>;
    result: { stopped: boolean; wasRunning?: boolean };
  };
  getEdgeStatus: {
    params: Record<string, never>;
    result: {
      running: boolean;
      connected: boolean;
      publicIp: string | null;
      activeStreams: number;
      listenPorts: number[];
    };
  };
};

export interface IEdgeConfig {
  hubHost: string;
  hubPort?: number;
  edgeId: string;
  secret: string;
  bindAddress?: string;
  transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback';
}

const MAX_RESTART_ATTEMPTS = 10;
const MAX_RESTART_BACKOFF_MS = 30_000;

export class RemoteIngressEdge extends EventEmitter {
  private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TEdgeCommands>>;
  private started = false;
  private stopping = false;
  private savedConfig: IEdgeConfig | null = null;
  private restartBackoffMs = 1000;
  private restartAttempts = 0;
  private statusInterval: ReturnType<typeof setInterval> | undefined;
  private nft: InstanceType<typeof plugins.smartnftables.SmartNftables> | null = null;
  private pendingFirewallConfig: IFirewallConfig | null = null;

  constructor() {
    super();

    const packageDir = plugins.path.resolve(
      plugins.path.dirname(new URL(import.meta.url).pathname),
      '..',
    );

    this.bridge = new plugins.smartrust.RustBridge<TEdgeCommands>({
      binaryName: 'remoteingress-bin',
      cliArgs: ['--management'],
      requestTimeoutMs: 30_000,
      readyTimeoutMs: 10_000,
      localPaths: [
        // Platform-suffixed binary in dist_rust (production)
        plugins.path.join(packageDir, 'dist_rust', `remoteingress-bin_${process.platform === 'win32' ? 'windows' : 'linux'}_${process.arch === 'x64' ? 'amd64' : process.arch}`),
        // Exact binaryName fallback in dist_rust
        plugins.path.join(packageDir, 'dist_rust', 'remoteingress-bin'),
        // Development build paths (cargo output uses exact name)
        plugins.path.join(packageDir, 'rust', 'target', 'release', 'remoteingress-bin'),
        plugins.path.join(packageDir, 'rust', 'target', 'debug', 'remoteingress-bin'),
      ],
      searchSystemPath: false,
      logger: {
        log: (level: string, message: string) => {
          if (level === 'error') {
            console.error(`[RemoteIngressEdge] ${message}`);
          } else {
            console.log(`[RemoteIngressEdge] ${message}`);
          }
        },
      },
    });

    // Forward events from Rust binary
    this.bridge.on('management:tunnelConnected', () => {
      this.emit('tunnelConnected');
    });
    this.bridge.on('management:tunnelDisconnected', (data: { reason?: string }) => {
      const reason = data?.reason ?? 'unknown';
      console.log(`[RemoteIngressEdge] Tunnel disconnected: ${reason}`);
      this.emit('tunnelDisconnected', data);
    });
    this.bridge.on('management:publicIpDiscovered', (data: { ip: string }) => {
      this.emit('publicIpDiscovered', data);
    });
    this.bridge.on('management:portsAssigned', (data: { listenPorts: number[] }) => {
      console.log(`[RemoteIngressEdge] Ports assigned by hub: ${data.listenPorts.join(', ')}`);
      this.emit('portsAssigned', data);
    });
    this.bridge.on('management:portsUpdated', (data: { listenPorts: number[] }) => {
      console.log(`[RemoteIngressEdge] Ports updated by hub: ${data.listenPorts.join(', ')}`);
      this.emit('portsUpdated', data);
    });
    this.bridge.on('management:firewallConfigUpdated', (data: { firewallConfig: IFirewallConfig }) => {
      console.log(`[RemoteIngressEdge] Firewall config updated from hub`);
      void this.applyFirewallConfig(data.firewallConfig).catch((err) => {
        console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`);
      });
      this.emit('firewallConfigUpdated', data);
    });
  }

  /**
   * Initialize the nftables manager. Fails gracefully if not running as root.
   */
  private async initNft(options: { reset?: boolean } = {}): Promise<void> {
    try {
      this.nft = new plugins.smartnftables.SmartNftables({
        tableName: 'remoteingress',
        dryRun: false,
      });
      if (options.reset) {
        await (this.nft as any).cleanup({ force: true });
      }
      await this.nft.initialize();
      console.log('[RemoteIngressEdge] SmartNftables initialized');
      if (this.pendingFirewallConfig) {
        const pending = this.pendingFirewallConfig;
        this.pendingFirewallConfig = null;
        await this.applyFirewallConfig(pending);
      }
    } catch (err) {
      console.warn(`[RemoteIngressEdge] Failed to initialize nftables (not root?): ${err}`);
      this.nft = null;
    }
  }

  /**
   * Apply firewall configuration received from the hub.
   * Performs a full replacement: cleans up existing rules, then applies the new config.
   */
  private async applyFirewallConfig(config: IFirewallConfig): Promise<void> {
    if (!this.nft) {
      this.pendingFirewallConfig = config;
      return;
    }

    try {
      // Full cleanup and reinitialize to replace all rules atomically
      await (this.nft as any).cleanup({ force: true });
      await this.nft.initialize();

      // Apply blocked IPs
      if (config.blockedIps && config.blockedIps.length > 0) {
        await (this.nft.firewall as any).blockIPSet('hub-blocklist', {
          setName: 'blocked_ipv4',
          ips: config.blockedIps,
          comment: 'RemoteIngress hub blocklist',
        });
        console.log(`[RemoteIngressEdge] Blocked ${config.blockedIps.length} IPs`);
      }

      // Apply rate limits
      if (config.rateLimits && config.rateLimits.length > 0) {
        for (const rl of config.rateLimits) {
          await this.nft.rateLimit.addRateLimit(rl.id, {
            port: rl.port,
            protocol: rl.protocol,
            rate: rl.rate,
            burst: rl.burst,
            perSourceIP: rl.perSourceIP,
          });
        }
        console.log(`[RemoteIngressEdge] Applied ${config.rateLimits.length} rate limits`);
      }

      // Apply firewall rules
      if (config.rules && config.rules.length > 0) {
        for (const rule of config.rules) {
          await this.nft.firewall.addRule(rule.id, {
            direction: rule.direction,
            action: rule.action,
            sourceIP: rule.sourceIP,
            destPort: rule.destPort,
            protocol: rule.protocol,
            comment: rule.comment,
          });
        }
        console.log(`[RemoteIngressEdge] Applied ${config.rules.length} firewall rules`);
      }
    } catch (err) {
      console.error(`[RemoteIngressEdge] Failed to apply firewall config: ${err}`);
    }
  }

  /**
   * Start the edge — spawns the Rust binary and connects to the hub.
   * Accepts either a connection token or an explicit IEdgeConfig.
   */
  public async start(config: { token: string } | IEdgeConfig): Promise<void> {
    let edgeConfig: IEdgeConfig;

    if ('token' in config) {
      const decoded = decodeConnectionToken(config.token);
      edgeConfig = {
        hubHost: decoded.hubHost,
        hubPort: decoded.hubPort,
        edgeId: decoded.edgeId,
        secret: decoded.secret,
      };
    } else {
      edgeConfig = config;
    }

    this.savedConfig = edgeConfig;
    this.stopping = false;

    // Clear any stale nftables state left by a prior process before the edge
    // can accept hub config or bind public listener ports.
    await this.initNft({ reset: true });

    const spawned = await this.bridge.spawn();
    if (!spawned) {
      throw new Error('Failed to spawn remoteingress-bin');
    }

    // Register crash recovery handler (remove first to avoid duplicates)
    this.bridge.removeListener('exit', this.handleCrashRecovery);
    this.bridge.on('exit', this.handleCrashRecovery);

    try {
      await this.bridge.sendCommand('startEdge', {
        hubHost: edgeConfig.hubHost,
        hubPort: edgeConfig.hubPort ?? 8443,
        edgeId: edgeConfig.edgeId,
        secret: edgeConfig.secret,
        ...(edgeConfig.bindAddress ? { bindAddress: edgeConfig.bindAddress } : {}),
        ...(edgeConfig.transportMode ? { transportMode: edgeConfig.transportMode } : {}),
      });
    } catch (err) {
      // Clean up the spawned process to avoid orphaning it
      this.bridge.removeListener('exit', this.handleCrashRecovery);
      this.bridge.kill();
      throw err;
    }

    this.started = true;
    this.restartAttempts = 0;
    this.restartBackoffMs = 1000;

    // Start periodic status logging
    this.statusInterval = setInterval(async () => {
      try {
        const status = await this.getStatus();
        console.log(
          `[RemoteIngressEdge] Status: connected=${status.connected}, ` +
          `streams=${status.activeStreams}, ports=[${status.listenPorts.join(',')}], ` +
          `publicIp=${status.publicIp ?? 'unknown'}`
        );
      } catch {
        // Bridge may be shutting down
      }
    }, 60_000);
  }

  /**
   * Stop the edge and kill the Rust process.
   */
  public async stop(): Promise<void> {
    this.stopping = true;
    if (this.statusInterval) {
      clearInterval(this.statusInterval);
      this.statusInterval = undefined;
    }
    // Clean up nftables rules before stopping
    if (this.nft) {
      try {
        await (this.nft as any).cleanup({ force: true });
      } catch (err) {
        console.warn(`[RemoteIngressEdge] nftables cleanup error: ${err}`);
      }
      this.nft = null;
    }
    if (this.started) {
      try {
        await this.bridge.sendCommand('stopEdge', {} as Record<string, never>);
      } catch {
        // Process may already be dead
      }
      this.bridge.removeListener('exit', this.handleCrashRecovery);
      this.bridge.kill();
      this.started = false;
    }
    this.savedConfig = null;
    this.pendingFirewallConfig = null;
    // Remove all listeners to prevent memory buildup
    this.bridge.removeAllListeners();
    this.removeAllListeners();
  }

  /**
   * Get the current edge status.
   */
  public async getStatus() {
    return this.bridge.sendCommand('getEdgeStatus', {} as Record<string, never>);
  }

  /**
   * Check if the bridge is running.
   */
  public get running(): boolean {
    return this.bridge.running;
  }

  /**
   * Handle unexpected Rust binary crash — auto-restart with backoff.
   */
  private handleCrashRecovery = async (code: number | null, signal: string | null) => {
    if (this.stopping || !this.started || !this.savedConfig) {
      return;
    }

    console.error(
      `[RemoteIngressEdge] Rust binary crashed (code=${code}, signal=${signal}), ` +
      `attempt ${this.restartAttempts + 1}/${MAX_RESTART_ATTEMPTS}`
    );

    this.started = false;

    // Clear orphaned status interval from previous run
    if (this.statusInterval) {
      clearInterval(this.statusInterval);
      this.statusInterval = undefined;
    }

    if (this.restartAttempts >= MAX_RESTART_ATTEMPTS) {
      console.error('[RemoteIngressEdge] Max restart attempts reached, giving up');
      this.emit('crashRecoveryFailed');
      return;
    }

    await new Promise(resolve => setTimeout(resolve, this.restartBackoffMs));
    // Re-check after backoff — stop() may have been called during the wait
    if (this.stopping || !this.savedConfig) {
      return;
    }
    this.restartBackoffMs = Math.min(this.restartBackoffMs * 2, MAX_RESTART_BACKOFF_MS);
    this.restartAttempts++;

    try {
      // Drop stale kernel rules before reconnecting. The hub will send the
      // current full firewall snapshot during handshake/config refresh.
      await this.initNft({ reset: true });

      const spawned = await this.bridge.spawn();
      if (!spawned) {
        console.error('[RemoteIngressEdge] Failed to respawn binary');
        return;
      }

      this.bridge.removeListener('exit', this.handleCrashRecovery);
      this.bridge.on('exit', this.handleCrashRecovery);

      await this.bridge.sendCommand('startEdge', {
        hubHost: this.savedConfig.hubHost,
        hubPort: this.savedConfig.hubPort ?? 8443,
        edgeId: this.savedConfig.edgeId,
        secret: this.savedConfig.secret,
        ...(this.savedConfig.bindAddress ? { bindAddress: this.savedConfig.bindAddress } : {}),
        ...(this.savedConfig.transportMode ? { transportMode: this.savedConfig.transportMode } : {}),
      });

      this.started = true;
      this.restartAttempts = 0;
      this.restartBackoffMs = 1000;

      // Restart periodic status logging
      this.statusInterval = setInterval(async () => {
        try {
          const status = await this.getStatus();
          console.log(
            `[RemoteIngressEdge] Status: connected=${status.connected}, ` +
            `streams=${status.activeStreams}, ports=[${status.listenPorts.join(',')}], ` +
            `publicIp=${status.publicIp ?? 'unknown'}`
          );
        } catch {
          // Bridge may be shutting down
        }
      }, 60_000);

      console.log('[RemoteIngressEdge] Successfully recovered from crash');
      this.emit('crashRecovered');
    } catch (err) {
      console.error(`[RemoteIngressEdge] Crash recovery failed: ${err}`);
    }
  };
}
