use cw20::{BalanceResponse, TokenInfoResponse};
use cw20_base::msg::{ExecuteMsg as TokenMsg, QueryMsg as TokenQuery};
use cw20_wrapped_2::msg::{
    ExecuteMsg as WrappedMsg, InitHook, InstantiateMsg as WrappedInit, QueryMsg as WrappedQuery,
    WrappedAssetInfoResponse,
};
use std::{
    cmp::{max, min},
    str::FromStr,
};

use cw_wormhole::{
    byte_utils::{
        extend_address_to_32, extend_address_to_32_array, extend_string_to_32, get_string_from_32,
        ByteUtils,
    },
    error::ContractError,
    msg::{ExecuteMsg as WormholeExecuteMsg, QueryMsg as WormholeQueryMsg},
    state::{vaa_archive_add, vaa_archive_check, GovernancePacket, ParsedVAA},
};

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;

use cosmwasm_std::{
    coin, to_binary, BankMsg, Binary, CanonicalAddr, CosmosMsg, Deps, DepsMut, Empty, Env,
    MessageInfo, QueryRequest, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg,
    WasmQuery,
};

use crate::{
    msg::{
        Asset, AssetInfo, ChainRegistrationResponse, CompleteTransferResponse, ExecuteMsg,
        ExternalIdResponse, InstantiateMsg, IsVaaRedeemedResponse, MigrateMsg, QueryMsg,
        TransferInfoResponse, WrappedRegistryResponse,
    },
    state::{
        bridge_contracts, bridge_contracts_read, bridge_deposit, config, config_read,
        is_wrapped_asset, is_wrapped_asset_read, receive_native, send_native, wrapped_asset,
        wrapped_asset_read, wrapped_asset_seq, wrapped_asset_seq_read, wrapped_transfer_tmp,
        Action, AssetMeta, ConfigInfo, RegisterChain, TokenBridgeMessage, TransferInfo,
        TransferState, TransferWithPayloadInfo, UpgradeContract,
    },
    token_address::{ContractId, ExternalTokenId, TokenId, WrappedCW20},
};

type HumanAddr = String;

pub enum TransferType<A> {
    WithoutPayload,
    WithPayload { payload: A },
}

/// Migration code that runs the next time the contract is upgraded.
/// This function will contain ephemeral code that we want to run once, and thus
/// can (and should be) safely deleted after the upgrade happened successfully.
///
/// Most upgrades won't require any special migration logic. In those cases,
/// this function can safely be implemented as:
/// ```ignore
/// Ok(Response::default())
/// ```
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
    // // This migration adds a new field to the [`ConfigInfo`] struct. The
    // // state stored on chain has the old version, so we first parse it as
    // // [`ConfigInfoLegacy`], then add the new fields, and write it back as [`ConfigInfo`].
    // // Since the only place the contract with the legacy state is deployed is
    // // terra2, we just hardcode the new value here for that chain.

    // // 1. make sure this contract doesn't already have the new ConfigInfo struct
    // // in storage. Note that this check is not strictly necessary, as the
    // // upgrade will only be issued for terra2, and no new chains. However, it is
    // // good practice to ensure that migration code cannot be run twice, which
    // // this check achieves.
    // if config_read(deps.storage).load().is_ok() {
    //     return Err(StdError::generic_err(
    //         "Can't migrate; this contract already has a new ConfigInfo struct",
    //     ));
    // }

    // // 2. parse old state
    // let ConfigInfoLegacy {
    //     gov_chain,
    //     gov_address,
    //     wormhole_contract,
    //     wrapped_asset_code_id,
    // } = config_read_legacy(deps.storage).load()?;

    // // 3. store new state with terra2 values hardcoded
    // let chain_id = 18;
    // let native_denom = "uluna".to_string();
    // let native_symbol = "LUNA".to_string();
    // let native_decimals = 6;

    // let config_info = ConfigInfo {
    //     gov_chain,
    //     gov_address,
    //     wormhole_contract,
    //     wrapped_asset_code_id,
    //     chain_id,
    //     native_denom,
    //     native_symbol,
    //     native_decimals
    // };

    // config(deps.storage).save(&config_info)?;
    Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    // Save general wormhole info
    let state = ConfigInfo {
        gov_chain: msg.gov_chain,
        gov_address: msg.gov_address.into(),
        wormhole_contract: msg.wormhole_contract,
        wrapped_asset_code_id: msg.wrapped_asset_code_id,
        chain_id: msg.chain_id,
        native_denom: msg.native_denom,
        native_symbol: msg.native_symbol,
        native_decimals: msg.native_decimals,
    };
    config(deps.storage).save(&state)?;

    Ok(Response::default())
}

