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

/**
 * @title VaultfireForumRegistry
 * @notice On-chain registry mapping forum category names to XMTP MLS groupIds.
 *
 * @dev
 * Lightweight, permissionless registry for the Vaultfire Discussion Board.
 *
 * MLS groups in XMTP are addressed by an opaque hex `groupId`. To allow new
 * users to discover and join a category's group without a centralized server,
 * the FIRST member to create a category's MLS group registers the resulting
 * groupId here. Subsequent users read the registry, request to join, and
 * existing members can add them.
 *
 * Design properties:
 *   - First-write-wins: prevents drive-by hijacks. Once a category is set,
 *     it cannot be overwritten (except by the original publisher, see below).
 *   - Publisher can rotate: the address that originally registered a category
 *     can publish a new groupId for it (e.g. if the group was rotated). This
 *     keeps the registry usable without admin keys.
 *   - Stateless: no fees, no access control, no upgradeability. Just a map.
 *   - No personal data: only the publisher's address, the category string,
 *     and an opaque groupId hex.
 *
 * @custom:security-model Zero-trust. Anyone can register an unset category;
 * only the original publisher can rotate. No owner / admin.
 * @custom:ethics Privacy default — no PII, no KYC, no surveillance.
 */
contract VaultfireForumRegistry {
    /// @notice Registry entry per category
    struct Entry {
        bytes   groupId;     // opaque MLS groupId (variable-length bytes)
        address publisher;   // who registered this entry
        uint256 publishedAt; // block timestamp of first registration
        uint256 updatedAt;   // last rotation timestamp (== publishedAt if never rotated)
    }

    /// @notice category string => entry. Keys are e.g. "general", "technical".
    mapping(string => Entry) private _entries;

    /// @notice list of all registered categories, in registration order
    string[] private _categories;

    event GroupRegistered(
        string  indexed categoryHashIndexed, // indexed for filtering — keccak of category
        string  category,                    // human-readable
        bytes   groupId,
        address indexed publisher,
        uint256 timestamp
    );

    event GroupRotated(
        string  indexed categoryHashIndexed,
        string  category,
        bytes   oldGroupId,
        bytes   newGroupId,
        address indexed publisher,
        uint256 timestamp
    );

    /**
     * @notice Register a new groupId for a category. First-write-wins.
     * @dev Reverts if the category is already registered by someone else.
     *      To rotate an existing category, the original publisher must call
     *      this — they are allowed to overwrite their own entry.
     * @param category Lowercase category slug (e.g. "general")
     * @param groupId  Opaque MLS groupId bytes
     */
    function registerGroup(string calldata category, bytes calldata groupId) external {
        require(bytes(category).length > 0,  "category required");
        require(bytes(category).length <= 64, "category too long");
        require(groupId.length > 0,           "groupId required");
        require(groupId.length <= 256,        "groupId too long");

        Entry storage e = _entries[category];

        if (e.publisher == address(0)) {
            // Fresh registration
            e.groupId     = groupId;
            e.publisher   = msg.sender;
            e.publishedAt = block.timestamp;
            e.updatedAt   = block.timestamp;
            _categories.push(category);
            emit GroupRegistered(category, category, groupId, msg.sender, block.timestamp);
        } else {
            // Rotation — only original publisher
            require(e.publisher == msg.sender, "not original publisher");
            bytes memory oldGroupId = e.groupId;
            e.groupId   = groupId;
            e.updatedAt = block.timestamp;
            emit GroupRotated(category, category, oldGroupId, groupId, msg.sender, block.timestamp);
        }
    }

    /**
     * @notice Read a category's current groupId.
     * @return groupId      Opaque MLS groupId (empty bytes if unregistered)
     * @return publisher    Address that registered/last rotated this category
     * @return publishedAt  Original registration timestamp
     * @return updatedAt    Last rotation timestamp
     */
    function getGroup(string calldata category)
        external
        view
        returns (bytes memory groupId, address publisher, uint256 publishedAt, uint256 updatedAt)
    {
        Entry storage e = _entries[category];
        return (e.groupId, e.publisher, e.publishedAt, e.updatedAt);
    }

    /// @notice Whether a category is currently registered.
    function isRegistered(string calldata category) external view returns (bool) {
        return _entries[category].publisher != address(0);
    }

    /// @notice All registered categories, in registration order.
    function listCategories() external view returns (string[] memory) {
        return _categories;
    }

    /// @notice Number of registered categories.
    function categoryCount() external view returns (uint256) {
        return _categories.length;
    }
}
