//! `plexus-gateway` — the Reticulum-compatible state machine.
//!
//! This is the heart of the project (`ARCHITECTURE.md` §3). It implements the
//! Reticulum protocol as a **runtime-agnostic, sans-I/O** state machine: feed it
//! commands, get events out, ask it when it next wants a tick. No async, no I/O,
//! no shared mutable state — the same shape as `quinn_proto::Connection` or
//! `rustls::ConnectionCommon` (§3.10). The daemon binds it to `tokio`; an
//! embedded target could bind it to `embassy`.
//!
//! v0 status: the facade and all module types exist; bodies are `todo!()`.
#![forbid(unsafe_code)]
#![allow(
    dead_code,
    unused_variables,
    clippy::missing_errors_doc,
    clippy::missing_panics_doc,
    clippy::unused_self,
    clippy::must_use_candidate,
    clippy::module_name_repetitions,
    clippy::large_enum_variant,
    clippy::needless_pass_by_value, // stub bodies don't consume args yet
    clippy::ptr_arg // encoders append to the &mut Vec
)]

pub mod announce;
pub mod destination;
pub mod error;
pub mod identity;
pub mod link;
pub mod path;
pub mod ratchet;
pub mod resource;

use std::collections::HashMap;
use std::time::{Duration, Instant};

use bytes::Bytes;
use rand_core::{OsRng, RngCore};

use plexus_codec::decode::decode_header;
use plexus_codec::encode::encode_header;
use plexus_core::{
    AddressHash, ContextByte, DestinationType, HeaderType, PacketHeader, PacketType, TransportType,
};

use crate::announce::{AnnouncePayload, AnnounceTable};
use crate::destination::{destination_address, DestinationName, DestinationTable};
use crate::identity::{Identity, PublicIdentity};
use crate::link::{LinkEvent, LinkTable};
use crate::path::{PathEntry, PathTable};
use crate::ratchet::RatchetStore;
use crate::resource::{ResourceEvent, ResourceTable};

/// Default lifetime for an announce-learned path (§3.6: one week for normal
/// paths; configurable policy lands with transport forwarding).
const PATH_EXPIRY: Duration = Duration::from_secs(7 * 24 * 60 * 60);

pub use error::Error;

/// Opaque identifier for an interface, assigned by the host (`plexus-iface`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InterfaceId(pub u64);

/// Gateway policy: timeouts, table sizes, transport mode, and similar knobs.
///
/// Fields are added next to the logic that reads them (§3.6, §3.7, §3.9).
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct GatewayConfig {}

/// Replay-protection set of recently-seen packet hashes (§9.2).
#[derive(Debug, Default)]
pub struct PacketDedupSet {}

/// Commands fed into the gateway. All interaction is via this enum (§3.10).
#[derive(Debug)]
pub enum GatewayCmd {
    /// Bytes arrived on an interface.
    SubmitInbound {
        /// Interface the bytes arrived on.
        from_iface: InterfaceId,
        /// Raw inbound bytes.
        bytes: Bytes,
    },
    /// Send an application payload to a destination.
    SendPacket {
        /// Target destination address.
        destination: AddressHash,
        /// Payload bytes.
        payload: Bytes,
        /// Packet context.
        context: ContextByte,
    },
    /// Announce a local destination.
    AnnounceLocal {
        /// Local destination to announce.
        destination: AddressHash,
        /// Opaque application data carried in the announce.
        app_data: Bytes,
    },
    /// Open a link to a destination.
    OpenLink {
        /// Destination to link to.
        destination: AddressHash,
    },
    /// Close an established link.
    CloseLink {
        /// Link to close.
        link_id: AddressHash,
    },
    /// Begin a resource transfer over a link.
    StartResource {
        /// Link to transfer over.
        link_id: AddressHash,
        /// Payload to transfer.
        payload: Bytes,
    },
    /// Advance time-based state.
    Tick {
        /// The current instant.
        now: Instant,
    },
    /// Begin a clean shutdown.
    Shutdown,
}