// When CW20 transfers complete, we need to verify the actual amount that is being transferred out
// of the bridge. This is to handle fee tokens where the amount expected to be transferred may be
// less due to burns, fees, etc.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;

    let state = wrapped_transfer_tmp(deps.storage).load()?;
    // NOTE: Reentrancy protection. See note in `handle_initiate_transfer_token`
    // for why this is necessary.
    wrapped_transfer_tmp(deps.storage).remove();

    let token_bridge_message = TokenBridgeMessage::deserialize(&state.message)?;

    let (mut transfer_info, transfer_type) = match token_bridge_message.action {
        Action::TRANSFER => {
            let info = TransferInfo::deserialize(&token_bridge_message.payload)?;
            Ok((info, TransferType::WithoutPayload))
        }
        Action::TRANSFER_WITH_PAYLOAD => {
            let info = TransferWithPayloadInfo::deserialize(&token_bridge_message.payload)?;
            Ok((
                info.as_transfer_info(),
                TransferType::WithPayload {
                    // put both the payload and sender_address into the payload
                    // field here (which we can do, since [`TransferType`] is
                    // parametric)
                    payload: (info.payload, info.sender_address),
                },
            ))
        }
        _ => Err(StdError::generic_err("Unreachable")),
    }?;

    // Fetch CW20 Balance post-transfer.
    let new_balance: BalanceResponse =
        deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
            contract_addr: state.token_address.to_string(),
            msg: to_binary(&TokenQuery::Balance {
                address: env.contract.address.to_string(),
            })?,
        }))?;

    // Actual amount should be the difference in balance of the CW20 account in question to account
    // for fee tokens.
    let multiplier = Uint128::from_str(&state.multiplier)?;
    let real_amount = new_balance.balance - Uint128::from_str(&state.previous_balance)?;
    let real_amount = real_amount / multiplier;

    // If the fee is too large the user would receive nothing.
    if transfer_info.fee.1 > real_amount.u128() {
        return Err(StdError::generic_err("fee greater than sent amount"));
    }

    // Update Wormhole message to correct amount.
    transfer_info.amount.1 = real_amount.u128();

    let token_bridge_message = match transfer_type {
        TransferType::WithoutPayload => TokenBridgeMessage {
            action: Action::TRANSFER,
            payload: transfer_info.serialize(),
        },
        TransferType::WithPayload { payload } => TokenBridgeMessage {
            action: Action::TRANSFER_WITH_PAYLOAD,
            payload: TransferWithPayloadInfo {
                amount: transfer_info.amount,
                token_address: transfer_info.token_address,
                token_chain: transfer_info.token_chain,
                recipient: transfer_info.recipient,
                recipient_chain: transfer_info.recipient_chain,
                sender_address: payload.1,
                payload: payload.0,
            }
            .serialize(),
        },
    };

    // Post Wormhole Message
    let message = CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: cfg.wormhole_contract,
        funds: vec![],
        msg: to_binary(&WormholeExecuteMsg::PostMessage {
            message: Binary::from(token_bridge_message.serialize()),
            nonce: state.nonce,
        })?,
    });

    let external_id = ExternalTokenId::from_native_cw20(&state.token_address)?;
    send_native(deps.storage, &external_id, real_amount)?;
    Ok(Response::default()
        .add_message(message)
        .add_attribute("action", "reply_handler"))
}

fn parse_vaa(deps: Deps, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
    let cfg = config_read(deps.storage).load()?;
    let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
        contract_addr: cfg.wormhole_contract,
        msg: to_binary(&WormholeQueryMsg::VerifyVAA {
            vaa: data.clone(),
            block_time,
        })?,
    }))?;
    Ok(vaa)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    match msg {
        ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data),

        // The following actions are disabled in "shutdown" mode
        #[cfg(feature = "full")]
        ExecuteMsg::RegisterAssetHook {
            chain,
            token_address,
        } => handle_register_asset(deps, env, info, chain, token_address),

        #[cfg(feature = "full")]
        ExecuteMsg::InitiateTransfer {
            asset,
            recipient_chain,
            recipient,
            fee,
            nonce,
        } => handle_initiate_transfer(
            deps,
            env,
            info,
            asset,
            recipient_chain,
            recipient.to_array()?,
            fee,
            TransferType::WithoutPayload,
            nonce,
        ),

        #[cfg(feature = "full")]
        ExecuteMsg::InitiateTransferWithPayload {
            asset,
            recipient_chain,
            recipient,
            fee,
            payload,
            nonce,
        } => handle_initiate_transfer(
            deps,
            env,
            info,
            asset,
            recipient_chain,
            recipient.to_array()?,
            fee,
            TransferType::WithPayload {
                payload: payload.into(),
            },
            nonce,
        ),

        #[cfg(feature = "full")]
        ExecuteMsg::DepositTokens {} => deposit_tokens(deps, env, info),

        #[cfg(feature = "full")]
        ExecuteMsg::WithdrawTokens { asset } => withdraw_tokens(deps, env, info, asset),

        #[cfg(feature = "full")]
        ExecuteMsg::CreateAssetMeta { asset_info, nonce } => {
            handle_create_asset_meta(deps, env, info, asset_info, nonce)
        }

        #[cfg(feature = "full")]
        ExecuteMsg::CompleteTransferWithPayload { data, relayer } => {
            handle_complete_transfer_with_payload(deps, env, info, &data, &relayer)
        }

        // When in "shutdown" mode, we reject any other action
        #[cfg(not(feature = "full"))]
        _ => Err(StdError::generic_err("Invalid during shutdown mode")),
    }
}

