use cfg_primitives::AccountId;
use cfg_types::{locations::RestrictedTransferLocation, tokens::FilterCurrency};
use frame_support::{
	traits::{Get, OnRuntimeUpgrade},
	weights::Weight,
};
use pallet_transfer_allowlist::AccountCurrencyTransferAllowance;
use parity_scale_codec::Encode;
use sp_arithmetic::traits::SaturatedConversion;
use sp_core::H256;
use sp_runtime::traits::{BlakeTwo256, Hash};
use sp_std::{boxed::Box, vec::Vec};
use staging_xcm::{v4, VersionedLocation};
mod old {
	use cfg_primitives::AccountId;
	use cfg_types::{domain_address::DomainAddress, tokens::FilterCurrency};
	use frame_support::{pallet_prelude::*, storage_alias};
	use frame_system::pallet_prelude::*;
	use pallet_transfer_allowlist::AllowanceDetails;
	use sp_core::H256;
	use staging_xcm::v3;
	#[derive(
		Clone,
		RuntimeDebugNoBound,
		Encode,
		parity_scale_codec::Decode,
		Eq,
		PartialEq,
		MaxEncodedLen,
		TypeInfo,
	)]
	pub enum RestrictedTransferLocation {
		Local(AccountId),
		Xcm(H256),
		Address(DomainAddress),
	}
	#[storage_alias]
	pub type AccountCurrencyTransferAllowance<T: pallet_transfer_allowlist::Config> = StorageNMap<
		pallet_transfer_allowlist::Pallet<T>,
		(
			NMapKey<Twox64Concat, AccountId>,
			NMapKey<Twox64Concat, FilterCurrency>,
			NMapKey<Blake2_128Concat, RestrictedTransferLocation>,
		),
		AllowanceDetails<BlockNumberFor<T>>,
		OptionQuery,
	>;
	pub fn location_v3_created_by_apps(account_id: AccountId) -> v3::Location {
		v3::Location::new(
			1,
			v3::Junctions::X2(
				v3::Junction::Parachain(1000), v3::Junction::AccountId32 {
					network: None,
					id: account_id.into(),
				},
			),
		)
	}
}
const LOG_PREFIX: &str = "MigrateRestrictedTransferLocation:";
pub struct MigrateRestrictedTransferLocation<T, AccountMap>(
	sp_std::marker::PhantomData<(T, AccountMap)>,
);
impl<T, AccountMap> OnRuntimeUpgrade for MigrateRestrictedTransferLocation<T, AccountMap>
where
	T: pallet_transfer_allowlist::Config<
		AccountId = AccountId,
		CurrencyId = FilterCurrency,
		Location = RestrictedTransferLocation,
	>,
	AccountMap: Get<Vec<(AccountId, AccountId)>>,
{
	fn on_runtime_upgrade() -> Weight {
		log::info!("{LOG_PREFIX} Check keys to migrate...");
		let mut weight = T::DbWeight::get().reads(
			old::AccountCurrencyTransferAllowance::<T>::iter_keys()
				.count()
				.saturated_into(),
		);
		let key_translations = Self::get_key_translations();
		for (acc, currency, old_location, maybe_new_location) in key_translations {
			let old_key = (&acc, ¤cy, &old_location);
			log::info!("{LOG_PREFIX} Removing old key {old_key:?}");
			let value = old::AccountCurrencyTransferAllowance::<T>::get(old_key);
			old::AccountCurrencyTransferAllowance::<T>::remove(old_key);
			if let Some(new_location) = maybe_new_location {
				let new_key = (&acc, ¤cy, &new_location);
				log::info!("{LOG_PREFIX} Adding new key {new_key:?}");
				AccountCurrencyTransferAllowance::<T>::set(new_key, value);
			}
			weight.saturating_accrue(T::DbWeight::get().writes(2));
		}
		weight
	}
	#[cfg(feature = "try-runtime")]
	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
		let key_translations = Self::get_key_translations();
		assert!(
			key_translations
				.iter()
				.all(|(_, _, _, new_location)| new_location.is_some()),
			"At least one XCM location could not be translated"
		);
		let count: u64 = old::AccountCurrencyTransferAllowance::<T>::iter_keys()
			.count()
			.saturated_into();
		log::info!("{LOG_PREFIX} Pre checks done!");
		Ok(count.encode())
	}
	#[cfg(feature = "try-runtime")]
	fn post_upgrade(pre_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
		let count_pre: u64 = parity_scale_codec::Decode::decode(&mut pre_state.as_slice())
			.expect("pre_upgrade provides a valid state; qed");
		let count_post: u64 = AccountCurrencyTransferAllowance::<T>::iter_keys()
			.count()
			.saturated_into();
		assert_eq!(count_pre, count_post, "Number of keys in AccountCurrencyTransferAllowance changed during migration: pre {count_pre} vs post {count_post}");
		log::info!("{LOG_PREFIX} Post checks done!");
		Ok(())
	}
}
impl<T, AccountMap> MigrateRestrictedTransferLocation<T, AccountMap>
where
	T: pallet_transfer_allowlist::Config<
		AccountId = AccountId,
		CurrencyId = FilterCurrency,
		Location = RestrictedTransferLocation,
	>,
	AccountMap: Get<Vec<(AccountId, AccountId)>>,
{
	fn migrate_location_key(
		account_id: AccountId,
		hash: H256,
	) -> Option<RestrictedTransferLocation> {
		let old_location = old::location_v3_created_by_apps(account_id);
		if BlakeTwo256::hash(&old_location.encode()) == hash {
			match v4::Location::try_from(old_location) {
				Ok(location) => {
					log::info!("{LOG_PREFIX} Hash: '{hash}' migrated!");
					let new_restricted_location =
						RestrictedTransferLocation::Xcm(Box::new(VersionedLocation::V4(location)));
					Some(new_restricted_location)
				}
				Err(_) => {
					log::error!("{LOG_PREFIX} Non isometric location v3 -> v4");
					None
				}
			}
		} else {
			log::error!("{LOG_PREFIX} Hash can not be recovered");
			None
		}
	}
	fn get_key_translations() -> Vec<(
		AccountId,
		FilterCurrency,
		old::RestrictedTransferLocation,
		Option<RestrictedTransferLocation>,
	)> {
		let accounts = AccountMap::get();
		old::AccountCurrencyTransferAllowance::<T>::iter_keys()
			.filter_map(|(account_id, currency_id, old_restricted_location)| {
				match (accounts.iter().find(|(key, _)| key == &account_id), old_restricted_location.clone()) {
					(Some((_, transfer_address)), old::RestrictedTransferLocation::Xcm(hash)) => Some((
						account_id.clone(),
						currency_id,
						old_restricted_location,
						Self::migrate_location_key(transfer_address.clone(), hash),
					)),
					(None, old::RestrictedTransferLocation::Xcm(_)) => {
						log::warn!("{LOG_PREFIX} Account {account_id:?} missing in AccountMap despite old XCM location storage");
						Some((
							account_id.clone(),
							currency_id,
							old_restricted_location,
							None,
						))
					}
					_ => None,
				}
			})
			.collect::<Vec<_>>()
	}
}