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

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

/**
 * @title AereMailboxV3 + AereIGPV3
 * @notice Fix for the quoteDispatch gap in AERE's Hyperlane core on chain 2800.
 *
 *   ROOT CAUSE this file fixes (diagnosed on-chain 2026-07-10):
 *     1. The deployed AereMessenger (0xe54c2329f0786CFE3420c566B646148D25477325)
 *        does NOT implement quoteDispatch(uint32,bytes32,bytes) at all. Selector
 *        0x9c42bd18 is absent from its runtime bytecode, so every eth_call to
 *        quoteDispatch reverts with empty data. It is not upgradeable, so no
 *        configuration can add the function.
 *     2. The deployed AereIGP (0x61B48615F490A23945988c92835eF35fdD86E837) has
 *        zero destination gas configs (gasOverhead=0, gasPriceQuoteAere=0 for
 *        every domain), so even its own quoteGasPayment(uint32) returns 0.
 *     3. Both are owned by the Foundation multisig, so neither could be
 *        reconfigured by the deployer anyway.
 *
 *   WHAT THIS IS:
 *     - AereMailboxV3: a Hyperlane v3 wire-format mailbox. Messages are packed
 *       exactly like Hyperlane v3 (version || nonce || origin || sender ||
 *       destination || recipient || body), messageId = keccak256(message).
 *       quoteDispatch(uint32,bytes32,bytes) works and returns the fee that
 *       dispatch requires as msg.value. dispatch REQUIRES the quoted fee and
 *       forwards it to the post-dispatch hook (the IGP), emitting the same
 *       Dispatch / DispatchId events as Hyperlane v3.
 *     - Inbound verification keeps the AereMessenger operational model: an
 *       owner-managed ECDSA validator set with a threshold, signature bytes are
 *       passed as the `metadata` argument of process(bytes,bytes). This is the
 *       same trust model as the live AereMessenger relayer, so no infra change.
 *     - AereIGPV3: a Hyperlane v3 post-dispatch hook (hookType 4,
 *       INTERCHAIN_GAS_PAYMASTER). quoteDispatch(metadata, message) parses the
 *       destination domain from the packed message and prices it as
 *       (gasOverhead + gasLimit) * gasPriceAereWei. gasLimit comes from
 *       StandardHookMetadata if provided, else DEFAULT_GAS_USAGE (50k, same
 *       default as Hyperlane's IGP).
 *
 *   NO TOKEN of any kind is introduced. Fees are native AERE.
 */

// ─────────────────────────── Hyperlane v3 message lib ───────────────────────────

library HypMessageV3 {
    uint8 internal constant VERSION = 3;

    // Packed layout offsets (bytes):
    //   version    [0,1)
    //   nonce      [1,5)
    //   origin     [5,9)
    //   sender     [9,41)
    //   destination[41,45)
    //   recipient  [45,77)
    //   body       [77,..)
    uint256 internal constant BODY_OFFSET = 77;

    function format(
        uint32 _nonce,
        uint32 _origin,
        bytes32 _sender,
        uint32 _destination,
        bytes32 _recipient,
        bytes calldata _body
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(VERSION, _nonce, _origin, _sender, _destination, _recipient, _body);
    }

    function id(bytes memory _message) internal pure returns (bytes32) {
        return keccak256(_message);
    }

    function version(bytes calldata _message) internal pure returns (uint8) {
        return uint8(bytes1(_message[0:1]));
    }

    function nonce(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[1:5]));
    }

    function origin(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[5:9]));
    }

    function sender(bytes calldata _message) internal pure returns (bytes32) {
        return bytes32(_message[9:41]);
    }

    function destination(bytes calldata _message) internal pure returns (uint32) {
        return uint32(bytes4(_message[41:45]));
    }

    function recipient(bytes calldata _message) internal pure returns (bytes32) {
        return bytes32(_message[45:77]);
    }

    function recipientAddress(bytes calldata _message) internal pure returns (address) {
        return address(uint160(uint256(recipient(_message))));
    }

    function body(bytes calldata _message) internal pure returns (bytes calldata) {
        return _message[BODY_OFFSET:];
    }
}

// ─────────────────────────── Interfaces ───────────────────────────

interface IPostDispatchHookV3 {
    function hookType() external view returns (uint8);
    function supportsMetadata(bytes calldata metadata) external view returns (bool);
    function postDispatch(bytes calldata metadata, bytes calldata message) external payable;
    function quoteDispatch(bytes calldata metadata, bytes calldata message) external view returns (uint256);
}

interface IMessageRecipientV3 {
    function handle(uint32 origin, bytes32 sender, bytes calldata body) external payable;
}

// ─────────────────────────── AereIGPV3 (post-dispatch hook) ───────────────────────────