fn deposit_tokens(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResult<Response> {
    for coin in info.funds {
        let deposit_key = format!("{}:{}", info.sender, coin.denom);
        bridge_deposit(deps.storage).update(
            deposit_key.as_bytes(),
            |amount: Option<Uint128>| -> StdResult<Uint128> {
                Ok(amount.unwrap_or(Uint128::new(0)) + coin.amount)
            },
        )?;
    }

    Ok(Response::new().add_attribute("action", "deposit_tokens"))
}

fn withdraw_tokens(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    data: AssetInfo,
) -> StdResult<Response> {
    let mut messages: Vec<CosmosMsg> = vec![];
    if let AssetInfo::NativeToken { denom } = data {
        let deposit_key = format!("{}:{}", info.sender, denom);
        let mut deposited_amount: u128 = 0;
        bridge_deposit(deps.storage).update(
            deposit_key.as_bytes(),
            |current: Option<Uint128>| match current {
                Some(v) => {
                    deposited_amount = v.u128();
                    Ok(Uint128::new(0))
                }
                None => Err(StdError::generic_err("no deposit found to withdraw")),
            },
        )?;
        messages.push(CosmosMsg::Bank(BankMsg::Send {
            to_address: info.sender.to_string(),
            amount: vec![coin(deposited_amount, &denom)],
        }));
    }

    Ok(Response::new()
        .add_messages(messages)
        .add_attribute("action", "withdraw_tokens"))
}

/// Handle wrapped asset registration messages
fn handle_register_asset(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    chain: u16,
    token_address: ExternalTokenId,
) -> StdResult<Response> {
    // We need to ensure that this registration request was initiated by the
    // token bridge contract. We do this by checking that the wrapped asset
    // already has an associated sequence number, but no address entry yet.
    // This is a necessary and sufficient condition, because having the sequence
    // number means that [`handle_attest_meta`] has been called, and not having
    // an address entry yet means precisely that the callback hasn't finished
    // yet.

    let _ = wrapped_asset_seq_read(deps.storage, chain)
        .load(&token_address.serialize())
        .map_err(|_| ContractError::RegistrationForbidden.std())?;

    let mut bucket = wrapped_asset(deps.storage, chain);
    let result = bucket.load(&token_address.serialize()).ok();
    if result.is_some() {
        return ContractError::AssetAlreadyRegistered.std_err();
    }

    bucket.save(
        &token_address.serialize(),
        &WrappedCW20 {
            human_address: info.sender.clone(),
        },
    )?;

    let contract_address: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?;
    is_wrapped_asset(deps.storage).save(contract_address.as_slice(), &())?;

    Ok(Response::new()
        .add_attribute("action", "register_asset")
        .add_attribute("token_chain", format!("{chain:?}"))
        .add_attribute("token_address", format!("{token_address:?}"))
        .add_attribute("contract_addr", info.sender))
}

fn handle_attest_meta(
    deps: DepsMut,
    env: Env,
    emitter_chain: u16,
    emitter_address: Vec<u8>,
    sequence: u64,
    data: &Vec<u8>,
) -> StdResult<Response> {
    let meta = AssetMeta::deserialize(data)?;

    let expected_contract =
        bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;

    // must be sent by a registered token bridge contract
    if expected_contract != emitter_address {
        return Err(StdError::generic_err("invalid emitter"));
    }

    let token_id = meta
        .token_address
        .to_token_id(deps.storage, emitter_chain)?;

    let token_address: [u8; 32] = match token_id {
        TokenId::Bank { denom } => Err(StdError::generic_err(format!(
            "{denom} is native to this chain and should not be attested"
        ))),
        TokenId::Contract(ContractId::NativeCW20 { contract_address }) => {
            Err(StdError::generic_err(format!(
                "Contract {contract_address} is native to this chain and should not be attested"
            )))
        }
        TokenId::Contract(ContractId::ForeignToken {
            chain_id: _,
            foreign_address,
        }) => Ok(foreign_address),
    }?;

    let cfg = config_read(deps.storage).load()?;
    // If a CW20 wrapped already exists and this message has a newer sequence ID
    // we allow updating the metadata. If not, we create a brand new token.
    let message = if let Ok(contract) =
        wrapped_asset_read(deps.storage, meta.token_chain).load(token_address.as_slice())
    {
        // Prevent anyone from re-attesting with old VAAs.
        if sequence
            <= wrapped_asset_seq_read(deps.storage, meta.token_chain)
                .load(token_address.as_slice())?
        {
            return Err(StdError::generic_err(
                "this asset has already been attested",
            ));
        }
        CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: contract.into_string(),
            msg: to_binary(&WrappedMsg::UpdateMetadata {
                name: get_string_from_32(&meta.name),
                symbol: get_string_from_32(&meta.symbol),
            })?,
            funds: vec![],
        })
    } else {
        CosmosMsg::Wasm(WasmMsg::Instantiate {
            admin: Some(env.contract.address.clone().into_string()),
            code_id: cfg.wrapped_asset_code_id,
            msg: to_binary(&WrappedInit {
                name: get_string_from_32(&meta.name),
                symbol: get_string_from_32(&meta.symbol),
                asset_chain: meta.token_chain,
                asset_address: token_address.into(),
                decimals: min(meta.decimals, 8u8),
                mint: None,
                init_hook: Some(InitHook {
                    contract_addr: env.contract.address.to_string(),
                    msg: to_binary(&ExecuteMsg::RegisterAssetHook {
                        chain: meta.token_chain,
                        token_address: meta.token_address.clone(),
                    })?,
                }),
            })?,
            funds: vec![],
            label: "Wormhole Wrapped CW20".to_string(),
        })
    };
    wrapped_asset_seq(deps.storage, meta.token_chain).save(&token_address, &sequence)?;
    Ok(Response::new().add_message(message))
}

fn handle_create_asset_meta(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    asset_info: AssetInfo,
    nonce: u32,
) -> StdResult<Response> {
    match asset_info {
        AssetInfo::Token { contract_addr } => {
            handle_create_asset_meta_token(deps, env, info, contract_addr, nonce)
        }
        AssetInfo::NativeToken { ref denom } => {
            handle_create_asset_meta_native_token(deps, env, info, denom.clone(), nonce)
        }
    }
}

fn handle_create_asset_meta_token(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    asset_address: HumanAddr,
    nonce: u32,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;

    let request = QueryRequest::Wasm(WasmQuery::Smart {
        contract_addr: asset_address.clone(),
        msg: to_binary(&TokenQuery::TokenInfo {})?,
    });

    let asset_human = deps.api.addr_validate(&asset_address)?;
    let token_id = TokenId::Contract(ContractId::NativeCW20 {
        contract_address: asset_human,
    });
    let external_id = token_id.store(deps.storage)?;
    let token_info: TokenInfoResponse = deps.querier.query(&request)?;

    let meta: AssetMeta = AssetMeta {
        token_chain: cfg.chain_id,
        token_address: external_id,
        decimals: token_info.decimals,
        symbol: extend_string_to_32(&token_info.symbol),
        name: extend_string_to_32(&token_info.name),
    };

    let token_bridge_message = TokenBridgeMessage {
        action: Action::ATTEST_META,
        payload: meta.serialize().to_vec(),
    };

    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: cfg.wormhole_contract,
            msg: to_binary(&WormholeExecuteMsg::PostMessage {
                message: Binary::from(token_bridge_message.serialize()),
                nonce,
            })?,
            // forward coins sent to this message
            funds: info.funds,
        }))
        .add_attribute("meta.token_chain", cfg.chain_id.to_string())
        .add_attribute("meta.token", asset_address)
        .add_attribute("meta.nonce", nonce.to_string())
        .add_attribute("meta.block_time", env.block.time.seconds().to_string()))
}

