use std::io::Read as _;

use num_enum::TryFromPrimitive as _;
use uuid::Uuid;

use crate::{FAMILY_ID, MANUFACTURER_ID, SysexOp};

#[derive(Debug, Clone)]
pub enum Response {
    Diag,
    PutResp {
        patch_id: Uuid,
        private_key: crypto::PrivateKey,
    },
    GetResp {
        public_key: crypto::PublicKey,
        encrypted_code: crypto::Ciphertext,
    },
    RenderHashResp {
        resulting_hash: [u8; 32],
    },
    GetCryptoParamsResp {
        modulus: crypto::U512,
        base: crypto::U512,
    },
    Error(String),
}

pub fn parse_sysex_response(
    frame: &[u8],
    crypto_params: Option<&'static crypto::CryptoParams>,
) -> anyhow::Result<Response> {
    if frame.len() < 5 || frame[0] != 0xF0 || frame[frame.len() - 1] != 0xF7 {
        return Err(anyhow::anyhow!("invalid SysEx frame"));
    }
    if frame[1] != MANUFACTURER_ID || frame[2] != FAMILY_ID[0] || frame[3] != FAMILY_ID[1] {
        return Err(anyhow::anyhow!("wrong manufacturer"));
    }
    let op = frame[4];
    let mut rdr = std::io::Cursor::new(&frame[5..frame.len() - 1]);
    match SysexOp::try_from_primitive(op).map_err(|_| anyhow::anyhow!("unknown op"))? {
        SysexOp::DiagResponse => {
            let mut data = Vec::new();
            rdr.read_to_end(&mut data)?;
            if data == b"SONOBANK_RUST" {
                Ok(Response::Diag)
            } else {
                anyhow::bail!("unexpected diag payload")
            }
        }
        SysexOp::PutResp => {
            let crypto_params = crypto_params.ok_or(anyhow::anyhow!(
                "Crypto params unknown, can't decode crypto packet"
            ))?;

            let patch_id = {
                let mut id_bytes = [0u8; 16];
                rdr.read_exact(&mut id_bytes)?;
                Uuid::from_bytes(id_bytes)
            };

            let k = {
                let mut key_bytes = [0u8; 64];
                rdr.read_exact(&mut key_bytes)?;
                crypto::U512::from_be_slice(&key_bytes)
            };
            let private_key = crypto::PrivateKey::from_k(crypto_params, k)?;

            if rdr.position() != rdr.get_ref().len() as u64 {
                anyhow::bail!("trailing extra data");
            }

            Ok(Response::PutResp {
                patch_id,
                private_key,
            })
        }
        SysexOp::GetResp => {
            let crypto_params = crypto_params.ok_or(anyhow::anyhow!(
                "Crypto params unknown, can't decode crypto packet"
            ))?;

            let h = {
                let mut h_bytes = [0u8; 64];
                rdr.read_exact(&mut h_bytes)?;
                let h_value = crypto::U512::from_be_slice(&h_bytes);
                crypto_params.ring().reduce(h_value.resize())
            };
            let public_key = crypto::PublicKey::from_h(crypto_params, h)?;

            let c1 = {
                let mut c1_bytes = [0u8; 64];
                rdr.read_exact(&mut c1_bytes)?;
                let c1_value = crypto::U512::from_be_slice(&c1_bytes);
                crypto_params.ring().reduce(c1_value.resize())
            };

            let iv = {
                let mut iv_bytes = [0u8; 16];
                rdr.read_exact(&mut iv_bytes)?;
                iv_bytes
            };

            let mut ct = Vec::new();
            rdr.read_to_end(&mut ct)?;
            let encrypted_code = crypto::Ciphertext { c1, iv, ct };

            Ok(Response::GetResp {
                public_key,
                encrypted_code,
            })
        }
        SysexOp::RenderHashResp => {
            let resulting_hash = {
                let mut hash_bytes = [0u8; 32];
                rdr.read_exact(&mut hash_bytes)?;
                hash_bytes
            };

            if rdr.position() != rdr.get_ref().len() as u64 {
                anyhow::bail!("trailing extra data");
            }

            Ok(Response::RenderHashResp { resulting_hash })
        }
        SysexOp::GetCryptoParamsRes => {
            let modulus = {
                let mut modulus_bytes = [0u8; 64];
                rdr.read_exact(&mut modulus_bytes)?;
                crypto::U512::from_be_slice(&modulus_bytes)
            };

            let base = {
                let mut base_bytes = [0u8; 64];
                rdr.read_exact(&mut base_bytes)?;
                crypto::U512::from_be_slice(&base_bytes)
            };

            if rdr.position() != rdr.get_ref().len() as u64 {
                anyhow::bail!("trailing extra data");
            }

            Ok(Response::GetCryptoParamsResp { modulus, base })
        }
        SysexOp::Error => {
            let mut data = Vec::new();
            rdr.read_to_end(&mut data)?;
            let message = String::from_utf8(data)?;
            Ok(Response::Error(message))
        }
        _ => anyhow::bail!("unexpected op"),
    }
}

impl Response {
    pub fn opcode(&self) -> SysexOp {
        match self {
            Response::Diag => SysexOp::DiagResponse,
            Response::PutResp { .. } => SysexOp::PutResp,
            Response::GetResp { .. } => SysexOp::GetResp,
            Response::RenderHashResp { .. } => SysexOp::RenderHashResp,
            Response::GetCryptoParamsResp { .. } => SysexOp::GetCryptoParamsRes,
            Response::Error(_) => SysexOp::Error,
        }
    }

    pub fn encode(&self) -> Vec<u8> {
        let mut out = vec![
            0xF0,
            MANUFACTURER_ID,
            FAMILY_ID[0],
            FAMILY_ID[1],
            self.opcode() as u8,
        ];
        match self {
            Response::Diag => {
                out.extend_from_slice(b"SONOBANK_RUST");
            }
            Response::PutResp {
                patch_id,
                private_key,
            } => {
                out.extend_from_slice(patch_id.as_bytes());
                out.extend_from_slice(&private_key.k().to_be_bytes());
            }
            Response::GetResp {
                public_key,
                encrypted_code,
            } => {
                out.extend_from_slice(&public_key.h().value().to_be_bytes());
                out.extend_from_slice(&encrypted_code.c1.value().to_be_bytes());
                out.extend_from_slice(&encrypted_code.iv);
                out.extend_from_slice(&encrypted_code.ct);
            }
            Response::RenderHashResp { resulting_hash } => {
                out.extend_from_slice(resulting_hash);
            }
            Response::GetCryptoParamsResp { modulus, base } => {
                out.extend_from_slice(&modulus.to_be_bytes());
                out.extend_from_slice(&base.to_be_bytes());
            }
            Response::Error(message) => {
                out.extend_from_slice(message.as_bytes());
            }
        }
        out.push(0xF7);
        out
    }
}