/**
 * @notice Interchain Gas Paymaster, Hyperlane v3 hook flavour.
 *   fee(domain, gasLimit) = (gasOverhead[domain] + gasLimit) * gasPriceAereWei[domain]
 *   gasPriceAereWei is the owner-set, AERE-denominated price of ONE unit of
 *   destination-chain gas (destination gas price x destination-token/AERE rate,
 *   folded into a single number until a cross-chain price oracle is wired).
 */
contract AereIGPV3 is Ownable, IPostDispatchHookV3 {
    // Hyperlane v3 IPostDispatchHook.Types.INTERCHAIN_GAS_PAYMASTER
    uint8 public constant HOOK_TYPE = 4;
    // Same default as Hyperlane's InterchainGasPaymaster when metadata omits gasLimit.
    uint256 public constant DEFAULT_GAS_USAGE = 50_000;

    struct DomainGasConfig {
        uint96 gasOverhead;       // destination-side fixed gas overhead (ISM verify + mailbox process)
        uint256 gasPriceAereWei;  // wei of AERE per unit of destination gas
    }

    struct GasParam {
        uint32 remoteDomain;
        DomainGasConfig config;
    }

    mapping(uint32 => DomainGasConfig) public destinationGasConfigs;

    /// Fees accumulate here and are claimable to the beneficiary (relayer funding wallet).
    address public beneficiary;

    event GasPayment(bytes32 indexed messageId, uint32 indexed destinationDomain, uint256 gasAmount, uint256 payment);
    event DestinationGasConfigSet(uint32 indexed remoteDomain, uint96 gasOverhead, uint256 gasPriceAereWei);
    event BeneficiarySet(address indexed beneficiary);
    event Claimed(address indexed beneficiary, uint256 amount);

    constructor(address _beneficiary) {
        require(_beneficiary != address(0), "AereIGPV3: zero beneficiary");
        beneficiary = _beneficiary;
    }

    // ── owner config ──

    function setDestinationGasConfigs(GasParam[] calldata params) external onlyOwner {
        for (uint256 i = 0; i < params.length; i++) {
            require(params[i].config.gasPriceAereWei > 0, "AereIGPV3: zero gas price");
            destinationGasConfigs[params[i].remoteDomain] = params[i].config;
            emit DestinationGasConfigSet(
                params[i].remoteDomain,
                params[i].config.gasOverhead,
                params[i].config.gasPriceAereWei
            );
        }
    }

    function setBeneficiary(address _beneficiary) external onlyOwner {
        require(_beneficiary != address(0), "AereIGPV3: zero beneficiary");
        beneficiary = _beneficiary;
        emit BeneficiarySet(_beneficiary);
    }

    // ── quoting ──

    function quoteGasPayment(uint32 destinationDomain, uint256 gasLimit) public view returns (uint256) {
        DomainGasConfig memory c = destinationGasConfigs[destinationDomain];
        require(c.gasPriceAereWei > 0, "AereIGPV3: domain not configured");
        return (uint256(c.gasOverhead) + gasLimit) * c.gasPriceAereWei;
    }

    // ── IPostDispatchHookV3 ──

    function hookType() external pure returns (uint8) {
        return HOOK_TYPE;
    }

    /// Metadata is either empty or StandardHookMetadata:
    ///   variant(2) || msgValue(32) || gasLimit(32) || refundAddress(20)
    function supportsMetadata(bytes calldata metadata) external pure returns (bool) {
        return metadata.length == 0 || (metadata.length >= 66 && uint16(bytes2(metadata[0:2])) == 1);
    }

    function _gasLimit(bytes calldata metadata) internal pure returns (uint256) {
        if (metadata.length >= 66 && uint16(bytes2(metadata[0:2])) == 1) {
            return uint256(bytes32(metadata[34:66]));
        }
        return DEFAULT_GAS_USAGE;
    }

    function _refundAddress(bytes calldata metadata) internal pure returns (address) {
        if (metadata.length >= 86 && uint16(bytes2(metadata[0:2])) == 1) {
            return address(bytes20(metadata[66:86]));
        }
        return address(0);
    }

    function quoteDispatch(bytes calldata metadata, bytes calldata message) public view returns (uint256) {
        return quoteGasPayment(HypMessageV3.destination(message), _gasLimit(metadata));
    }

    function postDispatch(bytes calldata metadata, bytes calldata message) external payable {
        uint256 gasLimit = _gasLimit(metadata);
        uint32 destination = HypMessageV3.destination(message);
        uint256 required = quoteGasPayment(destination, gasLimit);
        require(msg.value >= required, "AereIGPV3: insufficient gas payment");

        uint256 overpayment = msg.value - required;
        if (overpayment > 0) {
            address refundTo = _refundAddress(metadata);
            if (refundTo != address(0)) {
                (bool ok, ) = refundTo.call{ value: overpayment }("");
                require(ok, "AereIGPV3: refund failed");
            }
            // If no refund address was given, the overpayment stays claimable
            // by the beneficiary (identical to Hyperlane IGP behaviour when
            // refunding is not possible).
        }

        emit GasPayment(keccak256(message), destination, gasLimit, required);
    }

    // ── payout ──

    function claim() external {
        uint256 bal = address(this).balance;
        require(bal > 0, "AereIGPV3: nothing to claim");
        address to = beneficiary;
        (bool ok, ) = to.call{ value: bal }("");
        require(ok, "AereIGPV3: payout failed");
        emit Claimed(to, bal);
    }

    receive() external payable {}
}