/// Events emitted by the gateway in response to commands and time (§3.10).
#[derive(Debug)]
pub enum GatewayEvent {
    /// Bytes to write to an interface.
    PacketOut {
        /// Interface to write to.
        interface: InterfaceId,
        /// Bytes to write.
        bytes: Bytes,
    },
    /// An announce was received and verified.
    AnnounceReceived {
        /// Announced destination.
        destination: AddressHash,
        /// The announced public identity.
        identity: PublicIdentity,
        /// Opaque application data.
        app_data: Bytes,
        /// Hop count at reception.
        hops: u8,
    },
    /// A link-level event.
    LinkEvent(LinkEvent),
    /// A resource-transfer event.
    ResourceEvent(ResourceEvent),
    /// A path to a destination was learned.
    PathLearned {
        /// Destination reached.
        destination: AddressHash,
        /// Next-hop transport node.
        via: AddressHash,
        /// Distance in hops.
        hops: u8,
    },
    /// A path to a destination was lost.
    PathLost {
        /// Destination no longer reachable.
        destination: AddressHash,
    },
}

/// The runtime-agnostic, sans-I/O Reticulum state machine (§3.10).
pub struct Gateway {
    identity: Identity,
    destinations: DestinationTable,
    paths: PathTable,
    links: LinkTable,
    resources: ResourceTable,
    ratchets: RatchetStore,
    announces: AnnounceTable,
    packet_dedup: PacketDedupSet,
    config: GatewayConfig,
    /// Current time, advanced by [`GatewayCmd::Tick`]; the sans-I/O clock.
    now: Instant,
    /// Local destinations we own (under the gateway identity), keyed by address.
    local: HashMap<AddressHash, DestinationName>,
    /// Interfaces to broadcast outbound announces on.
    interfaces: Vec<InterfaceId>,
}

impl Gateway {
    /// Construct a gateway around an identity and policy config.
    pub fn new(identity: Identity, config: GatewayConfig) -> Self {
        Self {
            identity,
            destinations: DestinationTable::default(),
            paths: PathTable::default(),
            links: LinkTable::default(),
            resources: ResourceTable::default(),
            ratchets: RatchetStore::default(),
            announces: AnnounceTable::default(),
            packet_dedup: PacketDedupSet::default(),
            config,
            now: Instant::now(),
            local: HashMap::new(),
            interfaces: Vec::new(),
        }
    }

    /// Register a local Single destination under the gateway identity, returning
    /// its address. It can then be announced via [`GatewayCmd::AnnounceLocal`].
    pub fn register_destination(&mut self, name: DestinationName) -> AddressHash {
        let address = destination_address(&name.name_hash(), &self.identity.hash());
        self.local.insert(address, name);
        address
    }

    /// Register an interface the gateway should broadcast outbound announces on.
    pub fn register_interface(&mut self, id: InterfaceId) {
        if !self.interfaces.contains(&id) {
            self.interfaces.push(id);
        }
    }

    /// Feed one command in; get the (possibly empty) resulting events out.
    ///
    /// All gateway logic lives here. No async, no I/O, no locks held across the
    /// call. v0 implements the inbound announce path and time-based path
    /// eviction; outbound, link, and resource handling land in later phases.
    pub fn step(&mut self, cmd: GatewayCmd) -> Vec<GatewayEvent> {
        match cmd {
            GatewayCmd::SubmitInbound { from_iface, bytes } => self.on_inbound(from_iface, &bytes),
            GatewayCmd::Tick { now } => {
                self.now = now;
                self.paths.evict_expired(now);
                Vec::new()
            }
            // Outbound, link, and resource subsystems land in Phases 2–3
            // (§11.2–§11.3). Accepted as no-ops for now.
            GatewayCmd::AnnounceLocal {
                destination,
                app_data,
            } => self.announce_local(destination, &app_data),
            GatewayCmd::SendPacket { .. }
            | GatewayCmd::OpenLink { .. }
            | GatewayCmd::CloseLink { .. }
            | GatewayCmd::StartResource { .. }
            | GatewayCmd::Shutdown => Vec::new(),
        }
    }

