use std::{fmt, string::FromUtf8Error};

use base64::Engine;
use bech32::{Bech32, Hrp};
use num_bigint::{BigInt, BigUint, Sign};
use prost::bytes::Bytes;
use serde::ser::{SerializeMap, Serializer as _};
use serde::{Deserialize, Serialize};

use crate::generated::proto::Transaction as ProtoTransaction;

const ERD_HRP: Hrp = Hrp::parse_unchecked("erd");

/// JSON transaction shape used by gateway and signing APIs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
    /// The sender's nonce (transaction counter).
    pub nonce: u64,
    /// The value to transfer in atomic units (EGLD wei).
    pub value: String,
    /// The receiver's bech32 address.
    pub receiver: String,
    /// The sender's bech32 address.
    pub sender: String,
    /// Optional base64-encoded sender username.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sender_username: Option<String>,
    /// Optional base64-encoded receiver username.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub receiver_username: Option<String>,
    /// Gas price in atomic units.
    #[serde(rename = "gasPrice")]
    pub gas_price: u64,
    /// Maximum gas units to consume.
    #[serde(rename = "gasLimit")]
    pub gas_limit: u64,
    /// Optional transaction data/payload (base64 or hex encoded on input, base64 on output).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<String>,
    /// Chain identifier (e.g., "1" for mainnet, "D" for devnet).
    #[serde(rename = "chainID")]
    pub chain_id: String,
    /// Transaction version (typically 1 or 2).
    pub version: u32,
    /// Optional transaction options flags.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub options: Option<u32>,
    /// Optional guardian address.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub guardian: Option<String>,
    /// Optional guardian signature (hex-encoded).
    #[serde(
        rename = "guardianSignature",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub guardian_signature: Option<String>,
    /// Optional sender signature (hex-encoded).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
    /// Optional relayer address.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub relayer: Option<String>,
    /// Optional relayer signature (hex-encoded).
    #[serde(
        rename = "relayerSignature",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub relayer_signature: Option<String>,
}

/// Errors returned when converting between JSON and protobuf transaction forms.
#[derive(Debug)]
pub enum ConversionError {
    InvalidBech32(String),
    InvalidAddressLength(usize),
    InvalidNumeric(String),
    InvalidHex(String),
    InvalidBase64(String),
    InvalidUtf8(FromUtf8Error),
    InvalidBigIntEncoding(String),
    Serialization(String),
    Bech32Encode(bech32::EncodeError),
}

impl fmt::Display for ConversionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidBech32(err) => write!(f, "invalid bech32 address: {err}"),
            Self::InvalidAddressLength(len) => {
                write!(f, "invalid address length: expected 32 bytes, got {len}")
            }
            Self::InvalidNumeric(value) => write!(f, "invalid numeric value: {value}"),
            Self::InvalidHex(err) => write!(f, "invalid hex: {err}"),
            Self::InvalidBase64(err) => write!(f, "invalid base64: {err}"),
            Self::InvalidUtf8(err) => write!(f, "invalid utf-8: {err}"),
            Self::InvalidBigIntEncoding(err) => write!(f, "invalid BigIntCaster encoding: {err}"),
            Self::Serialization(err) => write!(f, "serialization failed: {err}"),
            Self::Bech32Encode(err) => write!(f, "bech32 encode failed: {err}"),
        }
    }
}

impl std::error::Error for ConversionError {}

impl From<FromUtf8Error> for ConversionError {
    fn from(value: FromUtf8Error) -> Self {
        Self::InvalidUtf8(value)
    }
}

impl From<bech32::EncodeError> for ConversionError {
    fn from(value: bech32::EncodeError) -> Self {
        Self::Bech32Encode(value)
    }
}

impl TryFrom<&Transaction> for ProtoTransaction {
    type Error = ConversionError;

    fn try_from(tx: &Transaction) -> Result<Self, Self::Error> {
        Ok(Self {
            nonce: tx.nonce,
            value: parse_big_uint(&tx.value)?,
            rcv_addr: decode_bech32(&tx.receiver)?,
            rcv_user_name: decode_optional_base64(tx.receiver_username.as_deref())?,
            snd_addr: decode_bech32(&tx.sender)?,
            snd_user_name: decode_optional_base64(tx.sender_username.as_deref())?,
            gas_price: tx.gas_price,
            gas_limit: tx.gas_limit,
            data: decode_data_field(tx.data.as_deref())?,
            chain_id: Bytes::copy_from_slice(tx.chain_id.as_bytes()),
            version: tx.version,
            signature: decode_optional_hex(tx.signature.as_deref())?,
            options: tx.options.unwrap_or_default(),
            guardian_addr: decode_optional_bech32(tx.guardian.as_deref())?,
            guardian_signature: decode_optional_hex(tx.guardian_signature.as_deref())?,
            relayer_addr: decode_optional_bech32(tx.relayer.as_deref())?,
            relayer_signature: decode_optional_hex(tx.relayer_signature.as_deref())?,
        })
    }
}