fn handle_create_asset_meta_native_token(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    denom: String,
    nonce: u32,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;

    if denom != cfg.native_denom {
        return Err(StdError::generic_err("unsupported native token"));
    }

    let token_id = TokenId::Bank { denom };
    let external_id = token_id.store(deps.storage)?;

    let meta: AssetMeta = AssetMeta {
        token_chain: cfg.chain_id,
        token_address: external_id.clone(),
        decimals: cfg.native_decimals,
        symbol: extend_string_to_32(&cfg.native_symbol),
        name: extend_string_to_32(&cfg.native_symbol),
    };
    let token_bridge_message = TokenBridgeMessage {
        action: Action::ATTEST_META,
        payload: meta.serialize().to_vec(),
    };
    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: cfg.wormhole_contract,
            msg: to_binary(&WormholeExecuteMsg::PostMessage {
                message: Binary::from(token_bridge_message.serialize()),
                nonce,
            })?,
            // forward coins sent to this message
            funds: info.funds,
        }))
        .add_attribute("meta.token_chain", cfg.chain_id.to_string())
        .add_attribute("meta.symbol", cfg.native_symbol)
        .add_attribute("meta.asset_id", hex::encode(external_id.serialize()))
        .add_attribute("meta.nonce", nonce.to_string())
        .add_attribute("meta.block_time", env.block.time.seconds().to_string()))
}

fn handle_complete_transfer_with_payload(
    mut deps: DepsMut,
    env: Env,
    info: MessageInfo,
    data: &Binary,
    relayer_address: &HumanAddr,
) -> StdResult<Response> {
    let (vaa, payload) = parse_and_archive_vaa(deps.branch(), env.clone(), data)?;

    if let Either::Right(message) = payload {
        match message.action {
            Action::TRANSFER_WITH_PAYLOAD => handle_complete_transfer(
                deps,
                env,
                info,
                vaa.emitter_chain,
                vaa.emitter_address,
                TransferType::WithPayload { payload: () },
                &message.payload,
                relayer_address,
            ),
            _ => ContractError::InvalidVAAAction.std_err(),
        }
    } else {
        ContractError::InvalidVAAAction.std_err()
    }
}

enum Either<A, B> {
    Left(A),
    Right(B),
}

fn parse_and_archive_vaa(
    deps: DepsMut,
    env: Env,
    data: &Binary,
) -> StdResult<(ParsedVAA, Either<GovernancePacket, TokenBridgeMessage>)> {
    let state = config_read(deps.storage).load()?;

    let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?;

    if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
        return ContractError::VaaAlreadyExecuted.std_err();
    }
    vaa_archive_add(deps.storage, vaa.hash.as_slice())?;

    // check if vaa is from governance
    if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) {
        let gov_packet = GovernancePacket::deserialize(&vaa.payload)?;
        return Ok((vaa, Either::Left(gov_packet)));
    }

    let message = TokenBridgeMessage::deserialize(&vaa.payload)?;
    Ok((vaa, Either::Right(message)))
}

fn submit_vaa(
    mut deps: DepsMut,
    env: Env,
    info: MessageInfo,
    data: &Binary,
) -> StdResult<Response> {
    let (vaa, payload) = parse_and_archive_vaa(deps.branch(), env.clone(), data)?;
    match payload {
        Either::Left(governance_packet) => handle_governance_payload(deps, env, &governance_packet),

        // In "shutdown" mode, we only handle governance payloads
        #[cfg(feature = "full")]
        Either::Right(message) => match message.action {
            Action::TRANSFER => {
                let sender = info.sender.to_string();
                handle_complete_transfer(
                    deps,
                    env,
                    info,
                    vaa.emitter_chain,
                    vaa.emitter_address,
                    TransferType::WithoutPayload,
                    &message.payload,
                    &sender,
                )
            }
            Action::ATTEST_META => handle_attest_meta(
                deps,
                env,
                vaa.emitter_chain,
                vaa.emitter_address,
                vaa.sequence,
                &message.payload,
            ),
            _ => ContractError::InvalidVAAAction.std_err(),
        },

        #[cfg(not(feature = "full"))]
        _ => ContractError::InvalidVAAAction.std_err(),
    }
}

fn handle_governance_payload(
    deps: DepsMut,
    env: Env,
    gov_packet: &GovernancePacket,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;
    let module = get_string_from_32(&gov_packet.module);

    if module != "TokenBridge" {
        return Err(StdError::generic_err("this is not a valid module"));
    }

    if gov_packet.chain != 0 && gov_packet.chain != cfg.chain_id {
        return Err(StdError::generic_err(
            "the governance VAA is for another chain",
        ));
    }

    match gov_packet.action {
        1u8 => handle_register_chain(deps, env, &gov_packet.payload),
        2u8 => handle_upgrade_contract(deps, env, &gov_packet.payload),
        _ => ContractError::InvalidVAAAction.std_err(),
    }
}

fn handle_upgrade_contract(_deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
    let UpgradeContract { new_contract } = UpgradeContract::deserialize(data)?;

    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
            contract_addr: env.contract.address.to_string(),
            new_code_id: new_contract,
            msg: to_binary(&MigrateMsg {})?,
        }))
        .add_attribute("action", "contract_upgrade"))
}

fn handle_register_chain(deps: DepsMut, _env: Env, data: &Vec<u8>) -> StdResult<Response> {
    let RegisterChain {
        chain_id,
        chain_address,
    } = RegisterChain::deserialize(data)?;

    let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes());
    if existing.is_ok() {
        return Err(StdError::generic_err(
            "bridge contract already exists for this chain",
        ));
    }

    let mut bucket = bridge_contracts(deps.storage);
    bucket.save(&chain_id.to_be_bytes(), &chain_address)?;

    Ok(Response::new()
        .add_attribute("chain_id", chain_id.to_string())
        .add_attribute("chain_address", hex::encode(chain_address)))
}

#[allow(clippy::too_many_arguments)]
fn handle_complete_transfer(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    emitter_chain: u16,
    emitter_address: Vec<u8>,
    transfer_type: TransferType<()>,
    data: &Vec<u8>,
    relayer_address: &HumanAddr,
) -> StdResult<Response> {
    let transfer_info = TransferInfo::deserialize(data)?;
    let token_id = transfer_info
        .token_address
        .to_token_id(deps.storage, transfer_info.token_chain)?;
    match token_id {
        TokenId::Bank { denom } => handle_complete_transfer_token_native(
            deps,
            env,
            info,
            emitter_chain,
            emitter_address,
            denom,
            transfer_type,
            data,
            relayer_address,
        ),
        TokenId::Contract(contract) => handle_complete_transfer_token(
            deps,
            env,
            info,
            emitter_chain,
            emitter_address,
            contract,
            transfer_type,
            data,
            relayer_address,
        ),
    }
}

