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

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

/// @dev Matches `DODOPMMIntegration.swapExactIn` surface (any registered pool).
interface IDODOQuotePushSwapExactIn {
    function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
        external
        returns (uint256 amountOut);
}

/// @dev Minimal external unwind interface for converting PMM base back into flash-borrowed quote.
interface IExternalUnwinder {
    function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
        external
        returns (uint256 amountOut);
}

/**
 * @title QuotePushFlashWorkflowBorrower
 * @notice ERC-3156 borrower for a quote-push loop:
 *         flash `quoteToken` -> buy `baseToken` from a DODO-style PMM -> unwind externally back into `quoteToken`
 *         -> repay `amount + fee`, leaving any quote surplus on this contract.
 * @dev `data` must be `abi.encode(QuotePushParams)`. The caller is responsible for choosing trusted integrations,
 *      setting conservative minimums, and sweeping any retained surplus from this contract after execution.
 */
contract QuotePushFlashWorkflowBorrower is IERC3156FlashBorrower {
    using SafeERC20 for IERC20;

    bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");

    address public immutable trustedLender;

    struct QuotePushParams {
        address integration;
        address pool;
        address baseToken;
        address externalUnwinder;
        uint256 minOutPmm;
        uint256 minOutUnwind;
        bytes unwindData;
    }

    error UntrustedLender();
    error BadParams();
    error InsufficientToRepay();

    event QuotePushExecuted(
        address indexed quoteToken,
        address indexed baseToken,
        uint256 borrowedAmount,
        uint256 fee,
        uint256 baseOut,
        uint256 unwindOut,
        uint256 surplus
    );

    constructor(address trustedLender_) {
        trustedLender = trustedLender_;
    }

    function onFlashLoan(
        address,
        address quoteToken,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external override returns (bytes32) {
        if (msg.sender != trustedLender) revert UntrustedLender();
        QuotePushParams memory p = abi.decode(data, (QuotePushParams));
        if (
            p.integration == address(0) || p.pool == address(0) || p.baseToken == address(0)
                || p.externalUnwinder == address(0)
        ) revert BadParams();
        if (p.baseToken == quoteToken) revert BadParams();

        IERC20 borrowed = IERC20(quoteToken);
        IERC20 base = IERC20(p.baseToken);

        borrowed.forceApprove(p.integration, amount);
        uint256 baseOut = IDODOQuotePushSwapExactIn(p.integration).swapExactIn(p.pool, quoteToken, amount, p.minOutPmm);

        uint256 baseBal = base.balanceOf(address(this));
        base.forceApprove(p.externalUnwinder, baseBal);
        uint256 unwindOut =
            IExternalUnwinder(p.externalUnwinder).unwind(p.baseToken, quoteToken, baseBal, p.minOutUnwind, p.unwindData);

        uint256 need = amount + fee;
        uint256 quoteBal = borrowed.balanceOf(address(this));
        if (quoteBal < need) revert InsufficientToRepay();

        uint256 surplus = quoteBal - need;
        borrowed.safeTransfer(msg.sender, need);
        emit QuotePushExecuted(quoteToken, p.baseToken, amount, fee, baseOut, unwindOut, surplus);

        return _RETURN_VALUE;
    }
}