impl TryFrom<Transaction> for ProtoTransaction {
    type Error = ConversionError;

    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
        Self::try_from(&tx)
    }
}

impl Transaction {
    /// Serializes the transaction into the canonical JSON payload used for signing.
    pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
        let data_len = self.data.as_ref().map_or(0, String::len);
        let mut buf = Vec::with_capacity(256 + data_len);
        let mut serializer = serde_json::Serializer::new(&mut buf);
        let mut map = serializer.serialize_map(None).map_err(serialize_err)?;

        map.serialize_entry("nonce", &self.nonce)
            .map_err(serialize_err)?;
        map.serialize_entry("value", &self.value)
            .map_err(serialize_err)?;
        map.serialize_entry("receiver", &self.receiver)
            .map_err(serialize_err)?;
        map.serialize_entry("sender", &self.sender)
            .map_err(serialize_err)?;

        if let Some(sender_username) = &self.sender_username
            && !sender_username.is_empty()
        {
            map.serialize_entry("senderUsername", sender_username)
                .map_err(serialize_err)?;
        }
        if let Some(receiver_username) = &self.receiver_username
            && !receiver_username.is_empty()
        {
            map.serialize_entry("receiverUsername", receiver_username)
                .map_err(serialize_err)?;
        }

        map.serialize_entry("gasPrice", &self.gas_price)
            .map_err(serialize_err)?;
        map.serialize_entry("gasLimit", &self.gas_limit)
            .map_err(serialize_err)?;
        if let Some(data) = &self.data {
            map.serialize_entry("data", data).map_err(serialize_err)?;
        }
        map.serialize_entry("chainID", &self.chain_id)
            .map_err(serialize_err)?;
        map.serialize_entry("version", &self.version)
            .map_err(serialize_err)?;
        if let Some(options) = &self.options {
            map.serialize_entry("options", options)
                .map_err(serialize_err)?;
        }
        if let Some(guardian) = &self.guardian {
            map.serialize_entry("guardian", guardian)
                .map_err(serialize_err)?;
        }
        if let Some(relayer) = &self.relayer {
            map.serialize_entry("relayer", relayer)
                .map_err(serialize_err)?;
        }
        map.end().map_err(serialize_err)?;

        Ok(buf)
    }
}

impl TryFrom<&ProtoTransaction> for Transaction {
    type Error = ConversionError;

    fn try_from(tx: &ProtoTransaction) -> Result<Self, Self::Error> {
        let value = decode_big_int_caster(&tx.value)?
            .map_or_else(|| "0".to_owned(), |value| value.to_str_radix(10));

        Ok(Self {
            nonce: tx.nonce,
            value,
            receiver: encode_required_bech32(&tx.rcv_addr)?,
            sender: encode_required_bech32(&tx.snd_addr)?,
            sender_username: encode_optional_base64(&tx.snd_user_name),
            receiver_username: encode_optional_base64(&tx.rcv_user_name),
            gas_price: tx.gas_price,
            gas_limit: tx.gas_limit,
            data: encode_optional_base64(&tx.data),
            chain_id: String::from_utf8(tx.chain_id.to_vec())?,
            version: tx.version,
            options: (tx.options != 0).then_some(tx.options),
            guardian: encode_optional_bech32(&tx.guardian_addr)?,
            guardian_signature: encode_optional_hex(&tx.guardian_signature),
            signature: encode_optional_hex(&tx.signature),
            relayer: encode_optional_bech32(&tx.relayer_addr)?,
            relayer_signature: encode_optional_hex(&tx.relayer_signature),
        })
    }
}

impl TryFrom<ProtoTransaction> for Transaction {
    type Error = ConversionError;

    fn try_from(tx: ProtoTransaction) -> Result<Self, Self::Error> {
        Self::try_from(&tx)
    }
}

impl ProtoTransaction {
    /// Serializes a protobuf transaction into the canonical JSON payload used for signing.
    pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
        Transaction::try_from(self)?.signing_bytes()
    }
}

fn parse_big_uint(value: &str) -> Result<Bytes, ConversionError> {
    let trimmed = value.trim();
    let number = if let Some(hex_body) = trimmed.strip_prefix("0x") {
        BigUint::parse_bytes(hex_body.as_bytes(), 16)
    } else {
        BigUint::parse_bytes(trimmed.as_bytes(), 10)
    };
    let num = number.ok_or_else(|| ConversionError::InvalidNumeric(value.to_owned()))?;
    Ok(encode_big_int_caster(&BigInt::from_biguint(
        Sign::Plus,
        num,
    )))
}

