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

import {DilithiumAttestorV3} from "./DilithiumAttestorV3.sol";
import {RewardStream} from "../deprecated/RewardStream.sol";

/// @title BeliefOracleV3
/// @notice Gateway for resonance scoring backed by Dilithium attestations.
/// @dev Resonance values are deterministic and bounded [0, 100].
///
/// @dev [audit-v3] V3 fixes applied:
///   HIGH-1: Per-user bonusApplied mapping (bytes32 => mapping(address => bool))
///   HIGH-2: Resonance gating — seeker must have at least 1 prior attestation to receive bonus
///           Production TODO: bind resonance formula to ZK proof belief scores (see _resolveResonance)
///   MEDIUM-1: Removed unbounded batchQuery; only batchQueryBounded is exposed
///   MEDIUM-2: CEI fix — cachedResonance and lastResonance written BEFORE external attestor call
///   LOW: previewResonance acknowledged as gameable; fundamental fix is in the resonance formula
contract BeliefOracleV3 {
    uint256 public constant MAX_RESONANCE = 100;
    uint256 public constant BONUS_THRESHOLD = 80;
    uint256 public constant BONUS_MULTIPLIER = 120;

    /// @notice Maximum vows in a single bounded batch call.
    uint256 public constant MAX_BATCH_QUERY = 50;

    /// @notice Minimum number of attested beliefs a seeker must already hold before
    ///         a bonus multiplier is applied.
    /// @dev HIGH-2 FIX: Prevents a brand-new address from gaming a single crafted vow
    ///      to immediately capture a 120% multiplier bonus.
    uint256 public constant MIN_BELIEF_COUNT = 1;

    DilithiumAttestorV3 public immutable attestor;
    RewardStream public immutable rewardStream;
    address public guardian;
    address public immutable ghostEcho;

    bool public resonanceDrifted;

    mapping(bytes32 => uint256) private _cachedResonance;
    mapping(address => uint256) public lastResonance;

    /// @notice Tracks whether a bonus multiplier has been applied for a given
    ///         (vowHash, seeker) pair.
    /// @dev HIGH-1 FIX: Per-user mapping prevents one user's bonus from blocking
    ///      all other users from ever receiving a bonus on the same vow hash.
    mapping(bytes32 => mapping(address => bool)) public bonusApplied;

    /// @notice Tracks how many beliefs each address has been attested for
    ///         (incremented inside queryBelief after a successful attestation).
    /// @dev HIGH-2 FIX: Used to enforce MIN_BELIEF_COUNT gate for bonus eligibility.
    mapping(address => uint256) public beliefCount;

    event ResonanceQueried(
        address indexed seeker,
        bytes32 indexed vowHash,
        uint256 resonance,
        bool multiplierApplied
    );
    event ResonanceDriftSet(bool active, address indexed guardian);
    event GuardianUpdated(address indexed previousGuardian, address indexed newGuardian);

    error GuardianRequired();
    error LengthMismatch();

    modifier onlyGuardian() {
        if (msg.sender != guardian) {
            revert GuardianRequired();
        }
        _;
    }

    constructor(
        DilithiumAttestorV3 attestor_,
        RewardStream rewardStream_,
        address guardian_,
        address ghostEcho_
    ) {
        if (address(attestor_) == address(0) || address(rewardStream_) == address(0)) {
            revert GuardianRequired();
        }
        if (guardian_ == address(0)) {
            revert GuardianRequired();
        }
        attestor = attestor_;
        rewardStream = rewardStream_;
        guardian = guardian_;
        ghostEcho = ghostEcho_;
    }

    /// @notice Toggle resonance drift protections.
    function setResonanceDrift(bool active) external onlyGuardian {
        resonanceDrifted = active;
        emit ResonanceDriftSet(active, msg.sender);
    }

    /// @notice Rotate the guardian responsible for drift controls.
    function updateGuardian(address newGuardian) external onlyGuardian {
        if (newGuardian == address(0)) {
            revert GuardianRequired();
        }
        address previous = guardian;
        guardian = newGuardian;
        emit GuardianUpdated(previous, newGuardian);
    }

    /// @notice Query belief resonance and optionally vest RewardStream multipliers.
    /// @dev CEI ORDER:
    ///      1. Resolve resonance values (pure computation, no state)
    ///      2. Write all state (_cachedResonance, lastResonance, bonusApplied, beliefCount)
    ///      3. External calls (attestor.attestBelief, rewardStream.updateMultiplier)
    /// @return resonance The deterministic resonance score [0, 100].
    function queryBelief(string memory vow, bytes calldata zkSig) public virtual returns (uint256) {
        bytes32 vowHash = keccak256(bytes(vow));

        uint256 resonance = _resolveResonance(vowHash, msg.sender);

        // MEDIUM-2 CEI FIX: Write all state BEFORE any external calls.
        // [audit-v3] Both cachedResonance and lastResonance are updated here,
        // before attestor.attestBelief (external) and rewardStream.updateMultiplier (external).
        _cachedResonance[vowHash] = resonance;
        lastResonance[msg.sender] = resonance;

        // HIGH-2 FIX: Seeker must have at least MIN_BELIEF_COUNT prior attestations
        // before a bonus can be granted. This prevents single-vow bonus gaming.
        // TODO (production): Replace _resolveResonance with a formula that incorporates
        // the ZK proof belief score from the attestation journal, binding resonance to
        // cryptographically verified belief strength rather than a hash-derived value.
        bool bonusEligible = (
            !resonanceDrifted &&
            resonance > BONUS_THRESHOLD &&
            !bonusApplied[vowHash][msg.sender] &&
            beliefCount[msg.sender] >= MIN_BELIEF_COUNT
        );

        if (bonusEligible) {
            // HIGH-1 FIX: bonusApplied is per (vowHash, caller), not global per vowHash.
            bonusApplied[vowHash][msg.sender] = true;
        }

        // Increment beliefCount BEFORE external attestor call (CEI).
        beliefCount[msg.sender]++;

        // External call 1: attestor.attestBelief — state already written above.
        attestor.attestBelief(vowHash, zkSig);

        bool applied = false;
        if (bonusEligible) {
            // External call 2: rewardStream.updateMultiplier — all state already written.
            try rewardStream.updateMultiplier(msg.sender, BONUS_MULTIPLIER) {
                applied = true;
            } catch {
                // updateMultiplier reverted — bonus flag already set; leave applied as false.
                applied = false;
            }
        }

        emit ResonanceQueried(msg.sender, vowHash, resonance, applied);
        return resonance;
    }

    /// @notice Bounded batch query for production use.
    /// @dev MEDIUM-1 FIX: Unbounded batchQuery has been removed. Only this bounded
    ///      variant is exposed, capping at MAX_BATCH_QUERY to bound gas usage and
    ///      prevent DoS from overly-large batches.
    function batchQueryBounded(string[] calldata vows, bytes[] calldata zkSigs)
        external
        returns (uint256[] memory)
    {
        if (vows.length != zkSigs.length) {
            revert LengthMismatch();
        }
        require(vows.length <= MAX_BATCH_QUERY, "Batch too large");

        uint256[] memory resonances = new uint256[](vows.length);
        for (uint256 i = 0; i < vows.length; ++i) {
            resonances[i] = queryBelief(vows[i], zkSigs[i]);
        }
        return resonances;
    }

    /// @notice Preview deterministic resonance without triggering attestations.
    /// @dev LOW (acknowledged): This function is gameable because _resolveResonance
    ///      is purely hash-derived and callers can precompute high-scoring vow strings.
    ///      The fundamental fix requires binding resonance to ZK proof belief scores
    ///      (see TODO in queryBelief). This view function itself is not the attack surface.
    function previewResonance(string calldata vow, address seeker) external view returns (uint256) {
        bytes32 vowHash = keccak256(bytes(vow));
        return _resolveResonance(vowHash, seeker);
    }

    /// @notice Return cached resonance for a vow hash, 0 if never queried.
    function cachedResonance(bytes32 vowHash) external view returns (uint256) {
        return _cachedResonance[vowHash];
    }

    /// @dev Deterministic resonance in [0, MAX_RESONANCE].
    ///      TODO (production): Replace with a formula that incorporates the attested
    ///      ZK proof belief score so high resonance requires cryptographic proof of
    ///      strong belief, not just a lucky hash prefix.
    function _resolveResonance(bytes32 vowHash, address seeker) internal view returns (uint256) {
        bytes32 digest = keccak256(abi.encodePacked(vowHash, seeker, ghostEcho));
        return uint256(digest) % (MAX_RESONANCE + 1);
    }
}
