//! FIPS outbound LAN gateway binary.
//!
//! Allows unmodified LAN hosts to reach FIPS mesh destinations via
//! DNS-allocated virtual IPs and kernel nftables NAT.

#[cfg(target_os = "linux")]
use clap::Parser;
#[cfg(target_os = "linux")]
use fips::Config;
#[cfg(target_os = "linux")]
use fips::gateway::{control, dns, nat, net, pool};
#[cfg(target_os = "linux")]
use fips::version;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::Arc;
#[cfg(target_os = "linux")]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(target_os = "linux")]
use std::time::Instant;
#[cfg(target_os = "linux")]
use tokio::signal::unix::{SignalKind, signal};
#[cfg(target_os = "linux")]
use tokio::sync::{Mutex, mpsc, watch};
#[cfg(target_os = "linux")]
use tracing::{error, info, warn};
#[cfg(target_os = "linux")]
use tracing_subscriber::{EnvFilter, fmt};

/// FIPS outbound LAN gateway
#[cfg(target_os = "linux")]
#[derive(Parser, Debug)]
#[command(
    name = "fips-gateway",
    version = version::short_version(),
    long_version = version::long_version(),
    about
)]
struct Args {
    /// Path to configuration file (overrides default search paths).
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,

    /// Log level (trace, debug, info, warn, error).
    #[arg(short, long, default_value = "info")]
    log_level: String,
}

#[cfg(not(target_os = "linux"))]
fn main() {
    eprintln!("fips-gateway requires Linux (nftables unavailable on this platform)");
    std::process::exit(1);
}

