import 'server-only'
import { exec } from 'child_process';
import { promisify } from 'util';
import { writeFile, readFile } from 'fs/promises';
import { PrismaClient } from '@prisma/client';

const execAsync = promisify(exec);
const prisma = new PrismaClient();

// Track last restart time to prevent concurrent restarts
let lastRestartTime = 0;
const RESTART_COOLDOWN = 3000; // 3 seconds cooldown

// Path to the auto-generated NAS clients file
const NAS_CLIENTS_FILE = '/etc/freeradius/3.0/clients.d/nas-from-db.conf';

/**
 * Sync NAS entries from database to FreeRADIUS clients.d/nas-from-db.conf.
 * Idempotent: only writes the file when content actually changed.
 * 
 * Generates:
 * 1. Per-NAS client entries (individual IP + secret)
 * 2. Per-VPN-server gateway client entries (gateway IP with shared secret)
 *    Required because VPN CHR servers masquerade RADIUS traffic — all packets
 *    from NAS behind the same CHR arrive with the CHR gateway IP as source.
 *    All NAS behind the same VPN server MUST share the same RADIUS secret.
 * 
 * @returns true if the file was updated (reload needed), false if already in sync
 */
export async function syncNasClients(): Promise<boolean> {
  try {
    // Use raw SQL to avoid Prisma silently dropping rows that have NULL in
    // non-nullable app columns (port, apiPort, username, password).
    // These fields are irrelevant for RADIUS sync but a Prisma findMany()
    // returns only rows it can fully deserialize — rows with NULL in non-nullable
    // Int/String fields are silently omitted, causing "unknown client" errors.
    const nasEntries: Array<{
      nasname: string;
      ipAddress: string | null;
      shortname: string | null;
      secret: string;
      type: string | null;
      vpnClientId: string | null;
      vpnIp: string | null;
      isRadiusServer: boolean | null;
      vpnServerId: string | null;
      vpnServerName: string | null;
      vpnServerSubnet: string | null;
    }> = await prisma.$queryRaw`
      SELECT
        r.nasname,
        r.ipAddress,
        r.shortname,
        r.secret,
        r.type,
        r.vpnClientId,
        vc.vpnIp,
        vc.isRadiusServer,
        vs.id   AS vpnServerId,
        vs.name AS vpnServerName,
        vs.subnet AS vpnServerSubnet
      FROM nas r
      LEFT JOIN vpn_clients vc ON vc.id = r.vpnClientId
      LEFT JOIN vpn_servers vs ON vs.id = vc.vpnServerId
      WHERE r.isActive = 1
    `;

    if (nasEntries.length === 0) return false;

    // Build the expected config (without timestamp so diff is stable)
    const lines: string[] = [
      '# Auto-generated by salfanet-radius - DO NOT EDIT MANUALLY',
      '',
    ];

    // Track VPN server gateways to generate gateway client entries.
    // gatewaySecrets maps serverId → Set of unique NAS secrets.
    const vpnGateways = new Map<string, { gatewayIp: string; secrets: Set<string>; serverName: string }>();

    for (const nas of nasEntries) {
      const safeShort = (nas.shortname || nas.nasname).replace(/[^a-z0-9_-]/gi, '_');
      lines.push(`client nas_${safeShort} {`);
      lines.push(`    ipaddr = ${nas.nasname}`);
      lines.push(`    secret = ${nas.secret}`);
      lines.push(`    shortname = ${safeShort}`);
      lines.push(`    nas_type = other`);
      lines.push(`    require_message_authenticator = no`);
      lines.push(`    limit_proxy_state = no`);
      lines.push(`}`);
      lines.push('');

      // Collect VPN server gateway info (skip the VPS itself = isRadiusServer)
      if (nas.vpnClientId && nas.vpnServerId && !nas.isRadiusServer) {
        if (!vpnGateways.has(nas.vpnServerId)) {
          // Derive gateway IP: subnet "10.20.30.0/24" → gateway "10.20.30.1"
          const subnetBase = (nas.vpnServerSubnet || '').split('/')[0]; // "10.20.30.0"
          const parts = subnetBase.split('.');
          parts[3] = '1';
          const gatewayIp = parts.join('.');
          const safeName = (nas.vpnServerName || nas.vpnServerId).replace(/[^a-z0-9_-]/gi, '_').toLowerCase();

          vpnGateways.set(nas.vpnServerId, {
            gatewayIp,
            secrets: new Set([nas.secret]),
            serverName: safeName,
          });
        } else {
          // Collect all distinct secrets so we can detect mismatches
          vpnGateways.get(nas.vpnServerId)!.secrets.add(nas.secret);
        }
      }
    }

    // Generate VPN gateway client entries (one per VPN server)
    // This handles CHR masquerade: all NAS packets arrive from the gateway IP.
    // REQUIREMENT: All NAS behind the same VPN server MUST share the same RADIUS secret.
    // If they differ, the gateway entry is skipped (NAS bypass rule must be active instead).
    if (vpnGateways.size > 0) {
      lines.push('# --- VPN Server Gateway Clients ---');
      lines.push('# When CHR VPN server masquerades traffic, all NAS packets');
      lines.push('# arrive from the gateway IP. All NAS behind the same VPN');
      lines.push('# server MUST use the same RADIUS shared secret.');
      lines.push('# If secrets differ, configure a NAT bypass on the CHR for RADIUS UDP ports.');
      lines.push('');

      for (const [, gw] of vpnGateways) {
        if (gw.secrets.size > 1) {
          // Multiple different secrets — gateway entry would use the wrong secret for some NAS.
          // Skip it and log a warning. A CHR NAT bypass rule for RADIUS (UDP 1812-1813) is required.
          console.warn(
            `[FreeRADIUS] VPN gateway ${gw.gatewayIp} skipped: NAS behind it have ` +
            `${gw.secrets.size} different RADIUS secrets. ` +
            `Add a CHR srcnat accept rule for UDP 1812-1813 to bypass masquerade, ` +
            `or make all NAS use the same shared secret.`
          );
          lines.push(`# SKIPPED: vpn_gw_${gw.serverName} — NAS secrets differ (${gw.secrets.size} unique secrets).`);
          lines.push(`# Add a CHR NAT bypass rule: srcnat accept, protocol=udp, dst-port=1812-1813, dst=${gw.gatewayIp.replace('.1', '.10')}`);
          lines.push('');
          continue;
        }

        const [secret] = gw.secrets;
        lines.push(`client vpn_gw_${gw.serverName} {`);
        lines.push(`    ipaddr = ${gw.gatewayIp}`);
        lines.push(`    secret = ${secret}`);
        lines.push(`    shortname = vpn_gw_${gw.serverName}`);
        lines.push(`    nas_type = other`);
        lines.push(`    require_message_authenticator = no`);
        lines.push(`    limit_proxy_state = no`);
        lines.push(`}`);
        lines.push('');
      }
    }

    const newContent = lines.join('\n');

    // Compare with current file — only write if different
    let currentContent = '';
    try {
      currentContent = await readFile(NAS_CLIENTS_FILE, 'utf8');
    } catch {
      // File doesn't exist yet — will be created
    }

    if (currentContent.trim() === newContent.trim()) {
      console.log(`[FreeRADIUS] NAS config already in sync (${nasEntries.length} entries)`);
      return false;
    }

    // Ensure directory exists and write updated config
    await execAsync('mkdir -p /etc/freeradius/3.0/clients.d');
    await writeFile(NAS_CLIENTS_FILE, newContent, 'utf8');

    // Ensure clients.d is included in clients.conf
    const { stdout: confContent } = await execAsync('cat /etc/freeradius/3.0/clients.conf').catch(() => ({ stdout: '' }));
    if (!confContent.includes('$INCLUDE clients.d/')) {
      await execAsync(`echo '\n$INCLUDE clients.d/' >> /etc/freeradius/3.0/clients.conf`);
    }

    console.log(`[FreeRADIUS] NAS config updated (${nasEntries.length} entries, ${vpnGateways.size} VPN gateways) — reload required`);
    return true;
  } catch (error: any) {
    console.error('[FreeRADIUS] Failed to sync NAS clients:', error.message);
    return false;
  }
}

/**
 * Restart FreeRADIUS service
 * Requires sudoers permission for PM2 user
 */
export async function reloadFreeRadius(): Promise<void> {
  const now = Date.now();
  
  // Prevent concurrent restarts
  if (now - lastRestartTime < RESTART_COOLDOWN) {
    console.log('FreeRADIUS restart skipped (cooldown active)');
    return;
  }

  try {
    lastRestartTime = now;

    // Sync NAS clients to clients.d before restart — only restart if config changed
    const configChanged = await syncNasClients();

    if (!configChanged) {
      console.log('[FreeRADIUS] Config unchanged, skipping restart');
      return;
    }

    // Restart FreeRADIUS service (PM2 runs as root, no sudo needed)
    const { stdout, stderr } = await execAsync('systemctl restart freeradius', {
      timeout: 10000, // 10 second timeout
    });

    if (stderr) {
      console.error('FreeRADIUS restart stderr:', stderr);
    }

    console.log('FreeRADIUS restarted successfully');
  } catch (error: any) {
    console.error('Failed to restart FreeRADIUS:', error.message);
    // Don't throw error - NAS update is more important
    // FreeRADIUS can be restarted manually if needed
  }
}