#[allow(clippy::too_many_arguments)]
#[allow(clippy::bind_instead_of_map)]
fn handle_complete_transfer_token(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    emitter_chain: u16,
    emitter_address: Vec<u8>,
    token_contract: ContractId,
    transfer_type: TransferType<()>,
    data: &Vec<u8>,
    relayer_address: &HumanAddr,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;
    let transfer_info = match transfer_type {
        TransferType::WithoutPayload => TransferInfo::deserialize(data)?,
        TransferType::WithPayload { payload: _ } => {
            TransferWithPayloadInfo::deserialize(data)?.as_transfer_info()
        }
    };

    let expected_contract =
        bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;

    // must be sent by a registered token bridge contract
    if expected_contract != emitter_address {
        return Err(StdError::generic_err("invalid emitter"));
    }

    if transfer_info.recipient_chain != cfg.chain_id {
        return Err(StdError::generic_err(
            "this transfer is not directed at this chain",
        ));
    }

    let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
    let recipient = deps.api.addr_humanize(&target_address)?;

    if let TransferType::WithPayload { payload: _ } = transfer_type {
        if recipient != info.sender {
            return Err(StdError::generic_err(
                "transfers with payload can only be redeemed by the recipient",
            ));
        }
    };

    let (not_supported_amount, mut amount) = transfer_info.amount;
    let (not_supported_fee, mut fee) = transfer_info.fee;

    amount = amount
        .checked_sub(fee)
        .ok_or_else(|| StdError::generic_err("Insufficient funds"))?;

    // Check high 128 bit of amount value to be empty
    if not_supported_amount != 0 || not_supported_fee != 0 {
        return ContractError::AmountTooHigh.std_err();
    }

    let external_id = ExternalTokenId::from_token_id(&TokenId::Contract(token_contract.clone()))?;

    match token_contract {
        ContractId::ForeignToken {
            chain_id,
            foreign_address,
        } => {
            // Check if this asset is already deployed
            let contract_addr = wrapped_asset_read(deps.storage, chain_id).load(&foreign_address).
                or_else(|_| Err(StdError::generic_err("Wrapped asset not deployed. To deploy, invoke CreateWrapped with the associated AssetMeta")))?;

            let contract_addr = contract_addr.into_string();

            // Asset already deployed, just mint
            let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: contract_addr.clone(),
                msg: to_binary(&WrappedMsg::Mint {
                    recipient: recipient.to_string(),
                    amount: Uint128::from(amount),
                })?,
                funds: vec![],
            })];
            if fee != 0 {
                messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: contract_addr.clone(),
                    msg: to_binary(&WrappedMsg::Mint {
                        recipient: relayer_address.to_string(),
                        amount: Uint128::from(fee),
                    })?,
                    funds: vec![],
                }))
            }

            // serialize response data that will be returned to the caller
            let response_data = to_binary(&CompleteTransferResponse {
                contract: Some(contract_addr.clone()),
                denom: None,
                recipient: recipient.clone().into_string(),
                amount: amount.into(),
                relayer: relayer_address.to_string(),
                fee: fee.into(),
            })?;

            Ok(Response::new()
                .add_messages(messages)
                .add_attribute("action", "complete_transfer_wrapped")
                .add_attribute("contract", contract_addr)
                .add_attribute("recipient", recipient)
                .add_attribute("amount", amount.to_string())
                .add_attribute("relayer", relayer_address)
                .add_attribute("fee", fee.to_string())
                .set_data(response_data))
        }
        ContractId::NativeCW20 { contract_address } => {
            // note -- here the amount is the amount the recipient will receive;
            // amount + fee is the total sent
            receive_native(deps.storage, &external_id, Uint128::new(amount + fee))?;

            // undo normalization to 8 decimals
            let token_info: TokenInfoResponse =
                deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
                    contract_addr: contract_address.to_string(),
                    msg: to_binary(&TokenQuery::TokenInfo {})?,
                }))?;

            let decimals = token_info.decimals;
            let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);
            amount = amount.checked_mul(multiplier).unwrap();
            fee = fee.checked_mul(multiplier).unwrap();

            let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: contract_address.to_string(),
                msg: to_binary(&TokenMsg::Transfer {
                    recipient: recipient.to_string(),
                    amount: Uint128::from(amount),
                })?,
                funds: vec![],
            })];

            if fee != 0 {
                messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: contract_address.to_string(),
                    msg: to_binary(&TokenMsg::Transfer {
                        recipient: relayer_address.to_string(),
                        amount: Uint128::from(fee),
                    })?,
                    funds: vec![],
                }))
            }

            // serialize response data that will be returned to the caller
            let response_data = to_binary(&CompleteTransferResponse {
                contract: Some(contract_address.to_string()),
                denom: None,
                recipient: recipient.clone().into_string(),
                amount: amount.into(),
                relayer: relayer_address.to_string(),
                fee: fee.into(),
            })?;

            Ok(Response::new()
                .add_messages(messages)
                .add_attribute("action", "complete_transfer_native")
                .add_attribute("recipient", recipient)
                .add_attribute("contract", contract_address)
                .add_attribute("amount", amount.to_string())
                .add_attribute("relayer", relayer_address)
                .add_attribute("fee", fee.to_string())
                .set_data(response_data))
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn handle_complete_transfer_token_native(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    emitter_chain: u16,
    emitter_address: Vec<u8>,
    denom: String,
    transfer_type: TransferType<()>,
    data: &Vec<u8>,
    relayer_address: &HumanAddr,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;
    let transfer_info = match transfer_type {
        TransferType::WithoutPayload => TransferInfo::deserialize(data)?,
        TransferType::WithPayload { payload: () } => {
            TransferWithPayloadInfo::deserialize(data)?.as_transfer_info()
        }
    };

    let expected_contract =
        bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;

    // must be sent by a registered token bridge contract
    if expected_contract != emitter_address {
        return Err(StdError::generic_err("invalid emitter"));
    }

    if transfer_info.recipient_chain != cfg.chain_id {
        return Err(StdError::generic_err(
            "this transfer is not directed at this chain",
        ));
    }

    let target_address = (&transfer_info.recipient.as_slice()).get_address(0);
    let recipient = deps.api.addr_humanize(&target_address)?;

    if let TransferType::WithPayload { payload: _ } = transfer_type {
        if recipient != info.sender {
            return Err(StdError::generic_err(
                "transfers with payload can only be redeemed by the recipient",
            ));
        }
    };

    let (not_supported_amount, mut amount) = transfer_info.amount;
    let (not_supported_fee, mut fee) = transfer_info.fee;

    amount = amount
        .checked_sub(fee)
        .ok_or_else(|| StdError::generic_err("Insufficient funds"))?;

    // Check high 128 bit of amount value to be empty
    if not_supported_amount != 0 || not_supported_fee != 0 {
        return ContractError::AmountTooHigh.std_err();
    }

    let external_address = ExternalTokenId::from_bank_token(&denom)?;
    receive_native(deps.storage, &external_address, Uint128::new(amount + fee))?;

    // undo normalization to 8 decimals
    let decimals = cfg.native_decimals;
    let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);
    amount = amount.checked_mul(multiplier).unwrap();
    fee = fee.checked_mul(multiplier).unwrap();

    let mut messages = vec![CosmosMsg::Bank(BankMsg::Send {
        to_address: recipient.to_string(),
        amount: vec![coin(amount, &denom)],
    })];

    if fee != 0 {
        messages.push(CosmosMsg::Bank(BankMsg::Send {
            to_address: relayer_address.to_string(),
            amount: vec![coin(fee, &denom)],
        }));
    }

    // serialize response data that will be returned to the caller
    let response_data = to_binary(&CompleteTransferResponse {
        contract: None,
        denom: Some(denom.clone()),
        recipient: recipient.clone().into_string(),
        amount: amount.into(),
        relayer: relayer_address.to_string(),
        fee: fee.into(),
    })?;

    Ok(Response::new()
        .add_messages(messages)
        .add_attribute("action", "complete_transfer_terra_native")
        .add_attribute("recipient", recipient)
        .add_attribute("denom", denom)
        .add_attribute("amount", amount.to_string())
        .add_attribute("relayer", relayer_address)
        .add_attribute("fee", fee.to_string())
        .set_data(response_data))
}

