// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../stablecoins/IHypMailbox.sol";

/**
 * @title AereWarpRouteV3
 * @notice Hyperlane v3 TokenRouter for USDC.e on AERE chain 2800, bound to the
 *         WORKING AereMailboxV3 (0x7BF113Ab1BCd2b6da01804764065776e3057605a) and
 *         its default post-dispatch hook AereIGPV3
 *         (0x5e4B8e9b196B1c7b3Be86148769aB0047c79744c).
 *
 *         This REPLACES the dead legacy path. The prior AereWarpRoute pointed at
 *         AereMessenger, whose runtime bytecode has NO quoteDispatch selector, so
 *         every quote reverted and the route was unusable. AereMailboxV3 exposes
 *         a real quoteDispatch(uint32,bytes32,bytes) and routes the quoted fee to
 *         AereIGPV3 internally on dispatch, so this router works end to end on the
 *         AERE side.
 *
 *   TOKEN HANDLING — two immutable modes, chosen at deploy:
 *
 *     Mode.MintBurn  (canonical synthetic; USDC.e is the bridged representation):
 *       - transferRemote burns the caller's USDC.e (TOKEN.burnFrom), then dispatches.
 *       - handle mints USDC.e to the recipient (TOKEN.mint).
 *       Use this ONLY when the existing USDC.e grants THIS router mint/burn rights
 *       (i.e. its immutable BRIDGE == this router). No NEW token is minted here;
 *       USDC.e is the existing bridged-collateral representation, 1:1 backed by the
 *       native USDC locked in the sister AereHypERC20Collateral on Ethereum.
 *
 *     Mode.Lock      (lock/release against an existing token this router does NOT
 *                     control the mint of):
 *       - transferRemote pulls (locks) the existing token via safeTransferFrom.
 *       - handle releases the locked token via safeTransfer to the recipient.
 *
 *   WIRE FORMAT: Hyperlane HypERC20 convention — body = abi.encodePacked(
 *   bytes32 recipient, uint256 amount) = exactly 64 bytes.
 *
 *   V3 FEE FLOW: dispatch() forwards the quoted fee to the mailbox's default hook
 *   (the IGP). Callers pay the fee as msg.value to transferRemote; anything above
 *   the quote is refunded to the caller. quoteDispatch on the mailbox already
 *   includes the IGP hook price, so this router quotes against the mailbox (the
 *   canonical v3 flow) and keeps an immutable IGP reference only for the
 *   transparency view quoteGasPayment().
 *
 *   NO new ERC-20 / wrapped / governance token is introduced by this contract.
 */
