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

import "./lib/MerklePatriciaProof.sol";
import "./lib/RLPReader.sol";

interface IAereEthLightClient {
    function verifyReceiptsRootAgainstFinalized(
        bytes32 receiptsRoot,
        bytes32 bodyRoot,
        bytes32[] calldata branch,
        uint256 depth,
        uint256 subtreeIndex
    ) external view returns (bool);

    function finalizedSlot() external view returns (uint64);
}

/**
 * @title AereEthMessageInbox — deliver an Ethereum event to AERE, trust-minimized.
 *
 * @notice A thin inbox over {AereEthLightClient}. It accepts (proof, message) and
 *         delivers a message ONLY if:
 *           1. the light client attests `receiptsRoot` is committed under the current
 *              FINALIZED Ethereum beacon header (execution-payload SSZ branch), and
 *           2. the referenced transaction receipt is included in that `receiptsRoot`
 *              via a Merkle-Patricia proof (key = rlp(txIndex)), and
 *           3. the chosen log inside that receipt was emitted by the registered source
 *              contract on Ethereum.
 *         Each (receiptsRoot, txIndex, logIndex) is delivered at most once.
 *
 *         The message payload is the log itself: (emitter, topics, data). No bridge
 *         operator can inject a message; forging one requires forging Ethereum
 *         sync-committee finality OR a keccak/MPT collision.
 *
 * @dev    SECURITY. Inherits the light client's model: >= 2/3 of Ethereum's 512-member
 *         sync committee (classical BLS12-381, quantum-vulnerable). This is Ethereum's
 *         trust model, not AERE's. See AereEthLightClient for the full statement.
 *
 *         The execution-payload generalized index (depth, subtreeIndex) locating
 *         receipts_root under the beacon body root is FORK-DEPENDENT and supplied by
 *         the caller; the inbox pins the expected values it was configured with so a
 *         caller cannot point the branch at the wrong field.
 */