    /// The next instant the gateway wants a [`GatewayCmd::Tick`] at, if any.
    pub fn next_deadline(&self) -> Option<Instant> {
        self.paths.earliest_expiry()
    }

    fn on_inbound(&mut self, from_iface: InterfaceId, bytes: &[u8]) -> Vec<GatewayEvent> {
        let Ok((header, consumed)) = decode_header(bytes) else {
            return Vec::new();
        };
        match header.packet_type {
            PacketType::Announce => self.on_announce(from_iface, &header, &bytes[consumed..]),
            // data / link-request / proof handling lands in later phases.
            PacketType::Data | PacketType::LinkRequest | PacketType::Proof => Vec::new(),
        }
    }

    fn on_announce(
        &mut self,
        from_iface: InterfaceId,
        header: &PacketHeader,
        payload: &[u8],
    ) -> Vec<GatewayEvent> {
        let Ok(announce) = AnnouncePayload::decode(payload, header.context_flag) else {
            return Vec::new();
        };
        // Drop forged or corrupt announces silently (address + signature check).
        if announce.verify(&header.destination).is_err() {
            return Vec::new();
        }
        let identity = PublicIdentity::from_parts(announce.encryption_pub, announce.signing_pub);
        // For a relayed (H2) announce the next hop is the transport id; for a
        // directly-heard (H1) announce it is the announcing identity itself.
        let via = header.transport_id.unwrap_or_else(|| identity.hash());

        self.paths.insert(PathEntry {
            destination: header.destination,
            next_hop: via,
            interface: from_iface,
            hops: header.hops,
            learned_at: self.now,
            expires_at: self.now + PATH_EXPIRY,
        });

        vec![
            GatewayEvent::PathLearned {
                destination: header.destination,
                via,
                hops: header.hops,
            },
            GatewayEvent::AnnounceReceived {
                destination: header.destination,
                identity,
                app_data: Bytes::copy_from_slice(&announce.app_data),
                hops: header.hops,
            },
        ]
    }

    fn announce_local(&self, destination: AddressHash, app_data: &[u8]) -> Vec<GatewayEvent> {
        let Some(name) = self.local.get(&destination) else {
            return Vec::new();
        };
        let bytes = Bytes::from(self.build_announce(name, app_data));
        self.interfaces
            .iter()
            .map(|&interface| GatewayEvent::PacketOut {
                interface,
                bytes: bytes.clone(),
            })
            .collect()
    }

    /// Build a signed announce packet (header + payload) for a local destination.
    fn build_announce(&self, name: &DestinationName, app_data: &[u8]) -> Vec<u8> {
        let public = self.identity.public();
        let name_hash = name.name_hash();
        let destination = destination_address(&name_hash, &self.identity.hash());

        let mut random_hash = [0u8; 10];
        OsRng.fill_bytes(&mut random_hash);

        let mut announce = AnnouncePayload {
            encryption_pub: *public.encryption_pub(),
            signing_pub: *public.signing_pub(),
            name_hash,
            random_hash,
            ratchet: None,
            signature: [0u8; 64],
            app_data: app_data.to_vec(),
        };
        announce.signature = self.identity.sign(&announce.signed_data(&destination));

        let header = PacketHeader {
            ifac: false,
            header_type: HeaderType::H1,
            context_flag: false,
            transport_type: TransportType::Broadcast,
            destination_type: DestinationType::Single,
            packet_type: PacketType::Announce,
            hops: 0,
            transport_id: None,
            destination,
            context: ContextByte::NONE,
        };
        let mut packet = Vec::new();
        encode_header(&header, &mut packet).expect("H1 header always encodes");
        announce.encode(&mut packet);
        packet
    }
}

#[cfg(test)]
mod tests {
    use super::{Gateway, GatewayCmd, GatewayConfig, GatewayEvent, InterfaceId};
    use crate::destination::DestinationName;
    use crate::identity::Identity;
    use bytes::Bytes;
    use hex_literal::hex;
    use plexus_core::AddressHash;