#[allow(clippy::too_many_arguments)]
fn handle_initiate_transfer(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    asset: Asset,
    recipient_chain: u16,
    recipient: [u8; 32],
    fee: Uint128,
    transfer_type: TransferType<Vec<u8>>,
    nonce: u32,
) -> StdResult<Response> {
    match asset.info {
        AssetInfo::Token { contract_addr } => handle_initiate_transfer_token(
            deps,
            env,
            info,
            contract_addr,
            asset.amount,
            recipient_chain,
            recipient,
            fee,
            transfer_type,
            nonce,
        ),
        AssetInfo::NativeToken { ref denom } => handle_initiate_transfer_native_token(
            deps,
            env,
            info,
            denom.clone(),
            asset.amount,
            recipient_chain,
            recipient,
            fee,
            transfer_type,
            nonce,
        ),
    }
}

#[allow(clippy::too_many_arguments)]
fn handle_initiate_transfer_token(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    asset: HumanAddr,
    mut amount: Uint128,
    recipient_chain: u16,
    recipient: [u8; 32],
    mut fee: Uint128,
    transfer_type: TransferType<Vec<u8>>,
    nonce: u32,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;

    if recipient_chain == cfg.chain_id {
        return ContractError::SameSourceAndTarget.std_err();
    }
    if amount.is_zero() {
        return ContractError::AmountTooLow.std_err();
    }

    let asset_chain: u16;
    let asset_address: [u8; 32];

    let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?;

    let mut messages: Vec<CosmosMsg> = vec![];
    let mut submessages: Vec<SubMsg> = vec![];

    // we'll only need this for payload 3 transfers
    let sender_address = deps.api.addr_canonicalize(info.sender.as_ref())?;
    let sender_address = extend_address_to_32_array(&sender_address);

    match is_wrapped_asset_read(deps.storage).load(asset_canonical.as_slice()) {
        Ok(_) => {
            // If the fee is too large the user will receive nothing.
            if fee > amount {
                return Err(StdError::generic_err("fee greater than sent amount"));
            }

            // This is a deployed wrapped asset, burn it
            messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: asset.clone(),
                msg: to_binary(&WrappedMsg::Burn {
                    account: info.sender.to_string(),
                    amount,
                })?,
                funds: vec![],
            }));
            let request = QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
                contract_addr: asset,
                msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?,
            });
            let wrapped_token_info: WrappedAssetInfoResponse = deps.querier.query(&request)?;
            asset_chain = wrapped_token_info.asset_chain;
            assert!(asset_chain != cfg.chain_id, "Expected a foreign chain id.");
            asset_address = wrapped_token_info.asset_address.to_array()?;

            let external_id = ExternalTokenId::from_foreign_token(asset_address);

            let token_bridge_message: TokenBridgeMessage = match transfer_type {
                TransferType::WithoutPayload => {
                    let transfer_info = TransferInfo {
                        amount: (0, amount.u128()),
                        token_address: external_id,
                        token_chain: asset_chain,
                        recipient,
                        recipient_chain,
                        fee: (0, fee.u128()),
                    };
                    TokenBridgeMessage {
                        action: Action::TRANSFER,
                        payload: transfer_info.serialize(),
                    }
                }
                TransferType::WithPayload { payload } => {
                    let transfer_info = TransferWithPayloadInfo {
                        amount: (0, amount.u128()),
                        token_address: external_id,
                        token_chain: asset_chain,
                        recipient,
                        recipient_chain,
                        sender_address,
                        payload,
                    };
                    TokenBridgeMessage {
                        action: Action::TRANSFER_WITH_PAYLOAD,
                        payload: transfer_info.serialize(),
                    }
                }
            };

            messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: cfg.wormhole_contract,
                msg: to_binary(&WormholeExecuteMsg::PostMessage {
                    message: Binary::from(token_bridge_message.serialize()),
                    nonce,
                })?,
                // forward coins sent to this message
                funds: info.funds.clone(),
            }));
        }
        Err(_) => {
            asset_chain = cfg.chain_id;

            // normalize amount to 8 decimals when it sent over the wormhole
            let token_info: TokenInfoResponse =
                deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
                    contract_addr: asset.clone(),
                    msg: to_binary(&TokenQuery::TokenInfo {})?,
                }))?;

            let decimals = token_info.decimals;
            let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);

            // chop off dust
            amount = amount
                .u128()
                .checked_rem(multiplier)
                .and_then(|rem| amount.u128().checked_sub(rem))
                .map(Uint128::new)
                .ok_or_else(|| StdError::generic_err("Insufficient funds"))?;

            fee = fee
                .u128()
                .checked_rem(multiplier)
                .and_then(|rem| fee.u128().checked_sub(rem))
                .map(Uint128::new)
                .ok_or_else(|| StdError::generic_err("Invalid fee"))?;

            // This is a regular asset, transfer its balance
            submessages.push(SubMsg::reply_on_success(
                CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: asset.clone(),
                    msg: to_binary(&TokenMsg::TransferFrom {
                        owner: info.sender.to_string(),
                        recipient: env.contract.address.to_string(),
                        amount,
                    })?,
                    funds: vec![],
                }),
                1,
            ));

            asset_address = extend_address_to_32_array(&asset_canonical);
            let address_human = deps.api.addr_humanize(&asset_canonical)?;
            // we store here just in case the token is transferred out before it's attested
            let external_id = TokenId::Contract(ContractId::NativeCW20 {
                contract_address: address_human,
            })
            .store(deps.storage)?;

            // convert to normalized amounts before recording & posting vaa
            amount = Uint128::new(amount.u128().checked_div(multiplier).unwrap());
            fee = Uint128::new(fee.u128().checked_div(multiplier).unwrap());

            // Fetch current CW20 Balance pre-transfer.
            let balance: BalanceResponse =
                deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
                    contract_addr: asset.to_string(),
                    msg: to_binary(&TokenQuery::Balance {
                        address: env.contract.address.to_string(),
                    })?,
                }))?;

            // NOTE: Reentrancy protection. It is crucial that there's no
            // ongoing transfer in progress here, otherwise we would override
            // its state.  This could happen if the asset's TransferFrom handler
            // sends us an InitiateTransfer message, which would be executed
            // before the reply handler due the the depth-first semantics of
            // message execution.  A simple protection mechanism is to require
            // that there's no execution in progress. The reply handler takes
            // care of clearing out this temporary storage when done.
            assert!(wrapped_transfer_tmp(deps.storage).load().is_err());

            let token_bridge_message: TokenBridgeMessage = match transfer_type {
                TransferType::WithoutPayload => {
                    let transfer_info = TransferInfo {
                        amount: (0, amount.u128()),
                        token_address: external_id,
                        token_chain: asset_chain,
                        recipient,
                        recipient_chain,
                        fee: (0, fee.u128()),
                    };
                    TokenBridgeMessage {
                        action: Action::TRANSFER,
                        payload: transfer_info.serialize(),
                    }
                }
                TransferType::WithPayload { payload } => {
                    let transfer_info = TransferWithPayloadInfo {
                        amount: (0, amount.u128()),
                        token_address: external_id,
                        token_chain: asset_chain,
                        recipient,
                        recipient_chain,
                        sender_address,
                        payload,
                    };
                    TokenBridgeMessage {
                        action: Action::TRANSFER_WITH_PAYLOAD,
                        payload: transfer_info.serialize(),
                    }
                }
            };

            let token_address = deps.api.addr_validate(&asset)?;

            // Wrap up state to be captured by the submessage reply.
            wrapped_transfer_tmp(deps.storage).save(&TransferState {
                previous_balance: balance.balance.to_string(),
                account: info.sender.to_string(),
                token_address,
                message: token_bridge_message.serialize(),
                multiplier: Uint128::new(multiplier).to_string(),
                nonce,
            })?;
        }
    };

    Ok(Response::new()
        .add_messages(messages)
        .add_submessages(submessages)
        .add_attribute("transfer.token_chain", asset_chain.to_string())
        .add_attribute("transfer.token", hex::encode(asset_address))
        .add_attribute(
            "transfer.sender",
            hex::encode(extend_address_to_32(
                &deps.api.addr_canonicalize(info.sender.as_str())?,
            )),
        )
        .add_attribute("transfer.recipient_chain", recipient_chain.to_string())
        .add_attribute("transfer.recipient", hex::encode(recipient))
        .add_attribute("transfer.amount", amount.to_string())
        .add_attribute("transfer.nonce", nonce.to_string())
        .add_attribute("transfer.block_time", env.block.time.seconds().to_string()))
}

