use std::fmt;

use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
use base64::Engine as _;
use ed25519_dalek::{SigningKey, VerifyingKey};
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256, Sha512};

type HmacSha512 = Hmac<Sha512>;

use crate::error::{Error, PathError};
use crate::path::{self, Path};

const MASTER_KEY: &[u8] = b"ed25519 seed";
const DERIVATION_TAG: &[u8] = b"hdssh-v1";

#[derive(Clone)]
pub struct Node {
    pub(crate) seed: [u8; 32],
    pub(crate) chain_code: [u8; 32],
    pub(crate) path: Path,
}

impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Node")
            .field("path", &self.path.canonical())
            .finish()
    }
}

impl Node {
    pub fn master(seed: &[u8]) -> Result<Self, Error> {
        if seed.len() < 16 {
            return Err(Error::MasterSeedTooShort);
        }
        let mut mac = HmacSha512::new_from_slice(MASTER_KEY).expect("static key");
        mac.update(seed);
        let sum = mac.finalize().into_bytes();

        let mut left = [0u8; 32];
        let mut right = [0u8; 32];
        left.copy_from_slice(&sum[..32]);
        right.copy_from_slice(&sum[32..]);

        Ok(Self {
            seed: left,
            chain_code: right,
            path: Path::root(),
        })
    }

    pub fn derive(&self, path_str: &str) -> Result<Self, Error> {
        let target = path::parse_path(path_str)?;
        let current = self.path.segments();
        let prefix = current.join("/");
        if !current.is_empty() && prefix != "m" {
            let expected_prefix = format!("{}/", prefix);
            if target.canonical() != prefix && !target.canonical().starts_with(&expected_prefix) {
                return Err(Error::InvalidDerivation {
                    target: target.canonical().to_string(),
                    prefix,
                });
            }
        }

        let mut curr = self.clone();
        for index in current.len()..target.segments().len() {
            let prefix_segments = target
                .prefix_segments(index + 1)
                .expect("prefix within range");
            curr = curr.derive_to(&prefix_segments)?;
        }
        Ok(curr)
    }

    pub fn derive_raw(&self, path_str: &str) -> Result<Self, Error> {
        let trimmed = path_str.trim();
        if trimmed.is_empty() {
            return Err(Error::Path(PathError::new(
                "hdpki: empty derivation path (raw mode)",
            )));
        }

        let mut parts = Vec::new();
        for (index, part) in trimmed.split('/').enumerate() {
            if part.is_empty() {
                return Err(Error::Path(PathError::new(format!(
                    "hdpki: empty path segment at position {index} in {path_str:?} (raw mode)"
                ))));
            }
            parts.push(part.to_string());
        }

        let mut curr = self.clone();
        let mut prefix = Vec::with_capacity(parts.len());
        for segment in parts {
            prefix.push(segment);
            curr = curr.derive_to(&prefix)?;
        }
        Ok(curr)
    }

    fn derive_to(&self, prefix: &[String]) -> Result<Self, Error> {
        if prefix.is_empty() {
            return Ok(self.clone());
        }
        let canonical = prefix.join("/");
        let mut mac = HmacSha512::new_from_slice(&self.chain_code).expect("chain code length");
        let mut data = Vec::with_capacity(1 + 32 + DERIVATION_TAG.len() + 10 + canonical.len());
        data.push(0x00);
        data.extend_from_slice(&self.seed);
        data.extend_from_slice(DERIVATION_TAG);
        data.extend_from_slice(&encode_uleb128(canonical.len()));
        data.extend_from_slice(canonical.as_bytes());
        mac.update(&data);
        let sum = mac.finalize().into_bytes();

        let mut child_seed = [0u8; 32];
        let mut child_chain = [0u8; 32];
        child_seed.copy_from_slice(&sum[..32]);
        child_chain.copy_from_slice(&sum[32..]);

        Ok(Self {
            seed: child_seed,
            chain_code: child_chain,
            path: Path::from_segments(prefix.to_vec()),
        })
    }