    const DEST: [u8; 16] = hex!("7dc45220f1867c50ead139c7e4083324");
    // The full announce packet captured from RNS 1.3.1 (header + payload), 172 B.
    const PKTRAW: [u8; 172] = hex!(
        "0100"
        "7dc45220f1867c50ead139c7e4083324"
        "00"
        "8f40c5adb68f25624ae5b214ea767a6ec94d829d3d7b5e1ad1ba6f3e2138285f"
        "29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7"
        "038b878cc925d841c2c5"
        "807c57e2ff006a13360b"
        "a271fe3b5e15eab7525c18201eed47ea5f6dbf4197ae27f5169c0210c58001b5"
        "e33a27f3b2493405e52565225ca5f32a39df2e3e95a7ddf0df2e72fbb23fb008"
        "68656c6c6f"
    );

    fn gateway() -> Gateway {
        let id = Identity::from_bytes(&[9u8; 64]).unwrap();
        Gateway::new(id, GatewayConfig::default())
    }

    #[test]
    fn processes_a_real_reticulum_announce() {
        let mut g = gateway();
        let events = g.step(GatewayCmd::SubmitInbound {
            from_iface: InterfaceId(1),
            bytes: Bytes::copy_from_slice(&PKTRAW),
        });
        assert_eq!(events.len(), 2, "PathLearned + AnnounceReceived");

        let mut saw_announce = false;
        let mut saw_path = false;
        for e in &events {
            match e {
                GatewayEvent::AnnounceReceived {
                    destination,
                    app_data,
                    hops,
                    ..
                } => {
                    assert_eq!(*destination.as_bytes(), DEST);
                    assert_eq!(&app_data[..], b"hello");
                    assert_eq!(*hops, 0);
                    saw_announce = true;
                }
                GatewayEvent::PathLearned {
                    destination, hops, ..
                } => {
                    assert_eq!(*destination.as_bytes(), DEST);
                    assert_eq!(*hops, 0);
                    saw_path = true;
                }
                other => panic!("unexpected event: {other:?}"),
            }
        }
        assert!(saw_announce && saw_path);
        assert_eq!(g.paths.len(), 1);
        assert!(g.paths.lookup(&AddressHash::new(DEST)).is_some());
    }

    #[test]
    fn forged_announce_is_dropped() {
        let mut g = gateway();
        let mut bad = PKTRAW;
        bad[171] ^= 0x01; // corrupt app_data → signature no longer verifies
        let events = g.step(GatewayCmd::SubmitInbound {
            from_iface: InterfaceId(1),
            bytes: Bytes::copy_from_slice(&bad),
        });
        assert!(events.is_empty());
        assert_eq!(g.paths.len(), 0);
    }

    #[test]
    fn emits_a_self_verifiable_announce() {
        let mut g = gateway();
        let addr = g.register_destination(DestinationName::new("plexus", &["emit"]));
        g.register_interface(InterfaceId(3));
        g.register_interface(InterfaceId(3)); // idempotent

        let events = g.step(GatewayCmd::AnnounceLocal {
            destination: addr,
            app_data: Bytes::from_static(b"hi"),
        });
        assert_eq!(events.len(), 1, "one PacketOut per registered interface");
        let GatewayEvent::PacketOut { interface, bytes } = &events[0] else {
            panic!("expected PacketOut");
        };
        assert_eq!(*interface, InterfaceId(3));

        // Our own announce must verify and surface in a fresh peer gateway.
        let mut peer = gateway();
        let peer_events = peer.step(GatewayCmd::SubmitInbound {
            from_iface: InterfaceId(9),
            bytes: bytes.clone(),
        });
        assert!(peer_events.iter().any(|e| matches!(
            e,
            GatewayEvent::AnnounceReceived { destination, .. } if *destination == addr
        )));
    }

    #[test]
    fn announce_for_unregistered_destination_is_noop() {
        let mut g = gateway();
        g.register_interface(InterfaceId(1));
        let events = g.step(GatewayCmd::AnnounceLocal {
            destination: AddressHash::new([0xAA; 16]),
            app_data: Bytes::new(),
        });
        assert!(events.is_empty());
    }
}
