//! `plexus-rnsd` — the Reticulum daemon (`ARCHITECTURE.md` §5).
//!
//! A minimal but real node: it owns one [`Gateway`] in a single task (no shared
//! mutable state — everything is messages, §5.4), runs a TCP **server**
//! interface that peers connect to, announces a local destination periodically,
//! and verifies + logs the announces it hears. Links, resources, transport
//! forwarding, and additional interfaces land in later phases.

use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use clap::Parser;
use rand_core::OsRng;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::mpsc;
use tokio::time::{interval, MissedTickBehavior};

use plexus_gateway::destination::DestinationName;
use plexus_gateway::identity::Identity;
use plexus_gateway::{Gateway, GatewayCmd, GatewayConfig, GatewayEvent, InterfaceId};
use plexus_iface::{Interface, Priority, TcpServer, TxFrame};

/// Command-line arguments for the plexus daemon.
#[derive(Debug, Parser)]
#[command(name = "plexus-rnsd", version, about = "Plexus Reticulum daemon")]
struct Args {
    /// Address for the TCP server interface to listen on.
    #[arg(long, default_value = "0.0.0.0:4242")]
    listen: String,
    /// Path to the 64-byte node identity (generated if missing).
    #[arg(long, default_value = "plexus.identity")]
    identity: PathBuf,
    /// Destination app name to announce.
    #[arg(long, default_value = "plexus")]
    app: String,
    /// Destination aspect to announce.
    #[arg(long, default_value = "node")]
    aspect: String,
    /// Seconds between announces.
    #[arg(long, default_value_t = 30)]
    announce_interval: u64,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
    tracing_subscriber::fmt().with_env_filter(filter).init();

    let identity = load_or_create_identity(&args.identity)?;
    let mut gateway = Gateway::new(identity, GatewayConfig::default());

    let iface_id = InterfaceId(1);
    let server = Box::new(TcpServer::new(iface_id, args.listen.clone()));
    let mut handle = server.start().await?;
    gateway.register_interface(iface_id);

    let aspects = [args.aspect.as_str()];
    let destination =
        gateway.register_destination(DestinationName::new(args.app.clone(), &aspects));
    tracing::info!(
        listen = %args.listen,
        destination = %hex16(destination.as_bytes()),
        name = %format!("{}.{}", args.app, args.aspect),
        "plexus-rnsd up: TCP server node, announcing every {}s",
        args.announce_interval
    );

    let mut sigint = signal(SignalKind::interrupt())?;
    let mut sigterm = signal(SignalKind::terminate())?;

    let mut announce_timer = interval(Duration::from_secs(args.announce_interval.max(1)));
    announce_timer.set_missed_tick_behavior(MissedTickBehavior::Delay);
    let mut tick_timer = interval(Duration::from_secs(1));
    tick_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);

    loop {
        tokio::select! {
            _ = sigint.recv()  => { tracing::info!("SIGINT received, shutting down"); break; }
            _ = sigterm.recv() => { tracing::info!("SIGTERM received, shutting down"); break; }
            inbound = handle.rx.recv() => {
                let Some(frame) = inbound else {
                    tracing::warn!("interface closed; exiting");
                    break;
                };
                let events = gateway.step(GatewayCmd::SubmitInbound {
                    from_iface: iface_id,
                    bytes: frame.bytes,
                });
                dispatch(events, &handle.tx).await;
            }
            _ = announce_timer.tick() => {
                let events = gateway.step(GatewayCmd::AnnounceLocal {
                    destination,
                    app_data: bytes::Bytes::from_static(b"plexus"),
                });
                dispatch(events, &handle.tx).await;
            }
            _ = tick_timer.tick() => {
                let _ = gateway.step(GatewayCmd::Tick { now: Instant::now() });
            }
        }
    }

    handle.cancel.cancel();
    let _ = gateway.step(GatewayCmd::Shutdown);
    Ok(())
}

/// Apply gateway events: send outbound packets to the interface, log the rest.
async fn dispatch(events: Vec<GatewayEvent>, tx: &mpsc::Sender<TxFrame>) {
    for event in events {
        match event {
            GatewayEvent::PacketOut { bytes, .. } => {
                if tx
                    .send(TxFrame {
                        bytes,
                        priority: Priority::Normal,
                    })
                    .await
                    .is_err()
                {
                    tracing::warn!("interface tx channel closed");
                }
            }
            GatewayEvent::AnnounceReceived {
                destination,
                app_data,
                hops,
                ..
            } => {
                tracing::info!(
                    dest = %hex16(destination.as_bytes()),
                    hops,
                    app_data_len = app_data.len(),
                    "announce received and verified"
                );
            }
            GatewayEvent::PathLearned {
                destination, hops, ..
            } => {
                tracing::info!(dest = %hex16(destination.as_bytes()), hops, "path learned");
            }
            GatewayEvent::PathLost { destination } => {
                tracing::info!(dest = %hex16(destination.as_bytes()), "path lost");
            }
            GatewayEvent::LinkEvent(_) | GatewayEvent::ResourceEvent(_) => {}
        }
    }
}

/// Load the 64-byte node identity from `path`, generating and persisting a fresh
/// one (mode 0600) if it does not exist.
fn load_or_create_identity(path: &Path) -> std::io::Result<Identity> {
    use std::io::{Error, ErrorKind};

    if path.exists() {
        let bytes = std::fs::read(path)?;
        let arr: [u8; 64] = bytes
            .as_slice()
            .try_into()
            .map_err(|_| Error::new(ErrorKind::InvalidData, "identity file must be 64 bytes"))?;
        return Identity::from_bytes(&arr)
            .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()));
    }

    let identity = Identity::generate(&mut OsRng);
    if let Some(parent) = path.parent() {
        if !parent.as_os_str().is_empty() {
            std::fs::create_dir_all(parent)?;
        }
    }
    std::fs::write(path, identity.to_bytes())?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
    }
    Ok(identity)
}

/// Lowercase hex of a 16-byte address hash.
fn hex16(bytes: &[u8; 16]) -> String {
    use std::fmt::Write;
    bytes.iter().fold(String::new(), |mut s, b| {
        let _ = write!(s, "{b:02x}");
        s
    })
}
