/**
 * Zabbix WAN IP Monitoring Setup
 *
 * Reads Datto RMM sites, resolves each site's WAN IP from online device extIpAddress,
 * optionally tests ICMP reachability, then creates/updates Zabbix hosts with ICMP Ping
 * monitoring in the "Datto RMM Sites" host group.
 *
 * Run with: npx tsx scripts/setup-zabbix-wan-monitoring.ts [options]
 */

import { config } from 'dotenv';
import { resolve } from 'path';
import { execSync } from 'child_process';
import { DattoRMMClient } from '../lib/services/datto-rmm-client';
import { ZabbixClient } from '../lib/services/zabbix-client';
import { DattoRMMDevice, DattoRMMSite } from '../lib/types/datto-rmm';
import { ZabbixHostMacro } from '../lib/types/zabbix';

// Load environment variables
config({ path: resolve(__dirname, '../.env.local') });

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------

interface CliOptions {
  dryRun: boolean;
  site: string | undefined;
  skipPing: boolean;
  help: boolean;
}

function parseArgs(): CliOptions {
  const args = process.argv.slice(2);
  const options: CliOptions = {
    dryRun: false,
    site: undefined,
    skipPing: false,
    help: false,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--dry-run' || arg === '-n') {
      options.dryRun = true;
    } else if (arg === '--site') {
      options.site = args[++i];
    } else if (arg === '--skip-ping') {
      options.skipPing = true;
    } else if (arg === '--help' || arg === '-h') {
      options.help = true;
    }
  }

  return options;
}

function printHelp(): void {
  console.log(`
Zabbix WAN IP Monitoring Setup

Usage: npx tsx scripts/setup-zabbix-wan-monitoring.ts [options]

Options:
  -n, --dry-run         Preview only — no writes to Zabbix
      --site <name>     Process a single named site
      --skip-ping       Skip ICMP reachability test
  -h, --help            Show this help message

Environment variables required:
  DATTO_RMM_API_URL     Datto RMM API base URL
  DATTO_RMM_API_KEY     Datto RMM API key
  DATTO_RMM_API_SECRET  Datto RMM API secret
  ZABBIX_API_URL        Zabbix instance URL (e.g. https://zabbix.example.com)
  ZABBIX_API_TOKEN      Zabbix API token (Zabbix 6.0+)

Examples:
  # Dry-run against a single site
  npx tsx scripts/setup-zabbix-wan-monitoring.ts --site "Acme Corp" --dry-run

  # Skip ping (useful inside Docker/cloud VMs)
  npx tsx scripts/setup-zabbix-wan-monitoring.ts --skip-ping --dry-run

  # Full run
  npx tsx scripts/setup-zabbix-wan-monitoring.ts
  `);
}

// ---------------------------------------------------------------------------
// WAN IP resolution
// ---------------------------------------------------------------------------

function isLaptop(d: DattoRMMDevice): boolean {
  const cat  = (d.deviceType?.category ?? '').toLowerCase();
  const type = (d.deviceType?.type     ?? '').toLowerCase();
  return cat.includes('laptop') || cat.includes('notebook') ||
         type.includes('laptop') || type.includes('notebook');
}

function resolveWanIp(devices: DattoRMMDevice[]): string | null {
  const online = devices.filter(
    (d) => d.online === true && !d.suspended && !d.deleted
  );

  // Group by IP
  const ipDevices = new Map<string, DattoRMMDevice[]>();
  for (const d of online) {
    const ip = d.extIpAddress;
    if (!ip || ip === '0.0.0.0' || ip.trim() === '') continue;
    if (!ipDevices.has(ip)) ipDevices.set(ip, []);
    ipDevices.get(ip)!.push(d);
  }

  // Drop IPs seen only once from a single laptop — likely a remote/travelling device
  for (const [ip, devs] of ipDevices) {
    if (devs.length === 1 && isLaptop(devs[0])) {
      ipDevices.delete(ip);
    }
  }

  if (ipDevices.size === 0) return null;

  const sorted = Array.from(ipDevices.entries())
    .map(([ip, devs]) => [ip, devs.length] as [string, number])
    .sort((a, b) => b[1] - a[1]);

  // Warn if tie between top two
  if (sorted.length >= 2 && sorted[0][1] === sorted[1][1]) {
    console.warn(
      `  [WARN] IP tie: ${sorted[0][0]} and ${sorted[1][0]} both seen ${sorted[0][1]}x — using ${sorted[0][0]}`
    );
  }

  return sorted[0][0];
}