contract AereEthMessageInbox {
    using RLPReader for bytes;

    IAereEthLightClient public immutable lightClient;

    /// @notice Registered Ethereum source contract whose logs this inbox accepts.
    address public immutable sourceContract;

    /// @notice Pinned execution-payload branch shape (receipts_root under body_root).
    uint256 public immutable execBranchDepth;
    uint256 public immutable execBranchIndex;

    mapping(bytes32 => bool) public delivered;

    event MessageDelivered(
        bytes32 indexed receiptsRoot,
        uint256 indexed txIndex,
        uint256 indexed logIndex,
        address emitter,
        bytes32[] topics,
        bytes data
    );

    constructor(
        address lightClient_,
        address sourceContract_,
        uint256 execBranchDepth_,
        uint256 execBranchIndex_
    ) {
        require(lightClient_ != address(0), "Inbox:zero lc");
        lightClient = IAereEthLightClient(lightClient_);
        sourceContract = sourceContract_;
        execBranchDepth = execBranchDepth_;
        execBranchIndex = execBranchIndex_;
    }

    struct ReceiptProof {
        bytes32 receiptsRoot;
        bytes32 finalizedBodyRoot; // the beacon body root the light client finalized
        bytes32[] execBranch; // receipts_root -> finalized body_root
        uint256 txIndex; // transaction index in the block
        bytes[] mptNodes; // receipts-trie nodes along rlp(txIndex)
        uint256 logIndex; // which log within the receipt to deliver
    }

    /// @notice Verify and deliver the log referenced by `p`. Reverts on any failure.
    /// @return emitter the log's emitting address (== sourceContract)
    /// @return topics  the log topics
    /// @return data    the log data
    function deliver(ReceiptProof calldata p)
        external
        returns (address emitter, bytes32[] memory topics, bytes memory data)
    {
        // 1. finality: receiptsRoot committed under the finalized beacon body.
        require(
            lightClient.verifyReceiptsRootAgainstFinalized(
                p.receiptsRoot, p.finalizedBodyRoot, p.execBranch, execBranchDepth, execBranchIndex
            ),
            "Inbox:not finalized"
        );

        // 2. inclusion: the receipt is in receiptsRoot at key rlp(txIndex).
        bytes memory key = _rlpUint(p.txIndex);
        (bool included, bytes memory receiptValue) =
            MerklePatriciaProof.verify(p.receiptsRoot, key, p.mptNodes);
        require(included, "Inbox:receipt not included");

        // 3. extract the chosen log and check the emitter.
        (emitter, topics, data) = _parseLog(receiptValue, p.logIndex);
        require(emitter == sourceContract, "Inbox:wrong source");

        // 4. replay protection.
        bytes32 id = keccak256(abi.encodePacked(p.receiptsRoot, p.txIndex, p.logIndex));
        require(!delivered[id], "Inbox:already delivered");
        delivered[id] = true;

        emit MessageDelivered(p.receiptsRoot, p.txIndex, p.logIndex, emitter, topics, data);
    }

    // -------------------------------------------------------------------------
    //                          receipt / log parsing
    // -------------------------------------------------------------------------

    /// @notice Parse the `logIndex`-th log out of an EIP-2718 receipt value.
    ///         Handles both legacy (rlp list) and typed (type byte || rlp list) receipts.
    function parseLog(bytes memory receiptValue, uint256 logIndex)
        external
        pure
        returns (address emitter, bytes32[] memory topics, bytes memory data)
    {
        return _parseLog(receiptValue, logIndex);
    }

    function _parseLog(bytes memory receiptValue, uint256 logIndex)
        internal
        pure
        returns (address emitter, bytes32[] memory topics, bytes memory data)
    {
        // Strip the EIP-2718 type prefix if present (first byte < 0xc0 => typed).
        bytes memory rlpList = receiptValue;
        if (receiptValue.length != 0 && uint8(receiptValue[0]) < 0xc0) {
            rlpList = new bytes(receiptValue.length - 1);
            for (uint256 i = 0; i < rlpList.length; i++) rlpList[i] = receiptValue[i + 1];
        }
        // receipt = [status, cumulativeGasUsed, logsBloom, logs]
        bytes[] memory fields = rlpList.splitList();
        require(fields.length == 4, "Inbox:bad receipt");
        bytes[] memory logs = fields[3].splitList();
        require(logIndex < logs.length, "Inbox:log index oob");
        // log = [address, topics[], data]
        bytes[] memory log = logs[logIndex].splitList();
        require(log.length == 3, "Inbox:bad log");
        emitter = _toAddress(log[0].readBytes());
        bytes[] memory rawTopics = log[1].splitList();
        topics = new bytes32[](rawTopics.length);
        for (uint256 i = 0; i < rawTopics.length; i++) {
            topics[i] = _toBytes32(rawTopics[i].readBytes());
        }
        data = log[2].readBytes();
    }

    // -------------------------------------------------------------------------
    //                                helpers
    // -------------------------------------------------------------------------

    /// @dev RLP-encode a non-negative integer (transaction index key).
    function _rlpUint(uint256 v) internal pure returns (bytes memory) {
        if (v == 0) return hex"80";
        // minimal big-endian bytes
        uint256 len = 0;
        uint256 t = v;
        while (t != 0) {
            len++;
            t >>= 8;
        }
        bytes memory be = new bytes(len);
        t = v;
        for (uint256 i = 0; i < len; i++) {
            be[len - 1 - i] = bytes1(uint8(t & 0xff));
            t >>= 8;
        }
        if (len == 1 && uint8(be[0]) < 0x80) {
            return be; // single small byte encodes as itself
        }
        return bytes.concat(bytes1(uint8(0x80 + len)), be);
    }

    function _toAddress(bytes memory b) internal pure returns (address a) {
        require(b.length == 20, "Inbox:addr len");
        assembly {
            a := shr(96, mload(add(b, 32)))
        }
    }

    function _toBytes32(bytes memory b) internal pure returns (bytes32 r) {
        require(b.length == 32, "Inbox:b32 len");
        assembly {
            r := mload(add(b, 32))
        }
    }
}