// ─────────────────────────── AereMailboxV3 ───────────────────────────

contract AereMailboxV3 is Ownable, ReentrancyGuard {
    using HypMessageV3 for bytes;

    uint8 public constant VERSION = HypMessageV3.VERSION;

    uint32 public immutable localDomain;

    /// Outbound nonce, incremented per dispatch (Hyperlane v3 semantics).
    uint32 public nonce;

    /// Latest dispatched message id (Hyperlane v3 parity).
    bytes32 public latestDispatchedId;

    /// Post-dispatch hooks. requiredHook is charged first, then defaultHook.
    /// Either may be zero (skipped). For AERE both quoting and payment go to
    /// the AereIGPV3 as defaultHook; requiredHook is left zero at deploy.
    IPostDispatchHookV3 public defaultHook;
    IPostDispatchHookV3 public requiredHook;

    /// Inbound verification: owner-managed ECDSA validator set (same
    /// operational model as the live AereMessenger, so the existing relayer
    /// design carries over; Foundation rotates the set).
    address[] public validators;
    mapping(address => bool) public isValidator;
    uint8 public threshold = 1;

    /// messageId => delivered (replay protection).
    mapping(bytes32 => bool) public deliveredMessages;

    bool public paused;

    // Hyperlane v3 events, byte-for-byte compatible with indexers.
    event Dispatch(address indexed sender, uint32 indexed destination, bytes32 indexed recipient, bytes message);
    event DispatchId(bytes32 indexed messageId);
    event Process(uint32 indexed origin, bytes32 indexed sender, address indexed recipient);
    event ProcessId(bytes32 indexed messageId);

    event ValidatorAdded(address indexed validator);
    event ValidatorRemoved(address indexed validator);
    event ThresholdChanged(uint8 newThreshold);
    event DefaultHookSet(address indexed hook);
    event RequiredHookSet(address indexed hook);
    event PausedSet(bool paused);

    constructor(uint32 _localDomain) {
        localDomain = _localDomain;
    }

    modifier whenNotPaused() {
        require(!paused, "AereMailboxV3: paused");
        _;
    }

    // ───────────────────── Outbound ─────────────────────

    function dispatch(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody
    ) external payable returns (bytes32) {
        return _dispatch(destinationDomain, recipientAddress, messageBody, "");
    }

    /// Hyperlane v3 overload: dispatch with StandardHookMetadata (custom gas limit / refund address).
    function dispatch(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody,
        bytes calldata hookMetadata
    ) external payable returns (bytes32) {
        return _dispatch(destinationDomain, recipientAddress, messageBody, hookMetadata);
    }

    function _dispatch(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody,
        bytes memory hookMetadata
    ) internal whenNotPaused nonReentrant returns (bytes32) {
        uint32 n = nonce;
        bytes memory message = HypMessageV3.format(
            n,
            localDomain,
            bytes32(uint256(uint160(msg.sender))),
            destinationDomain,
            recipientAddress,
            messageBody
        );
        bytes32 messageId = HypMessageV3.id(message);

        nonce = n + 1;
        latestDispatchedId = messageId;

        emit Dispatch(msg.sender, destinationDomain, recipientAddress, message);
        emit DispatchId(messageId);

        // Charge hooks: required first, then default.
        uint256 remaining = msg.value;
        remaining = _payHook(requiredHook, hookMetadata, message, remaining);
        remaining = _payHook(defaultHook, hookMetadata, message, remaining);

        // Refund anything the hooks did not require.
        if (remaining > 0) {
            (bool ok, ) = msg.sender.call{ value: remaining }("");
            require(ok, "AereMailboxV3: refund failed");
        }
        return messageId;
    }

    function _payHook(
        IPostDispatchHookV3 hook,
        bytes memory metadata,
        bytes memory message,
        uint256 available
    ) internal returns (uint256) {
        if (address(hook) == address(0)) return available;
        uint256 fee = _quoteHook(hook, metadata, message);
        require(available >= fee, "AereMailboxV3: insufficient hook payment");
        hook.postDispatch{ value: fee }(metadata, message);
        return available - fee;
    }

    function _quoteHook(
        IPostDispatchHookV3 hook,
        bytes memory metadata,
        bytes memory message
    ) internal view returns (uint256) {
        if (address(hook) == address(0)) return 0;
        return hook.quoteDispatch(metadata, message);
    }

    // ───────────────────── Quoting (THE FIX) ─────────────────────

    /// @notice Hyperlane v3 quoteDispatch — the function the old AereMessenger
    ///         never had. Returns the exact msg.value dispatch() requires.
    function quoteDispatch(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody
    ) external view returns (uint256) {
        return _quote(destinationDomain, recipientAddress, messageBody, "");
    }

    /// v3 overload with hook metadata.
    function quoteDispatch(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody,
        bytes calldata hookMetadata
    ) external view returns (uint256) {
        return _quote(destinationDomain, recipientAddress, messageBody, hookMetadata);
    }

    function _quote(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes calldata messageBody,
        bytes memory hookMetadata
    ) internal view returns (uint256) {
        bytes memory message = HypMessageV3.format(
            nonce,
            localDomain,
            bytes32(uint256(uint160(msg.sender))),
            destinationDomain,
            recipientAddress,
            messageBody
        );
        return
            _quoteHook(requiredHook, hookMetadata, message) +
            _quoteHook(defaultHook, hookMetadata, message);
    }

    // ───────────────────── Inbound ─────────────────────

    /// @notice Hyperlane v3 process signature. `metadata` carries concatenated
    ///         65-byte ECDSA validator signatures over the eth-signed messageId.
    function process(bytes calldata metadata, bytes calldata message) external nonReentrant whenNotPaused {
        require(message.version() == VERSION, "AereMailboxV3: bad version");
        require(message.destination() == localDomain, "AereMailboxV3: wrong destination");

        bytes32 messageId = keccak256(message);
        require(!deliveredMessages[messageId], "AereMailboxV3: already delivered");

        bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageId));
        require(metadata.length % 65 == 0, "AereMailboxV3: bad sig length");
        uint256 sigCount = metadata.length / 65;
        require(sigCount >= threshold, "AereMailboxV3: below threshold");

        address[] memory seen = new address[](sigCount);
        for (uint256 i = 0; i < sigCount; i++) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            assembly {
                let off := add(metadata.offset, mul(i, 65))
                r := calldataload(off)
                s := calldataload(add(off, 32))
                v := byte(0, calldataload(add(off, 64)))
            }
            address signer = ecrecover(digest, v, r, s);
            require(signer != address(0), "AereMailboxV3: bad signature");
            require(isValidator[signer], "AereMailboxV3: not a validator");
            for (uint256 j = 0; j < i; j++) {
                require(seen[j] != signer, "AereMailboxV3: duplicate signer");
            }
            seen[i] = signer;
        }

        deliveredMessages[messageId] = true;
        IMessageRecipientV3(message.recipientAddress()).handle(
            message.origin(),
            message.sender(),
            message.body()
        );
        emit Process(message.origin(), message.sender(), message.recipientAddress());
        emit ProcessId(messageId);
    }

    function delivered(bytes32 messageId) external view returns (bool) {
        return deliveredMessages[messageId];
    }

    // ───────────────────── Admin ─────────────────────

    function setDefaultHook(address _hook) external onlyOwner {
        defaultHook = IPostDispatchHookV3(_hook);
        emit DefaultHookSet(_hook);
    }

    function setRequiredHook(address _hook) external onlyOwner {
        requiredHook = IPostDispatchHookV3(_hook);
        emit RequiredHookSet(_hook);
    }

    function addValidator(address v) external onlyOwner {
        require(v != address(0), "AereMailboxV3: zero validator");
        require(!isValidator[v], "AereMailboxV3: already added");
        isValidator[v] = true;
        validators.push(v);
        emit ValidatorAdded(v);
    }

    function removeValidator(address v) external onlyOwner {
        require(isValidator[v], "AereMailboxV3: not found");
        isValidator[v] = false;
        for (uint256 i = 0; i < validators.length; i++) {
            if (validators[i] == v) {
                validators[i] = validators[validators.length - 1];
                validators.pop();
                break;
            }
        }
        require(threshold <= validators.length, "AereMailboxV3: threshold too high after removal");
        emit ValidatorRemoved(v);
    }

    function setThreshold(uint8 _threshold) external onlyOwner {
        require(_threshold > 0 && _threshold <= validators.length, "AereMailboxV3: bad threshold");
        threshold = _threshold;
        emit ThresholdChanged(_threshold);
    }

    function setPaused(bool _paused) external onlyOwner {
        paused = _paused;
        emit PausedSet(_paused);
    }

    function validatorCount() external view returns (uint256) {
        return validators.length;
    }
}
