#![allow(unused)]
use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor};

use futures_executor::block_on;
use futures_util::lock::Mutex;

use ethers_core::{
    types::{
        transaction::{eip2718::TypedTransaction, eip712::Eip712},
        Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
    },
    utils::keccak256,
};
use home;
use std::{
    convert::TryFrom,
    env, fs,
    io::{Read, Write},
    path,
    path::PathBuf,
};
use thiserror::Error;

use super::types::*;

/// A Trezor Ethereum App.
///
/// This is a simple wrapper around the [Trezor transport](Trezor)
#[derive(Debug)]
pub struct TrezorEthereum {
    derivation: DerivationType,
    session_id: Vec<u8>,
    cache_dir: PathBuf,
    pub(crate) chain_id: u64,
    pub(crate) address: Address,
}

const FIRMWARE_MIN_VERSION: &str = ">=2.4.2";

// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html
const SESSION_ID_LENGTH: usize = 32;
const SESSION_FILE_NAME: &str = "trezor.session";

impl TrezorEthereum {
    pub async fn new(
        derivation: DerivationType,
        chain_id: u64,
        cache_dir: Option<PathBuf>,
    ) -> Result<Self, TrezorError> {
        let cache_dir = (match cache_dir.or_else(home::home_dir) {
            Some(path) => path,
            None => match env::current_dir() {
                Ok(path) => path,
                Err(e) => return Err(TrezorError::CacheError(e.to_string())),
            },
        })
        .join(".ethers-rs")
        .join("trezor")
        .join("cache");

        let mut blank = Self {
            derivation: derivation.clone(),
            chain_id,
            cache_dir,
            address: Address::from([0_u8; 20]),
            session_id: vec![],
        };

        // Check if reachable
        blank.initate_session()?;
        blank.address = blank.get_address_with_path(&derivation).await?;
        Ok(blank)
    }

    fn check_version(version: String) -> Result<(), TrezorError> {
        let req = semver::VersionReq::parse(FIRMWARE_MIN_VERSION)?;
        let version = semver::Version::parse(&version)?;

        // Enforce firmware version is greater than FIRMWARE_MIN_VERSION
        if !req.matches(&version) {
            return Err(TrezorError::UnsupportedFirmwareVersion(FIRMWARE_MIN_VERSION.to_string()))
        }

        Ok(())
    }

    fn get_cached_session(&self) -> Result<Option<Vec<u8>>, TrezorError> {
        let mut session = [0; SESSION_ID_LENGTH];

        if let Ok(mut file) = fs::File::open(self.cache_dir.join(SESSION_FILE_NAME)) {
            file.read_exact(&mut session).map_err(|e| TrezorError::CacheError(e.to_string()))?;
            Ok(Some(session.to_vec()))
        } else {
            Ok(None)
        }
    }

    fn save_session(&mut self, session_id: Vec<u8>) -> Result<(), TrezorError> {
        fs::create_dir_all(&self.cache_dir).map_err(|e| TrezorError::CacheError(e.to_string()))?;

        let mut file = fs::File::create(self.cache_dir.join(SESSION_FILE_NAME))
            .map_err(|e| TrezorError::CacheError(e.to_string()))?;

        file.write_all(&session_id).map_err(|e| TrezorError::CacheError(e.to_string()))?;

        self.session_id = session_id;
        Ok(())
    }

    fn initate_session(&mut self) -> Result<(), TrezorError> {
        let mut client = trezor_client::unique(false)?;
        client.init_device(self.get_cached_session()?)?;

        let features = client.features().ok_or(TrezorError::FeaturesError)?;

        Self::check_version(format!(
            "{}.{}.{}",
            features.get_major_version(),
            features.get_minor_version(),
            features.get_patch_version()
        ))?;

        self.save_session(features.get_session_id().to_vec())?;

        Ok(())
    }

    /// You need to drop(client) once you're done with it
    fn get_client(&self, session_id: Vec<u8>) -> Result<Trezor, TrezorError> {
        let mut client = trezor_client::unique(false)?;
        client.init_device(Some(session_id))?;
        Ok(client)
    }

    /// Get the account which corresponds to our derivation path
    pub async fn get_address(&self) -> Result<Address, TrezorError> {
        self.get_address_with_path(&self.derivation).await
    }

    /// Gets the account which corresponds to the provided derivation path
    pub async fn get_address_with_path(
        &self,
        derivation: &DerivationType,
    ) -> Result<Address, TrezorError> {
        let mut client = self.get_client(self.session_id.clone())?;

        let address_str = client.ethereum_get_address(Self::convert_path(derivation))?;

        let mut address = [0; 20];
        address.copy_from_slice(&hex::decode(&address_str[2..])?);

        Ok(Address::from(address))
    }

    /// Signs an Ethereum transaction (requires confirmation on the Trezor)
    pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, TrezorError> {
        let mut client = self.get_client(self.session_id.clone())?;

        let arr_path = Self::convert_path(&self.derivation);

        let transaction = TrezorTransaction::load(tx)?;

        let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id);