#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
async fn main() {
    let args = Args::parse();

    // Initialize logging
    let filter = EnvFilter::builder()
        .with_default_directive(
            args.log_level
                .parse()
                .unwrap_or_else(|_| tracing::level_filters::LevelFilter::INFO.into()),
        )
        .from_env_lossy();

    // As in the daemon: a failed log write must not panic whoever logged. The
    // default reports write failures with `eprintln!`, which panics when stderr
    // fails too, and both units send stdout and stderr to journald. Here the
    // casualty is a spawned task — the DNS resolver or the pool tick — whose
    // handle nothing observes until shutdown.
    fmt()
        .with_env_filter(filter)
        .with_target(true)
        .log_internal_errors(false)
        .init();

    info!("fips-gateway {} starting", version::short_version());

    // Load configuration
    let config = if let Some(config_path) = &args.config {
        match Config::load_file(config_path) {
            Ok(config) => {
                info!(path = %config_path.display(), "Loaded config file");
                config
            }
            Err(e) => {
                error!(
                    "Failed to load config from {}: {}",
                    config_path.display(),
                    e
                );
                std::process::exit(1);
            }
        }
    } else {
        match Config::load() {
            Ok((config, paths)) => {
                if paths.is_empty() {
                    warn!("No config files found, using defaults");
                } else {
                    for path in &paths {
                        info!(path = %path.display(), "Loaded config file");
                    }
                }
                config
            }
            Err(e) => {
                error!("Failed to load config: {}", e);
                std::process::exit(1);
            }
        }
    };

    // Validate gateway config
    let gw_config = match &config.gateway {
        Some(gw) if gw.enabled => gw.clone(),
        Some(_) => {
            error!("Gateway section exists but is not enabled (gateway.enabled = false)");
            std::process::exit(1);
        }
        None => {
            error!("No gateway section in configuration");
            std::process::exit(1);
        }
    };

    if let Err(e) = gw_config.validate_port_forwards() {
        error!("Invalid gateway.port_forwards: {e}");
        std::process::exit(1);
    }

    info!(
        pool = %gw_config.pool,
        lan_interface = %gw_config.lan_interface,
        port_forwards = gw_config.port_forwards.len(),
        "Gateway config loaded"
    );

    // --- Prerequisites ---

    // Check IPv6 forwarding
    net::check_ipv6_forwarding();

    // Check fips0 interface exists
    if let Err(e) = net::check_interface_exists("fips0").await {
        error!(error = %e, "fips0 interface not found — is the FIPS daemon running?");
        std::process::exit(1);
    }

    // Check LAN interface exists
    if let Err(e) = net::check_interface_exists(&gw_config.lan_interface).await {
        error!(
            error = %e,
            interface = %gw_config.lan_interface,
            "LAN interface not found"
        );
        std::process::exit(1);
    }

    // Check DNS upstream reachability (proves the FIPS daemon is running)
    {
        let upstream = gw_config.dns.upstream();
        info!(upstream = %upstream, "Checking DNS upstream reachability");

        use std::net::ToSocketAddrs;
        let upstream_addr = match upstream.to_socket_addrs() {
            Ok(mut addrs) => match addrs.next() {
                Some(addr) => addr,
                None => {
                    error!(upstream = %upstream, "DNS upstream address resolved to nothing");
                    std::process::exit(1);
                }
            },
            Err(e) => {
                error!(upstream = %upstream, error = %e, "Invalid DNS upstream address");
                std::process::exit(1);
            }
        };

        // Build a minimal DNS query for "test.fips" AAAA
        // Header: ID=0x1234, flags=0x0100 (standard query, RD=1),
        //   QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0
        // Question: 4test4fips0 QTYPE=AAAA(28) QCLASS=IN(1)
        let query: Vec<u8> = vec![
            0x12, 0x34, // ID
            0x01, 0x00, // Flags: standard query, RD=1
            0x00, 0x01, // QDCOUNT = 1
            0x00, 0x00, // ANCOUNT = 0
            0x00, 0x00, // NSCOUNT = 0
            0x00, 0x00, // ARCOUNT = 0
            // QNAME: "test.fips"
            0x04, b't', b'e', b's', b't', 0x04, b'f', b'i', b'p', b's', 0x00, 0x00,
            0x1C, // QTYPE = AAAA (28)
            0x00, 0x01, // QCLASS = IN (1)
        ];

        let bind_addr = if upstream_addr.is_ipv4() {
            "0.0.0.0:0"
        } else {
            "[::]:0"
        };
        let sock = match tokio::net::UdpSocket::bind(bind_addr).await {
            Ok(s) => s,
            Err(e) => {
                error!(error = %e, "Failed to bind UDP socket for DNS check");
                std::process::exit(1);
            }
        };

        // Retry the upstream probe up to MAX_PROBE_ATTEMPTS times with a
        // 1-second per-attempt timeout and a 1-second sleep between
        // attempts. Total worst-case wait: ~10 seconds.
        //
        // Bounded retry covers the cold-boot race where this gateway and
        // the fips daemon start at approximately the same time: the
        // daemon's TUN may be up (the systemd ExecStartPre wait already
        // gates on that) while its DNS responder is still binding
        // [::1]:5354. Without this retry, the gateway hard-failed after
        // a single 3-second probe and relied on Restart=on-failure for
        // recovery.
        const MAX_PROBE_ATTEMPTS: u32 = 5;
        const PROBE_TIMEOUT_SECS: u64 = 1;
        const PROBE_RETRY_DELAY_SECS: u64 = 1;

        let mut buf = [0u8; 512];
        let mut last_failure: Option<String> = None;
        let mut succeeded = false;
        for attempt in 1..=MAX_PROBE_ATTEMPTS {
            if let Err(e) = sock.send_to(&query, upstream_addr).await {
                last_failure = Some(format!("send_to failed: {}", e));
            } else {
                match tokio::time::timeout(
                    std::time::Duration::from_secs(PROBE_TIMEOUT_SECS),
                    sock.recv_from(&mut buf),
                )
                .await
                {
                    Ok(Ok(_)) => {
                        info!(
                            upstream = %upstream, attempt = attempt,
                            "DNS upstream is reachable"
                        );
                        succeeded = true;
                        break;
                    }
                    Ok(Err(e)) => {
                        last_failure = Some(format!("recv_from failed: {}", e));
                    }
                    Err(_) => {
                        last_failure = Some(format!("no response within {}s", PROBE_TIMEOUT_SECS));
                    }
                }
            }
            if attempt < MAX_PROBE_ATTEMPTS {
                info!(
                    upstream = %upstream, attempt = attempt,
                    last = ?last_failure,
                    "DNS upstream probe attempt failed; retrying"
                );
                tokio::time::sleep(std::time::Duration::from_secs(PROBE_RETRY_DELAY_SECS)).await;
            }
        }
        if !succeeded {
            error!(
                upstream = %upstream,
                attempts = MAX_PROBE_ATTEMPTS,
                last = ?last_failure,
                "DNS upstream did not become reachable after exhausting retries — is the FIPS daemon running?"
            );
            std::process::exit(1);
        }
    }

    // --- Initialize components ---

    // Virtual IP pool
    let ip_pool = match pool::VirtualIpPool::new(
        &gw_config.pool,
        gw_config.dns.ttl() as u64,
        gw_config.grace_period(),
    ) {
        Ok(p) => Arc::new(Mutex::new(p)),
        Err(e) => {
            error!(error = %e, "Failed to create virtual IP pool");
            std::process::exit(1);
        }
    };

    // NAT manager
    let mut nat_mgr = match nat::NatManager::new(gw_config.lan_interface.clone()) {
        Ok(n) => n,
        Err(e) => {
            error!(error = %e, "Failed to create nftables table — do you have CAP_NET_ADMIN?");
            std::process::exit(1);
        }
    };

    // Install inbound port-forward rules (TASK-2026-0061).
    if let Err(e) = nat_mgr.set_port_forwards(&gw_config.port_forwards) {
        error!(error = %e, "Failed to install port-forward rules");
        let _ = nat_mgr.cleanup();
        std::process::exit(1);
    }

    // Network setup
    let mut net_setup = net::NetSetup::new(gw_config.lan_interface.clone(), gw_config.pool.clone());

    // Add pool route
    if let Err(e) = net_setup.add_pool_route().await {
        error!(error = %e, "Failed to add pool route");
        // Clean up NAT table before exit
        let _ = nat_mgr.cleanup();
        std::process::exit(1);
    }

    // --- Channels ---

    // Pool events (new/removed mappings) → NAT + net modules
    let (event_tx, mut event_rx) = mpsc::channel::<pool::PoolEvent>(64);

    // Shutdown signal
    let (shutdown_tx, shutdown_rx) = watch::channel(false);

    // --- Start DNS resolver task ---

    let dns_pool = Arc::clone(&ip_pool);
    let dns_event_tx = event_tx.clone();
    let dns_shutdown = shutdown_rx.clone();
    let dns_listen = gw_config.dns.listen().to_string();
    let dns_upstream = gw_config.dns.upstream().to_string();
    let dns_ttl = gw_config.dns.ttl();

    let dns_task = tokio::spawn(async move {
        if let Err(e) = dns::run_dns_resolver(
            &dns_listen,
            &dns_upstream,
            dns_ttl,
            dns_pool,
            dns_event_tx,
            dns_shutdown,
        )
        .await
        {
            error!(error = %e, "DNS resolver error");
        }
    });

    // --- Snapshot channel for control socket ---

    let (snapshot_tx, snapshot_rx) = watch::channel::<Option<control::GatewaySnapshot>>(None);
    let start_time = Instant::now();

    // --- Start control socket ---

    let control_task = match control::GatewayControlSocket::bind() {
        Ok(socket) => {
            let rx = snapshot_rx.clone();
            Some(tokio::spawn(async move {
                socket.accept_loop(rx).await;
            }))
        }
        Err(e) => {
            warn!(error = %e, "Failed to bind gateway control socket — continuing without it");
            None
        }
    };

    // --- NAT mapping counter (shared with tick task for snapshots) ---

    let nat_count = Arc::new(AtomicUsize::new(0));

    // --- Start pool tick task ---

    let tick_pool = Arc::clone(&ip_pool);
    let tick_event_tx = event_tx;
    let tick_nat_count = Arc::clone(&nat_count);
    let mut tick_shutdown = shutdown_rx.clone();
    let conntrack = pool::ProcConntrack;
    let snap_config = control::SnapshotConfig {
        pool_cidr: gw_config.pool.clone(),
        lan_interface: gw_config.lan_interface.clone(),
        dns_upstream: gw_config.dns.upstream().to_string(),
        dns_listen: gw_config.dns.listen().to_string(),
        dns_ttl: gw_config.dns.ttl(),
        pool_grace_period: gw_config.grace_period(),
    };

    let tick_task = tokio::spawn(async move {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
        loop {
            tokio::select! {
                _ = interval.tick() => {
                    let now = Instant::now();
                    let mut pool_guard = tick_pool.lock().await;
                    let events = pool_guard.tick(now, &conntrack);

                    // Build snapshot for control socket
                    let pool_status = pool_guard.status();
                    let mappings = pool_guard.mapping_info(now);
                    drop(pool_guard);

                    let snapshot = control::build_snapshot(
                        pool_status,
                        mappings,
                        tick_nat_count.load(Ordering::Relaxed),
                        start_time,
                        &snap_config,
                    );
                    let _ = snapshot_tx.send(Some(snapshot));

                    for event in events {
                        let _ = tick_event_tx.send(event).await;
                    }
                }
                _ = tick_shutdown.changed() => break,
            }
        }
    });

    // --- Event processing loop ---

    let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");

    info!("fips-gateway running");

    loop {
        tokio::select! {
            Some(event) = event_rx.recv() => {
                match event {
                    pool::PoolEvent::MappingCreated { virtual_ip, mesh_addr } => {
                        // Add NAT rules
                        if let Err(e) = nat_mgr.add_mapping(virtual_ip, mesh_addr) {
                            error!(error = %e, virtual_ip = %virtual_ip, "Failed to add NAT rules");
                        }
                        nat_count.store(nat_mgr.mapping_count(), Ordering::Relaxed);
                        // Add proxy NDP entry
                        if let Err(e) = net_setup.add_proxy_ndp(virtual_ip).await {
                            error!(error = %e, virtual_ip = %virtual_ip, "Failed to add proxy NDP");
                        }
                    }
                    pool::PoolEvent::MappingRemoved { virtual_ip, mesh_addr: _ } => {
                        // Remove NAT rules
                        if let Err(e) = nat_mgr.remove_mapping(virtual_ip) {
                            warn!(error = %e, virtual_ip = %virtual_ip, "Failed to remove NAT rules");
                        }
                        nat_count.store(nat_mgr.mapping_count(), Ordering::Relaxed);
                        // Remove proxy NDP entry
                        if let Err(e) = net_setup.remove_proxy_ndp(virtual_ip).await {
                            warn!(error = %e, virtual_ip = %virtual_ip, "Failed to remove proxy NDP");
                        }
                    }
                }
            }
            _ = tokio::signal::ctrl_c() => {
                info!("Received SIGINT, shutting down");
                break;
            }
            _ = sigterm.recv() => {
                info!("Received SIGTERM, shutting down");
                break;
            }
        }
    }

    // --- Shutdown ---

    info!("fips-gateway shutting down");

    // Signal all tasks to stop
    let _ = shutdown_tx.send(true);

    // Wait for tasks (control task is cancelled by dropping the listener)
    if let Some(task) = control_task {
        task.abort();
        let _ = task.await;
    }
    let _ = dns_task.await;
    let _ = tick_task.await;

    // Log final pool status
    {
        let pool_guard = ip_pool.lock().await;
        let status = pool_guard.status();
        info!(
            total = status.total,
            allocated = status.allocated,
            active = status.active,
            draining = status.draining,
            free = status.free,
            "Final pool status"
        );
    }

    // Clean up network and NAT
    net_setup.cleanup().await;
    if let Err(e) = nat_mgr.cleanup() {
        warn!(error = %e, "Failed to clean up nftables table");
    }

    info!("fips-gateway shutdown complete");
}
