// 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 "./IHypMailbox.sol";

/**
 * @title AereHypERC20Collateral — Ethereum-mainnet lockbox for Phase-1
 *                                  wrapped stablecoins bridged to AERE.
 *
 * @notice Deployed on Ethereum mainnet (chain 1) per bridged asset:
 *           - USDC mainnet → AereHypERC20Collateral(USDC) ↔ USDC.e on AERE
 *           - USDT mainnet → AereHypERC20Collateral(USDT) ↔ USDT.e on AERE
 *           - USDe mainnet → AereHypERC20Collateral(USDe) ↔ USDe.e on AERE
 *           - EURC mainnet → AereHypERC20Collateral(EURC) ↔ EURC.e on AERE
 *
 * @dev    Outbound (lock + dispatch):
 *           User approves N native-USDC and calls transferRemote(destDomain=2800,
 *           recipient, N). Contract pulls N USDC into its lockbox, dispatches a
 *           Hyperlane message to the AERE synthetic router with [recipient | N].
 *
 *         Inbound (release):
 *           Mailbox calls handle(originDomain=2800, senderBytes32, body). Body is
 *           [recipient | amount]. Contract releases `amount` of native USDC to
 *           `recipient`. Sender must match the enrolled AERE router.
 *
 *         Reserve invariant: the contract's native-USDC balance equals the
 *         outstanding USDC.e supply on AERE (modulo in-flight messages). The
 *         AereNavOracle (Q3 2026) snapshots both sides nightly for public
 *         attestation.
 *
 *         Trusted-router enrolment, IGP fees, message format and admin model
 *         mirror the AERE-side synthetic exactly.
 */
contract AereHypERC20Collateral is Ownable, ReentrancyGuard, IHypMessageRecipient {
    using TypeCasts for address;
    using TypeCasts for bytes32;

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

    IHypMailbox public immutable MAILBOX;
    IERC20 public immutable COLLATERAL_TOKEN;   // native USDC / USDT / USDe / EURC

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

    mapping(uint32 => bytes32) public routers;

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

    event RouterEnrolled(uint32 indexed domain, bytes32 indexed 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 InsufficientIgpPayment(uint256 paid, uint256 required);
    error ZeroAmount();
    error ZeroRecipient();
    error PullFailed();
    error PushFailed();
    error RefundFailed();

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

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

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

    constructor(address mailbox, address collateralToken) {
        require(mailbox != address(0) && collateralToken != address(0), "zero address");
        MAILBOX = IHypMailbox(mailbox);
        COLLATERAL_TOKEN = IERC20(collateralToken);
    }

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

    function enrollRouter(uint32 domain, bytes32 router) external onlyOwner {
        routers[domain] = router;
        emit RouterEnrolled(domain, router);
    }

    function unenrollRouter(uint32 domain) external onlyOwner {
        delete routers[domain];
        emit RouterEnrolled(domain, bytes32(0));
    }

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

    /**
     * @notice Lock `amount` of COLLATERAL_TOKEN from caller and dispatch a
     *         mint instruction to the enrolled router on the destination.
     */
    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);

        uint256 fee = MAILBOX.quoteDispatch(destination, remoteRouter, body);
        if (msg.value < fee) revert InsufficientIgpPayment(msg.value, fee);

        // Lock the collateral.
        bool pulled = COLLATERAL_TOKEN.transferFrom(msg.sender, address(this), amount);
        if (!pulled) revert PullFailed();

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

        if (msg.value > fee) {
            (bool ok, ) = msg.sender.call{value: msg.value - fee}("");
            if (!ok) revert RefundFailed();
        }

        emit SentTransferRemote(destination, recipient, amount, messageId);
    }

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

    function handle(
        uint32 origin,
        bytes32 sender,
        bytes calldata body
    ) external payable override onlyMailbox {
        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();

        bool ok = COLLATERAL_TOKEN.transfer(recipient, amount);
        if (!ok) revert PushFailed();

        emit ReceivedTransferRemote(origin, recipient, amount);
    }

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

    function quoteDispatchFee(
        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 Lockbox balance — should equal outstanding USDC.e supply on AERE
    ///         (modulo in-flight Hyperlane messages).
    function lockedSupply() external view returns (uint256) {
        return COLLATERAL_TOKEN.balanceOf(address(this));
    }
}
