use core::cmp::min;
use std::rc::Rc;
use crate::{
    budget::AsBudget,
    err,
    host::metered_clone::{MeteredAlloc, MeteredClone},
    storage::{InstanceStorageMap, Storage},
    xdr::{
        AccountEntry, AccountId, Asset, BytesM, ContractCodeEntry, ContractDataDurability,
        ContractDataEntry, ContractExecutable, ContractIdPreimage, ExtensionPoint, Hash,
        HashIdPreimage, HashIdPreimageContractId, LedgerEntry, LedgerEntryData, LedgerEntryExt,
        LedgerKey, LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyContractData,
        LedgerKeyTrustLine, PublicKey, ScAddress, ScContractInstance, ScErrorCode, ScErrorType,
        ScMap, ScVal, Signer, SignerKey, ThresholdIndexes, TrustLineAsset, Uint256,
    },
    AddressObject, Env, Host, HostError, StorageType, U32Val, Val,
};
impl Host {
    pub fn with_mut_storage<F, U>(&self, f: F) -> Result<U, HostError>
    where
        F: FnOnce(&mut Storage) -> Result<U, HostError>,
    {
        f(&mut *self.try_borrow_storage_mut()?)
    }
    pub(crate) fn with_instance_storage<F, U>(&self, f: F) -> Result<U, HostError>
    where
        F: FnOnce(&InstanceStorageMap) -> Result<U, HostError>,
    {
        self.with_current_context_mut(|ctx| {
            self.maybe_init_instance_storage(ctx)?;
            f(ctx.storage.as_ref().ok_or_else(|| {
                self.err(
                    ScErrorType::Context,
                    ScErrorCode::InternalError,
                    "missing instance storage",
                    &[],
                )
            })?)
        })
    }
    pub(crate) fn with_mut_instance_storage<F, U>(&self, f: F) -> Result<U, HostError>
    where
        F: FnOnce(&mut InstanceStorageMap) -> Result<U, HostError>,
    {
        self.with_current_context_mut(|ctx| {
            self.maybe_init_instance_storage(ctx)?;
            let storage = ctx.storage.as_mut().ok_or_else(|| {
                self.err(
                    ScErrorType::Context,
                    ScErrorCode::InternalError,
                    "missing instance storage",
                    &[],
                )
            })?;
            storage.is_modified = true;
            f(storage)
        })
    }
    pub(crate) fn contract_instance_ledger_key(
        &self,
        contract_id: &Hash,
    ) -> Result<Rc<LedgerKey>, HostError> {
        let contract_id = contract_id.metered_clone(self)?;
        Rc::metered_new(
            LedgerKey::ContractData(LedgerKeyContractData {
                key: ScVal::LedgerKeyContractInstance,
                durability: ContractDataDurability::Persistent,
                contract: ScAddress::Contract(contract_id),
            }),
            self,
        )
    }
    pub(crate) fn retrieve_contract_instance_from_storage(
        &self,
        key: &Rc<LedgerKey>,
    ) -> Result<ScContractInstance, HostError> {
        let entry = self.try_borrow_storage_mut()?.get(key, self.as_budget())?;
        match &entry.data {
            LedgerEntryData::ContractData(e) => match &e.val {
                ScVal::ContractInstance(instance) => instance.metered_clone(self),
                other => Err(err!(
                    self,
                    (ScErrorType::Storage, ScErrorCode::InternalError),
                    "ledger entry for contract instance does not contain contract instance",
                    *other
                )),
            },
            _ => Err(self.err(
                ScErrorType::Storage,
                ScErrorCode::InternalError,
                "expected ContractData ledger entry",
                &[],
            )),
        }
    }
    pub(crate) fn contract_code_ledger_key(
        &self,
        wasm_hash: &Hash,
    ) -> Result<Rc<LedgerKey>, HostError> {
        let wasm_hash = wasm_hash.metered_clone(self)?;
        Rc::metered_new(
            LedgerKey::ContractCode(LedgerKeyContractCode { hash: wasm_hash }),
            self,
        )
    }
    pub(crate) fn retrieve_wasm_from_storage(&self, wasm_hash: &Hash) -> Result<BytesM, HostError> {
        let key = self.contract_code_ledger_key(wasm_hash)?;
        match &self
            .try_borrow_storage_mut()?
            .get(&key, self.as_budget())
            .map_err(|e| self.decorate_contract_code_storage_error(e, wasm_hash))?
            .data
        {
            LedgerEntryData::ContractCode(e) => e.code.metered_clone(self),
            _ => Err(err!(
                self,
                (ScErrorType::Storage, ScErrorCode::InternalError),
                "expected ContractCode ledger entry",
                *wasm_hash
            )),
        }
    }
    pub(crate) fn wasm_exists(&self, wasm_hash: &Hash) -> Result<bool, HostError> {
        let key = self.contract_code_ledger_key(wasm_hash)?;
        self.try_borrow_storage_mut()?
            .has(&key, self.as_budget())
            .map_err(|e| self.decorate_contract_code_storage_error(e, wasm_hash))
    }
    pub(crate) fn store_contract_instance(
        &self,
        executable: Option<ContractExecutable>,
        instance_storage: Option<ScMap>,
        contract_id: Hash,
        key: &Rc<LedgerKey>,
    ) -> Result<(), HostError> {
        if self
            .try_borrow_storage_mut()?
            .has(key, self.as_budget())
            .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))?
        {
            let (current, live_until_ledger) = self
                .try_borrow_storage_mut()?
                .get_with_live_until_ledger(key, self.as_budget())?;
            let mut current = (*current).metered_clone(self)?;
            if let LedgerEntryData::ContractData(ref mut entry) = current.data {
                if let ScVal::ContractInstance(ref mut instance) = entry.val {
                    if let Some(executable) = executable {
                        instance.executable = executable;
                    }
                    if let Some(storage) = instance_storage {
                        instance.storage = Some(storage);
                    }
                } else {
                    return Err(self.err(
                        ScErrorType::Storage,
                        ScErrorCode::InternalError,
                        "expected ScVal::ContractInstance for contract instance",
                        &[],
                    ));
                }
            } else {
                return Err(self.err(
                    ScErrorType::Storage,
                    ScErrorCode::InternalError,
                    "expected DataEntry for contract instance",
                    &[],
                ));
            }
            self.try_borrow_storage_mut()?
                .put(
                    &key,
                    &Rc::metered_new(current, self)?,
                    live_until_ledger,
                    self.as_budget(),
                )
                .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))?;
        } else {
            let data = ContractDataEntry {
                contract: ScAddress::Contract(contract_id.metered_clone(self)?),
                key: ScVal::LedgerKeyContractInstance,
                val: ScVal::ContractInstance(ScContractInstance {
                    executable: executable.ok_or_else(|| {
                        self.err(
                            ScErrorType::Context,
                            ScErrorCode::InternalError,
                            "can't initialize contract without executable",
                            &[],
                        )
                    })?,
                    storage: instance_storage,
                }),
                durability: ContractDataDurability::Persistent,
                ext: ExtensionPoint::V0,
            };
            self.try_borrow_storage_mut()?
                .put(
                    key,
                    &Host::new_contract_data(self, data)?,
                    Some(self.get_min_live_until_ledger(ContractDataDurability::Persistent)?),
                    self.as_budget(),
                )
                .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))?;
        }
        Ok(())
    }
    pub(crate) fn extend_contract_instance_and_code_ttl_from_contract_id(
        &self,
        contract_id: &Hash,
        threshold: u32,
        extend_to: u32,
    ) -> Result<(), HostError> {
        let key = self.contract_instance_ledger_key(&contract_id)?;
        self.try_borrow_storage_mut()?
            .extend_ttl(self, key.metered_clone(self)?, threshold, extend_to)
            .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))?;
        match self
            .retrieve_contract_instance_from_storage(&key)?
            .executable
        {
            ContractExecutable::Wasm(wasm_hash) => {
                let key = self.contract_code_ledger_key(&wasm_hash)?;
                self.try_borrow_storage_mut()?
                    .extend_ttl(self, key, threshold, extend_to)
                    .map_err(|e| self.decorate_contract_code_storage_error(e, &wasm_hash))?;
            }
            ContractExecutable::StellarAsset => {}
        }
        Ok(())
    }
    pub(crate) fn get_full_contract_id_preimage(
        &self,
        init_preimage: ContractIdPreimage,
    ) -> Result<HashIdPreimage, HostError> {
        Ok(HashIdPreimage::ContractId(HashIdPreimageContractId {
            network_id: self
                .hash_from_bytesobj_input("network_id", self.get_ledger_network_id()?)?,
            contract_id_preimage: init_preimage,
        }))
    }
    pub(crate) fn load_account(&self, account_id: AccountId) -> Result<AccountEntry, HostError> {
        let acc = self.to_account_key(account_id)?;
        self.with_mut_storage(|storage| {
            match &storage
                .get(&acc, self.as_budget())
                .map_err(|e| {
                    self.decorate_account_footprint_error(
                        e,
                        &acc,
                        "trying to access account entry outside of the footprint",
                    )
                })?
                .data
            {
                LedgerEntryData::Account(ae) => ae.metered_clone(self),
                e => Err(err!(
                    self,
                    (ScErrorType::Storage, ScErrorCode::InternalError),
                    "ledger entry is not account",
                    e.name()
                )),
            }
        })
    }
    pub(crate) fn to_account_key(&self, account_id: AccountId) -> Result<Rc<LedgerKey>, HostError> {
        Rc::metered_new(LedgerKey::Account(LedgerKeyAccount { account_id }), self)
    }
    pub(crate) fn create_asset_4(&self, asset_code: [u8; 4], issuer: AccountId) -> Asset {
        use crate::xdr::{AlphaNum4, AssetCode4};
        Asset::CreditAlphanum4(AlphaNum4 {
            asset_code: AssetCode4(asset_code),
            issuer,
        })
    }
    pub(crate) fn create_asset_12(&self, asset_code: [u8; 12], issuer: AccountId) -> Asset {
        use crate::xdr::{AlphaNum12, AssetCode12};
        Asset::CreditAlphanum12(AlphaNum12 {
            asset_code: AssetCode12(asset_code),
            issuer,
        })
    }
    pub(crate) fn to_trustline_key(
        &self,
        account_id: AccountId,
        asset: TrustLineAsset,
    ) -> Result<Rc<LedgerKey>, HostError> {
        Rc::metered_new(
            LedgerKey::Trustline(LedgerKeyTrustLine { account_id, asset }),
            self,
        )
    }
    pub(crate) fn get_signer_weight_from_account(
        &self,
        target_signer: Uint256,
        account: &AccountEntry,
    ) -> Result<u8, HostError> {
        if account.account_id
            == AccountId(PublicKey::PublicKeyTypeEd25519(
                target_signer.metered_clone(self)?,
            ))
        {
            let Some(threshold) = account
                .thresholds
                .0
                .get(ThresholdIndexes::MasterWeight as usize)
            else {
                return Err(self.error(
                    (ScErrorType::Value, ScErrorCode::InternalError).into(),
                    "unexpected thresholds-array size",
                    &[],
                ));
            };
            Ok(*threshold)
        } else {
            let signers: &Vec<Signer> = account.signers.as_ref();
            for signer in signers {
                if let SignerKey::Ed25519(ref this_signer) = signer.key {
                    if &target_signer == this_signer {
                        let weight = min(signer.weight, u8::MAX as u32);
                        return weight.try_into().map_err(|_| {
                            self.err(
                                ScErrorType::Auth,
                                ScErrorCode::ArithDomain,
                                "signer weight does not fit in u8",
                                &[U32Val::from(weight).to_val()],
                            )
                        });
                    }
                }
            }
            Ok(0u8)
        }
    }
    pub(crate) fn new_contract_data(
        &self,
        data: ContractDataEntry,
    ) -> Result<Rc<LedgerEntry>, HostError> {
        Rc::metered_new(
            LedgerEntry {
                last_modified_ledger_seq: 0,
                data: LedgerEntryData::ContractData(data),
                ext: LedgerEntryExt::V0,
            },
            self,
        )
    }
    pub(crate) fn new_contract_code(
        &self,
        data: ContractCodeEntry,
    ) -> Result<Rc<LedgerEntry>, HostError> {
        Rc::metered_new(
            LedgerEntry {
                last_modified_ledger_seq: 0,
                data: LedgerEntryData::ContractCode(data),
                ext: LedgerEntryExt::V0,
            },
            self,
        )
    }
    pub(crate) fn modify_ledger_entry_data(
        &self,
        original_entry: &LedgerEntry,
        new_data: LedgerEntryData,
    ) -> Result<Rc<LedgerEntry>, HostError> {
        Rc::metered_new(
            LedgerEntry {
                last_modified_ledger_seq: 0,
                data: new_data,
                ext: original_entry.ext.metered_clone(self)?,
            },
            self,
        )
    }
    pub(crate) fn contract_id_from_scaddress(&self, address: ScAddress) -> Result<Hash, HostError> {
        match address {
            ScAddress::Account(_) => Err(self.err(
                ScErrorType::Object,
                ScErrorCode::InvalidInput,
                "not a contract address",
                &[],
            )),
            ScAddress::Contract(contract_id) => Ok(contract_id),
        }
    }
    pub(crate) fn contract_id_from_address(
        &self,
        address: AddressObject,
    ) -> Result<Hash, HostError> {
        self.visit_obj(address, |addr: &ScAddress| {
            self.contract_id_from_scaddress(addr.metered_clone(self)?)
        })
    }
    pub(super) fn put_contract_data_into_ledger(
        &self,
        k: Val,
        v: Val,
        t: StorageType,
    ) -> Result<(), HostError> {
        let durability: ContractDataDurability = t.try_into()?;
        let key = self.contract_data_key_from_val(k, durability)?;
        if self
            .try_borrow_storage_mut()?
            .has(&key, self.as_budget())
            .map_err(|e| self.decorate_contract_data_storage_error(e, k))?
        {
            let (current, live_until_ledger) = self
                .try_borrow_storage_mut()?
                .get_with_live_until_ledger(&key, self.as_budget())
                .map_err(|e| self.decorate_contract_data_storage_error(e, k))?;
            let mut current = (*current).metered_clone(self)?;
            match current.data {
                LedgerEntryData::ContractData(ref mut entry) => {
                    entry.val = self.from_host_val(v)?;
                }
                _ => {
                    return Err(self.err(
                        ScErrorType::Storage,
                        ScErrorCode::InternalError,
                        "expected DataEntry",
                        &[],
                    ));
                }
            }
            self.try_borrow_storage_mut()?
                .put(
                    &key,
                    &Rc::metered_new(current, self)?,
                    live_until_ledger,
                    self.as_budget(),
                )
                .map_err(|e| self.decorate_contract_data_storage_error(e, k))?;
        } else {
            let data = ContractDataEntry {
                contract: ScAddress::Contract(self.get_current_contract_id_internal()?),
                key: self.from_host_val(k)?,
                val: self.from_host_val(v)?,
                durability,
                ext: ExtensionPoint::V0,
            };
            self.try_borrow_storage_mut()?
                .put(
                    &key,
                    &Host::new_contract_data(self, data)?,
                    Some(self.get_min_live_until_ledger(durability)?),
                    self.as_budget(),
                )
                .map_err(|e| self.decorate_contract_data_storage_error(e, k))?;
        }
        Ok(())
    }
}
#[cfg(any(test, feature = "testutils"))]
use super::crypto;
#[cfg(any(test, feature = "testutils"))]
use crate::storage::{AccessType, Footprint};
#[cfg(any(test, feature = "testutils"))]
impl Host {
    pub fn add_ledger_entry(
        &self,
        key: &Rc<LedgerKey>,
        val: &Rc<soroban_env_common::xdr::LedgerEntry>,
        live_until_ledger: Option<u32>,
    ) -> Result<(), HostError> {
        self.with_mut_storage(|storage| storage.put(key, val, live_until_ledger, self.as_budget()))
    }
    pub fn setup_storage_entry(
        &self,
        key: Rc<LedgerKey>,
        val: Option<(Rc<soroban_env_common::xdr::LedgerEntry>, Option<u32>)>,
        access_type: AccessType,
    ) -> Result<(), HostError> {
        self.with_mut_storage(|storage| {
            storage
                .footprint
                .record_access(&key, access_type, self.as_budget())?;
            storage.map = storage.map.insert(key, val, self.as_budget())?;
            Ok(())
        })
    }
    pub fn setup_storage_footprint(&self, footprint: Footprint) -> Result<(), HostError> {
        for (key, access_type) in footprint.0.map {
            self.setup_storage_entry(key, None, access_type)?;
        }
        Ok(())
    }
    pub(crate) fn is_test_contract_executable(
        &self,
        contract_id: &Hash,
    ) -> Result<bool, HostError> {
        let key = self.contract_instance_ledger_key(contract_id)?;
        let instance = self.retrieve_contract_instance_from_storage(&key)?;
        let test_contract_executable = ContractExecutable::Wasm(
            crypto::sha256_hash_from_bytes(&[], self)?
                .try_into()
                .map_err(|_| {
                    self.err(
                        ScErrorType::Value,
                        ScErrorCode::InternalError,
                        "unexpected hash length",
                        &[],
                    )
                })?,
        );
        Ok(test_contract_executable == instance.executable)
    }
    #[cfg(test)]
    pub(crate) fn create_tl_asset_4(
        &self,
        asset_code: [u8; 4],
        issuer: AccountId,
    ) -> TrustLineAsset {
        use crate::xdr::{AlphaNum4, AssetCode4};
        TrustLineAsset::CreditAlphanum4(AlphaNum4 {
            asset_code: AssetCode4(asset_code),
            issuer,
        })
    }
    #[cfg(test)]
    pub(crate) fn create_tl_asset_12(
        &self,
        asset_code: [u8; 12],
        issuer: AccountId,
    ) -> TrustLineAsset {
        use crate::xdr::{AlphaNum12, AssetCode12};
        TrustLineAsset::CreditAlphanum12(AlphaNum12 {
            asset_code: AssetCode12(asset_code),
            issuer,
        })
    }
}