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

import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/ISeason1.sol";

/**
 * @title AereQuestRegistry — AERE Season 1 "Altitude" quest catalogue
 * @notice Foundation-owned registry describing every quest: its point weight,
 *         active window, per-user cap, active flag, and whether it requires a
 *         verified human. Fully TUNABLE without redeploying the ledger or the
 *         attestor — those read quest config live from here.
 *
 *  A quest is keyed by a bytes32 questId (e.g. keccak256("create-passkey")).
 *
 *  This contract holds NO funds and issues NO credits. It is pure config that
 *  the attestor and referral registry consult. Owner = Foundation multisig.
 */
contract AereQuestRegistry is Ownable, IAereQuestRegistry {

    mapping(bytes32 => Quest) private _quests;
    bytes32[] public questIds;

    event QuestCreated(
        bytes32 indexed questId,
        uint256 weight,
        uint64  startTime,
        uint64  endTime,
        uint256 perUserCap,
        bool    requiresVerifiedHuman
    );
    event QuestUpdated(
        bytes32 indexed questId,
        uint256 weight,
        uint64  startTime,
        uint64  endTime,
        uint256 perUserCap,
        bool    requiresVerifiedHuman
    );
    event QuestActiveSet(bytes32 indexed questId, bool active);

    error QuestExists();
    error UnknownQuest();
    error BadWindow();
    error BadWeight();

    /* ------------------------------- mutations ------------------------------ */

    function createQuest(
        bytes32 questId,
        uint256 weight,
        uint64  startTime,
        uint64  endTime,
        uint256 perUserCap,
        bool    requiresVerifiedHuman
    ) external onlyOwner {
        if (questId == bytes32(0)) revert UnknownQuest();
        if (_quests[questId].exists) revert QuestExists();
        _validate(weight, startTime, endTime, perUserCap);

        _quests[questId] = Quest({
            weight: weight,
            startTime: startTime,
            endTime: endTime,
            perUserCap: perUserCap,
            active: true,
            requiresVerifiedHuman: requiresVerifiedHuman,
            exists: true
        });
        questIds.push(questId);
        emit QuestCreated(questId, weight, startTime, endTime, perUserCap, requiresVerifiedHuman);
    }

    function updateQuest(
        bytes32 questId,
        uint256 weight,
        uint64  startTime,
        uint64  endTime,
        uint256 perUserCap,
        bool    requiresVerifiedHuman
    ) external onlyOwner {
        Quest storage q = _quests[questId];
        if (!q.exists) revert UnknownQuest();
        _validate(weight, startTime, endTime, perUserCap);

        q.weight = weight;
        q.startTime = startTime;
        q.endTime = endTime;
        q.perUserCap = perUserCap;
        q.requiresVerifiedHuman = requiresVerifiedHuman;
        emit QuestUpdated(questId, weight, startTime, endTime, perUserCap, requiresVerifiedHuman);
    }

    function setQuestActive(bytes32 questId, bool active) external onlyOwner {
        if (!_quests[questId].exists) revert UnknownQuest();
        _quests[questId].active = active;
        emit QuestActiveSet(questId, active);
    }

    function _validate(uint256 weight, uint64 startTime, uint64 endTime, uint256 perUserCap) internal pure {
        if (weight == 0) revert BadWeight();
        // perUserCap must admit at least one completion.
        if (perUserCap < weight) revert BadWeight();
        // endTime 0 == open-ended; otherwise must be after start.
        if (endTime != 0 && endTime <= startTime) revert BadWindow();
    }

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

    function getQuest(bytes32 questId) external view returns (Quest memory) {
        Quest memory q = _quests[questId];
        if (!q.exists) revert UnknownQuest();
        return q;
    }

    /// @notice True iff the quest exists, is active, and `atTime` is within its
    ///         window. endTime == 0 means open-ended.
    function isOpen(bytes32 questId, uint256 atTime) external view returns (bool) {
        Quest memory q = _quests[questId];
        if (!q.exists || !q.active) return false;
        if (atTime < q.startTime) return false;
        if (q.endTime != 0 && atTime > q.endTime) return false;
        return true;
    }

    function questCount() external view returns (uint256) {
        return questIds.length;
    }
}
