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

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

interface IAavePoolLike {
    /// @notice Standard V3 flash loan (single-asset via length-1 arrays). Use this on Aave V3.2+ pools
    ///         where `flashLoanSimple` may revert with `NotActivated()`.
    function flashLoan(
        address receiverAddress,
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata interestRateModes,
        address onBehalfOf,
        bytes calldata params,
        uint16 referralCode
    ) external;

    /// @dev Retained for compatibility with older pool deployments; prefer `flashLoan` for new integrations.
    function flashLoanSimple(
        address receiverAddress,
        address asset,
        uint256 amount,
        bytes calldata params,
        uint16 referralCode
    ) external;
}

/// @dev Callback for `flashLoan` (multi-asset API).
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:
 *         flash borrow quote (`flashLoan` single-asset) -> buy PMM base -> unwind base externally -> repay lender, retaining any surplus.
 */
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver, IAaveFlashLoanReceiver, Ownable {
    using SafeERC20 for IERC20;

    address public immutable pool;

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

    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();
    error NothingToSweep();

    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 TokenSwept(address indexed token, address indexed to, uint256 amount);
    event SurplusSwept(address indexed token, address indexed to, uint256 amount, uint256 reserveRetained);

    constructor(address pool_, address initialOwner) Ownable(initialOwner) {
        if (pool_ == address(0) || initialOwner == address(0)) revert BadParams();
        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;
        // `onBehalfOf` must not be zero on some Aave V3.2+ deployments (mapping lookups revert NotActivated).
        IAavePoolLike(pool).flashLoan(
            address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0
        );
    }

    function quoteSurplusBalance(address quoteToken, uint256 reserveRetained) public view returns (uint256 surplus) {
        uint256 quoteBal = IERC20(quoteToken).balanceOf(address(this));
        if (quoteBal > reserveRetained) {
            surplus = quoteBal - reserveRetained;
        }
    }

    function sweepQuoteSurplus(address quoteToken, address to, uint256 reserveRetained)
        external
        onlyOwner
        returns (uint256 amount)
    {
        if (quoteToken == address(0) || to == address(0)) revert BadParams();
        amount = quoteSurplusBalance(quoteToken, reserveRetained);
        if (amount == 0) revert NothingToSweep();
        IERC20(quoteToken).safeTransfer(to, amount);
        emit SurplusSwept(quoteToken, to, amount, reserveRetained);
    }

    function sweepToken(address token, address to, uint256 amount) external onlyOwner {
        if (token == address(0) || to == address(0) || amount == 0) revert BadParams();
        IERC20(token).safeTransfer(to, amount);
        emit TokenSwept(token, to, amount);
    }

    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();

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

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

        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 _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
        );
    }
}