fn encode_big_int_caster(value: &BigInt) -> Bytes {
    let (sign, magnitude) = value.to_bytes_be();
    if magnitude.is_empty() {
        return Bytes::from_static(&[0, 0]);
    }

    let mut encoded = Vec::with_capacity(magnitude.len() + 1);
    encoded.push(match sign {
        Sign::Minus => 1,
        Sign::NoSign | Sign::Plus => 0,
    });
    encoded.extend_from_slice(&magnitude);
    Bytes::from(encoded)
}

fn decode_big_int_caster(bytes: &[u8]) -> Result<Option<BigInt>, ConversionError> {
    match bytes.len() {
        0 => Err(ConversionError::InvalidBigIntEncoding(
            "empty buffer is not a valid BigIntCaster value".to_owned(),
        )),
        1 => {
            if bytes[0] == 0 {
                Ok(None)
            } else {
                Err(ConversionError::InvalidBigIntEncoding(format!(
                    "single-byte encoding must be nil marker 0x00, got 0x{:02x}",
                    bytes[0]
                )))
            }
        }
        _ => {
            let magnitude = BigUint::from_bytes_be(&bytes[1..]);
            let value = match bytes[0] {
                0 => BigInt::from_biguint(Sign::Plus, magnitude),
                1 => BigInt::from_biguint(Sign::Minus, magnitude),
                sign => {
                    return Err(ConversionError::InvalidBigIntEncoding(format!(
                        "invalid sign byte 0x{sign:02x}"
                    )));
                }
            };
            Ok(Some(value))
        }
    }
}

fn decode_bech32(addr: &str) -> Result<Bytes, ConversionError> {
    let (_hrp, raw) =
        bech32::decode(addr).map_err(|e| ConversionError::InvalidBech32(e.to_string()))?;
    if raw.len() != 32 {
        return Err(ConversionError::InvalidAddressLength(raw.len()));
    }
    Ok(Bytes::from(raw))
}

fn decode_optional_bech32(addr: Option<&str>) -> Result<Bytes, ConversionError> {
    match addr {
        Some(a) if !a.trim().is_empty() => decode_bech32(a),
        _ => Ok(Bytes::new()),
    }
}

fn decode_optional_hex(value: Option<&str>) -> Result<Bytes, ConversionError> {
    match value {
        Some(s) if !s.trim().is_empty() => hex::decode(s.trim())
            .map(Bytes::from)
            .map_err(|e| ConversionError::InvalidHex(e.to_string())),
        _ => Ok(Bytes::new()),
    }
}

fn decode_optional_base64(value: Option<&str>) -> Result<Bytes, ConversionError> {
    match value {
        Some(s) if !s.trim().is_empty() => base64::engine::general_purpose::STANDARD
            .decode(s.trim())
            .map(Bytes::from)
            .map_err(|e| ConversionError::InvalidBase64(e.to_string())),
        _ => Ok(Bytes::new()),
    }
}

fn decode_data_field(value: Option<&str>) -> Result<Bytes, ConversionError> {
    match value {
        Some(s) => match base64::engine::general_purpose::STANDARD.decode(s.trim()) {
            Ok(bytes) => Ok(Bytes::from(bytes)),
            Err(_) => hex::decode(s.trim())
                .map(Bytes::from)
                .map_err(|e| ConversionError::InvalidHex(e.to_string())),
        },
        None => Ok(Bytes::new()),
    }
}

fn encode_required_bech32(bytes: &[u8]) -> Result<String, ConversionError> {
    if bytes.len() != 32 {
        return Err(ConversionError::InvalidAddressLength(bytes.len()));
    }
    Ok(bech32::encode::<Bech32>(ERD_HRP, bytes)?)
}

fn encode_optional_bech32(bytes: &[u8]) -> Result<Option<String>, ConversionError> {
    if bytes.is_empty() {
        return Ok(None);
    }
    encode_required_bech32(bytes).map(Some)
}

fn encode_optional_hex(bytes: &[u8]) -> Option<String> {
    (!bytes.is_empty()).then(|| hex::encode(bytes))
}

fn encode_optional_base64(bytes: &[u8]) -> Option<String> {
    (!bytes.is_empty()).then(|| base64::engine::general_purpose::STANDARD.encode(bytes))
}

