use stellar_axelar_gas_service::AxelarGasServiceClient;
use stellar_axelar_gateway::executable::{AxelarExecutableInterface, CustomAxelarExecutable};
use stellar_axelar_gateway::AxelarGatewayMessagingClient;
use stellar_axelar_std::events::Event;
use stellar_axelar_std::types::Token;
use stellar_axelar_std::{
    contract, contracterror, contractimpl, ensure, soroban_sdk, token, Address, AxelarExecutable,
    Bytes, BytesN, Env, InterchainTokenExecutable, String,
};
use stellar_interchain_token_service::executable::CustomInterchainTokenExecutable;
use stellar_interchain_token_service::InterchainTokenServiceClient;

use crate::event::{ExecutedEvent, TokenReceivedEvent, TokenSentEvent};
use crate::interface::AxelarExampleInterface;
use crate::storage;

#[contract]
#[derive(InterchainTokenExecutable, AxelarExecutable)]
pub struct AxelarExample;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum AxelarExampleError {
    NotApproved = 1,
    InvalidItsAddress = 2,
    InvalidAmount = 3,
}

impl CustomAxelarExecutable for AxelarExample {
    type Error = AxelarExampleError;

    fn __gateway(env: &Env) -> Address {
        storage::gateway(env)
    }

    fn __execute(
        env: &Env,
        source_chain: String,
        message_id: String,
        source_address: String,
        payload: Bytes,
    ) -> Result<(), Self::Error> {
        ExecutedEvent {
            source_chain,
            message_id,
            source_address,
            payload,
        }
        .emit(env);

        Ok(())
    }
}

impl CustomInterchainTokenExecutable for AxelarExample {
    type Error = AxelarExampleError;

    fn __interchain_token_service(env: &Env) -> Address {
        storage::interchain_token_service(env)
    }

    fn __authorized_execute_with_token(
        env: &Env,
        source_chain: String,
        message_id: String,
        source_address: Bytes,
        payload: Bytes,
        token_id: BytesN<32>,
        token_address: Address,
        amount: i128,
    ) -> Result<(), Self::Error> {
        ensure!(amount >= 0, AxelarExampleError::InvalidAmount);

        let destination_address = Address::from_string_bytes(&payload);

        let token = token::TokenClient::new(env, &token_address);
        token.transfer(
            &env.current_contract_address(),
            &destination_address,
            &amount,
        );

        TokenReceivedEvent {
            source_chain,
            message_id,
            source_address,
            token_id,
            token_address,
            amount,
            payload,
        }
        .emit(env);

        Ok(())
    }
}

#[contractimpl]
impl AxelarExample {
    pub fn __constructor(
        env: &Env,
        gateway: Address,
        gas_service: Address,
        interchain_token_service: Address,
    ) {
        storage::set_gateway(env, &gateway);
        storage::set_gas_service(env, &gas_service);
        storage::set_interchain_token_service(env, &interchain_token_service);
    }
}

#[contractimpl]
impl AxelarExampleInterface for AxelarExample {
    fn gas_service(env: &Env) -> Address {
        storage::gas_service(env)
    }

    fn send(
        env: &Env,
        caller: Address,
        destination_chain: String,
        destination_address: String,
        message: Bytes,
        gas_token: Option<Token>,
    ) {
        let gateway = AxelarGatewayMessagingClient::new(env, &Self::gateway(env));
        let gas_service = AxelarGasServiceClient::new(env, &Self::gas_service(env));

        caller.require_auth();

        if let Some(gas_token) = gas_token {
            gas_service.pay_gas(
                &env.current_contract_address(),
                &destination_chain,
                &destination_address,
                &message,
                &caller,
                &gas_token,
                &Bytes::new(env),
            );
        }

        gateway.call_contract(
            &env.current_contract_address(),
            &destination_chain,
            &destination_address,
            &message,
        );
    }

    fn send_token(
        env: &Env,
        caller: Address,
        token_id: BytesN<32>,
        destination_chain: String,
        destination_app_contract: Bytes,
        amount: i128,
        recipient: Option<Bytes>,
        gas_token: Option<Token>,
    ) -> Result<(), AxelarExampleError> {
        caller.require_auth();

        let client = InterchainTokenServiceClient::new(env, &Self::interchain_token_service(env));

        client.interchain_transfer(
            &caller,
            &token_id,
            &destination_chain,
            &destination_app_contract,
            &amount,
            &recipient,
            &gas_token,
        );

        TokenSentEvent {
            sender: caller,
            token_id,
            destination_chain,
            destination_app_contract,
            amount,
            recipient,
        }
        .emit(env);

        Ok(())
    }
}
