import * as plugins from './smartvpn.plugins.js';
import { VpnBridge } from './smartvpn.classes.vpnbridge.js';
import type {
  IVpnServerOptions,
  IVpnServerConfig,
  IVpnStatus,
  IVpnServerStatistics,
  IVpnClientInfo,
  IVpnKeypair,
  IVpnClientTelemetry,
  IWgPeerConfig,
  IWgPeerInfo,
  IClientEntry,
  IClientConfigBundle,
  IDestinationPolicy,
  TVpnServerCommands,
} from './smartvpn.interfaces.js';

/**
 * VPN Server — manages a smartvpn daemon in server mode.
 */
export class VpnServer extends plugins.events.EventEmitter {
  private bridge: VpnBridge<TVpnServerCommands>;
  private options: IVpnServerOptions;
  private nft?: plugins.smartnftables.SmartNftables;
  private nftHealthInterval?: ReturnType<typeof setInterval>;
  private nftSubnet?: string;
  private nftPolicy?: IDestinationPolicy;

  constructor(options: IVpnServerOptions) {
    super();
    this.options = options;
    this.bridge = new VpnBridge<TVpnServerCommands>({
      transport: options.transport,
      mode: 'server',
    });

    // Forward bridge events
    this.bridge.on('exit', (code: number | null, signal: string | null) => {
      this.emit('exit', { code, signal });
    });
    this.bridge.on('reconnected', () => {
      this.emit('reconnected');
    });
  }

  /**
   * Start the daemon bridge (spawn or connect).
   */
  public async start(config?: IVpnServerConfig): Promise<void> {
    const started = await this.bridge.start();
    if (!started) {
      throw new Error('VpnServer: failed to start daemon bridge');
    }
    const cfg = config || this.options.config;
    if (cfg) {
      await this.bridge.sendCommand('start', { config: cfg });

      // For TUN mode with a destination policy, set up nftables rules
      if (cfg.forwardingMode === 'tun' && cfg.destinationPolicy) {
        await this.setupTunDestinationPolicy(cfg.subnet, cfg.destinationPolicy);
      }
    }
  }

  /**
   * Stop the VPN server.
   */
  public async stopServer(): Promise<void> {
    await this.bridge.sendCommand('stop', {} as Record<string, never>);
  }

  /**
   * Get server status.
   */
  public async getStatus(): Promise<IVpnStatus> {
    return this.bridge.sendCommand('getStatus', {} as Record<string, never>);
  }

  /**
   * Get server statistics.
   */
  public async getStatistics(): Promise<IVpnServerStatistics> {
    return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
  }

  /**
   * List connected clients.
   */
  public async listClients(): Promise<IVpnClientInfo[]> {
    const result = await this.bridge.sendCommand('listClients', {} as Record<string, never>);
    return result.clients;
  }

  /**
   * Disconnect a specific client.
   */
  public async disconnectClient(clientId: string): Promise<void> {
    await this.bridge.sendCommand('disconnectClient', { clientId });
  }

  /**
   * Generate a new Noise keypair.
   */
  public async generateKeypair(): Promise<IVpnKeypair> {
    return this.bridge.sendCommand('generateKeypair', {} as Record<string, never>);
  }

  /**
   * Set rate limit for a specific client.
   */
  public async setClientRateLimit(
    clientId: string,
    rateBytesPerSec: number,
    burstBytes: number,
  ): Promise<void> {
    await this.bridge.sendCommand('setClientRateLimit', {
      clientId,
      rateBytesPerSec,
      burstBytes,
    });
  }

  /**
   * Remove rate limit for a specific client (unlimited).
   */
  public async removeClientRateLimit(clientId: string): Promise<void> {
    await this.bridge.sendCommand('removeClientRateLimit', { clientId });
  }

  /**
   * Get telemetry for a specific client.
   */
  public async getClientTelemetry(clientId: string): Promise<IVpnClientTelemetry> {
    return this.bridge.sendCommand('getClientTelemetry', { clientId });
  }

  /**
   * Generate a WireGuard-compatible X25519 keypair.
   */
  public async generateWgKeypair(): Promise<IVpnKeypair> {
    return this.bridge.sendCommand('generateWgKeypair', {} as Record<string, never>);
  }

  /**
   * Add a WireGuard peer (server must be running in wireguard mode).
   */
  public async addWgPeer(peer: IWgPeerConfig): Promise<void> {
    await this.bridge.sendCommand('addWgPeer', { peer });
  }

  /**
   * Remove a WireGuard peer by public key.
   */
  public async removeWgPeer(publicKey: string): Promise<void> {
    await this.bridge.sendCommand('removeWgPeer', { publicKey });
  }

  /**
   * List WireGuard peers with stats.
   */
  public async listWgPeers(): Promise<IWgPeerInfo[]> {
    const result = await this.bridge.sendCommand('listWgPeers', {} as Record<string, never>);
    return result.peers;
  }

  // ── Client Registry (Hub) Methods ─────────────────────────────────────

  /**
   * Create a new client. Generates keypairs, assigns IP, returns full config bundle.
   * The secrets (private keys) are only returned at creation time.
   */
  public async createClient(opts: Partial<IClientEntry>): Promise<IClientConfigBundle> {
    return this.bridge.sendCommand('createClient', { client: opts });
  }

