use serde::{Deserialize, Serialize};

use crate::address;
use crate::crypto;
use crate::error::BrowserCoreError;
use crate::SIGNATURE_LENGTH;

pub const TRANSFER_TYPE: u8 = 2;
pub const COIN_BYTES: usize = 15;
pub const TRANSFER_BYTES: usize = 750;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UnsignedTransfer {
    pub txtype: u8,
    pub time: u32,
    pub value: u64,
    pub coin: String,
    pub nft_series: u32,
    pub sender: String,
    pub receiver: String,
    pub txfee: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignedTransfer {
    pub unsigned_transfer: UnsignedTransfer,
    pub signature: String,
}

#[derive(Debug, Serialize)]
pub struct BrowserTransferOutput {
    pub txtype: u8,
    pub time: u32,
    pub value: String,
    pub coin: String,
    pub nft_series: u32,
    pub sender: String,
    pub receiver: String,
    pub txfee: String,
    pub hash: String,
    pub signature: String,
    pub bytes_hex: String,
}

impl UnsignedTransfer {
    pub fn validate(&self) -> Result<(), BrowserCoreError> {
        if self.txtype != TRANSFER_TYPE {
            return Err(BrowserCoreError::InvalidTransfer(
                "transaction type must be 2".into(),
            ));
        }
        if self.coin.len() != COIN_BYTES || !self.coin.is_ascii() {
            return Err(BrowserCoreError::InvalidTransfer(
                "coin must use the canonical 15-byte padded format".into(),
            ));
        }
        address::short_address_to_bytes(&self.sender)?;
        address::short_address_to_bytes(&self.receiver)?;
        Ok(())
    }

    pub fn json(&self) -> Result<String, BrowserCoreError> {
        serde_json::to_string(self).map_err(|error| BrowserCoreError::Json(error.to_string()))
    }

    pub fn hash(&self) -> Result<Vec<u8>, BrowserCoreError> {
        Ok(crypto::skein_256(self.json()?.as_bytes()))
    }
}

impl SignedTransfer {
    pub fn validate_shape(&self) -> Result<(), BrowserCoreError> {
        self.unsigned_transfer.validate()?;
        let signature =
            hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?;
        if signature.len() != SIGNATURE_LENGTH {
            return Err(BrowserCoreError::InvalidSignature);
        }
        Ok(())
    }

    pub fn to_bytes(&self) -> Result<Vec<u8>, BrowserCoreError> {
        self.validate_shape()?;
        let mut bytes = Vec::with_capacity(TRANSFER_BYTES);
        bytes.push(self.unsigned_transfer.txtype);
        bytes.extend_from_slice(&self.unsigned_transfer.time.to_le_bytes());
        bytes.extend_from_slice(&self.unsigned_transfer.value.to_le_bytes());
        bytes.extend_from_slice(self.unsigned_transfer.coin.as_bytes());
        bytes.extend_from_slice(&self.unsigned_transfer.nft_series.to_le_bytes());
        bytes.extend_from_slice(&address::short_address_to_bytes(
            &self.unsigned_transfer.sender,
        )?);
        bytes.extend_from_slice(&address::short_address_to_bytes(
            &self.unsigned_transfer.receiver,
        )?);
        bytes.extend_from_slice(&self.unsigned_transfer.txfee.to_le_bytes());
        bytes.extend_from_slice(
            &hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?,
        );
        debug_assert_eq!(bytes.len(), TRANSFER_BYTES);
        Ok(bytes)
    }

    pub fn from_bytes(bytes: &[u8]) -> Result<Self, BrowserCoreError> {
        if bytes.len() != TRANSFER_BYTES || bytes[0] != TRANSFER_TYPE {
            return Err(BrowserCoreError::InvalidTransfer(format!(
                "expected a {TRANSFER_BYTES}-byte type-2 transaction"
            )));
        }

        let mut offset = 1;
        let time = read_u32(bytes, &mut offset);
        let value = read_u64(bytes, &mut offset);
        let coin = String::from_utf8(bytes[offset..offset + COIN_BYTES].to_vec())
            .map_err(|_| BrowserCoreError::InvalidTransfer("coin is not UTF-8".into()))?;
        offset += COIN_BYTES;
        let nft_series = read_u32(bytes, &mut offset);
        let sender = address::bytes_to_short_address(&bytes[offset..offset + 22])?;
        offset += 22;
        let receiver = address::bytes_to_short_address(&bytes[offset..offset + 22])?;
        offset += 22;
        let txfee = read_u64(bytes, &mut offset);
        let signature = hex::encode(&bytes[offset..offset + SIGNATURE_LENGTH]);

        let transfer = Self {
            unsigned_transfer: UnsignedTransfer {
                txtype: TRANSFER_TYPE,
                time,
                value,
                coin,
                nft_series,
                sender,
                receiver,
                txfee,
            },
            signature,
        };
        transfer.validate_shape()?;
        Ok(transfer)
    }

    pub fn browser_output(&self) -> Result<BrowserTransferOutput, BrowserCoreError> {
        let bytes_hex = hex::encode(self.to_bytes()?);
        let hash = hex::encode(self.unsigned_transfer.hash()?);
        Ok(BrowserTransferOutput {
            txtype: self.unsigned_transfer.txtype,
            time: self.unsigned_transfer.time,
            value: self.unsigned_transfer.value.to_string(),
            coin: self.unsigned_transfer.coin.clone(),
            nft_series: self.unsigned_transfer.nft_series,
            sender: self.unsigned_transfer.sender.clone(),
            receiver: self.unsigned_transfer.receiver.clone(),
            txfee: self.unsigned_transfer.txfee.to_string(),
            hash,
            signature: self.signature.clone(),
            bytes_hex,
        })
    }
}

fn read_u32(bytes: &[u8], offset: &mut usize) -> u32 {
    let value = u32::from_le_bytes(bytes[*offset..*offset + 4].try_into().unwrap());
    *offset += 4;
    value
}

fn read_u64(bytes: &[u8], offset: &mut usize) -> u64 {
    let value = u64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap());
    *offset += 8;
    value
}

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

    fn address(byte: u8) -> String {
        format!("{}.{}", hex::encode([byte; 20]), crate::address::network_suffix())
    }

    fn unsigned() -> UnsignedTransfer {
        UnsignedTransfer {
            txtype: TRANSFER_TYPE,
            time: 1_700_000_000,
            value: 123_456_789,
            coin: "CLTC           ".to_string(),
            nft_series: 0,
            sender: address(0x11),
            receiver: address(0x22),
            txfee: 1_234_568,
        }
    }

    #[test]
    fn unsigned_json_matches_node_field_order() {
        let serialized = unsigned().json().unwrap();
        assert!(serialized.contains(&format!("1111111111111111111111111111111111111111.{}", crate::address::network_suffix())));
        assert!(serialized.contains(&format!("2222222222222222222222222222222222222222.{}", crate::address::network_suffix())));
        #[cfg(feature = "testnet")]
        assert_eq!(
            hex::encode(unsigned().hash().unwrap()),
            "7e6a1ef0232eee327dd6c18640f85b16e632762a7c424abfc720ee72fb002c32"
        );
    }

    #[test]
    fn transfer_wire_layout_round_trips() {
        let transfer = SignedTransfer {
            unsigned_transfer: unsigned(),
            signature: hex::encode([0x33; SIGNATURE_LENGTH]),
        };
        let bytes = transfer.to_bytes().unwrap();
        assert_eq!(bytes.len(), TRANSFER_BYTES);
        assert_eq!(SignedTransfer::from_bytes(&bytes).unwrap(), transfer);
    }
}