        let signature = match tx {
            TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx(
                arr_path,
                transaction.nonce,
                transaction.gas_price,
                transaction.gas,
                transaction.to,
                transaction.value,
                transaction.data,
                chain_id,
            )?,
            TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx(
                arr_path,
                transaction.nonce,
                transaction.gas,
                transaction.to,
                transaction.value,
                transaction.data,
                chain_id,
                transaction.max_fee_per_gas,
                transaction.max_priority_fee_per_gas,
                transaction.access_list,
            )?,
        };

        Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
    }

    /// Signs an ethereum personal message
    pub async fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, TrezorError> {
        let message = message.as_ref();
        let mut client = self.get_client(self.session_id.clone())?;
        let apath = Self::convert_path(&self.derivation);

        let signature = client.ethereum_sign_message(message.into(), apath)?;

        Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
    }

    /// Signs an EIP712 encoded domain separator and message
    pub async fn sign_typed_struct<T>(&self, payload: &T) -> Result<Signature, TrezorError>
    where
        T: Eip712,
    {
        unimplemented!()
    }

    // helper which converts a derivation path to [u32]
    fn convert_path(derivation: &DerivationType) -> Vec<u32> {
        let derivation = derivation.to_string();
        let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
        let depth = elements.len();

        let mut path = vec![];
        for derivation_index in elements {
            let hardened = derivation_index.contains('\'');
            let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
            if hardened {
                index |= 0x80000000;
            }
            path.push(index);
        }

        path
    }
}

#[cfg(all(test, feature = "trezor"))]
mod tests {
    use super::*;
    use crate::Signer;
    use ethers_contract::EthAbiType;
    use ethers_core::types::{
        transaction::{
            eip2930::{AccessList, AccessListItem},
            eip712::Eip712,
        },
        Address, Eip1559TransactionRequest, TransactionRequest, I256, U256,
    };
    use ethers_derive_eip712::*;
    use std::str::FromStr;

    #[derive(Debug, Clone, Eip712, EthAbiType)]
    #[eip712(
        name = "Eip712Test",
        version = "1",
        chain_id = 1,
        verifying_contract = "0x0000000000000000000000000000000000000001",
        salt = "eip712-test-75F0CCte"
    )]
    struct FooBar {
        foo: I256,
        bar: U256,
        fizz: Vec<u8>,
        buzz: [u8; 32],
        far: String,
        out: Address,
    }

    #[tokio::test]
    #[ignore]
    // Replace this with your ETH addresses.
    async fn test_get_address() {
        // Instantiate it with the default trezor derivation path
        let trezor =
            TrezorEthereum::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir")))
                .await
                .unwrap();
        assert_eq!(
            trezor.get_address().await.unwrap(),
            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
        );
        assert_eq!(
            trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(),
            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
        );
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_tx() {
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();

        // approve uni v2 router 0xff
        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();

        let tx_req = TransactionRequest::new()
            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
            .gas(1000000)
            .gas_price(400e9 as u64)
            .nonce(5)
            .data(data)
            .value(ethers_core::utils::parse_ether(100).unwrap())
            .into();
        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_big_data_tx() {
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();

        // invalid data
        let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").unwrap();
        let tx_req = TransactionRequest::new()
            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
            .gas(1000000)
            .gas_price(400e9 as u64)
            .nonce(5)
            .data(big_data)
            .value(ethers_core::utils::parse_ether(100).unwrap())
            .into();
        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_empty_txes() {
        // Contract creation (empty `to`), requires data.
        // To test without the data field, we need to specify a `to` address.
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
        {
            let tx_req = Eip1559TransactionRequest::new()
                .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
                .into();
            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
        }
        {
            let tx_req = TransactionRequest::new()
                .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
                .into();
            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
        }

        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();

        // Contract creation (empty `to`, with data) should show on the trezor device as:
        //  ` "0 Wei ETH
        //  ` new contract?"
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
        {
            let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into();
            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
        }
        {
            let tx_req = TransactionRequest::new().data(data.clone()).into();
            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
        }
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_eip1559_tx() {
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();

        // approve uni v2 router 0xff
        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();

        let lst = AccessList(vec![
            AccessListItem {
                address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(),
                storage_keys: vec![
                    "0x0000000000000000000000000000000000000000000000000000000000000000"
                        .parse()
                        .unwrap(),
                    "0x0000000000000000000000000000000000000000000000000000000000000042"
                        .parse()
                        .unwrap(),
                ],
            },
            AccessListItem {
                address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(),
                storage_keys: vec![
                    "0x0000000000000000000000000000000000000000000000000000000000000000"
                        .parse()
                        .unwrap(),
                    "0x0000000000000000000000000000000000000000000000000000000000000042"
                        .parse()
                        .unwrap(),
                ],
            },
        ]);

        let tx_req = Eip1559TransactionRequest::new()
            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
            .gas(1000000)
            .max_fee_per_gas(400e9 as u64)
            .max_priority_fee_per_gas(400e9 as u64)
            .nonce(5)
            .data(data)
            .access_list(lst)
            .value(ethers_core::utils::parse_ether(100).unwrap())
            .into();

        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_message() {
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
        let message = "hello world";
        let sig = trezor.sign_message(message).await.unwrap();
        let addr = trezor.get_address().await.unwrap();
        sig.verify(message, addr).unwrap();
    }

    #[tokio::test]
    #[ignore]
    async fn test_sign_eip712_struct() {
        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1u64, None).await.unwrap();

        let foo_bar = FooBar {
            foo: I256::from(10),
            bar: U256::from(20),
            fizz: b"fizz".to_vec(),
            buzz: keccak256("buzz"),
            far: String::from("space"),
            out: Address::from([0; 20]),
        };

        let sig = trezor.sign_typed_struct(&foo_bar).await.expect("failed to sign typed data");
        let foo_bar_hash = foo_bar.encode_eip712().unwrap();
        sig.verify(foo_bar_hash, trezor.address).unwrap();
    }
}