  /**
   * Remove a registered client (also disconnects if connected).
   */
  public async removeClient(clientId: string): Promise<void> {
    await this.bridge.sendCommand('removeClient', { clientId });
  }

  /**
   * Get a registered client by ID.
   */
  public async getClient(clientId: string): Promise<IClientEntry> {
    return this.bridge.sendCommand('getClient', { clientId });
  }

  /**
   * List all registered clients.
   */
  public async listRegisteredClients(): Promise<IClientEntry[]> {
    const result = await this.bridge.sendCommand('listRegisteredClients', {} as Record<string, never>);
    return result.clients;
  }

  /**
   * Update a registered client's fields (ACLs, tags, description, etc.).
   */
  public async updateClient(clientId: string, update: Partial<IClientEntry>): Promise<void> {
    await this.bridge.sendCommand('updateClient', { clientId, update });
  }

  /**
   * Enable a previously disabled client.
   */
  public async enableClient(clientId: string): Promise<void> {
    await this.bridge.sendCommand('enableClient', { clientId });
  }

  /**
   * Disable a client (also disconnects if connected).
   */
  public async disableClient(clientId: string): Promise<void> {
    await this.bridge.sendCommand('disableClient', { clientId });
  }

  /**
   * Rotate a client's keys. Returns a new config bundle with fresh keypairs.
   */
  public async rotateClientKey(clientId: string): Promise<IClientConfigBundle> {
    return this.bridge.sendCommand('rotateClientKey', { clientId });
  }

  /**
   * Export a client config (without secrets) in the specified format.
   */
  public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
    const result = await this.bridge.sendCommand('exportClientConfig', { clientId, format });
    return result.config;
  }

  /**
   * Generate a standalone Noise IK keypair (not tied to a client).
   */
  public async generateClientKeypair(): Promise<IVpnKeypair> {
    return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
  }

  // ── TUN Destination Policy via nftables ──────────────────────────────

  /**
   * Set up nftables rules for TUN mode destination policy.
   * Also starts a 60-second health check interval to re-apply if rules are removed externally.
   */
  private async setupTunDestinationPolicy(subnet: string, policy: IDestinationPolicy): Promise<void> {
    this.nftSubnet = subnet;
    this.nftPolicy = policy;
    this.nft = new plugins.smartnftables.SmartNftables({
      tableName: 'smartvpn_tun',
      dryRun: process.getuid?.() !== 0,
    });

    await this.nft.initialize();
    await this.applyDestinationPolicyRules();

    // Health check: re-apply rules if they disappear
    this.nftHealthInterval = setInterval(async () => {
      if (!this.nft) return;
      try {
        const exists = await this.nft.tableExists();
        if (!exists) {
          console.warn('[smartvpn] nftables rules missing, re-applying destination policy');
          this.nft = new plugins.smartnftables.SmartNftables({
            tableName: 'smartvpn_tun',
          });
          await this.nft.initialize();
          await this.applyDestinationPolicyRules();
        }
      } catch (err) {
        console.warn(`[smartvpn] nftables health check failed: ${err}`);
      }
    }, 60_000);
  }

  /**
   * Apply destination policy as nftables rules.
   * Order: blockList (drop) → allowList (accept) → default action.
   */
  private async applyDestinationPolicyRules(): Promise<void> {
    if (!this.nft || !this.nftSubnet || !this.nftPolicy) return;

    const subnet = this.nftSubnet;
    const policy = this.nftPolicy;
    const family = 'ip';
    const table = 'smartvpn_tun';
    const commands: string[] = [];

    // 1. Block list (deny wins — evaluated first)
    if (policy.blockList) {
      for (const dest of policy.blockList) {
        commands.push(
          `nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} drop`
        );
      }
    }

    // 2. Allow list (pass through directly — skip DNAT)
    if (policy.allowList) {
      for (const dest of policy.allowList) {
        commands.push(
          `nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} accept`
        );
      }
    }

    // 3. Default action
    switch (policy.default) {
      case 'forceTarget': {
        const target = policy.target || '127.0.0.1';
        commands.push(
          `nft add rule ${family} ${table} prerouting ip saddr ${subnet} dnat to ${target}`
        );
        break;
      }
      case 'block':
        commands.push(
          `nft add rule ${family} ${table} prerouting ip saddr ${subnet} drop`
        );
        break;
      case 'allow':
        // No rule needed — kernel default allows
        break;
    }

    if (commands.length > 0) {
      await this.nft.applyRuleGroup('vpn-destination-policy', commands);
    }
  }

  /**
   * Stop the daemon bridge.
   */
  public async stop(): Promise<void> {
    // Clean up nftables rules
    if (this.nftHealthInterval) {
      clearInterval(this.nftHealthInterval);
      this.nftHealthInterval = undefined;
    }
    if (this.nft) {
      try {
        await this.nft.cleanup();
      } catch (e) {
        console.warn(`[smartvpn] nftables cleanup failed: ${e}`);
      }
      this.nft = undefined;
    }

    // Wait for bridge process to exit (with timeout)
    const exitPromise = new Promise<void>((resolve) => {
      if (!this.bridge.running) {
        resolve();
        return;
      }
      const timeout = setTimeout(() => resolve(), 5000);
      this.bridge.once('exit', () => {
        clearTimeout(timeout);
        resolve();
      });
    });
    this.bridge.stop();
    await exitPromise;
  }

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