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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title ReceiverExecutorMainnet
 * @notice Receives WETH9 from CCIP (via mainnet CCIPWETH9Bridge transfer). Unwraps to ETH or swaps to canonical USDC/USDT only.
 * @dev Only hardcoded WETH9, USDC, USDT. No calldata-provided stablecoin address.
 *      WETH9 is expected to be transferred only from the mainnet CCIPWETH9Bridge (the bridge receives from CCIP Router and transfers here).
 *      Use setExpectedWeth9Source() to record the bridge address for operator/off-chain checks; no on-chain transfer restriction.
 *      See docs/treasury/EXECUTOR_ALLOWLIST_MATRIX.md and EXPORT_STATE_MACHINE.md.
 */
contract ReceiverExecutorMainnet is AccessControl, ReentrancyGuard {
    using SafeERC20 for IERC20;

    address public constant WETH9_MAINNET = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant USDC_MAINNET = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address public constant USDT_MAINNET = 0xdAC17F958D2ee523a2206206994597C13D831ec7;

    bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");

    /// @notice When set, operator should treat WETH9 as valid only when transferred from this address (e.g. mainnet CCIPWETH9Bridge).
    address public expectedWeth9Source;

    event Unwrapped(uint256 amount);
    event SwappedToUsdc(uint256 amountIn, uint256 amountOut);
    event SwappedToUsdt(uint256 amountIn, uint256 amountOut);

    error ZeroAmount();
    error InsufficientOutput();

    event ExpectedWeth9SourceSet(address indexed source);

    constructor(address admin) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(KEEPER_ROLE, admin);
    }

    /// @notice Set the address from which WETH9 is expected (e.g. mainnet CCIPWETH9Bridge). For documentation and off-chain checks only; no on-chain transfer restriction.
    function setExpectedWeth9Source(address source) external onlyRole(DEFAULT_ADMIN_ROLE) {
        expectedWeth9Source = source;
        emit ExpectedWeth9SourceSet(source);
    }

    /// @notice Returns the address from which WETH9 is expected. Zero means not configured.
    function getExpectedWeth9Source() external view returns (address) {
        return expectedWeth9Source;
    }

    function unwrapWeth9(uint256 amount, address recipient) external nonReentrant onlyRole(KEEPER_ROLE) {
        if (amount == 0) revert ZeroAmount();
        if (recipient == address(0)) revert("ReceiverExecutorMainnet: zero recipient");
        (bool ok,) = WETH9_MAINNET.call(
            abi.encodeWithSignature("withdraw(uint256)", amount)
        );
        require(ok, "ReceiverExecutorMainnet: withdraw failed");
        (bool sent,) = payable(recipient).call{value: amount}("");
        require(sent, "ReceiverExecutorMainnet: ETH send failed");
        emit Unwrapped(amount);
    }

    function swapWeth9ToUsdc(address router, uint256 amountIn, uint256 minOut, bytes calldata data)
        external
        nonReentrant
        onlyRole(KEEPER_ROLE)
    {
        if (amountIn == 0) revert ZeroAmount();
        if (router == address(0)) revert("ReceiverExecutorMainnet: zero router");
        uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(this));
        IERC20(WETH9_MAINNET).approve(router, amountIn);
        (bool ok,) = router.call(data);
        require(ok, "ReceiverExecutorMainnet: swap failed");
        uint256 amountOut = IERC20(USDC_MAINNET).balanceOf(address(this)) - balanceBefore;
        if (amountOut < minOut) revert InsufficientOutput();
        emit SwappedToUsdc(amountIn, amountOut);
    }

    function swapWeth9ToUsdt(address router, uint256 amountIn, uint256 minOut, bytes calldata data)
        external
        nonReentrant
        onlyRole(KEEPER_ROLE)
    {
        if (amountIn == 0) revert ZeroAmount();
        if (router == address(0)) revert("ReceiverExecutorMainnet: zero router");
        uint256 balanceBefore = IERC20(USDT_MAINNET).balanceOf(address(this));
        IERC20(WETH9_MAINNET).approve(router, amountIn);
        (bool ok,) = router.call(data);
        require(ok, "ReceiverExecutorMainnet: swap failed");
        uint256 amountOut = IERC20(USDT_MAINNET).balanceOf(address(this)) - balanceBefore;
        if (amountOut < minOut) revert InsufficientOutput();
        emit SwappedToUsdt(amountIn, amountOut);
    }

    receive() external payable {}
}