fn serialize_err(err: serde_json::Error) -> ConversionError {
    ConversionError::Serialization(err.to_string())
}

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

    fn make_json_tx() -> Transaction {
        Transaction {
            nonce: 42,
            value: "1000000000000000000".to_owned(),
            receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l".to_owned(),
            sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th".to_owned(),
            sender_username: Some("c2VuZGVy".to_owned()),
            receiver_username: Some("cmVjZWl2ZXI=".to_owned()),
            gas_price: 1_000_000_000,
            gas_limit: 50_000,
            data: Some("dGVzdA==".to_owned()),
            chain_id: "1".to_owned(),
            version: 2,
            options: Some(1),
            guardian: None,
            guardian_signature: None,
            signature: Some("ab".repeat(64)),
            relayer: None,
            relayer_signature: None,
        }
    }

    #[test]
    fn json_to_proto_converts_fields() {
        let tx = make_json_tx();
        let proto = ProtoTransaction::try_from(&tx).unwrap();

        assert_eq!(proto.nonce, 42);
        assert_eq!(proto.gas_price, 1_000_000_000);
        assert_eq!(proto.gas_limit, 50_000);
        assert_eq!(proto.chain_id.as_ref(), b"1");
        assert_eq!(proto.data.as_ref(), b"test");
        assert_eq!(proto.snd_addr.len(), 32);
        assert_eq!(proto.rcv_addr.len(), 32);
        assert_eq!(proto.value[0], 0);
    }

    #[test]
    fn proto_to_json_roundtrip() {
        let tx = make_json_tx();
        let proto = ProtoTransaction::try_from(&tx).unwrap();
        let roundtrip = Transaction::try_from(&proto).unwrap();

        assert_eq!(roundtrip.nonce, tx.nonce);
        assert_eq!(roundtrip.value, tx.value);
        assert_eq!(roundtrip.receiver, tx.receiver);
        assert_eq!(roundtrip.sender, tx.sender);
        assert_eq!(roundtrip.chain_id, tx.chain_id);
        assert_eq!(roundtrip.data, Some("dGVzdA==".to_owned()));
        assert_eq!(roundtrip.signature, tx.signature);
    }

    #[test]
    fn proto_to_json_zero_value_maps_to_zero_string() {
        let proto = ProtoTransaction {
            value: Bytes::from_static(&[0, 0]),
            chain_id: Bytes::from_static(b"1"),
            rcv_addr: decode_bech32(
                "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l",
            )
            .unwrap(),
            snd_addr: decode_bech32(
                "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
            )
            .unwrap(),
            ..Default::default()
        };

        let json = Transaction::try_from(&proto).unwrap();
        assert_eq!(json.value, "0");
    }

    #[test]
    fn signing_bytes_field_order_matches_protocol() {
        let tx = make_json_tx();
        let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();

        let fields: Vec<&str> = json_str
            .trim_matches(|c| c == '{' || c == '}')
            .split(',')
            .map(|s| s.split(':').next().unwrap().trim().trim_matches('"'))
            .collect();

        assert_eq!(fields[0], "nonce");
        assert_eq!(fields[1], "value");
        assert_eq!(fields[2], "receiver");
        assert_eq!(fields[3], "sender");
        assert_eq!(fields[4], "senderUsername");
        assert_eq!(fields[5], "receiverUsername");
        assert_eq!(fields[6], "gasPrice");
        assert_eq!(fields[7], "gasLimit");
        assert_eq!(fields[8], "data");
        assert_eq!(fields[9], "chainID");
        assert_eq!(fields[10], "version");
        assert_eq!(fields[11], "options");
    }

    #[test]
    fn signing_bytes_omit_signatures_and_include_relayer_when_present() {
        let mut tx = make_json_tx();
        tx.guardian =
            Some("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8".to_owned());
        tx.guardian_signature = Some("cd".repeat(64));
        tx.relayer =
            Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());
        tx.relayer_signature = Some("ef".repeat(64));

        let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();

        assert!(json_str.contains("\"relayer\":"));
        assert!(json_str.contains("\"guardian\":"));
        assert!(!json_str.contains("signature"));
        assert!(!json_str.contains("guardianSignature"));
        assert!(!json_str.contains("relayerSignature"));
    }

    #[test]
    fn signing_bytes_omits_empty_usernames() {
        let mut tx = make_json_tx();
        tx.sender_username = Some(String::new());
        tx.receiver_username = Some(String::new());
        tx.relayer =
            Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());

        let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();

        assert!(json_str.contains("\"relayer\":"));
        assert!(!json_str.contains("senderUsername"));
        assert!(!json_str.contains("receiverUsername"));
    }

    #[test]
    fn proto_signing_bytes_match_json_signing_bytes() {
        let tx = make_json_tx();
        let proto = ProtoTransaction::try_from(&tx).unwrap();

        assert_eq!(proto.signing_bytes().unwrap(), tx.signing_bytes().unwrap());
    }
}
