//! Self-sovereign mesh identity backed by quicproquo-core Ed25519 keypairs.
//!
//! A [`MeshIdentity`] wraps an [`IdentityKeypair`] with a peer directory,
//! enabling P2P nodes to persist identity and track known peers across
//! restarts.

use std::collections::HashMap;
use std::path::Path;

use quicproquo_core::IdentityKeypair;
use serde::{Deserialize, Serialize};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

/// Information about a known peer in the mesh network.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeerInfo {
    /// Raw Ed25519 public key (32 bytes).
    pub public_key: Vec<u8>,
    /// Unix timestamp of last observed activity.
    pub last_seen: u64,
    /// Known network addresses (e.g. iroh `NodeAddr` serializations).
    pub addresses: Vec<String>,
}

/// Persisted form of a mesh identity (JSON on disk).
#[derive(Serialize, Deserialize)]
struct IdentityFile {
    /// Hex-encoded 32-byte Ed25519 seed.
    seed: String,
    /// Known peers, keyed by hex-encoded peer public key.
    peers: HashMap<String, PeerInfo>,
}

/// A self-sovereign mesh identity: an Ed25519 keypair + a known-peers directory.
pub struct MeshIdentity {
    keypair: IdentityKeypair,
    known_peers: HashMap<String, PeerInfo>,
}

impl MeshIdentity {
    /// Generate a fresh random mesh identity.
    pub fn generate() -> Self {
        Self {
            keypair: IdentityKeypair::generate(),
            known_peers: HashMap::new(),
        }
    }

    /// Recreate a mesh identity from a 32-byte Ed25519 seed.
    pub fn from_seed(seed: [u8; 32]) -> Self {
        Self {
            keypair: IdentityKeypair::from_seed(seed),
            known_peers: HashMap::new(),
        }
    }

    /// Load a mesh identity from a JSON file.
    pub fn load(path: &Path) -> anyhow::Result<Self> {
        let data = std::fs::read_to_string(path)?;
        let file: IdentityFile = serde_json::from_str(&data)?;
        let seed_bytes = hex::decode(&file.seed)?;
        let seed: [u8; 32] = seed_bytes
            .as_slice()
            .try_into()
            .map_err(|_| anyhow::anyhow!("seed must be 32 bytes"))?;
        Ok(Self {
            keypair: IdentityKeypair::from_seed(seed),
            known_peers: file.peers,
        })
    }

    /// Save this mesh identity to a JSON file with restrictive permissions.
    ///
    /// On Unix, the file is set to `0o600` (owner read/write only) since it
    /// contains the Ed25519 seed in the clear.
    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
        let file = IdentityFile {
            seed: hex::encode(self.keypair.seed_bytes()),
            peers: self.known_peers.clone(),
        };
        let json = serde_json::to_string_pretty(&file)?;
        std::fs::write(path, json)?;

        // Restrict permissions to owner-only on Unix.
        #[cfg(unix)]
        {
            let perms = std::fs::Permissions::from_mode(0o600);
            std::fs::set_permissions(path, perms)?;
        }

        Ok(())
    }

    /// Return the raw 32-byte Ed25519 public key.
    pub fn public_key(&self) -> [u8; 32] {
        self.keypair.public_key_bytes()
    }

    /// Sign arbitrary bytes, returning a 64-byte Ed25519 signature.
    pub fn sign(&self, message: &[u8]) -> [u8; 64] {
        self.keypair.sign_raw(message)
    }

    /// Return the underlying seed (for deriving iroh `SecretKey`, etc.).
    pub fn seed_bytes(&self) -> [u8; 32] {
        *self.keypair.seed_bytes()
    }

    /// Register or update a known peer.
    pub fn add_peer(&mut self, id: String, info: PeerInfo) {
        self.known_peers.insert(id, info);
    }

    /// Immutable view of the known-peers directory.
    pub fn known_peers(&self) -> &HashMap<String, PeerInfo> {
        &self.known_peers
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn generate_and_sign_verify() {
        let id = MeshIdentity::generate();
        let msg = b"test message";
        let sig = id.sign(msg);

        // Verify through quicproquo_core
        let pk = id.public_key();
        IdentityKeypair::verify_raw(&pk, msg, &sig).expect("valid signature");
    }

    #[test]
    fn from_seed_deterministic() {
        let seed = [42u8; 32];
        let a = MeshIdentity::from_seed(seed);
        let b = MeshIdentity::from_seed(seed);
        assert_eq!(a.public_key(), b.public_key());
    }

    #[test]
    fn save_and_load_roundtrip() {
        let dir = tempfile::tempdir().expect("tmp dir");
        let path = dir.path().join("mesh_id.json");

        let mut original = MeshIdentity::generate();
        original.add_peer(
            "deadbeef".into(),
            PeerInfo {
                public_key: vec![0xde, 0xad],
                last_seen: SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .expect("time")
                    .as_secs(),
                addresses: vec!["127.0.0.1:4433".into()],
            },
        );
        original.save(&path).expect("save");

        let loaded = MeshIdentity::load(&path).expect("load");
        assert_eq!(original.public_key(), loaded.public_key());
        assert_eq!(loaded.known_peers().len(), 1);
        assert!(loaded.known_peers().contains_key("deadbeef"));
    }

    #[test]
    fn add_and_query_peers() {
        let mut id = MeshIdentity::generate();
        assert!(id.known_peers().is_empty());

        id.add_peer(
            "peer1".into(),
            PeerInfo {
                public_key: vec![1; 32],
                last_seen: 0,
                addresses: vec![],
            },
        );
        assert_eq!(id.known_peers().len(), 1);
        assert_eq!(id.known_peers()["peer1"].public_key, vec![1; 32]);
    }
}
