// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IAavePoolLike {
    function flashLoan(
        address receiverAddress,
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata interestRateModes,
        address onBehalfOf,
        bytes calldata params,
        uint16 referralCode
    ) external;

    function flashLoanSimple(
        address receiverAddress,
        address asset,
        uint256 amount,
        bytes calldata params,
        uint16 referralCode
    ) external;

    function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;

    function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external;
}

interface IAaveFlashLoanReceiver {
    function executeOperation(
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata premiums,
        address initiator,
        bytes calldata params
    ) external returns (bool);
}

interface IAaveFlashLoanSimpleReceiver {
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool);
}

interface IAaveDODOQuotePushSwapExactIn {
    function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
        external
        returns (uint256 amountOut);
}

interface IAaveExternalUnwinder {
    function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
        external
        returns (uint256 amountOut);
}

interface IAaveAtomicBridgeCoordinator {
    struct CreateIntentParams {
        uint64 sourceChain;
        uint64 destinationChain;
        address assetIn;
        address assetOut;
        uint256 amountIn;
        uint256 minAmountOut;
        address recipient;
        uint256 deadline;
        bytes32 routeId;
    }

    function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId);

    function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external;

    function obligationEscrow() external view returns (address);
}

/**
 * @title AaveQuotePushFlashReceiver
 * @notice Aave V3 flash-loan receiver for the quote-push workflow (uses `flashLoan` with singleton arrays).
 */
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver, IAaveFlashLoanReceiver {
    using SafeERC20 for IERC20;

    address public immutable pool;

    enum CollateralHook {
        BeforeSwap,
        AfterSwap,
        BeforeUnwind
    }

    struct CollateralSupplyStep {
        address asset;
        uint256 amount;
    }

    struct CollateralToggleStep {
        address asset;
        bool useAsCollateral;
    }

    struct CollateralParams {
        CollateralSupplyStep[] supplyBeforeSwap;
        CollateralToggleStep[] toggleBeforeSwap;
        CollateralToggleStep[] toggleAfterSwap;
        CollateralToggleStep[] toggleBeforeUnwind;
    }

    struct QuotePushParams {
        address integration;
        address pmmPool;
        address baseToken;
        address externalUnwinder;
        uint256 minOutPmm;
        uint256 minOutUnwind;
        bytes unwindData;
        AtomicBridgeParams atomicBridge;
        CollateralParams collateral;
    }

    struct AtomicBridgeParams {
        address coordinator;
        uint64 sourceChain;
        uint64 destinationChain;
        address destinationAsset;
        uint256 bridgeAmount;
        uint256 minDestinationAmount;
        address destinationRecipient;
        uint256 destinationDeadline;
        bytes32 routeId;
        bytes32 settlementMode;
        bool submitCommitment;
    }

    error UntrustedPool();
    error UntrustedInitiator();
    error BadParams();
    error InsufficientToRepay();
    error InvalidAtomicBridge();

    event QuotePushExecuted(
        address indexed quoteToken,
        address indexed baseToken,
        uint256 borrowedAmount,
        uint256 premium,
        uint256 baseOut,
        uint256 unwindOut,
        uint256 surplus
    );
    event AtomicBridgeTriggered(
        bytes32 indexed obligationId,
        address indexed coordinator,
        address indexed destinationRecipient,
        uint256 bridgeAmount,
        uint256 minDestinationAmount
    );
    event CollateralSupplied(address indexed asset, uint256 amount);
    event CollateralToggled(address indexed asset, bool useAsCollateral, CollateralHook hook);

    function emptyCollateralParams() external pure returns (CollateralParams memory params) {
        return params;
    }

    function collateralHooksVersion() external pure returns (uint256) {
        return 1;
    }

    constructor(address pool_) {
        pool = pool_;
    }

    function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external {
        address[] memory assets = new address[](1);
        uint256[] memory amts = new uint256[](1);
        uint256[] memory modes = new uint256[](1);
        assets[0] = asset;
        amts[0] = amount;
        modes[0] = 0;
        IAavePoolLike(pool).flashLoan(
            address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0
        );
    }

    function executeOperation(
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata premiums,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
        if (msg.sender != pool) revert UntrustedPool();
        if (assets.length != 1 || amounts.length != 1 || premiums.length != 1) revert BadParams();
        _executeQuotePush(assets[0], amounts[0], premiums[0], initiator, params);
        return true;
    }

    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
        if (msg.sender != pool) revert UntrustedPool();
        _executeQuotePush(asset, amount, premium, initiator, params);
        return true;
    }

    function _executeQuotePush(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) internal {
        (address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams));
        if (initiator != expectedInitiator) revert UntrustedInitiator();
        if (
            p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0)
                || p.externalUnwinder == address(0)
        ) revert BadParams();
        if (p.baseToken == asset) revert BadParams();

        _applyCollateralSupplies(p.collateral.supplyBeforeSwap);
        _applyCollateralToggles(p.collateral.toggleBeforeSwap, CollateralHook.BeforeSwap);

        uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm);

        _applyCollateralToggles(p.collateral.toggleAfterSwap, CollateralHook.AfterSwap);

        uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this));
        if (p.atomicBridge.coordinator != address(0)) {
            _triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge);
        }

        _applyCollateralToggles(p.collateral.toggleBeforeUnwind, CollateralHook.BeforeUnwind);

        uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData);
        uint256 surplus = _approveRepayment(asset, amount + premium);
        emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus);
    }

    function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm)
        internal
        returns (uint256 baseOut)
    {
        IERC20(asset).forceApprove(integration, amount);
        baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm);
    }

    function _unwindBaseIntoQuote(
        address baseToken,
        address quoteToken,
        address externalUnwinder,
        uint256 minOutUnwind,
        bytes memory unwindData
    ) internal returns (uint256 unwindOut) {
        uint256 remainingBase = IERC20(baseToken).balanceOf(address(this));
        IERC20(baseToken).forceApprove(externalUnwinder, remainingBase);
        unwindOut =
            IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData);
    }

    function _applyCollateralSupplies(CollateralSupplyStep[] memory steps) internal {
        uint256 n = steps.length;
        for (uint256 i = 0; i < n; ++i) {
            CollateralSupplyStep memory step = steps[i];
            if (step.asset == address(0) || step.amount == 0) continue;
            IERC20(step.asset).forceApprove(pool, step.amount);
            IAavePoolLike(pool).supply(step.asset, step.amount, address(this), 0);
            emit CollateralSupplied(step.asset, step.amount);
        }
    }

    function _applyCollateralToggles(CollateralToggleStep[] memory steps, CollateralHook hook) internal {
        uint256 n = steps.length;
        for (uint256 i = 0; i < n; ++i) {
            CollateralToggleStep memory step = steps[i];
            if (step.asset == address(0)) continue;
            IAavePoolLike(pool).setUserUseReserveAsCollateral(step.asset, step.useAsCollateral);
            emit CollateralToggled(step.asset, step.useAsCollateral, hook);
        }
    }

    function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) {
        IERC20 quote = IERC20(quoteToken);
        uint256 quoteBal = quote.balanceOf(address(this));
        if (quoteBal < need) revert InsufficientToRepay();
        surplus = quoteBal - need;
        quote.forceApprove(pool, need);
    }

    function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal {
        if (
            atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0)
                || atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal
                || atomicBridge.destinationDeadline <= block.timestamp
        ) revert InvalidAtomicBridge();

        address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow();
        IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount);
        bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent(
            IAaveAtomicBridgeCoordinator.CreateIntentParams({
                sourceChain: atomicBridge.sourceChain,
                destinationChain: atomicBridge.destinationChain,
                assetIn: baseToken,
                assetOut: atomicBridge.destinationAsset,
                amountIn: atomicBridge.bridgeAmount,
                minAmountOut: atomicBridge.minDestinationAmount,
                recipient: atomicBridge.destinationRecipient,
                deadline: atomicBridge.destinationDeadline,
                routeId: atomicBridge.routeId
            })
        );
        if (atomicBridge.submitCommitment) {
            IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment(
                obligationId, atomicBridge.settlementMode
            );
        }
        emit AtomicBridgeTriggered(
            obligationId,
            atomicBridge.coordinator,
            atomicBridge.destinationRecipient,
            atomicBridge.bridgeAmount,
            atomicBridge.minDestinationAmount
        );
    }
}