// ---------------------------------------------------------------------------
// ICMP ping
// ---------------------------------------------------------------------------

interface PingResult {
  success: boolean;
  rtt: number | null; // avg RTT in ms
}

function pingHost(ip: string): PingResult {
  try {
    const output = execSync(`ping -c 3 -W 2 -q ${ip}`, {
      timeout: 10000,
      encoding: 'utf8',
    });

    // Parse: rtt min/avg/max/mdev = 1.234/5.678/9.012/3.456 ms
    const match = output.match(/rtt[^=]+=\s*[\d.]+\/([\d.]+)\//);
    const rtt = match ? parseFloat(match[1]) : null;
    return { success: true, rtt };
  } catch {
    return { success: false, rtt: null };
  }
}

// ---------------------------------------------------------------------------
// ICMP template discovery
// ---------------------------------------------------------------------------

const ICMP_TEMPLATE_NAMES = [
  'ICMP Ping',
  'Template Module ICMP Ping',
  'Template Module ICMP Ping by Zabbix agent',
];

async function discoverIcmpTemplate(
  zabbix: ZabbixClient
): Promise<string | null> {
  for (const name of ICMP_TEMPLATE_NAMES) {
    const tmpl = await zabbix.findTemplate(name);
    if (tmpl) {
      console.log(`  Found ICMP template: "${tmpl.host}" (id=${tmpl.templateid})`);
      return tmpl.templateid;
    }
  }
  console.warn(
    `  [WARN] No ICMP template found. Hosts will be created without a template.`
  );
  return null;
}

// ---------------------------------------------------------------------------
// Site → Autotask mapping lookup
// ---------------------------------------------------------------------------

interface SiteMapping {
  companyId: number;
  companyName: string;
  rmm_site_uid: string;
}

async function fetchSiteMappings(): Promise<Map<string, SiteMapping>> {
  const baseUrl = process.env.PULSE_BASE_URL || process.env.WEBHOOK_BASE_URL || 'http://localhost:3100';
  try {
    const res = await fetch(`${baseUrl}/api/rmm/site-mappings`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data: any = await res.json();
    const map = new Map<string, SiteMapping>();
    for (const m of data.mappings ?? []) {
      if (m.company_id && m.rmm_site_uid) {
        map.set(m.rmm_site_name, {
          companyId: m.company_id,
          companyName: m.company_name ?? m.rmm_site_name,
          rmm_site_uid: m.rmm_site_uid,
        });
      }
    }
    console.log(`  Loaded ${map.size} site→Autotask mappings\n`);
    return map;
  } catch (err) {
    console.warn(`  [WARN] Could not load site mappings (${err}). Hosts will be created without Autotask macros.\n`);
    return new Map();
  }
}

function buildMacros(mapping: SiteMapping | undefined): ZabbixHostMacro[] | undefined {
  if (!mapping) return undefined;
  return [
    { macro: '{$AUTOTASK_COMPANY_ID}',   value: String(mapping.companyId),   description: 'Autotask company ID' },
    { macro: '{$AUTOTASK_COMPANY_NAME}', value: mapping.companyName,          description: 'Autotask company name' },
    { macro: '{$RMM_SITE_UID}',          value: mapping.rmm_site_uid,         description: 'Datto RMM site UID' },
  ];
}

// ---------------------------------------------------------------------------
// Result types
// ---------------------------------------------------------------------------

type SiteAction = 'created' | 'updated' | 'no-ip' | 'skipped' | 'error';

interface SiteResult {
  siteName: string;
  wanIp: string | null;
  pingOk: boolean | null;
  pingRtt: number | null;
  action: SiteAction;
  hostId: string | null;
  error?: string;
}

// ---------------------------------------------------------------------------
// Table formatting
// ---------------------------------------------------------------------------

function padEnd(str: string, len: number): string {
  return str.length >= len ? str.substring(0, len) : str + ' '.repeat(len - str.length);
}

function printResultsTable(results: SiteResult[]): void {
  const COL = { site: 30, ip: 17, ping: 10, action: 10, hostid: 8 };

  const header =
    padEnd('Site Name', COL.site) +
    padEnd('WAN IP', COL.ip) +
    padEnd('Ping', COL.ping) +
    padEnd('Action', COL.action) +
    'Host ID';

  const sep =
    '-'.repeat(COL.site - 2) + '  ' +
    '-'.repeat(COL.ip - 2) + '  ' +
    '-'.repeat(COL.ping - 2) + '  ' +
    '-'.repeat(COL.action - 2) + '  ' +
    '-------';

  console.log('\n' + header);
  console.log(sep);

  for (const r of results) {
    let pingCol = '-';
    if (r.pingOk === true) {
      pingCol = r.pingRtt !== null ? `OK(${Math.round(r.pingRtt)}ms)` : 'OK';
    } else if (r.pingOk === false) {
      pingCol = 'FAIL';
    }

    const row =
      padEnd(r.siteName, COL.site) +
      padEnd(r.wanIp ?? '-', COL.ip) +
      padEnd(pingCol, COL.ping) +
      padEnd(r.action, COL.action) +
      (r.hostId ?? '-');

    console.log(row);
  }

  console.log();

  const counts: Record<SiteAction, number> = {
    created: 0,
    updated: 0,
    'no-ip': 0,
    skipped: 0,
    error: 0,
  };
  for (const r of results) counts[r.action]++;

  console.log(
    `TOTALS: ${counts.created} created | ${counts.updated} updated | ` +
    `${counts['no-ip']} no-ip | ${counts.skipped} skipped | ${counts.error} errors`
  );
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main(): Promise<void> {
  const opts = parseArgs();

  if (opts.help) {
    printHelp();
    process.exit(0);
  }

  // Validate required env vars
  const requiredVars = [
    'DATTO_RMM_API_URL',
    'DATTO_RMM_API_KEY',
    'DATTO_RMM_API_SECRET',
    'ZABBIX_API_URL',
    'ZABBIX_API_TOKEN',
  ];
  const missing = requiredVars.filter((v) => !process.env[v]);
  if (missing.length > 0) {
    console.error(`Missing required environment variables: ${missing.join(', ')}`);
    console.error('Add them to .env.local and try again.');
    process.exit(1);
  }

  if (opts.dryRun) {
    console.log('[DRY RUN] No changes will be written to Zabbix.\n');
  }

  // Initialise clients
  const rmmClient = new DattoRMMClient({
    apiUrl: process.env.DATTO_RMM_API_URL!,
    apiKey: process.env.DATTO_RMM_API_KEY!,
    apiSecret: process.env.DATTO_RMM_API_SECRET!,
  });

  const zabbix = new ZabbixClient({
    apiUrl: process.env.ZABBIX_API_URL!,
    apiToken: process.env.ZABBIX_API_TOKEN!,
  });

  // Verify Zabbix connectivity — fail fast
  console.log('Verifying Zabbix connectivity...');
  let groupId: string;
  try {
    groupId = opts.dryRun
      ? 'dry-run'
      : await zabbix.ensureHostGroup('Datto RMM Sites');
    console.log(`  Host group "Datto RMM Sites" ready (id=${groupId})\n`);
  } catch (err) {
    console.error(`Failed to connect to Zabbix: ${err}`);
    process.exit(1);
  }

  // Discover ICMP template
  console.log('Discovering ICMP template...');
  const icmpTemplateId = opts.dryRun ? null : await discoverIcmpTemplate(zabbix);
  console.log();

  // Load Autotask site mappings
  console.log('Loading Autotask site mappings...');
  const siteMappings = await fetchSiteMappings();

  // Fetch sites
  console.log('Fetching Datto RMM sites...');
  let sites: DattoRMMSite[] = await rmmClient.getAllSites();

  if (opts.site) {
    const lower = opts.site.toLowerCase();
    sites = sites.filter((s) => s.name.toLowerCase() === lower);
    if (sites.length === 0) {
      console.error(`No site found matching: "${opts.site}"`);
      process.exit(1);
    }
  } else {
    // Skip sites with no Autotask mapping unless a specific site was requested
    const before = sites.length;
    sites = sites.filter((s) => siteMappings.has(s.name));
    const skipped = before - sites.length;
    if (skipped > 0) console.log(`  Skipped ${skipped} unmapped site(s)\n`);
  }

  console.log(`  Processing ${sites.length} site(s)\n`);

  // Process each site
  const results: SiteResult[] = [];

  for (const site of sites) {
    process.stdout.write(`${site.name}... `);

    let wanIp: string | null = null;
    let onlineCount = 0;

    try {
      const devices = await rmmClient.getDevicesBySite(site.uid);
      onlineCount = devices.filter((d) => d.online && !d.suspended && !d.deleted).length;
      wanIp = resolveWanIp(devices);
    } catch (err) {
      console.log('ERROR (device fetch)');
      results.push({
        siteName: site.name,
        wanIp: null,
        pingOk: null,
        pingRtt: null,
        action: 'error',
        hostId: null,
        error: String(err),
      });
      continue;
    }

    if (!wanIp) {
      console.log('no IP');
      results.push({
        siteName: site.name,
        wanIp: null,
        pingOk: null,
        pingRtt: null,
        action: 'no-ip',
        hostId: null,
      });
      continue;
    }

    // Ping test
    let pingOk: boolean | null = null;
    let pingRtt: number | null = null;

    if (!opts.skipPing) {
      const ping = pingHost(wanIp);
      pingOk = ping.success;
      pingRtt = ping.rtt;
    }

    // Dry-run: stop here
    if (opts.dryRun) {
      const pingLabel = opts.skipPing
        ? 'skipped'
        : pingOk
        ? `OK(${pingRtt !== null ? Math.round(pingRtt) + 'ms' : '?'})`
        : 'FAIL';
      console.log(`${wanIp} ping=${pingLabel} [DRY RUN]`);
      results.push({
        siteName: site.name,
        wanIp,
        pingOk,
        pingRtt,
        action: 'skipped',
        hostId: null,
      });
      continue;
    }

    // Upsert Zabbix host
    try {
      const templates = icmpTemplateId
        ? [{ templateid: icmpTemplateId }]
        : undefined;

      const mapping = siteMappings.get(site.name);
      const macros = buildMacros(mapping);

      const { action, hostid } = await zabbix.upsertHost({
        host: site.name,
        name: site.name,
        description: `Datto RMM site – WAN IP from ${onlineCount} online devices`,
        interfaces: [
          {
            type: 1,
            main: 1,
            useip: 1,
            ip: wanIp,
            dns: '',
            port: '10050',
          },
        ],
        groups: [{ groupid: groupId }],
        templates,
        macros,
      });

      const pingLabel = opts.skipPing
        ? 'skipped'
        : pingOk
        ? `OK(${pingRtt !== null ? Math.round(pingRtt) + 'ms' : '?'})`
        : 'FAIL';

      console.log(`${wanIp} ping=${pingLabel} ${action} (id=${hostid})`);
      results.push({
        siteName: site.name,
        wanIp,
        pingOk,
        pingRtt,
        action,
        hostId: hostid,
      });
    } catch (err) {
      console.log(`ERROR (zabbix upsert): ${err}`);
      results.push({
        siteName: site.name,
        wanIp,
        pingOk,
        pingRtt,
        action: 'error',
        hostId: null,
        error: String(err),
      });
    }
  }

  // Print summary table
  printResultsTable(results);

  // Print any errors in detail
  const errors = results.filter((r) => r.action === 'error');
  if (errors.length > 0) {
    console.log('\nERRORS:');
    for (const e of errors) {
      console.log(`  ${e.siteName}: ${e.error}`);
    }
    process.exit(1);
  }
}

// Run
main()
  .then(() => process.exit(0))
  .catch((err) => {
    console.error('Fatal error:', err);
    process.exit(1);
  });
