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

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

/**
 * @title AereMessenger
 * @notice Cross-chain messaging hub for AERE Network, designed for compatibility
 *         with the Hyperlane Mailbox interface so we can plug into the public
 *         Hyperlane validator/relayer network once registered.
 *
 *   For now this is a self-contained messaging contract: an off-chain validator
 *   set we operate signs outgoing messages. The signature scheme + ISM design
 *   are deliberately compatible with Hyperlane's ECDSA-multisig ISM, so when
 *   Hyperlane Labs accepts AERE into the public registry we can swap our
 *   signers for the public validator set with one parameter change — no
 *   contract redeploy.
 *
 *   Compatible with Hyperlane Mailbox semantics:
 *     - 32-bit domain identifier (we use Chain ID 2800)
 *     - dispatch(destDomain, recipient, body) emits outbound messages
 *     - process(metadata, message) verifies + delivers inbound messages
 *     - Recipients implement IMessageRecipient
 *     - Default ISM is a multisig of named validators
 *
 *   When AERE is added to the Hyperlane registry, this contract continues to
 *   work — Hyperlane validators sign with the same ECDSA scheme.
 */

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

interface IPostDispatchHook {
    function postDispatch(bytes calldata metadata, bytes calldata message) external payable;
}

contract AereMessenger is Ownable, ReentrancyGuard {
    /// AERE's domain (= chain ID).
    uint32 public immutable localDomain;

    /// Outbound nonce — incremented per dispatch.
    uint32 public nonce;

    /// Validator set authorised to sign inbound messages. ECDSA-signed digests.
    address[] public validators;
    mapping(address => bool) public isValidator;

    /// Threshold: how many distinct validator signatures required to accept an inbound message.
    uint8 public threshold = 1;

    /// Delivered message digest tracking (per-origin) to prevent replay.
    mapping(uint32 => mapping(bytes32 => bool)) public delivered;

    /// Optional post-dispatch hook (e.g. AereIGP — Interchain Gas Paymaster).
    address public defaultHook;

    /// Pause flag for emergency halt.
    bool public paused;

    event Dispatch(
        uint32 indexed destDomain,
        bytes32 indexed recipient,
        uint32 indexed nonce,
        bytes32 sender,
        bytes body,
        bytes32 messageId
    );
    event Process(
        uint32 indexed origin,
        bytes32 indexed sender,
        address indexed recipient,
        bytes32 messageId
    );
    event ValidatorAdded(address indexed validator);
    event ValidatorRemoved(address indexed validator);
    event ThresholdChanged(uint8 newThreshold);
    event HookSet(address indexed hook);
    event Paused(bool paused);

    constructor(uint32 _localDomain) {
        localDomain = _localDomain;
    }

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

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

    /**
     * @notice Send a cross-chain message.
     * @param destDomain Destination chain's domain ID (= chain ID).
     * @param recipient  Recipient contract on destination chain, padded to bytes32.
     * @param body       Application-defined payload.
     * @return messageId Unique digest of this message.
     */
    function dispatch(
        uint32 destDomain,
        bytes32 recipient,
        bytes calldata body
    ) external payable whenNotPaused returns (bytes32 messageId) {
        uint32 n = nonce;
        nonce = n + 1;
        bytes32 sender = bytes32(uint256(uint160(msg.sender)));
        messageId = keccak256(abi.encode(localDomain, destDomain, n, sender, recipient, body));
        emit Dispatch(destDomain, recipient, n, sender, body, messageId);

        if (defaultHook != address(0) && msg.value > 0) {
            // Forward the IGP fee to the hook (e.g. interchain gas payment).
            IPostDispatchHook(defaultHook).postDispatch{ value: msg.value }("", abi.encode(messageId, destDomain));
        }
    }

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

    /**
     * @notice Process an inbound message after verifying validator signatures.
     * @param signatures Concatenated 65-byte ECDSA signatures (one per validator).
     * @param origin     Origin chain's domain ID.
     * @param sender     Sender on origin chain (address padded to bytes32).
     * @param recipient  Recipient contract address on AERE.
     * @param body       Payload to deliver.
     * @param originNonce Sender's nonce on origin chain — used for replay protection.
     */
    function process(
        bytes calldata signatures,
        uint32 origin,
        bytes32 sender,
        address recipient,
        bytes calldata body,
        uint32 originNonce
    ) external nonReentrant whenNotPaused {
        bytes32 messageId = keccak256(abi.encode(origin, localDomain, originNonce, sender, bytes32(uint256(uint160(recipient))), body));
        require(!delivered[origin][messageId], "AereMessenger: replay");

        // Verify threshold signatures over the messageId (Ethereum-signed-message prefix).
        bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageId));
        require(signatures.length % 65 == 0, "AereMessenger: bad sig length");
        uint256 sigCount = signatures.length / 65;
        require(sigCount >= threshold, "AereMessenger: below threshold");

        address[] memory seen = new address[](sigCount);
        uint256 validCount = 0;
        for (uint256 i = 0; i < sigCount; i++) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            assembly {
                let off := add(signatures.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), "AereMessenger: bad signature");
            require(isValidator[signer], "AereMessenger: not a validator");

            // No-double-counting: ensure each signer is distinct.
            for (uint256 j = 0; j < validCount; j++) {
                require(seen[j] != signer, "AereMessenger: duplicate signer");
            }
            seen[validCount] = signer;
            validCount++;
        }

        delivered[origin][messageId] = true;
        IMessageRecipient(recipient).handle(origin, sender, body);
        emit Process(origin, sender, recipient, messageId);
    }

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

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

    function removeValidator(address v) external onlyOwner {
        require(isValidator[v], "AereMessenger: 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;
            }
        }
        emit ValidatorRemoved(v);
    }

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

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

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

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