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

/**
 * @title AereDelegationRegistry — public index of EIP-7702 delegated EOAs
 * @notice Permissionless registry that records which EOAs have delegated
 *         to the AereDelegate7702 implementation. dApps query this to:
 *           - Render the "smart-wallet enabled" badge in UI
 *           - Discover EOAs eligible for batch execution
 *           - Coordinate paymaster eligibility checks
 *
 *         Each registered account stores:
 *           - delegationCodeHash (EIP-7702 designator hash for cross-check)
 *           - registeredAt timestamp
 *           - lastSeenNonce (for indexers; updated by the account itself or
 *             permissionless `notifyNonce`)
 *
 *         REGISTRATION is permissionless: any EOA can self-register via
 *         `register()`. UNREGISTRATION is also permissionless to the
 *         account itself (e.g. before reverting their EIP-7702 delegation).
 *
 * @dev    The registry is purely INFORMATIONAL — no funds custody, no
 *         access control over the account. Wrong registration data hurts
 *         only the registrant's UX.
 */
contract AereDelegationRegistry {

    struct Entry {
        bool registered;
        bytes32 delegationCodeHash;   // hash of msg.sender's delegated code at registration time
        uint64 registeredAt;
        uint64 lastSeenAt;
        uint256 lastSeenNonce;
    }

    /// @notice account → entry
    mapping(address => Entry) public entries;
    /// @notice paged listing for indexers
    address[] public registered;
    /// @notice account → index in `registered` array (1-based; 0 = not in list)
    mapping(address => uint256) internal _registeredIndex;

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

    event Registered(address indexed account, bytes32 delegationCodeHash, uint64 registeredAt);
    event Unregistered(address indexed account);
    event NonceSeen(address indexed account, uint256 nonce, uint64 timestamp);

    /* --------------------------------- errors ------------------------------- */

    error AlreadyRegistered();
    error NotRegistered();

    /* ------------------------------ registration ---------------------------- */

    /// @notice Self-register the caller. The contract reads `msg.sender`'s
    ///         current code hash via `extcodehash` — if the EOA has
    ///         delegated under EIP-7702, this returns the designated
    ///         implementation's code hash. If the EOA has NOT delegated,
    ///         the hash will be the empty-account hash and the registration
    ///         is still allowed (with that empty hash recorded) — front-end
    ///         filters can ignore entries with the empty hash.
    function register() external {
        if (entries[msg.sender].registered) revert AlreadyRegistered();
        bytes32 codeHash;
        assembly { codeHash := extcodehash(caller()) }
        entries[msg.sender] = Entry({
            registered: true,
            delegationCodeHash: codeHash,
            registeredAt: uint64(block.timestamp),
            lastSeenAt: uint64(block.timestamp),
            lastSeenNonce: 0
        });
        registered.push(msg.sender);
        _registeredIndex[msg.sender] = registered.length; // 1-based
        emit Registered(msg.sender, codeHash, uint64(block.timestamp));
    }

    function unregister() external {
        if (!entries[msg.sender].registered) revert NotRegistered();
        delete entries[msg.sender];
        uint256 idx = _registeredIndex[msg.sender];
        if (idx != 0) {
            uint256 lastIdx = registered.length;
            if (idx != lastIdx) {
                address swapped = registered[lastIdx - 1];
                registered[idx - 1] = swapped;
                _registeredIndex[swapped] = idx;
            }
            registered.pop();
            delete _registeredIndex[msg.sender];
        }
        emit Unregistered(msg.sender);
    }

    /// @notice Anyone may call to update `lastSeenNonce` for an account.
    ///         Indexers + paymasters call this after observing a tx from
    ///         the account so the registry reflects fresh activity.
    function notifyNonce(address account, uint256 nonce) external {
        Entry storage e = entries[account];
        if (!e.registered) revert NotRegistered();
        if (nonce > e.lastSeenNonce) {
            e.lastSeenNonce = nonce;
            e.lastSeenAt = uint64(block.timestamp);
            emit NonceSeen(account, nonce, uint64(block.timestamp));
        }
    }

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

    function registeredCount() external view returns (uint256) { return registered.length; }

    function isRegistered(address account) external view returns (bool) {
        return entries[account].registered;
    }

    function page(uint256 start, uint256 count) external view returns (address[] memory out) {
        uint256 n = registered.length;
        if (start >= n) return new address[](0);
        uint256 end = start + count;
        if (end > n) end = n;
        out = new address[](end - start);
        for (uint256 i = start; i < end; i++) {
            out[i - start] = registered[i];
        }
    }
}
