// Copyright 2024 Forecasting Technologies LTD.
//
// This file is part of Zeitgeist.
//
// Zeitgeist is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at
// your option) any later version.
//
// Zeitgeist is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Zeitgeist. If not, see <https://www.gnu.org/licenses/>.

#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod mock;
mod tests;
mod types;
mod utils;
pub mod weights;

pub use pallet::*;

#[frame_support::pallet]
mod pallet {
    use crate::{
        types::{OrderAmmTradesInfo, Strategy, Trade, TradeEventInfo, TxType},
        weights::WeightInfoZeitgeist,
    };
    use alloc::{vec, vec::Vec};
    use core::marker::PhantomData;
    use frame_support::{
        ensure, require_transactional,
        traits::{IsType, StorageVersion},
        PalletId,
    };
    use frame_system::{
        ensure_signed,
        pallet_prelude::{BlockNumberFor, OriginFor},
    };
    use orml_traits::MultiCurrency;
    use sp_runtime::{
        traits::{Get, Zero},
        DispatchError, DispatchResult, SaturatedConversion, Saturating,
    };
    #[cfg(feature = "runtime-benchmarks")]
    use zeitgeist_primitives::traits::{CompleteSetOperationsApi, DeployPoolApi};
    use zeitgeist_primitives::{
        hybrid_router_api_types::{
            AmmSoftFail, AmmTrade, ApiError, OrderbookSoftFail, OrderbookTrade,
        },
        math::{
            checked_ops_res::CheckedSubRes,
            fixed::{BaseProvider, FixedDiv, FixedMul, ZeitgeistBase},
        },
        orderbook::{Order, OrderId},
        traits::{HybridRouterAmmApi, HybridRouterOrderbookApi},
        types::Asset,
    };
    use zrml_market_commons::MarketCommonsPalletApi;

    #[pallet::config]
    pub trait Config: frame_system::Config {
        /// The API to handle different asset classes.
        type AssetManager: MultiCurrency<Self::AccountId, CurrencyId = AssetOf<Self>>;

        #[cfg(feature = "runtime-benchmarks")]
        type AmmPoolDeployer: DeployPoolApi<
                AccountId = AccountIdOf<Self>,
                Balance = BalanceOf<Self>,
                MarketId = MarketIdOf<Self>,
            >;

        #[cfg(feature = "runtime-benchmarks")]
        type CompleteSetOperations: CompleteSetOperationsApi<
                AccountId = AccountIdOf<Self>,
                Balance = BalanceOf<Self>,
                MarketId = MarketIdOf<Self>,
            >;

        /// The identifier of individual markets.
        type MarketCommons: MarketCommonsPalletApi<
                AccountId = Self::AccountId,
                BlockNumber = BlockNumberFor<Self>,
                Balance = BalanceOf<Self>,
            >;

        /// The API to handle the Automated Market Maker (AMM).
        type Amm: HybridRouterAmmApi<
                AccountId = AccountIdOf<Self>,
                MarketId = MarketIdOf<Self>,
                Asset = AssetOf<Self>,
                Balance = BalanceOf<Self>,
            >;

        /// The maximum number of orders that can be used to execute a trade.
        #[pallet::constant]
        type MaxOrders: Get<u32>;

        /// The API to handle the order book.
        type Orderbook: HybridRouterOrderbookApi<
                AccountId = AccountIdOf<Self>,
                MarketId = MarketIdOf<Self>,
                Balance = BalanceOf<Self>,
                Asset = AssetOf<Self>,
                Order = OrderOf<Self>,
                OrderId = OrderId,
            >;

        type PalletId: Get<PalletId>;

        /// The event type for this pallet.
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

        /// Weights generated by benchmarks.
        type WeightInfo: WeightInfoZeitgeist;
    }

