//! Destinations (`ARCHITECTURE.md` §3.4).

use std::sync::Arc;

use sha2::{Digest, Sha256};

use plexus_core::AddressHash;

use crate::identity::{Identity, PublicIdentity};

const NAME_HASH_LEN: usize = 10;
const ADDR_LEN: usize = 16;

/// A destination name: an app name plus ordered aspects, e.g. `lxmf.delivery`.
#[derive(Debug, Clone)]
pub struct DestinationName {
    /// Application name, e.g. `"lxmf"`.
    pub app_name: String,
    /// Ordered aspects, e.g. `["delivery"]`.
    pub aspects: Vec<String>,
}

impl DestinationName {
    /// Construct from an app name and aspects.
    #[must_use]
    pub fn new(app_name: impl Into<String>, aspects: &[&str]) -> Self {
        Self {
            app_name: app_name.into(),
            aspects: aspects.iter().map(|s| (*s).to_owned()).collect(),
        }
    }

    /// The dotted full name: `app.aspect1.aspect2`.
    #[must_use]
    pub fn full_name(&self) -> String {
        let mut s = self.app_name.clone();
        for aspect in &self.aspects {
            s.push('.');
            s.push_str(aspect);
        }
        s
    }

    /// Name hash: `SHA256("app.aspect1.aspect2")[:10]`.
    #[must_use]
    pub fn name_hash(&self) -> [u8; NAME_HASH_LEN] {
        let digest = Sha256::digest(self.full_name().as_bytes());
        let mut out = [0u8; NAME_HASH_LEN];
        out.copy_from_slice(&digest[..NAME_HASH_LEN]);
        out
    }
}

/// Derive a destination address: `truncate16(SHA256(name_hash(10) ||
/// identity_hash(16)))` (§3.4). Invariant across implementations.
#[must_use]
pub fn destination_address(
    name_hash: &[u8; NAME_HASH_LEN],
    identity_hash: &AddressHash,
) -> AddressHash {
    let mut hasher = Sha256::new();
    hasher.update(name_hash);
    hasher.update(identity_hash.as_bytes());
    let digest = hasher.finalize();
    let mut out = [0u8; ADDR_LEN];
    out.copy_from_slice(&digest[..ADDR_LEN]);
    AddressHash::new(out)
}

/// Whether a destination is inbound (ours) or outbound (a peer's).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DestinationDirection {
    /// We receive on this destination.
    In,
    /// We send to this destination.
    Out,
}

/// What we know about a destination's keys.
pub enum DestinationKind {
    /// We hold the private key.
    Single {
        /// The owned identity.
        identity: Arc<Identity>,
    },
    /// We know only the public key.
    SingleRemote {
        /// The peer's public identity.
        identity: PublicIdentity,
    },
    /// Pre-shared symmetric key.
    Group {
        /// The shared key.
        shared_key: [u8; 32],
    },
    /// Unencrypted.
    Plain,
}

/// A destination (§3.4).
pub struct Destination {
    /// Inbound or outbound.
    pub direction: DestinationDirection,
    /// Key material we hold or know.
    pub kind: DestinationKind,
    /// The destination name.
    pub name: DestinationName,
    /// `truncate_16(SHA256(name_hash(10) || identity_hash(16)))`.
    pub address: AddressHash,
}

/// Table of destinations we own or know about.
#[derive(Default)]
pub struct DestinationTable {}

#[cfg(test)]
mod tests {
    use super::{destination_address, DestinationName};
    use hex_literal::hex;
    use plexus_core::AddressHash;

    // Vectors from RNS 1.3.1: identity 0x00..0x3F, name "plexus.test".
    const ID_HASH: [u8; 16] = hex!("aca31af0441d81dbec71e82da0b4b5f5");
    const NAME_HASH: [u8; 10] = hex!("038b878cc925d841c2c5");
    const DEST: [u8; 16] = hex!("7dc45220f1867c50ead139c7e4083324");

    #[test]
    fn name_hash_matches_reference() {
        let n = DestinationName::new("plexus", &["test"]);
        assert_eq!(n.full_name(), "plexus.test");
        assert_eq!(n.name_hash(), NAME_HASH);
    }

    #[test]
    fn destination_address_matches_reference() {
        let addr = destination_address(&NAME_HASH, &AddressHash::new(ID_HASH));
        assert_eq!(*addr.as_bytes(), DEST);
    }
}
