use near_account_id::AccountId;
use near_sdk_macros::near;

use crate::CryptoHash;
use crate::json_types::Base58CryptoHash;

#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AccountContract {
    None,
    Local(Base58CryptoHash),
    Global(Base58CryptoHash),
    GlobalByAccount(AccountId),
}

#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
#[near(inside_nearsdk, serializers = [
    json,
    borsh(use_discriminant = true),
])]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum GlobalContractId {
    #[serde(rename = "hash")]
    CodeHash(Base58CryptoHash) = 0,
    #[serde(rename = "account_id")]
    AccountId(AccountId) = 1,
}

impl From<CryptoHash> for GlobalContractId {
    #[inline]
    fn from(hash: CryptoHash) -> Self {
        Self::CodeHash(hash.into())
    }
}

impl From<Base58CryptoHash> for GlobalContractId {
    #[inline]
    fn from(hash: Base58CryptoHash) -> Self {
        Self::CodeHash(hash)
    }
}

impl From<AccountId> for GlobalContractId {
    #[inline]
    fn from(account_id: AccountId) -> Self {
        Self::AccountId(account_id)
    }
}

#[cfg(all(
    not(target_arch = "wasm32"),
    any(feature = "unit-testing", feature = "non-contract-usage")
))]
const _: () = {
    use near_primitives_core::{
        account::AccountContract as NearAccountContract,
        global_contract::GlobalContractIdentifier as NearGlobalContractIdentifier,
        hash::CryptoHash,
    };

    impl From<NearAccountContract> for AccountContract {
        fn from(value: NearAccountContract) -> Self {
            match value {
                NearAccountContract::None => Self::None,
                NearAccountContract::Local(contract) => Self::Local(contract.0.into()),
                NearAccountContract::Global(contract) => Self::Global(contract.0.into()),
                NearAccountContract::GlobalByAccount(account_id) => {
                    Self::GlobalByAccount(account_id)
                }
            }
        }
    }

    impl From<AccountContract> for NearAccountContract {
        fn from(value: AccountContract) -> Self {
            match value {
                AccountContract::None => Self::None,
                AccountContract::Local(contract) => Self::Local(CryptoHash(contract.into())),
                AccountContract::Global(contract) => Self::Global(CryptoHash(contract.into())),
                AccountContract::GlobalByAccount(account_id) => Self::GlobalByAccount(account_id),
            }
        }
    }

    impl From<NearGlobalContractIdentifier> for GlobalContractId {
        fn from(value: NearGlobalContractIdentifier) -> Self {
            match value {
                NearGlobalContractIdentifier::CodeHash(code_hash) => {
                    Self::CodeHash(code_hash.0.into())
                }
                NearGlobalContractIdentifier::AccountId(account_id) => Self::AccountId(account_id),
            }
        }
    }

    impl From<GlobalContractId> for NearGlobalContractIdentifier {
        fn from(value: GlobalContractId) -> Self {
            match value {
                GlobalContractId::CodeHash(code_hash) => {
                    Self::CodeHash(CryptoHash(code_hash.into()))
                }
                GlobalContractId::AccountId(account_id) => Self::AccountId(account_id),
            }
        }
    }
};

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

    #[test]
    fn test_global_contract_id_json_serialization_code_hash() {
        let hash: Base58CryptoHash =
            "4reLvkAWfqk5fsqio1KLudk46cqRz9erQdaHkWZKMJDZ".parse().unwrap();
        let id = GlobalContractId::CodeHash(hash);

        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, r#"{"hash":"4reLvkAWfqk5fsqio1KLudk46cqRz9erQdaHkWZKMJDZ"}"#);

        let deserialized: GlobalContractId = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, id);
    }

    #[test]
    fn test_global_contract_id_json_serialization_account_id() {
        let account_id: AccountId = "alice.near".parse().unwrap();
        let id = GlobalContractId::AccountId(account_id.clone());

        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, r#"{"account_id":"alice.near"}"#);

        let deserialized: GlobalContractId = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, id);
    }
}