    /// The current storage version.
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);

    pub(crate) type AssetOf<T> = Asset<MarketIdOf<T>>;
    pub(crate) type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
    pub(crate) type BalanceOf<T> =
        <<T as Config>::AssetManager as MultiCurrency<AccountIdOf<T>>>::Balance;
    pub(crate) type OrderOf<T> = Order<AccountIdOf<T>, BalanceOf<T>, MarketIdOf<T>>;
    pub(crate) type MarketIdOf<T> =
        <<T as Config>::MarketCommons as MarketCommonsPalletApi>::MarketId;
    pub(crate) type AmmTradeOf<T> = AmmTrade<BalanceOf<T>>;
    pub(crate) type OrderTradeOf<T> = OrderbookTrade<AccountIdOf<T>, BalanceOf<T>>;

    #[pallet::pallet]
    #[pallet::storage_version(STORAGE_VERSION)]
    pub struct Pallet<T>(PhantomData<T>);

    #[pallet::event]
    #[pallet::generate_deposit(pub(crate) fn deposit_event)]
    pub enum Event<T>
    where
        T: Config,
    {
        /// A trade was executed.
        HybridRouterExecuted {
            /// The type of transaction (Buy or Sell).
            tx_type: TxType,
            /// The account ID of the user performing the trade.
            who: AccountIdOf<T>,
            /// The ID of the market.
            market_id: MarketIdOf<T>,
            /// The maximum price limit for buying or the minimum price limit for selling.
            price_limit: BalanceOf<T>,
            /// The asset provided by the trader.
            asset_in: AssetOf<T>,
            /// The amount of the `asset_in` provided by the trader.
            /// It includes swap and external fees.
            /// It is an amount before fees are deducted.
            amount_in: BalanceOf<T>,
            /// The asset received by the trader.
            asset_out: AssetOf<T>,
            /// The aggregated amount of the `asset_out` already received
            /// by the trader from AMM and orderbook.
            /// It is an amount after fees are deducted.
            amount_out: BalanceOf<T>,
            /// The external fee amount paid in the base asset.
            external_fee_amount: BalanceOf<T>,
            /// The swap fee amount paid in the base asset.
            swap_fee_amount: BalanceOf<T>,
        },
    }

    #[pallet::error]
    pub enum Error<T> {
        /// The specified amount is zero.
        AmountIsZero,
        /// The price limit is too high.
        PriceLimitTooHigh,
        /// The price of an order is above the specified maximum price.
        OrderPriceAboveMaxPrice,
        /// The price of an order is below the specified minimum price.
        OrderPriceBelowMinPrice,
        /// The asset of an order is not equal to the maker asset of the order book.
        AssetNotEqualToOrderbookMakerAsset,
        /// The asset of an order is not equal to the taker asset of the order book.
        AssetNotEqualToOrderbookTakerAsset,
        /// The strategy "immediate or cancel" was applied.
        CancelStrategyApplied,
        /// The asset count does not match the markets asset count.
        AssetCountMismatch,
        /// The maximum number of orders was exceeded.
        MaxOrdersExceeded,
    }

    #[pallet::call]
    impl<T: Config> Pallet<T> {
        /// Routes a buy order to AMM and CDA to achieve the best average execution price.
        ///
        /// # Parameters
        ///
        /// * `market_id`: The ID of the market to buy from.
        /// * `asset_count`: The number of assets traded on the market.
        /// * `asset`: The asset to buy.
        /// * `amount_in`: The amount of the market's base asset to sell.
        /// * `max_price`: The maximum price to buy at.
        /// * `orders`: A list of orders from the book to use.
        /// * `strategy`: The strategy to handle the remaining order when the `max_price` is reached.
        ///
        /// The elements of `orders` are the orders that the router may use to execute the order. If any of
        /// these orders are already filled, they are ignored. It is not necessary for the router to use all
        /// specified orders. The smaller the vector, the larger the risk that the AMM is used to fill large
        /// chunks of the order.
        ///
        /// The `orders` vector **must** be sorted in ascending order by the price of their associated
        /// orders. Failing this, the behavior of `buy` is undefined.
        ///
        /// If the maximum price is reached before the entire buy order is filled, the `strategy` parameter
        /// decides if the order is rolled back (`Strategy::ImmediateOrCancel`) or if a limit order for the
        /// remaining amount is placed (`Strategy::LimitOrder`).
        ///
        /// Complexity: `O(n)`
        #[pallet::call_index(0)]
        #[pallet::weight(T::WeightInfo::buy(*asset_count as u32, orders.len().saturated_into::<u32>()))]
        #[frame_support::transactional]
        #[allow(clippy::too_many_arguments)]
        pub fn buy(
            origin: OriginFor<T>,
            market_id: MarketIdOf<T>,
            #[pallet::compact] asset_count: u16,
            asset: AssetOf<T>,
            #[pallet::compact] amount_in: BalanceOf<T>,
            #[pallet::compact] max_price: BalanceOf<T>,
            orders: Vec<OrderId>,
            strategy: Strategy,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;

            Self::do_trade(
                TxType::Buy,
                who,
                market_id,
                asset_count,
                asset,
                amount_in,
                max_price,
                &orders,
                strategy,
            )?;

            Ok(())
        }

        /// Routes a sell order to AMM and CDA to achieve the best average execution price.
        ///
        /// # Parameters
        ///
        /// * `market_id`: The ID of the market to sell on.
        /// * `asset_count`: The number of assets traded on the market.
        /// * `asset`: The asset to sell.
        /// * `amount_in`: The amount of `asset` to sell.
        /// * `min_price`: The minimum price to sell at.
        /// * `orders`: A list of orders from the book to use.
        /// * `strategy`: The strategy to handle the remaining order when the `min_price` is reached.
        ///
        /// The elements of `orders` are the orders that the router may use to execute the order. If any of
        /// these orders are already filled, they are ignored. It is not necessary for the router to use all
        /// specified orders. The smaller the vector, the larger the risk that the AMM is used to fill large
        /// chunks of the order.
        ///
        /// The `orders` vector **must** be sorted in ascending order by the price of their associated
        /// orders. Failing this, the behavior of `sell` is undefined.
        ///
        /// If the maximum price is reached before the entire buy order is filled, the `strategy` parameter
        /// decides if the order is rolled back (`Strategy::ImmediateOrCancel`) or if a limit order for the
        /// remaining amount is placed (`Strategy::LimitOrder`).
        ///
        /// Complexity: `O(n)`
        #[pallet::call_index(1)]
        #[pallet::weight(T::WeightInfo::sell(*asset_count as u32, orders.len().saturated_into::<u32>()))]
        #[frame_support::transactional]
        #[allow(clippy::too_many_arguments)]
        pub fn sell(
            origin: OriginFor<T>,
            market_id: MarketIdOf<T>,
            #[pallet::compact] asset_count: u16,
            asset: AssetOf<T>,
            #[pallet::compact] amount_in: BalanceOf<T>,
            #[pallet::compact] min_price: BalanceOf<T>,
            orders: Vec<OrderId>,
            strategy: Strategy,
        ) -> DispatchResult {
            let who = ensure_signed(origin)?;

            Self::do_trade(
                TxType::Sell,
                who,
                market_id,
                asset_count,
                asset,
                amount_in,
                min_price,
                &orders,
                strategy,
            )?;

            Ok(())
        }
    }

    impl<T> Pallet<T>
    where
        T: Config,
    {
        /// Fills the order from the Automated Market Maker (AMM) if it exists.
        /// The order is filled until the `price_limit` is reached.
        ///
        /// # Arguments
        ///
        /// * `tx_type` - The type of transaction (Buy or Sell).
        /// * `who` - The account ID of the user performing the transaction.
        /// * `market_id` - The ID of the market.
        /// * `asset` - The asset to be traded.
        /// * `amount_in` - The amount to be traded.
        /// * `price_limit` - The maximum or minimum price at which the trade can be executed.
        fn maybe_fill_from_amm(
            tx_type: TxType,
            who: &AccountIdOf<T>,
            market_id: MarketIdOf<T>,
            asset: AssetOf<T>,
            amount_in: BalanceOf<T>,
            price_limit: BalanceOf<T>,
        ) -> Result<(BalanceOf<T>, Option<AmmTradeOf<T>>), DispatchError> {
            if !T::Amm::pool_exists(market_id) {
                return Ok((amount_in, None));
            }

            let spot_price = T::Amm::get_spot_price(market_id, asset)?;

            let amm_amount_in = match tx_type {
                TxType::Buy => {
                    if spot_price >= price_limit {
                        return Ok((amount_in, None));
                    }
                    T::Amm::calculate_buy_amount_until(market_id, asset, price_limit)?
                }
                TxType::Sell => {
                    if spot_price <= price_limit {
                        return Ok((amount_in, None));
                    }
                    T::Amm::calculate_sell_amount_until(market_id, asset, price_limit)?
                }
            };

            let amm_amount_in = amm_amount_in.min(amount_in);

            if amm_amount_in.is_zero() {
                return Ok((amount_in, None));
            }

            let amm_trade_info = Self::handle_amm_trade(
                tx_type,
                who.clone(),
                market_id,
                asset,
                amm_amount_in,
                BalanceOf::<T>::zero(),
            )?;

            amm_trade_info
                .map(|amm_trade| Ok((amount_in.checked_sub_res(&amm_amount_in)?, Some(amm_trade))))
                .unwrap_or(Ok((amount_in, None)))
        }

        fn handle_amm_trade(
            tx_type: TxType,
            who: AccountIdOf<T>,
            market_id: MarketIdOf<T>,
            asset: AssetOf<T>,
            amount_in: BalanceOf<T>,
            min_amount_out: BalanceOf<T>,
        ) -> Result<Option<AmmTradeOf<T>>, DispatchError> {
            match tx_type {
                TxType::Buy => {
                    match T::Amm::buy(who, market_id, asset, amount_in, min_amount_out) {
                        Ok(amm_trade) => Ok(Some(amm_trade)),
                        Err(ApiError::SoftFailure(AmmSoftFail::Numerical)) => Ok(None),
                        Err(ApiError::HardFailure(dispatch_error)) => Err(dispatch_error),
                    }
                }
                TxType::Sell => {
                    match T::Amm::sell(who, market_id, asset, amount_in, min_amount_out) {
                        Ok(amm_trade) => Ok(Some(amm_trade)),
                        Err(ApiError::SoftFailure(AmmSoftFail::Numerical)) => Ok(None),
                        Err(ApiError::HardFailure(dispatch_error)) => Err(dispatch_error),
                    }
                }
            }
        }

        /// Fills the order from the order book if it exists and meets the price conditions.
        /// If the order is partially filled, the remaining amount is returned.
        ///
        /// # Arguments
        ///
        /// * `tx_type` - The type of transaction (Buy or Sell).
        /// * `orders` - A list of orders from the order book.
        /// * `remaining` - The amount to be traded.
        /// * `who` - The account ID of the user performing the transaction.
        /// * `market_id` - The ID of the market.
        /// * `base_asset` - The base asset of the market.
        /// * `asset` - The asset to be traded.
        /// * `price_limit` - The maximum or minimum price at which the trade can be executed.
        #[allow(clippy::too_many_arguments)]
        fn maybe_fill_orders(
            tx_type: TxType,
            orders: &[OrderId],
            mut remaining: BalanceOf<T>,
            who: &AccountIdOf<T>,
            market_id: MarketIdOf<T>,
            base_asset: AssetOf<T>,
            asset: AssetOf<T>,
            price_limit: BalanceOf<T>,
        ) -> Result<OrderAmmTradesInfo<T>, DispatchError> {
            let mut amm_trades = Vec::new();
            let mut order_trades = Vec::new();
            for &order_id in orders {
                if remaining.is_zero() {
                    break;
                }

                let order = match T::Orderbook::order(order_id) {
                    Ok(order) => order,
                    Err(_) => continue,
                };

                let order_price = order.price(base_asset)?;

                match tx_type {
                    TxType::Buy => {
                        // existing order is willing to give the required `asset` as the `maker_asset`
                        ensure!(
                            asset == order.maker_asset,
                            Error::<T>::AssetNotEqualToOrderbookMakerAsset
                        );
                        ensure!(order_price <= price_limit, Error::<T>::OrderPriceAboveMaxPrice);
                    }
                    TxType::Sell => {
                        // existing order is willing to receive the required `asset` as the `taker_asset`
                        ensure!(
                            asset == order.taker_asset,
                            Error::<T>::AssetNotEqualToOrderbookTakerAsset
                        );
                        ensure!(order_price >= price_limit, Error::<T>::OrderPriceBelowMinPrice);
                    }
                }

                let amm_trade_info = Self::maybe_fill_from_amm(
                    tx_type,
                    who,
                    market_id,
                    asset,
                    remaining,
                    order_price,
                )?;

                if let Some(t) = amm_trade_info.1 {
                    amm_trades.push(t);
                }
                remaining = amm_trade_info.0;

                if remaining.is_zero() {
                    break;
                }

                // `remaining` is always denominated in the `taker_asset`
                // because this is what the order owner (maker) wants to receive
                let (_taker_fill, maker_fill) =
                    order.taker_and_maker_fill_from_taker_amount(remaining)?;
                // and the `maker_partial_fill` of `fill_order` is specified in `taker_asset`
                let order_trade_opt = Self::handle_fill_order(who.clone(), order_id, maker_fill)?;
                if let Some(order_trade) = order_trade_opt {
                    order_trades.push(order_trade);
                    // `maker_fill` is the amount the order owner (maker) wants to receive
                    remaining = remaining.checked_sub_res(&maker_fill)?;
                }
            }

            Ok(OrderAmmTradesInfo { remaining, order_trades, amm_trades })
        }

        fn handle_fill_order(
            who: AccountIdOf<T>,
            order_id: OrderId,
            maker_fill: BalanceOf<T>,
        ) -> Result<Option<OrderTradeOf<T>>, DispatchError> {
            match T::Orderbook::fill_order(who, order_id, Some(maker_fill)) {
                Ok(order_trade) => Ok(Some(order_trade)),
                Err(ApiError::SoftFailure(OrderbookSoftFail::BelowMinimumBalance))
                | Err(ApiError::SoftFailure(
                    OrderbookSoftFail::PartialFillNearFullFillNotAllowed,
                )) => Ok(None),
                Err(ApiError::HardFailure(dispatch_error)) => Err(dispatch_error),
            }
        }

        /// Places a limit order if the strategy is `Strategy::LimitOrder`.
        /// If the strategy is `Strategy::ImmediateOrCancel`, an error is returned.
        /// A bool is returned to indicate if the order was placed successfully.
        ///
        /// # Arguments
        ///
        /// * `strategy` - The strategy to handle the remaining non-zero amount when the `max_price` is reached.
        /// * `who` - The account ID of the user performing the transaction.
        /// * `market_id` - The ID of the market.
        /// * `maker_asset` - The asset to provide.
        /// * `maker_amount` - The amount of the `maker_asset` to be provided.
        /// * `taker_asset` - The asset to be received.
        /// * `taker_amount` - The amount of the `taker_asset` to be received.
        fn maybe_place_limit_order(
            strategy: Strategy,
            who: &AccountIdOf<T>,
            market_id: MarketIdOf<T>,
            maker_asset: AssetOf<T>,
            maker_amount: BalanceOf<T>,
            taker_asset: AssetOf<T>,
            taker_amount: BalanceOf<T>,
        ) -> Result<bool, DispatchError> {
            match strategy {
                Strategy::ImmediateOrCancel => Err(Error::<T>::CancelStrategyApplied.into()),
                Strategy::LimitOrder => {
                    match T::Orderbook::place_order(
                        who.clone(),
                        market_id,
                        maker_asset,
                        maker_amount,
                        taker_asset,
                        taker_amount,
                    ) {
                        Ok(()) => Ok(true),
                        Err(ApiError::SoftFailure(OrderbookSoftFail::BelowMinimumBalance))
                        | Err(ApiError::SoftFailure(
                            OrderbookSoftFail::PartialFillNearFullFillNotAllowed,
                        )) => Ok(false),
                        Err(ApiError::HardFailure(dispatch_error)) => Err(dispatch_error),
                    }
                }
            }
        }

        /// Executes a trade by routing the order to the Automated Market Maker (AMM) and the Order Book
        /// to achieve the best average execution price.
        ///
        /// # Arguments
        ///
        /// * `tx_type` - The type of transaction (Buy or Sell).
        /// * `who` - The account ID of the user performing the transaction.
        /// * `market_id` - The ID of the market.
        /// * `asset_count` - The number of assets traded on the market.
        /// * `asset` - The asset to be traded.
        /// * `amount_in` - The amount to be traded.
        /// * `price_limit` - The maximum or minimum price at which the trade can be executed.
        /// * `orders` - A list of orders from the order book.
        /// * `strategy` - The strategy to handle the remaining non-zero amount when the `max_price` is reached.
        #[require_transactional]
        #[allow(clippy::too_many_arguments)]
        pub(crate) fn do_trade(
            tx_type: TxType,
            who: AccountIdOf<T>,
            market_id: MarketIdOf<T>,
            asset_count: u16,
            asset: AssetOf<T>,
            amount_in: BalanceOf<T>,
            price_limit: BalanceOf<T>,
            orders: &[OrderId],
            strategy: Strategy,
        ) -> DispatchResult {
            ensure!(amount_in > BalanceOf::<T>::zero(), Error::<T>::AmountIsZero);
            ensure!(
                price_limit <= ZeitgeistBase::<BalanceOf<T>>::get()?,
                Error::<T>::PriceLimitTooHigh
            );
            ensure!(
                orders.len().saturated_into::<u32>() <= T::MaxOrders::get(),
                Error::<T>::MaxOrdersExceeded
            );
            let market = T::MarketCommons::market(&market_id)?;
            let assets = market.outcome_assets();
            ensure!(asset_count as usize == assets.len(), Error::<T>::AssetCountMismatch);

            let (asset_in, asset_out) = match tx_type {
                TxType::Buy => (market.base_asset, asset),
                TxType::Sell => (asset, market.base_asset),
            };
            T::AssetManager::ensure_can_withdraw(asset_in, &who, amount_in)?;

            let mut amm_trades: Vec<AmmTradeOf<T>> = Vec::new();
            let mut remaining = amount_in;

            let order_amm_trades_info = Self::maybe_fill_orders(
                tx_type,
                orders,
                remaining,
                &who,
                market_id,
                market.base_asset,
                asset,
                price_limit,
            )?;

            remaining = order_amm_trades_info.remaining;
            amm_trades.extend(order_amm_trades_info.amm_trades);
            let orderbook_trades = order_amm_trades_info.order_trades;

            if !remaining.is_zero() {
                let amm_trade_info = Self::maybe_fill_from_amm(
                    tx_type,
                    &who,
                    market_id,
                    asset,
                    remaining,
                    price_limit,
                )?;

                amm_trades.extend(amm_trade_info.1);
                remaining = amm_trade_info.0;
            }

            let mut limit_order_was_placed = false;

            if !remaining.is_zero() {
                let (maker_asset, maker_amount, taker_asset, taker_amount) = match tx_type {
                    TxType::Buy => {
                        let maker_asset = market.base_asset;
                        let maker_amount = remaining;
                        let taker_asset = asset;
                        let taker_amount = remaining.bdiv_ceil(price_limit)?;
                        (maker_asset, maker_amount, taker_asset, taker_amount)
                    }
                    TxType::Sell => {
                        let maker_asset = asset;
                        let maker_amount = remaining;
                        let taker_asset = market.base_asset;
                        let taker_amount = price_limit.bmul_floor(remaining)?;
                        (maker_asset, maker_amount, taker_asset, taker_amount)
                    }
                };

                limit_order_was_placed = Self::maybe_place_limit_order(
                    strategy,
                    &who,
                    market_id,
                    maker_asset,
                    maker_amount,
                    taker_asset,
                    taker_amount,
                )?;
            }

            let TradeEventInfo { amount_out, external_fee_amount, swap_fee_amount } =
                Self::get_event_info(&who, &orderbook_trades, &amm_trades)?;

            Self::deposit_event(Event::HybridRouterExecuted {
                tx_type,
                who,
                market_id,
                price_limit,
                asset_in,
                amount_in: if limit_order_was_placed {
                    amount_in
                } else {
                    amount_in.saturating_sub(remaining)
                },
                asset_out,
                amount_out,
                external_fee_amount,
                swap_fee_amount,
            });

            Ok(())
        }

        fn get_event_info(
            who: &AccountIdOf<T>,
            orderbook_trades: &[OrderTradeOf<T>],
            amm_trades: &[AmmTradeOf<T>],
        ) -> Result<TradeEventInfo<T>, DispatchError> {
            orderbook_trades
                .iter()
                .map(|trade| Trade::<T>::Orderbook(trade))
                .chain(amm_trades.iter().map(|trade| Trade::Amm(*trade)))
                .try_fold(TradeEventInfo::<T>::new(), |event_info: TradeEventInfo<T>, trade| {
                    Self::update_event_info(who, trade, event_info)
                })
        }

        fn update_event_info(
            who: &AccountIdOf<T>,
            trade: Trade<T>,
            mut event_info: TradeEventInfo<T>,
        ) -> Result<TradeEventInfo<T>, DispatchError> {
            match trade {
                Trade::Orderbook(orderbook_trade) => {
                    let external_fee_amount = if &orderbook_trade.external_fee.account == who {
                        orderbook_trade.external_fee.amount
                    } else {
                        BalanceOf::<T>::zero()
                    };
                    event_info.add_amount_out_minus_fees(TradeEventInfo::<T> {
                        amount_out: orderbook_trade.filled_maker_amount,
                        external_fee_amount,
                        swap_fee_amount: BalanceOf::<T>::zero(),
                    })?;
                }
                Trade::Amm(amm_trade) => {
                    event_info.add_amount_out_and_fees(TradeEventInfo::<T> {
                        amount_out: amm_trade.amount_out,
                        external_fee_amount: amm_trade.external_fee_amount,
                        swap_fee_amount: amm_trade.swap_fee_amount,
                    })?;
                }
            }

            Ok(event_info)
        }
    }
}