    pub fn ed25519(&self) -> Result<Ed25519Material, Error> {
        let secret = SigningKey::from_bytes(&self.seed);
        let public = VerifyingKey::from(&secret);
        let mut private_key = [0u8; 64];
        private_key[..32].copy_from_slice(secret.to_bytes().as_slice());
        private_key[32..].copy_from_slice(public.as_bytes());

        let blob = ssh_public_blob(public.as_bytes());
        let authorized_key = format!("ssh-ed25519 {}\n", STANDARD.encode(&blob));

        Ok(Ed25519Material {
            private_key,
            public_key: *public.as_bytes(),
            authorized_key,
            blob,
        })
    }

    pub fn fingerprint(&self) -> Result<String, Error> {
        let material = self.ed25519()?;
        let digest = Sha256::digest(&material.blob);
        let digest_bytes: &[u8] = digest.as_ref();
        Ok(format!("SHA256:{}", STANDARD_NO_PAD.encode(digest_bytes)))
    }

    pub fn path(&self) -> &str {
        self.path.canonical()
    }
}

pub struct Ed25519Material {
    pub private_key: [u8; 64],
    pub public_key: [u8; 32],
    pub authorized_key: String,
    pub(crate) blob: Vec<u8>,
}

fn encode_uleb128(mut x: usize) -> Vec<u8> {
    let mut out = Vec::new();
    loop {
        let mut byte = (x & 0x7f) as u8;
        x >>= 7;
        if x != 0 {
            byte |= 0x80;
        }
        out.push(byte);
        if x == 0 {
            break;
        }
    }
    out
}

fn ssh_public_blob(public: &[u8; 32]) -> Vec<u8> {
    let mut blob = Vec::with_capacity(4 + 11 + 4 + 32);
    append_ssh_string(&mut blob, b"ssh-ed25519");
    append_ssh_string(&mut blob, public);
    blob
}

fn append_ssh_string(buf: &mut Vec<u8>, data: &[u8]) {
    let len = data.len() as u32;
    buf.extend_from_slice(&len.to_be_bytes());
    buf.extend_from_slice(data);
}

#[cfg(test)]
mod tests {
    use super::*;
    use hex::ToHex;

    #[test]
    fn master_node_deterministic() {
        let seed = b"test-seed-for-hdpki";
        let node = Node::master(seed).unwrap();
        let material = node.ed25519().unwrap();
        assert_eq!(material.private_key.len(), 64);
        assert_eq!(material.public_key.len(), 32);
        assert!(material.authorized_key.starts_with("ssh-ed25519 "));
    }

    #[test]
    fn derive_node_deterministic() {
        let seed = b"another-seed-value-for-testing";
        let node = Node::master(seed).unwrap();
        let child_path = "m/ssh/user:alice/example.com/bastion/1700000000";
        let child = node.derive(child_path).unwrap();
        let child2 = node.derive(child_path).unwrap();

        assert_eq!(child.path(), child2.path());
        let mat1 = child.ed25519().unwrap();
        let mat2 = child2.ed25519().unwrap();
        assert_eq!(
            mat1.private_key.encode_hex::<String>(),
            mat2.private_key.encode_hex::<String>()
        );
        assert_eq!(
            mat1.public_key.encode_hex::<String>(),
            mat2.public_key.encode_hex::<String>()
        );
    }

    #[test]
    fn derive_raw_allows_nonstandard_path() {
        let seed = b"raw-path-seed-value-for-testing";
        let node = Node::master(seed).unwrap();
        let raw_path = "lala/1234";
        let child = node.derive_raw(raw_path).unwrap();
        let child2 = node.derive_raw(raw_path).unwrap();

        assert_eq!(child.path(), raw_path);
        assert_eq!(child2.path(), raw_path);

        let mat1 = child.ed25519().unwrap();
        let mat2 = child2.ed25519().unwrap();
        assert_eq!(
            mat1.private_key.encode_hex::<String>(),
            mat2.private_key.encode_hex::<String>()
        );
        assert_eq!(
            mat1.public_key.encode_hex::<String>(),
            mat2.public_key.encode_hex::<String>()
        );
    }

    #[test]
    fn derive_invalid_ancestor() {
        let seed = b"seed-123-unsupported-length";
        let node = Node::master(seed).unwrap();
        let err = node.derive("m/https/user/example.com/host").unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("must contain ssh scheme"), "{msg}");
    }
}