#[allow(clippy::too_many_arguments)]
fn handle_initiate_transfer_native_token(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    denom: String,
    mut amount: Uint128,
    recipient_chain: u16,
    recipient: [u8; 32],
    mut fee: Uint128,
    transfer_type: TransferType<Vec<u8>>,
    nonce: u32,
) -> StdResult<Response> {
    let cfg = config_read(deps.storage).load()?;

    if denom != cfg.native_denom {
        return Err(StdError::generic_err("unsupported native token"));
    }

    if recipient_chain == cfg.chain_id {
        return ContractError::SameSourceAndTarget.std_err();
    }
    if amount.is_zero() {
        return ContractError::AmountTooLow.std_err();
    }
    if fee > amount {
        return Err(StdError::generic_err("fee greater than sent amount"));
    }

    let decimals = cfg.native_decimals;
    let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32);

    // chop off dust
    amount = Uint128::new(
        amount
            .u128()
            .checked_sub(amount.u128().checked_rem(multiplier).unwrap())
            .unwrap(),
    );

    fee = Uint128::new(
        fee.u128()
            .checked_sub(fee.u128().checked_rem(multiplier).unwrap())
            .unwrap(),
    );

    let deposit_key = format!("{}:{}", info.sender, denom);
    bridge_deposit(deps.storage).update(deposit_key.as_bytes(), |current: Option<Uint128>| {
        match current {
            Some(v) => Ok(v.checked_sub(amount)?),
            None => Err(StdError::generic_err("no deposit found to transfer")),
        }
    })?;

    let mut messages: Vec<CosmosMsg> = vec![];

    let asset_chain: u16 = cfg.chain_id;
    // we store here just in case the token is transferred out before it's attested
    let asset_address = TokenId::Bank { denom }.store(deps.storage)?;

    // convert to normalized amounts before recording & posting vaa
    amount = Uint128::new(amount.u128().checked_div(multiplier).unwrap());
    fee = Uint128::new(fee.u128().checked_div(multiplier).unwrap());

    send_native(deps.storage, &asset_address, amount)?;

    let token_bridge_message: TokenBridgeMessage = match transfer_type {
        TransferType::WithoutPayload => {
            let transfer_info = TransferInfo {
                amount: (0, amount.u128()),
                token_address: asset_address.clone(),
                token_chain: asset_chain,
                recipient,
                recipient_chain,
                fee: (0, fee.u128()),
            };
            TokenBridgeMessage {
                action: Action::TRANSFER,
                payload: transfer_info.serialize(),
            }
        }
        TransferType::WithPayload { payload } => {
            let sender_address = deps.api.addr_canonicalize(info.sender.as_ref())?;
            let sender_address = extend_address_to_32_array(&sender_address);
            let transfer_info = TransferWithPayloadInfo {
                amount: (0, amount.u128()),
                token_address: asset_address.clone(),
                token_chain: asset_chain,
                recipient,
                recipient_chain,
                sender_address,
                payload,
            };
            TokenBridgeMessage {
                action: Action::TRANSFER_WITH_PAYLOAD,
                payload: transfer_info.serialize(),
            }
        }
    };

    let sender = deps.api.addr_canonicalize(info.sender.as_str())?;
    messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: cfg.wormhole_contract,
        msg: to_binary(&WormholeExecuteMsg::PostMessage {
            message: Binary::from(token_bridge_message.serialize()),
            nonce,
        })?,
        funds: info.funds,
    }));

    Ok(Response::new()
        .add_messages(messages)
        .add_attribute("transfer.token_chain", asset_chain.to_string())
        .add_attribute("transfer.token", hex::encode(asset_address.serialize()))
        .add_attribute(
            "transfer.sender",
            hex::encode(extend_address_to_32(&sender)),
        )
        .add_attribute("transfer.recipient_chain", recipient_chain.to_string())
        .add_attribute("transfer.recipient", hex::encode(recipient))
        .add_attribute("transfer.amount", amount.to_string())
        .add_attribute("transfer.nonce", nonce.to_string())
        .add_attribute("transfer.block_time", env.block.time.seconds().to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::WrappedRegistry { chain, address } => {
            to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
        }
        QueryMsg::TransferInfo { vaa } => to_binary(&query_transfer_info(deps, env, &vaa)?),
        QueryMsg::ExternalId { external_id } => to_binary(&query_external_id(deps, external_id)?),
        QueryMsg::IsVaaRedeemed { vaa } => to_binary(&query_is_vaa_redeemed(deps, env, &vaa)?),
        QueryMsg::ChainRegistration { chain } => {
            query_chain_registration(deps, chain).and_then(|r| to_binary(&r))
        }
    }
}