contract AereWarpRouteV3 is Ownable, ReentrancyGuard, IHypMessageRecipient {
    using SafeERC20 for IERC20;
    using TypeCasts for address;
    using TypeCasts for bytes32;

    enum Mode { Lock, MintBurn }

    /* --------------------------------- immutables --------------------------------- */

    /// @notice AereMailboxV3 — dispatch + quoteDispatch endpoint on chain 2800.
    IHypMailbox public immutable MAILBOX;
    /// @notice AereIGPV3 — the mailbox default post-dispatch hook (fee sink).
    ///         Referenced for the quoteGasPayment() transparency view only; the
    ///         authoritative fee for a dispatch is MAILBOX.quoteDispatch(...).
    IAereIGPV3View public immutable IGP;
    /// @notice Existing USDC.e representation on AERE (or the token this router
    ///         locks/releases). NEVER a token minted by this contract's deploy.
    IERC20 public immutable TOKEN;
    /// @notice Lock (release existing) or MintBurn (mint/burn existing USDC.e).
    Mode public immutable MODE;

    /* ----------------------------------- state ------------------------------------ */

    /// @notice Hyperlane domain id => trusted remote router (bytes32-padded address).
    mapping(uint32 => bytes32) public routers;

    uint256 public totalSentOut;
    uint256 public totalReceivedIn;

    /* ----------------------------------- events ----------------------------------- */

    event RemoteRouterEnrolled(uint32 indexed domain, bytes32 router);
    event SentTransferRemote(uint32 indexed destination, bytes32 indexed recipient, uint256 amount, bytes32 messageId);
    event ReceivedTransferRemote(uint32 indexed origin, address indexed recipient, uint256 amount);

    /* ----------------------------------- errors ----------------------------------- */

    error NotMailbox();
    error UnknownRouter(uint32 origin, bytes32 sender);
    error NoRouterEnrolled(uint32 destination);
    error InsufficientGasPayment(uint256 paid, uint256 required);
    error ZeroAmount();
    error ZeroRecipient();
    error RefundFailed();

    /* --------------------------------- modifiers ---------------------------------- */

    modifier onlyMailbox() {
        if (msg.sender != address(MAILBOX)) revert NotMailbox();
        _;
    }

    /* -------------------------------- constructor --------------------------------- */

    /**
     * @param mailbox  AereMailboxV3 address (must be non-zero).
     * @param igp      AereIGPV3 address (mailbox default hook). May be zero if the
     *                 operator does not want the on-chain transparency view; the
     *                 dispatch fee path never depends on it.
     * @param token    Existing USDC.e (MintBurn) or existing token to lock (Lock).
     * @param mode     Lock or MintBurn.
     */
    constructor(address mailbox, address igp, address token, Mode mode) {
        require(mailbox != address(0) && token != address(0), "zero address");
        MAILBOX = IHypMailbox(mailbox);
        IGP = IAereIGPV3View(igp);
        TOKEN = IERC20(token);
        MODE = mode;
    }

    /* ------------------------------- router enroll -------------------------------- */

    /// @notice Bind the single trusted remote router for a domain (owner-gated).
    function enrollRemoteRouter(uint32 domain, bytes32 router) external onlyOwner {
        routers[domain] = router;
        emit RemoteRouterEnrolled(domain, router);
    }

    /// @notice Remove the trusted remote router for a domain (owner-gated).
    function unenrollRemoteRouter(uint32 domain) external onlyOwner {
        delete routers[domain];
        emit RemoteRouterEnrolled(domain, bytes32(0));
    }

    /* ----------------------------------- outbound --------------------------------- */

    /**
     * @notice Pull `amount` of TOKEN from the caller and dispatch a Hyperlane v3
     *         message instructing the enrolled router on `destination` to release
     *         (or mint) the corresponding asset to `recipient`.
     * @param  destination Hyperlane domain id of the remote chain.
     * @param  recipient   bytes32-padded recipient address on the remote chain.
     * @param  amount      Token units to send (USDC.e has 6 decimals).
     * @return messageId   The dispatched Hyperlane v3 message id.
     */
    function transferRemote(
        uint32 destination,
        bytes32 recipient,
        uint256 amount
    ) external payable nonReentrant returns (bytes32 messageId) {
        if (amount == 0) revert ZeroAmount();
        if (recipient == bytes32(0)) revert ZeroRecipient();
        bytes32 remoteRouter = routers[destination];
        if (remoteRouter == bytes32(0)) revert NoRouterEnrolled(destination);

        bytes memory body = abi.encodePacked(recipient, amount);

        // Authoritative fee for the v3 dispatch. The mailbox forwards this to the
        // IGP hook internally, so we send exactly this as msg.value to dispatch().
        uint256 fee = MAILBOX.quoteDispatch(destination, remoteRouter, body);
        if (msg.value < fee) revert InsufficientGasPayment(msg.value, fee);

        // Pull the caller's tokens BEFORE the cross-chain dispatch.
        if (MODE == Mode.MintBurn) {
            IWarpMintBurn(address(TOKEN)).burnFrom(msg.sender, amount);
        } else {
            TOKEN.safeTransferFrom(msg.sender, address(this), amount);
        }

        messageId = MAILBOX.dispatch{value: fee}(destination, remoteRouter, body);

        // Refund any msg.value above the quoted fee to the caller.
        if (msg.value > fee) {
            (bool ok, ) = msg.sender.call{value: msg.value - fee}("");
            if (!ok) revert RefundFailed();
        }

        totalSentOut += amount;
        emit SentTransferRemote(destination, recipient, amount, messageId);
    }

    /* ----------------------------------- inbound ---------------------------------- */

    /**
     * @notice Called by AereMailboxV3 after its validator set verified the
     *         message. Releases (Lock) or mints (MintBurn) `amount` to the
     *         recipient parsed from the 64-byte Hyperlane body.
     */
    function handle(
        uint32 origin,
        bytes32 sender,
        bytes calldata body
    ) external payable override onlyMailbox nonReentrant {
        bytes32 expected = routers[origin];
        if (expected == bytes32(0) || expected != sender) revert UnknownRouter(origin, sender);

        require(body.length == 64, "invalid body length");
        bytes32 recipientB32;
        uint256 amount;
        assembly {
            recipientB32 := calldataload(body.offset)
            amount := calldataload(add(body.offset, 32))
        }
        address recipient = recipientB32.bytes32ToAddress();

        if (MODE == Mode.MintBurn) {
            IWarpMintBurn(address(TOKEN)).mint(recipient, amount);
        } else {
            TOKEN.safeTransfer(recipient, amount);
        }

        totalReceivedIn += amount;
        emit ReceivedTransferRemote(origin, recipient, amount);
    }

    /* ------------------------------------ views ----------------------------------- */

    /// @notice Fee that transferRemote(destination, recipient, amount) requires as
    ///         msg.value, quoted against AereMailboxV3 (includes the IGP hook).
    function quoteTransferRemote(
        uint32 destination,
        bytes32 recipient,
        uint256 amount
    ) external view returns (uint256 fee) {
        bytes memory body = abi.encodePacked(recipient, amount);
        return MAILBOX.quoteDispatch(destination, routers[destination], body);
    }

    /// @notice Transparency view straight off AereIGPV3 for a given destination
    ///         and gas limit. Reverts if IGP was not set at deploy.
    function quoteGasPayment(uint32 destination, uint256 gasLimit) external view returns (uint256) {
        return IGP.quoteGasPayment(destination, gasLimit);
    }

    /// @notice Locked balance held by this router (Lock mode reserve view).
    function lockedSupply() external view returns (uint256) {
        return TOKEN.balanceOf(address(this));
    }
}

/* ------------------------------- helper interfaces -------------------------------- */

/// @notice USDC.e mint/burn surface (matches contracts/stablecoins/USDCe.sol).
interface IWarpMintBurn {
    function mint(address to, uint256 amount) external;
    function burnFrom(address from, uint256 amount) external;
}

/// @notice AereIGPV3 quoting surface for the transparency view.
interface IAereIGPV3View {
    function quoteGasPayment(uint32 destinationDomain, uint256 gasLimit) external view returns (uint256);
}
