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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./IHypMailbox.sol";

interface IUSDCeMintable {
    function mint(address to, uint256 amount) external;
    function burnFrom(address from, uint256 amount) external;
    function decimals() external view returns (uint8);
}

/**
 * @title AereHypERC20Synthetic — AERE-side Hyperlane Warp Route endpoint
 *                                  for USDC.e (and other Phase-1 wrapped
 *                                  stablecoins).
 *
 * @notice One synthetic deployed per bridged asset (USDC.e Q3 2026, USDT.e Q4,
 *         USDe.e Q3-4, EURC.e Q4 per immediate_build_plan). Each synthetic
 *         knows its sister "Collateral" contract on Ethereum mainnet and the
 *         Hyperlane Mailbox + interchain gas paymaster on AERE.
 *
 * @dev    Message format (matches Hyperlane HypERC20 convention):
 *           bytes32(recipient) | uint256(amount)
 *
 *         Inbound:
 *           Mailbox calls handle(originDomain, senderBytes32, body). Body is
 *           parsed; mint() called on USDC.e to recipient with amount.
 *
 *         Outbound:
 *           User holds USDC.e and calls transferRemote(destDomain, recipient,
 *           amount). The synthetic burns the user's USDC.e and dispatches a
 *           message to the trusted-router on the destination chain, which
 *           releases the locked USDC.
 *
 *         Trusted-router enrollment:
 *           Owner (Foundation 2-of-3) registers exactly ONE router per remote
 *           domain. The address is bytes32-padded as required by Hyperlane.
 *           Handle() rejects any sender not in the enrolment.
 *
 *         IGP fees:
 *           transferRemote is payable. msg.value MUST cover the Hyperlane
 *           IGP quote for the destination. Excess is refunded to caller.
 *
 *         No admin pause / no upgrade proxy: the synthetic itself is
 *         immutable except for trusted-router enrolment (an admittedly
 *         centralised choice; consistent with Hyperlane's reference
 *         design). Router rotation has no timelock at Phase 1.
 *
 *         Phase-1 contract — vendoring the canonical Hyperlane HypERC20
 *         logic would import the whole at-hyperlane-xyz/core dep tree;
 *         we conform to the same wire protocol but implement the
 *         minimal subset here.
 */
contract AereHypERC20Synthetic is Ownable, ReentrancyGuard, IHypMessageRecipient {
    using TypeCasts for address;
    using TypeCasts for bytes32;

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

    IHypMailbox public immutable MAILBOX;
    IUSDCeMintable public immutable TOKEN;     // USDC.e (or analogous Phase-1 wrapped stable)

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

    /// @notice domain id → trusted remote router (bytes32-padded address)
    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,
        bytes32 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 RefundFailed();

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

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

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

    uint8 public immutable EXPECTED_DECIMALS;

    /// AUDIT FIX (HIGH #17): require explicit decimals at deploy; constructor
    /// reverts if the local TOKEN.decimals() doesn't match. Phase 2 enrollment
    /// MUST also pin remote decimals in the router handshake (off-chain).
    constructor(address mailbox, address token, uint8 expectedDecimals) {
        require(mailbox != address(0) && token != address(0), "zero address");
        MAILBOX = IHypMailbox(mailbox);
        TOKEN = IUSDCeMintable(token);
        // probe token decimals; fail loudly if mismatch.
        (bool ok, bytes memory data) = token.staticcall(abi.encodeWithSignature("decimals()"));
        require(ok && data.length >= 32, "decimals-probe-failed");
        require(uint8(abi.decode(data, (uint256))) == expectedDecimals, "decimals-mismatch");
        EXPECTED_DECIMALS = expectedDecimals;
    }

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

    /// AUDIT FIX (HIGH #16): 7-day timelock on router enrollment. Foundation
    /// proposes; after delay, anyone can finalise. Prevents same-block hijack
    /// of bridge to attacker-controlled remote router.
    uint64 public constant ROUTER_ENROLL_DELAY = 7 days;
    mapping(uint32 => bytes32) public pendingRouter;
    mapping(uint32 => uint64)  public pendingRouterReadyAt;

    event RouterEnrollProposed(uint32 indexed domain, bytes32 router, uint64 readyAt);

    error RouterEnrollDelayOpen(uint64 nowTs, uint64 earliest);
    error NoPendingRouter();

    function proposeEnrollRouter(uint32 domain, bytes32 router) external onlyOwner {
        pendingRouter[domain] = router;
        pendingRouterReadyAt[domain] = uint64(block.timestamp + ROUTER_ENROLL_DELAY);
        emit RouterEnrollProposed(domain, router, pendingRouterReadyAt[domain]);
    }

    function finalizeEnrollRouter(uint32 domain) external {
        uint64 readyAt = pendingRouterReadyAt[domain];
        if (readyAt == 0) revert NoPendingRouter();
        if (block.timestamp < readyAt) revert RouterEnrollDelayOpen(uint64(block.timestamp), readyAt);
        routers[domain] = pendingRouter[domain];
        emit RouterEnrolled(domain, pendingRouter[domain]);
        delete pendingRouter[domain];
        delete pendingRouterReadyAt[domain];
    }

    function cancelEnrollRouter(uint32 domain) external onlyOwner {
        if (pendingRouterReadyAt[domain] == 0) revert NoPendingRouter();
        delete pendingRouter[domain];
        delete pendingRouterReadyAt[domain];
    }

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

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

    /**
     * @notice Burn `amount` of TOKEN from the caller and dispatch a Hyperlane
     *         message instructing the destination domain's enrolled router to
     *         release the corresponding native asset to `recipient`.
     * @param  destination Hyperlane domain id of the remote chain.
     * @param  recipient   bytes32-padded address that will receive on the remote.
     * @param  amount      Token units to send (USDC.e has 6 decimals).
     * @return messageId   The dispatched Hyperlane message hash.
     */
    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);

        // Encode the transfer payload — matches Hyperlane HypERC20 wire format.
        bytes memory body = abi.encodePacked(recipient, amount);

        // Quote and verify the IGP fee.
        uint256 fee = MAILBOX.quoteDispatch(destination, remoteRouter, body);
        if (msg.value < fee) revert InsufficientIgpPayment(msg.value, fee);

        // Burn the caller's tokens BEFORE the cross-chain dispatch.
        TOKEN.burnFrom(msg.sender, amount);

        // Dispatch via Mailbox with the exact fee.
        messageId = MAILBOX.dispatch{value: fee}(destination, remoteRouter, body);

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

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

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

    /**
     * @notice Called by the Mailbox after the configured ISM has verified the
     *         message from `origin`. Parses [bytes32 recipient | uint256 amount]
     *         and mints the corresponding USDC.e to the recipient on AERE.
     */
    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);

        // Parse [bytes32(recipient) | uint256(amount)] = 64 bytes
        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();

        TOKEN.mint(recipient, amount);
        emit ReceivedTransferRemote(origin, recipientB32, 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);
    }
}