fn query_external_id(deps: Deps, external_id: Binary) -> StdResult<ExternalIdResponse> {
    let cfg = config_read(deps.storage).load()?;
    let external_id = ExternalTokenId::deserialize(external_id.to_array()?);
    Ok(ExternalIdResponse {
        token_id: external_id.to_token_id(deps.storage, cfg.chain_id)?,
    })
}

pub fn query_wrapped_registry(
    deps: Deps,
    chain: u16,
    address: &[u8],
) -> StdResult<WrappedRegistryResponse> {
    // Check if this asset is already deployed
    match wrapped_asset_read(deps.storage, chain).load(address) {
        Ok(address) => Ok(WrappedRegistryResponse {
            address: address.into_string(),
        }),
        Err(_) => ContractError::AssetNotFound.std_err(),
    }
}

fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult<TransferInfoResponse> {
    let cfg = config_read(deps.storage).load()?;

    let parsed = parse_vaa(deps, env.block.time.seconds(), vaa)?;
    let data = parsed.payload;

    // check if vaa is from governance
    if is_governance_emitter(&cfg, parsed.emitter_chain, &parsed.emitter_address) {
        return ContractError::InvalidVAAAction.std_err();
    }

    let message = TokenBridgeMessage::deserialize(&data)?;
    match message.action {
        Action::ATTEST_META => ContractError::InvalidVAAAction.std_err(),
        Action::TRANSFER => {
            let core = TransferInfo::deserialize(&message.payload)?;

            Ok(TransferInfoResponse {
                amount: core.amount.1.into(),
                token_address: core.token_address.serialize(),
                token_chain: core.token_chain,
                recipient: core.recipient,
                recipient_chain: core.recipient_chain,
                fee: core.fee.1.into(),
                payload: vec![],
            })
        }
        Action::TRANSFER_WITH_PAYLOAD => {
            let info = TransferWithPayloadInfo::deserialize(&message.payload)?;
            let core = info.as_transfer_info();

            Ok(TransferInfoResponse {
                amount: core.amount.1.into(),
                token_address: core.token_address.serialize(),
                token_chain: core.token_chain,
                recipient: core.recipient,
                recipient_chain: core.recipient_chain,
                fee: core.fee.1.into(),
                payload: info.payload,
            })
        }
        other => Err(StdError::generic_err(format!("Invalid action: {other}"))),
    }
}

fn query_is_vaa_redeemed(deps: Deps, _env: Env, vaa: &Binary) -> StdResult<IsVaaRedeemedResponse> {
    let vaa = ParsedVAA::deserialize(vaa)?;
    Ok(IsVaaRedeemedResponse {
        is_redeemed: vaa_archive_check(deps.storage, vaa.hash.as_slice()),
    })
}

fn query_chain_registration(deps: Deps, chain: u16) -> StdResult<ChainRegistrationResponse> {
    bridge_contracts_read(deps.storage)
        .load(&chain.to_be_bytes())
        .map(Binary::from)
        .map(|address| ChainRegistrationResponse { address })
}

fn is_governance_emitter(cfg: &ConfigInfo, emitter_chain: u16, emitter_address: &[u8]) -> bool {
    cfg.gov_chain == emitter_chain && cfg.gov_address == emitter_address
}
