use crate::{Error, named_registry::PrefixedName, registry::Registry};
use sha2::{Digest, Sha256};
use soroban_rpc as rpc;
use stellar_build::Network;
use stellar_cli::{
    commands::{contract::invoke, txn_result::TxnResult},
    config::{self, UnresolvedContract},
    xdr::{self, WriteXdr as _},
};
use stellar_strkey::ed25519::PublicKey;

pub struct Contract {
    id: stellar_strkey::Contract,
    config: config::Args,
}

impl Contract {
    pub fn new(id: stellar_strkey::Contract, config: &config::Args) -> Self {
        Self {
            id,
            config: config.clone(),
        }
    }

    pub fn rpc_client(&self) -> Result<rpc::Client, config::Error> {
        Ok(rpc::Client::new(&self.config.get_network()?.rpc_url)?)
    }

    pub fn sc_address(&self) -> xdr::ScAddress {
        xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(self.id.0)))
    }

    pub fn id(&self) -> stellar_strkey::Contract {
        self.id
    }

    pub fn build_invoke_cmd(&self, slop: &[&str], view_only: bool) -> invoke::Cmd {
        invoke::Cmd {
            contract_id: UnresolvedContract::Resolved(self.id),
            slop: slop.iter().map(Into::into).collect(),
            config: self.config.clone(),
            send: if view_only {
                invoke::Send::No
            } else {
                invoke::Send::Default
            },
            ..Default::default()
        }
    }

    pub async fn invoke(
        &self,
        slop: &[&str],
        view_only: bool,
    ) -> Result<TxnResult<String>, invoke::Error> {
        self.build_invoke_cmd(slop, view_only)
            .execute(&self.config, false, false)
            .await
    }

    pub async fn invoke_with_result(
        &self,
        slop: &[&str],
        view_only: bool,
    ) -> Result<String, invoke::Error> {
        Ok(self.invoke(slop, view_only).await?.into_result().unwrap())
    }

    pub(crate) fn config(&self) -> &config::Args {
        &self.config
    }
}

pub trait ToSalt {
    fn into_salt(self) -> Salt;
}

pub type Salt = [u8; 32];

impl ToSalt for &str {
    fn into_salt(self) -> Salt {
        Sha256::digest(self.as_bytes()).into()
    }
}

#[derive(Clone)]
pub enum ContractId {
    Resolved(stellar_strkey::Contract),
    Unresolved(stellar_cli::config::UnresolvedContract),
    PreHash(PreHashContractID),
    FromRegistry(PrefixedName),
}

impl ContractId {
    pub async fn resolve_id(
        &self,
        config: &config::Args,
    ) -> Result<stellar_strkey::Contract, Error> {
        let network_passphrase = config.get_network()?.network_passphrase;
        Ok(match self {
            ContractId::Resolved(contract) => *contract,
            ContractId::Unresolved(unresolved_contract) => {
                unresolved_contract.resolve_contract_id(&config.locator, &network_passphrase)?
            }
            ContractId::PreHash(pre_hash_contract_id) => {
                pre_hash_contract_id.id(&network_passphrase.parse()?)
            }
            ContractId::FromRegistry(PrefixedName { channel, name }) => {
                Registry::new(config, channel.as_deref())
                    .await?
                    .fetch_contract_id(name)
                    .await?
            }
        })
    }

    pub async fn resolve_contract(&self, config: &config::Args) -> Result<Contract, Error> {
        Ok(Contract::new(self.resolve_id(config).await?, config))
    }
}

#[derive(Clone, Debug)]
pub struct PreHashContractID {
    pub salt: Salt,
    pub deployer: stellar_strkey::ed25519::PublicKey,
}

impl PreHashContractID {
    pub fn new<T: ToSalt>(deployer: PublicKey, salt: T) -> Self {
        Self {
            salt: salt.into_salt(),
            deployer,
        }
    }

    pub fn id(&self, network_passphrase: &Network) -> stellar_strkey::Contract {
        let network_id = network_passphrase.id().into();
        let preimage = xdr::HashIdPreimage::ContractId(xdr::HashIdPreimageContractId {
            network_id,
            contract_id_preimage: xdr::ContractIdPreimage::Address(
                xdr::ContractIdPreimageFromAddress {
                    address: xdr::ScAddress::Account(xdr::AccountId(
                        xdr::PublicKey::PublicKeyTypeEd25519(self.deployer.0.into()),
                    )),
                    salt: xdr::Uint256(self.salt),
                },
            ),
        });
        let preimage_xdr = preimage
            .to_xdr(xdr::Limits::none())
            .expect("HashIdPreimage should not fail encoding to xdr");
        stellar_strkey::Contract(Sha256::digest(preimage_xdr).into())
    }
}
