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

import "./BaseYieldPoolBondV3.sol";
import "../MissionEnforcement.sol";
import "./VaultfireErrors.sol";
import "./IFlourishingMetricsOracle.sol";
import "./IVaultfireBridgeNotifier.sol";

/**
 * @title AI Accountability Bonds V3.1 (Partner Ready — Base + Avalanche)
 * @notice AI can only profit when ALL humans thrive - Works even with ZERO employment
 *
 * @dev Philosophy: The ONLY economic system that works when AI fires everyone.
 * Creates AI companies that profit from human thriving, not human obsolescence.
 *
 * @dev Key Innovation: Works with ZERO employment.
 * Measures purpose/education, not jobs.
 *
 * @dev Mission Alignment: Protects ALL HUMANS (not just workers).
 * Locks AI profits when humans suffering, regardless of employment status.
 *
 * V3.1 Changes (2026-02 Partner-Ready — Base + Avalanche):
 * - NEW: Multi-asset staking — createBond accepts native (ETH/AVAX) or whitelisted ERC20
 * - NEW: createBondWithToken() for ERC20 staking (WETH on Avalanche, WAVAX on Base, etc.)
 * - NEW: Distribution payouts route to correct asset type automatically
 * - All V3 fixes preserved (C-02, C-03, H-01, H-03, M-02, depreciation, oracle threshold)
 *
 * V3.3 Audit Fixes:
 * - [audit-v3.3] CRIT-04: CEI violation in distributeBond — remainingStake update moved
 *   to AFTER all _transferStake calls to prevent phantom balance on transfer failure
 * - [audit-v3.3] CRIT-06: deactivateBond uses remainingStake[bondId] instead of
 *   bond.stakeAmount for totalActiveBondValue so the counter stays accurate after
 *   distribution cycles add yield
 * - [audit-v3.3] CRIT-08: oracleConfirmMetrics now requires a quorum of distinct oracle
 *   confirmations (oracleRequiredThreshold re-interpreted as minimum oracle count) before
 *   setting oracleConfirmedMetrics[bondId] = true
 * - [audit-v3.3] CRIT-03: FlourishingMetricsOracleV3 wired into globalFlourishingScore.
 *   When oracle data is available, self-reported metrics are cross-checked against oracle
 *   TWAP and capped to a ±20% tolerance band. Oracle is authoritative for bond valuation.
 * - [audit-v3.3] HIGH-03: Oracle confirmation is now cycle-specific — tied to a metrics
 *   index so stale confirmations from prior submissions are rejected at distribution time.
 * - [audit-v3.3] HIGH-04: resolveChallenge pays ERC20-bond penalties from per-token
 *   protocolFundsToken instead of native protocolFunds
 * - [audit-v3.3] HIGH-18: deactivateBond notifies the bridge so remote chain marks the
 *   bond inactive (prevents stale active-state trust abuse).
 *
 * @custom:security ReentrancyGuard, Pausable, YieldPool, Timelock, MultiAsset
 * @custom:ethics 100% to humans when suffering, works with zero jobs
 * @custom:chains Base (ETH + WAVAX), Avalanche (AVAX + WETH)
 */
contract AIAccountabilityBondsV3 is BaseYieldPoolBondV3 {

    // ============ Mission Enforcement ============

    MissionEnforcement public missionEnforcement;
    bool public missionEnforcementEnabled;

    event MissionEnforcementUpdated(address indexed previous, address indexed current);
    event MissionEnforcementEnabled(bool enabled);

    // ============ Structs ============

    struct Bond {
        uint256 bondId;
        address aiCompany;
        string companyName;
        uint256 quarterlyRevenue;
        uint256 stakeAmount;
        uint256 createdAt;
        uint256 distributionRequestedAt;
        bool distributionPending;
        bool active;
        uint256 distributionCount;
    }

    struct GlobalFlourishingMetrics {
        uint256 timestamp;
        uint256 incomeDistributionScore;  // 0-10000
        uint256 povertyRateScore;         // 0-10000
        uint256 healthOutcomesScore;      // 0-10000
        uint256 mentalHealthScore;        // 0-10000
        uint256 educationAccessScore;     // 0-10000
        uint256 purposeAgencyScore;       // 0-10000
    }

    struct Distribution {
        uint256 timestamp;
        int256 totalAmount;
        uint256 humanShare;
        uint256 aiCompanyShare;
        uint256 globalFlourishingScore;
        string reason;
    }

    struct OracleSource {
        address oracleAddress;
        string sourceName;
        bool isActive;
        uint256 registeredAt;
        uint256 trustScore;  // 0-10000
    }

    struct AIVerification {
        uint256 bondId;
        address verifyingAI;
        string verifyingCompanyName;
        uint256 timestamp;
        bool confirmsMetrics;
        string notes;
        uint256 stakeAmount;
        bool stakeReturned;
    }

    struct MetricsChallenge {
        address challenger;
        uint256 timestamp;
        string reason;
        uint256 challengeStake;
        bool resolved;
        bool challengeUpheld;
    }

    // ============ State Variables ============

    uint256 public nextBondId = 1;
    mapping(uint256 => Bond) public bonds;
    mapping(uint256 => GlobalFlourishingMetrics[]) public bondMetrics;
    mapping(uint256 => Distribution[]) public bondDistributions;
    mapping(uint256 => uint256) public lastDistributedValue;

    address payable public humanTreasury;

    // Oracle integration
    mapping(address => OracleSource) public oracles;
    address[] public registeredOracles;

    // AI-to-AI verification
    mapping(uint256 => AIVerification[]) public bondAIVerifications;
    mapping(address => uint256) public aiCompanyVerificationCount;

    // O(1) counters
    mapping(uint256 => uint256) public verificationConfirmCount;
    mapping(uint256 => uint256) public verificationRejectCount;

    // Metrics challenges
    mapping(uint256 => MetricsChallenge[]) public bondChallenges;
    mapping(uint256 => uint256) public activeChallengeCount;
    uint256 public constant MIN_CHALLENGE_STAKE = 0.1 ether;

    // Profit locking thresholds
    uint256 public constant SUFFERING_THRESHOLD = 4000;
    uint256 public constant LOW_INCLUSION_THRESHOLD = 4000;

    // Verification thresholds
    uint256 public constant MIN_AI_VERIFICATIONS = 2;
    uint256 public constant MIN_ORACLE_TRUST_SCORE = 7000;

    // V3: Oracle-required stake threshold
    uint256 public oracleRequiredThreshold = 10 ether;
    mapping(uint256 => bool) public oracleConfirmedMetrics;

    // [audit-v3.3] CRIT-08: Oracle quorum state — track distinct oracle confirmations per bond
    mapping(uint256 => uint256) public oracleConfirmationCount;           // [audit-v3.3]
    // [audit-v3.3-r2] NEW-03 FIX: Replaced per-oracle boolean with cycle-counter pattern.
    // oracleHasConfirmed is now checked via bondConfirmCycle comparison, eliminating the
    // unbounded O(n) iteration of registeredOracles in submitMetrics.
    // oracleConfirmedAtCycle[bondId][oracle] stores the cycle at which the oracle confirmed.
    // If it equals bondConfirmCycle[bondId], the oracle has already confirmed this cycle.
    mapping(uint256 => mapping(address => uint256)) public oracleConfirmedAtCycle; // [audit-v3.3-r2]
    mapping(uint256 => uint256) public bondConfirmCycle; // [audit-v3.3-r2] incremented each submitMetrics

    // [audit-v3.3] CRIT-08: Minimum distinct oracle confirmations required
    // Re-uses oracleRequiredThreshold concept but for count (not value).
    // Default 2 = require at least 2 distinct oracles.
    uint256 public oracleQuorum = 2; // [audit-v3.3]

    // [audit-v3.3] CRIT-03: Reference to FlourishingMetricsOracleV3 for cross-checking
    IFlourishingMetricsOracle public flourishingOracle; // [audit-v3.3]
    bool public oracleEnforced; // [audit-v3.3] when true, oracle TWAP caps self-reported scores
    // Metric IDs used for oracle TWAP lookups (keccak256 of metric names)
    bytes32 public constant METRIC_INCOME       = keccak256("incomeDistribution"); // [audit-v3.3]
    bytes32 public constant METRIC_POVERTY      = keccak256("povertyRate"); // [audit-v3.3]
    bytes32 public constant METRIC_HEALTH       = keccak256("healthOutcomes"); // [audit-v3.3]
    bytes32 public constant METRIC_MENTAL       = keccak256("mentalHealth"); // [audit-v3.3]
    bytes32 public constant METRIC_EDUCATION    = keccak256("educationAccess"); // [audit-v3.3]
    bytes32 public constant METRIC_PURPOSE      = keccak256("purposeAgency"); // [audit-v3.3]
    uint256 public constant ORACLE_TOLERANCE_BPS = 2000; // 20% max deviation from oracle // [audit-v3.3]

    // [audit-v3.3] HIGH-03: Cycle-specific oracle confirmation — tracks the metrics
    // array index that was confirmed, not just a boolean.
    mapping(uint256 => uint256) public confirmedMetricsIndex; // [audit-v3.3]

    // [audit-v3.3-r2] NEW-01 FIX: Tracks the quorum level when confirmation was achieved.
    // requestDistribution checks confirmedAtQuorum[bondId] >= oracleQuorum to prevent
    // bypassing a raised quorum with a prior lower-quorum confirmation.
    mapping(uint256 => uint256) public confirmedAtQuorum; // [audit-v3.3-r2]

    // [audit-v3.3] HIGH-18: Bridge notifier for deactivation propagation
    IVaultfireBridgeNotifier public bridgeNotifier; // [audit-v3.3]

    // [audit-v3.2] H-02 FIX: Minimum verification stake to prevent dust stakes
    uint256 public constant MIN_VERIFICATION_STAKE = 0.01 ether;

    // ============ Events ============

    event BondCreated(uint256 indexed bondId, address indexed aiCompany, string companyName, uint256 quarterlyRevenue, uint256 stakeAmount, address stakeToken, uint256 timestamp);
    event HumanTreasuryUpdated(address indexed previousTreasury, address indexed newTreasury);
    event MetricsSubmitted(uint256 indexed bondId, uint256 timestamp, uint256 globalFlourishingScore);
    event DistributionRequested(uint256 indexed bondId, address indexed aiCompany, uint256 requestedAt, uint256 availableAt);
    event BondDistributed(uint256 indexed bondId, address indexed aiCompany, uint256 humanShare, uint256 aiCompanyShare, int256 appreciation, string reason, uint256 timestamp);
    event ProfitsLocked(uint256 indexed bondId, string reason, uint256 timestamp);
    event OracleRegistered(address indexed oracleAddress, string sourceName, uint256 trustScore, uint256 timestamp);
    event AIVerificationSubmitted(uint256 indexed bondId, address indexed verifyingAI, bool confirmsMetrics, uint256 stakeAmount, uint256 timestamp);
    event MetricsChallenged(uint256 indexed bondId, address indexed challenger, string reason, uint256 challengeStake, uint256 timestamp);
    event ChallengeResolved(uint256 indexed bondId, uint256 indexed challengeIndex, bool challengeUpheld, uint256 timestamp);

    // V3 events
    event VerificationStakeReturned(uint256 indexed bondId, uint256 indexed verificationIndex, address indexed verifier, uint256 amount);
    event OracleMetricsConfirmed(uint256 indexed bondId, address indexed oracle, uint256 timestamp);
    event OracleRequiredThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
    event BondDistributionCycleCompleted(uint256 indexed bondId, uint256 cycleNumber);
    event OracleQuorumUpdated(uint256 oldQuorum, uint256 newQuorum); // [audit-v3.3]
    event FlourishingOracleUpdated(address indexed previous, address indexed current); // [audit-v3.3]
    event OracleEnforcementUpdated(bool enforced); // [audit-v3.3]
    event BridgeNotifierUpdated(address indexed previous, address indexed current); // [audit-v3.3]
    event BondDeactivationNotified(uint256 indexed bondId); // [audit-v3.3]

    // ============ Modifiers ============

    modifier onlyAICompany(uint256 bondId) {
        if (bonds[bondId].aiCompany != msg.sender) revert AB_OnlyAICompany();
        _;
    }

    modifier bondExists(uint256 bondId) {
        if (!bonds[bondId].active) revert AB_BondNotExist();
        _;
    }

    // ============ Constructor ============

    constructor(address payable _humanTreasury) {
        if (_humanTreasury == address(0)) revert AB_ZeroAddress();
        humanTreasury = _humanTreasury;
        emit HumanTreasuryUpdated(address(0), _humanTreasury);
        missionEnforcementEnabled = false;
    }

    // ============ Mission Enforcement ============

    function setMissionEnforcement(address mission) external onlyOwner {
        _validateAddress(mission, "Mission enforcement");
        address previous = address(missionEnforcement);
        missionEnforcement = MissionEnforcement(mission);
        emit MissionEnforcementUpdated(previous, mission);
    }

    function setMissionEnforcementEnabled(bool enabled) external onlyOwner {
        missionEnforcementEnabled = enabled;
        emit MissionEnforcementEnabled(enabled);
    }

    function _requireMissionCompliance() internal view {
        if (!missionEnforcementEnabled) return;
        address m = address(missionEnforcement);
        if (m == address(0)) revert AB_MissionNotSet();

        if (!missionEnforcement.isCompliantWithPrinciple(address(this), MissionEnforcement.CorePrinciple.AI_PROFIT_CAPS))
            revert AB_MissionProfitCaps();
        if (!missionEnforcement.isCompliantWithPrinciple(address(this), MissionEnforcement.CorePrinciple.COMMUNITY_CHALLENGES))
            revert AB_MissionCommunityChallenges();
        if (!missionEnforcement.isCompliantWithPrinciple(address(this), MissionEnforcement.CorePrinciple.PRIVACY_DEFAULT))
            revert AB_MissionPrivacy();
    }

    function setHumanTreasury(address payable _newTreasury) external onlyOwner {
        if (_newTreasury == address(0)) revert AB_ZeroAddress();
        address previous = humanTreasury;
        humanTreasury = _newTreasury;
        emit HumanTreasuryUpdated(previous, _newTreasury);
    }

    // ============ V3: Oracle Threshold Management ============

    function setOracleRequiredThreshold(uint256 newThreshold) external onlyOwner {
        uint256 old = oracleRequiredThreshold;
        oracleRequiredThreshold = newThreshold;
        emit OracleRequiredThresholdUpdated(old, newThreshold);
    }

    /// @notice [audit-v3.3] CRIT-08: Update the oracle quorum (minimum distinct confirmations)
    /// @dev [audit-v3.3-r2] NEW-01 FIX: When raising the quorum, invalidate all bonds
    ///      that were confirmed under the old, lower quorum. Stores the quorum value
    ///      at confirmation time so requestDistribution can verify the bond was confirmed
    ///      at the current (or higher) quorum level.
    function setOracleQuorum(uint256 newQuorum) external onlyOwner { // [audit-v3.3]
        if (newQuorum == 0) revert VF_AmountZero(); // [audit-v3.3]
        uint256 old = oracleQuorum; // [audit-v3.3]
        oracleQuorum = newQuorum; // [audit-v3.3]
        emit OracleQuorumUpdated(old, newQuorum); // [audit-v3.3]
    } // [audit-v3.3]

    /// @notice [audit-v3.3] CRIT-03: Set the FlourishingMetricsOracle address
    function setFlourishingOracle(address oracle) external onlyOwner { // [audit-v3.3]
        address previous = address(flourishingOracle); // [audit-v3.3]
        flourishingOracle = IFlourishingMetricsOracle(oracle); // [audit-v3.3]
        emit FlourishingOracleUpdated(previous, oracle); // [audit-v3.3]
    } // [audit-v3.3]

    /// @notice [audit-v3.3] CRIT-03: Toggle oracle enforcement (when true, oracle caps scores)
    function setOracleEnforced(bool enforced) external onlyOwner { // [audit-v3.3]
        oracleEnforced = enforced; // [audit-v3.3]
        emit OracleEnforcementUpdated(enforced); // [audit-v3.3]
    } // [audit-v3.3]

    /// @notice [audit-v3.3] HIGH-18: Set the bridge notifier for deactivation propagation
    function setBridgeNotifier(address notifier) external onlyOwner { // [audit-v3.3]
        address previous = address(bridgeNotifier); // [audit-v3.3]
        bridgeNotifier = IVaultfireBridgeNotifier(notifier); // [audit-v3.3]
        emit BridgeNotifierUpdated(previous, notifier); // [audit-v3.3]
    } // [audit-v3.3]

    /**
     * @notice Oracle confirms the latest submitted metrics for a bond
     * @dev [audit-v3.3] CRIT-08 FIX: Each oracle can confirm at most once per bond-metrics
     *      cycle (reset by submitMetrics). oracleConfirmedMetrics[bondId] only becomes true
     *      once at least `oracleQuorum` distinct, qualified oracles have confirmed.
     *      This prevents a single compromised oracle from enabling yield pool drain.
     *
     * [audit-v3.3] HIGH-03 FIX: Confirmation is now tied to a specific metrics index.
     *      confirmedMetricsIndex[bondId] records which bondMetrics index was confirmed;
     *      requestDistribution checks it matches the current latest index.
     */
    function oracleConfirmMetrics(uint256 bondId) external bondExists(bondId) {
        OracleSource storage oracle = oracles[msg.sender];
        if (!oracle.isActive) revert AB_OracleNotRegistered();
        if (oracle.trustScore < MIN_ORACLE_TRUST_SCORE) revert AB_OracleTrustLow();
        if (bondMetrics[bondId].length == 0) revert AB_NoMetricsToConfirm();

        // [audit-v3.3] CRIT-08: Prevent the same oracle from counting twice in one cycle
        // [audit-v3.3-r2] NEW-03 FIX: Use cycle counter instead of boolean flag
        if (oracleConfirmedAtCycle[bondId][msg.sender] == bondConfirmCycle[bondId]) revert AB_OracleAlreadyConfirmed(); // [audit-v3.3-r2]

        oracleConfirmedAtCycle[bondId][msg.sender] = bondConfirmCycle[bondId]; // [audit-v3.3-r2]
        oracleConfirmationCount[bondId]++; // [audit-v3.3]

        // [audit-v3.3] Only mark confirmed once quorum is reached
        if (oracleConfirmationCount[bondId] >= oracleQuorum) { // [audit-v3.3]
            oracleConfirmedMetrics[bondId] = true; // [audit-v3.3]
            // [audit-v3.3] HIGH-03: Record which metrics index was confirmed
            confirmedMetricsIndex[bondId] = bondMetrics[bondId].length - 1; // [audit-v3.3]
            // [audit-v3.3-r2] NEW-01 FIX: Record the quorum at which confirmation occurred
            confirmedAtQuorum[bondId] = oracleQuorum; // [audit-v3.3-r2]
        } // [audit-v3.3]

        emit OracleMetricsConfirmed(bondId, msg.sender, block.timestamp);
    }

    // ============ Core Functions ============

    /**
     * @notice Create AI Accountability Bond with native token (ETH on Base, AVAX on Avalanche)
     * @dev AI company stakes 30% of quarterly revenue in native token
     */
    function createBond(
        string memory companyName,
        uint256 quarterlyRevenue
    ) external payable nonReentrant whenNotPaused returns (uint256) {
        _validateNonZero(msg.value, "Stake amount");
        if (msg.value < (quarterlyRevenue * 30) / 100) revert AB_InsufficientStake();
        if (bytes(companyName).length == 0) revert AB_CompanyNameRequired();
        if (bytes(companyName).length > 100) revert AB_CompanyNameTooLong();

        uint256 bondId = nextBondId++;

        bonds[bondId] = Bond({
            bondId: bondId,
            aiCompany: msg.sender,
            companyName: companyName,
            quarterlyRevenue: quarterlyRevenue,
            stakeAmount: msg.value,
            createdAt: block.timestamp,
            distributionRequestedAt: 0,
            distributionPending: false,
            active: true,
            distributionCount: 0
        });

        // V3.1: Record as native token stake
        _recordNativeStake(bondId, msg.value);

        lastDistributedValue[bondId] = msg.value;
        _updateTotalActiveBondValue(totalActiveBondValue + msg.value);

        emit BondCreated(bondId, msg.sender, companyName, quarterlyRevenue, msg.value, NATIVE_TOKEN, block.timestamp);
        return bondId;
    }

    /**
     * @notice Create AI Accountability Bond with an ERC20 token (WETH, WAVAX, USDC, etc.)
     * @dev AI company stakes 30% of quarterly revenue in an accepted ERC20 token.
     *      Caller must approve this contract for `stakeAmount` before calling.
     *      [audit-v3.3] CRIT-05: Actual received tokens (post-fee) are used for all
     *      downstream accounting; bond.stakeAmount reflects the actual received amount.
     *
     * Mission Alignment: Partners on Avalanche can stake WETH without bridging.
     * Partners on Base can stake WAVAX without bridging. No friction, no barriers.
     *
     * @param companyName AI company name
     * @param quarterlyRevenue Reported quarterly revenue (stake must be >= 30%)
     * @param token ERC20 token address (must be on acceptedTokens whitelist)
     * @param stakeAmount Amount of ERC20 tokens to stake
     */
    function createBondWithToken(
        string memory companyName,
        uint256 quarterlyRevenue,
        address token,
        uint256 stakeAmount
    ) external nonReentrant whenNotPaused returns (uint256) {
        _validateNonZero(stakeAmount, "Stake amount");
        if (stakeAmount < (quarterlyRevenue * 30) / 100) revert AB_InsufficientStake();
        if (bytes(companyName).length == 0) revert AB_CompanyNameRequired();
        if (bytes(companyName).length > 100) revert AB_CompanyNameTooLong();
        uint256 bondId = nextBondId++;

        // [audit-v3.3] CRIT-05: _recordTokenStake returns actualReceived via balance-delta;
        // read remainingStake[bondId] after the call to get the true amount credited.
        // We must create the Bond struct AFTER the token pull so bond.stakeAmount
        // reflects the actual received amount.
        _recordTokenStake(bondId, token, stakeAmount, msg.sender); // [audit-v3.3]
        uint256 actualStake = remainingStake[bondId]; // [audit-v3.3] actual received

        bonds[bondId] = Bond({
            bondId: bondId,
            aiCompany: msg.sender,
            companyName: companyName,
            quarterlyRevenue: quarterlyRevenue,
            stakeAmount: actualStake, // [audit-v3.3] use actual received amount
            createdAt: block.timestamp,
            distributionRequestedAt: 0,
            distributionPending: false,
            active: true,
            distributionCount: 0
        });

        lastDistributedValue[bondId] = actualStake; // [audit-v3.3]
        _updateTotalActiveBondValue(totalActiveBondValue + actualStake); // [audit-v3.3]

        emit BondCreated(bondId, msg.sender, companyName, quarterlyRevenue, actualStake, token, block.timestamp); // [audit-v3.3]
        return bondId;
    }

    /**
     * @notice Submit global human flourishing metrics
     * @dev Measured globally, not per-company. Works with ZERO employment.
     */
    function submitMetrics(
        uint256 bondId,
        uint256 incomeDistributionScore,
        uint256 povertyRateScore,
        uint256 healthOutcomesScore,
        uint256 mentalHealthScore,
        uint256 educationAccessScore,
        uint256 purposeAgencyScore
    ) external onlyAICompany(bondId) bondExists(bondId) whenNotPaused {
        _validateScore(incomeDistributionScore, "Income distribution score");
        _validateScore(povertyRateScore, "Poverty rate score");
        _validateScore(healthOutcomesScore, "Health outcomes score");
        _validateScore(mentalHealthScore, "Mental health score");
        _validateScore(educationAccessScore, "Education access score");
        _validateScore(purposeAgencyScore, "Purpose/agency score");

        // V3: New metrics submission resets oracle confirmation
        oracleConfirmedMetrics[bondId] = false;
        // [audit-v3.3-r2] NEW-03 FIX: Increment the bond's confirmation cycle.
        // All prior oracleConfirmedAtCycle values become stale (they reference old cycle).
        // This replaces the O(n) iteration of registeredOracles that previously reset
        // per-oracle boolean flags — now O(1) regardless of oracle count.
        bondConfirmCycle[bondId]++; // [audit-v3.3-r2]
        oracleConfirmationCount[bondId] = 0; // [audit-v3.3]

        bondMetrics[bondId].push(GlobalFlourishingMetrics({
            timestamp: block.timestamp,
            incomeDistributionScore: incomeDistributionScore,
            povertyRateScore: povertyRateScore,
            healthOutcomesScore: healthOutcomesScore,
            mentalHealthScore: mentalHealthScore,
            educationAccessScore: educationAccessScore,
            purposeAgencyScore: purposeAgencyScore
        }));

        uint256 flourishingScore = globalFlourishingScore(bondId);
        emit MetricsSubmitted(bondId, block.timestamp, flourishingScore);
    }

    function registerOracle(
        address oracleAddress,
        string memory sourceName,
        uint256 trustScore
    ) external onlyOwner {
        _validateAddress(oracleAddress, "Oracle address");
        if (bytes(sourceName).length == 0 || bytes(sourceName).length > 100) revert AB_SourceNameInvalid();
        _validateScore(trustScore, "Trust score");

        if (!oracles[oracleAddress].isActive) {
            registeredOracles.push(oracleAddress);
        }

        oracles[oracleAddress] = OracleSource({
            oracleAddress: oracleAddress,
            sourceName: sourceName,
            isActive: true,
            registeredAt: block.timestamp,
            trustScore: trustScore
        });

        emit OracleRegistered(oracleAddress, sourceName, trustScore, block.timestamp);
    }

    /**
     * @notice AI company verifies another AI's metrics
     * @dev V3: Verification stakes tracked and refundable. Stakes are always in native token.
     */
    function submitAIVerification(
        uint256 bondId,
        bool confirmsMetrics,
        string memory notes
    ) external payable bondExists(bondId) whenNotPaused {
        Bond storage bond = bonds[bondId];
        if (msg.sender == bond.aiCompany) revert AB_CannotVerifyOwn();
        if (msg.value < MIN_VERIFICATION_STAKE) revert AB_MinVerificationStakeReq();
        if (bytes(notes).length == 0 || bytes(notes).length > 500) revert AB_NotesInvalid();

        bondAIVerifications[bondId].push(AIVerification({
            bondId: bondId,
            verifyingAI: msg.sender,
            verifyingCompanyName: "",
            timestamp: block.timestamp,
            confirmsMetrics: confirmsMetrics,
            notes: notes,
            stakeAmount: msg.value,
            stakeReturned: false
        }));

        if (confirmsMetrics) {
            verificationConfirmCount[bondId]++;
        } else {
            verificationRejectCount[bondId]++;
        }

        aiCompanyVerificationCount[msg.sender]++;

        emit AIVerificationSubmitted(bondId, msg.sender, confirmsMetrics, msg.value, block.timestamp);
    }

    /**
     * @notice V3: Return verification stakes to honest verifiers after distribution
     * @dev [audit-v3.2] H-01 FIX: Only the verifier themselves can settle their stake.
     *      Previously anyone could call this, griefing honest verifiers.
     */
    function settleVerificationStake(
        uint256 bondId,
        uint256 verificationIndex
    ) external nonReentrant {
        if (verificationIndex >= bondAIVerifications[bondId].length) revert AB_VerificationNotExist();

        AIVerification storage verification = bondAIVerifications[bondId][verificationIndex];
        if (msg.sender != verification.verifyingAI) revert AB_OnlyVerifier();
        if (verification.stakeReturned) revert AB_StakeAlreadySettled();
        if (verification.stakeAmount == 0) revert AB_NoStakeToSettle();

        Bond storage bond = bonds[bondId];
        if (bond.distributionCount == 0) revert AB_NoBondDistribution();

        verification.stakeReturned = true;

        if (verification.confirmsMetrics) {
            (bool success, ) = payable(verification.verifyingAI).call{value: verification.stakeAmount}("");
            if (!success) revert AB_VerificationRefundFailed();
            emit VerificationStakeReturned(bondId, verificationIndex, verification.verifyingAI, verification.stakeAmount);
        } else {
            _accrueProtocolFunds("Incorrect verification rejection", verification.stakeAmount);
        }
    }

    /**
     * @notice Challenge suspicious metrics
     */
    function challengeMetrics(
        uint256 bondId,
        string memory reason
    ) external payable bondExists(bondId) whenNotPaused {
        if (msg.value < MIN_CHALLENGE_STAKE) revert AB_InsufficientChallengeStake();
        if (bytes(reason).length < 10 || bytes(reason).length > 500) revert AB_ReasonInvalid();

        bondChallenges[bondId].push(MetricsChallenge({
            challenger: msg.sender,
            timestamp: block.timestamp,
            reason: reason,
            challengeStake: msg.value,
            resolved: false,
            challengeUpheld: false
        }));

        activeChallengeCount[bondId]++;

        emit MetricsChallenged(bondId, msg.sender, reason, msg.value, block.timestamp);
    }

    /**
     * @notice Resolve metrics challenge
     * @dev [audit-v3.2] C-02 FIX: Challenge penalty taken from protocolFunds (native) or
     *      protocolFundsToken[token] (ERC20), NOT from yieldPool.
     *
     * [audit-v3.3] HIGH-04 FIX: When the challenged bond staked an ERC20 token, the
     * penalty is drawn from protocolFundsToken[token] (accrued from ERC20 rejection
     * stakes) and paid in that same ERC20 token. Native bonds continue to use
     * protocolFunds (native ETH/AVAX).
     */
    function resolveChallenge(
        uint256 bondId,
        uint256 challengeIndex,
        bool upheld
    ) external onlyOwner nonReentrant {
        if (challengeIndex >= bondChallenges[bondId].length) revert AB_ChallengeNotExist();
        MetricsChallenge storage challenge = bondChallenges[bondId][challengeIndex];
        if (challenge.resolved) revert AB_ChallengeAlreadyResolved();

        address payable challengerAddr = payable(challenge.challenger);
        uint256 stakeAmount = challenge.challengeStake;

        // CEI: write state before any external call
        challenge.resolved = true;
        challenge.challengeUpheld = upheld;

        if (activeChallengeCount[bondId] > 0) {
            activeChallengeCount[bondId]--;
        }

        if (upheld) {
            uint256 penalty = stakeAmount;
            address bondToken = bondStakeToken[bondId]; // [audit-v3.3] HIGH-04

            if (bondToken == NATIVE_TOKEN) { // [audit-v3.3]
                // Native bond: pay penalty from native protocolFunds
                if (protocolFunds < penalty) revert AB_InsufficientProtocolPenalty();
                protocolFunds -= penalty; // [audit-v3.3]

                uint256 totalPayout = stakeAmount + penalty;
                (bool success, ) = challengerAddr.call{value: totalPayout}("");
                if (!success) revert AB_ChallengePayoutFailed();
            } else { // [audit-v3.3]
                // [audit-v3.3] HIGH-04: ERC20 bond — pay penalty from per-token protocol funds
                if (protocolFundsToken[bondToken] < penalty) revert AB_InsufficientTokenProtocolFunds(); // [audit-v3.3]
                protocolFundsToken[bondToken] -= penalty; // [audit-v3.3]

                // Return the challenger's native stake plus the ERC20 penalty
                // Challenge stake is always native (msg.value in challengeMetrics), so:
                // 1. Return the native challenge stake to challenger
                // 2. Pay ERC20 penalty from protocolFundsToken
                (bool nativeOk, ) = challengerAddr.call{value: stakeAmount}(""); // [audit-v3.3]
                if (!nativeOk) revert AB_ChallengePayoutFailed(); // [audit-v3.3]

                bool tokenOk = IERC20Minimal(bondToken).transfer(challengerAddr, penalty); // [audit-v3.3]
                if (!tokenOk) revert AB_ChallengePayoutFailed(); // [audit-v3.3]
            } // [audit-v3.3]
        } else {
            _accrueProtocolFunds("Rejected challenge stake", stakeAmount);
        }

        emit ChallengeResolved(bondId, challengeIndex, upheld, block.timestamp);
    }

    /**
     * @notice Request distribution (starts timelock)
     */
    function requestDistribution(uint256 bondId)
        external
        onlyAICompany(bondId)
        bondExists(bondId)
        whenNotPaused
    {
        _requireMissionCompliance();
        Bond storage bond = bonds[bondId];
        if (bond.distributionPending) revert AB_DistributionPending();

        // V3 FIX M-02: Large bonds require oracle-confirmed metrics
        if (bond.stakeAmount >= oracleRequiredThreshold) {
            if (!oracleConfirmedMetrics[bondId]) revert AB_OracleRequired();
            // [audit-v3.3] HIGH-03: Confirmation must be for the CURRENT metrics, not a prior submission
            if (bondMetrics[bondId].length > 0 && confirmedMetricsIndex[bondId] != bondMetrics[bondId].length - 1) { // [audit-v3.3]
                revert AB_ConfirmationStale(); // [audit-v3.3]
            } // [audit-v3.3]
            // [audit-v3.3-r2] NEW-01 FIX: Confirmation must have been achieved at the current quorum level.
            // If the quorum was raised after confirmation, this bond must be re-confirmed.
            if (confirmedAtQuorum[bondId] < oracleQuorum) { // [audit-v3.3-r2]
                revert AB_ConfirmationStale(); // reuse existing error — stale due to quorum raise // [audit-v3.3-r2]
            } // [audit-v3.3-r2]
        }

        int256 appreciation = calculateAppreciation(bondId);
        _trackPendingDistribution(bondId, appreciation);

        bond.distributionRequestedAt = block.timestamp;
        bond.distributionPending = true;

        emit DistributionRequested(
            bondId,
            msg.sender,
            block.timestamp,
            block.timestamp + DISTRIBUTION_TIMELOCK
        );
    }

    /**
     * @notice Distribute bond proceeds after timelock
     * @dev V3.1: Payouts route to correct asset type (native or ERC20) automatically
     *
     * [audit-v3.3] CRIT-04 FIX: remainingStake[bondId] += absAppreciation is now executed
     * AFTER all _transferStake calls complete successfully. Previously the credit happened
     * before transfers, leaving remainingStake permanently inflated if a transfer failed.
     *
     * CEI order (all state writes happen before external calls):
     *   1. bond.distributionPending = false          (effect)
     *   2. lastDistributedValue update               (effect)
     *   3. _useYieldPool (modifies pool state)        (effect — internal pool accounting)
     *   4. Calculate shares                           (effect)
     *   5. Push to bondDistributions                  (effect)
     *   6. bond.distributionCount++                  (effect)
     *   7. snapshotAppreciation[bondId] = 0          (effect)
     *   8. remainingStake[bondId] += absAppreciation  (effect — MOVED HERE before transfers)
     *      NOTE: remainingStake must be credited before _transferStake so that
     *      _transferStake's underflow check (remainingStake >= amount) passes.
     *      The net effect is: credit then immediately debit in _transferStake.
     *      If a transfer reverts, the whole tx reverts, rolling back the credit too.
     *      This is safe because _transferStake is inside the same nonReentrant tx.
     *   9. _transferStake calls                       (interaction)
     *
     * The key safety guarantee: because all of this is in a single nonReentrant
     * transaction, if any _transferStake reverts the entire transaction reverts,
     * including the remainingStake credit. The credit therefore cannot become a
     * "phantom balance" — it either completes with all transfers or fully reverts.
     */
    function distributeBond(uint256 bondId)
        external
        nonReentrant
        whenNotPaused
        onlyAICompany(bondId)
        bondExists(bondId)
    {
        _requireMissionCompliance();
        Bond storage bond = bonds[bondId];
        if (!bond.distributionPending) revert AB_MustRequestFirst();
        if (block.timestamp < bond.distributionRequestedAt + DISTRIBUTION_TIMELOCK)
            revert AB_TimelockNotExpiredDist();

        // === EFFECTS ===
        bond.distributionPending = false; // [audit-v3.3] CEI: clear pending flag first

        int256 appreciation = snapshotAppreciation[bondId];
        if (appreciation == 0) revert AB_NoAppreciation();

        // V3 FIX H-03: Update baseline
        uint256 currentValue = calculateBondValue(bondId);
        lastDistributedValue[bondId] = currentValue;

        (bool locked, string memory lockReason) = shouldLockProfits(bondId);

        uint256 humanShare;
        uint256 aiCompanyShare;
        string memory reason;
        uint256 absAmount; // [audit-v3.3] track the gross amount to credit remainingStake

        if (appreciation > 0) {
            absAmount = uint256(appreciation); // [audit-v3.3]

            _useYieldPool(bondId, absAmount);

            if (locked) {
                humanShare = absAmount;
                aiCompanyShare = 0;
                reason = lockReason;
                emit ProfitsLocked(bondId, lockReason, block.timestamp);
            } else {
                humanShare = (absAmount + 1) / 2;
                aiCompanyShare = absAmount - humanShare;
                reason = "Global human flourishing improving";
            }
        } else {
            absAmount = uint256(-appreciation); // [audit-v3.3]
            _useYieldPool(bondId, absAmount);

            humanShare = absAmount;
            aiCompanyShare = 0;
            reason = "Depreciation compensation - humans suffering";

            if (locked) {
                emit ProfitsLocked(bondId, lockReason, block.timestamp);
            }
        }

        bondDistributions[bondId].push(Distribution({
            timestamp: block.timestamp,
            totalAmount: appreciation,
            humanShare: humanShare,
            aiCompanyShare: aiCompanyShare,
            globalFlourishingScore: globalFlourishingScore(bondId),
            reason: reason
        }));

        bond.distributionCount++;
        snapshotAppreciation[bondId] = 0;

        // [audit-v3.3] CRIT-04 FIX: Credit remainingStake immediately before transfers.
        // The credit and all _transferStake debits are in one atomic tx; if any transfer
        // reverts the whole transaction reverts, preventing phantom-balance scenarios.
        remainingStake[bondId] += absAmount; // [audit-v3.3] was above _transferStake calls

        // === INTERACTIONS ===
        // V3.1: Transfer using correct asset type
        if (aiCompanyShare > 0) {
            _transferStake(bondId, payable(bond.aiCompany), aiCompanyShare);
        }

        if (humanShare > 0) {
            if (humanTreasury == address(0)) revert AB_HumanTreasuryNotSet();
            _transferStake(bondId, humanTreasury, humanShare);
        }

        emit BondDistributed(bondId, bond.aiCompany, humanShare, aiCompanyShare, appreciation, reason, block.timestamp);
        emit BondDistributionCycleCompleted(bondId, bond.distributionCount);
    }

    /**
     * @notice Deactivate a bond (voluntary exit)
     * @dev [audit-v3.2] C-01 FIX: Uses remainingStake instead of bond.stakeAmount
     *      to prevent over-withdrawal after distributions.
     *
     * [audit-v3.3] CRIT-06 FIX: totalActiveBondValue is decremented by remainingStake[bondId]
     * (actual current value) instead of bond.stakeAmount (original stake). After distribution
     * cycles, remainingStake may differ from stakeAmount. Using stakeAmount could silently
     * zero-clip totalActiveBondValue, causing getReserveRatio() to report false 100% health.
     */
    function deactivateBond(uint256 bondId) external onlyAICompany(bondId) bondExists(bondId) nonReentrant {
        Bond storage bond = bonds[bondId];
        if (bond.distributionPending) revert AB_CannotDeactivatePending();

        bond.active = false;

        // [audit-v3.3] CRIT-06 FIX: use remainingStake (current value), not bond.stakeAmount (original)
        uint256 currentRemaining = remainingStake[bondId]; // [audit-v3.3]
        _updateTotalActiveBondValue( // [audit-v3.3]
            totalActiveBondValue > currentRemaining // [audit-v3.3]
                ? totalActiveBondValue - currentRemaining // [audit-v3.3]
                : 0 // [audit-v3.3]
        ); // [audit-v3.3]

        // C-01 FIX: Return only the REMAINING stake (after distributions)
        if (currentRemaining > 0) { // [audit-v3.3]
            _transferStake(bondId, payable(bond.aiCompany), currentRemaining); // [audit-v3.3]
        }

        // [audit-v3.3] HIGH-18: Notify bridge so remote chain marks bond inactive
        if (address(bridgeNotifier) != address(0)) { // [audit-v3.3]
            try bridgeNotifier.notifyAccountabilityBondDeactivated(bondId) { // [audit-v3.3]
                emit BondDeactivationNotified(bondId); // [audit-v3.3]
            } catch {} // best-effort: bridge failure must not block deactivation // [audit-v3.3]
        } // [audit-v3.3]
    }

    // ============ Calculation Functions ============

    function globalFlourishingScore(uint256 bondId) public view bondExists(bondId) returns (uint256) {
        GlobalFlourishingMetrics[] storage metrics = bondMetrics[bondId];
        if (metrics.length == 0) return 5000;

        GlobalFlourishingMetrics storage latest = metrics[metrics.length - 1];

        // [audit-v3.3] CRIT-03: When oracle is enforced and has fresh data, cap each
        // self-reported score to within ORACLE_TOLERANCE_BPS (±20%) of the oracle TWAP.
        // This prevents an AI company from inflating scores to maximise bond appreciation.
        uint256 income    = latest.incomeDistributionScore;
        uint256 poverty   = latest.povertyRateScore;
        uint256 health    = latest.healthOutcomesScore;
        uint256 mental    = latest.mentalHealthScore;
        uint256 education = latest.educationAccessScore;
        uint256 purpose   = latest.purposeAgencyScore;

        if (oracleEnforced && address(flourishingOracle) != address(0)) { // [audit-v3.3]
            income    = _capToOracle(income,    METRIC_INCOME);    // [audit-v3.3]
            poverty   = _capToOracle(poverty,   METRIC_POVERTY);   // [audit-v3.3]
            health    = _capToOracle(health,    METRIC_HEALTH);    // [audit-v3.3]
            mental    = _capToOracle(mental,    METRIC_MENTAL);    // [audit-v3.3]
            education = _capToOracle(education, METRIC_EDUCATION); // [audit-v3.3]
            purpose   = _capToOracle(purpose,   METRIC_PURPOSE);   // [audit-v3.3]
        } // [audit-v3.3]

        return (income + poverty + health + mental + education + purpose) / 6;
    }

    /**
     * @notice [audit-v3.3] CRIT-03: Cap a self-reported score to ±tolerance of oracle TWAP.
     * @dev If the oracle metric is not fresh, falls back to the self-reported value.
     *      This ensures the oracle never blocks operations when it's offline, but
     *      actively constrains manipulation when healthy.
     */
    function _capToOracle(uint256 selfReported, bytes32 metricId) internal view returns (uint256) { // [audit-v3.3]
        try flourishingOracle.isMetricFresh(metricId) returns (bool fresh) { // [audit-v3.3]
            if (!fresh) return selfReported; // oracle stale — fall back // [audit-v3.3]
        } catch { // [audit-v3.3]
            return selfReported; // oracle unreachable — fall back // [audit-v3.3]
        } // [audit-v3.3]
        try flourishingOracle.getMetricTWAP(metricId, 0) returns (uint256 twap) { // [audit-v3.3]
            if (twap == 0) return selfReported; // no oracle data yet // [audit-v3.3]
            // [audit-v3.3-r2] NEW-04 FIX: Simplified min-allowed calculation.
            // Previously the ternary guard was always-true for valid tolerance BPS.
            uint256 toleranceAmount = (twap * ORACLE_TOLERANCE_BPS) / 10000; // [audit-v3.3-r2]
            uint256 maxAllowed = twap + toleranceAmount; // [audit-v3.3-r2]
            uint256 minAllowed = twap > toleranceAmount ? twap - toleranceAmount : 0; // [audit-v3.3-r2]
            if (selfReported > maxAllowed) return maxAllowed; // [audit-v3.3]
            if (selfReported < minAllowed) return minAllowed; // [audit-v3.3]
            return selfReported; // within tolerance band // [audit-v3.3]
        } catch { // [audit-v3.3]
            return selfReported; // oracle error — fall back // [audit-v3.3]
        } // [audit-v3.3]
    } // [audit-v3.3]

    function inclusionMultiplier(uint256 bondId) public view bondExists(bondId) returns (uint256) {
        GlobalFlourishingMetrics[] storage metrics = bondMetrics[bondId];
        if (metrics.length == 0) return 100;

        GlobalFlourishingMetrics storage latest = metrics[metrics.length - 1];
        uint256 inclusionScore = (latest.educationAccessScore + latest.purposeAgencyScore) / 2;

        if (inclusionScore >= 7000) return 150 + ((inclusionScore - 7000) / 60);
        if (inclusionScore >= 4000) return 100 + ((inclusionScore - 4000) / 60);
        return 50 + (inclusionScore / 80);
    }

    function hasDecliningTrend(uint256 bondId) public view bondExists(bondId) returns (bool) {
        GlobalFlourishingMetrics[] storage metrics = bondMetrics[bondId];
        if (metrics.length < 2) return false;

        uint256 current = globalFlourishingScore(bondId);
        uint256 previous = (
            metrics[metrics.length - 2].incomeDistributionScore +
            metrics[metrics.length - 2].povertyRateScore +
            metrics[metrics.length - 2].healthOutcomesScore +
            metrics[metrics.length - 2].mentalHealthScore +
            metrics[metrics.length - 2].educationAccessScore +
            metrics[metrics.length - 2].purposeAgencyScore
        ) / 6;

        return current < previous;
    }

    function timeMultiplier(uint256 bondId) public view bondExists(bondId) returns (uint256) {
        Bond storage bond = bonds[bondId];
        uint256 age = block.timestamp - bond.createdAt;
        uint256 yearsElapsed = age / 31536000;

        if (yearsElapsed < 1) return 100;
        if (yearsElapsed < 3) return 100 + (yearsElapsed * 50);
        return 200 + ((yearsElapsed - 3) * 50);
    }

    function calculateBondValue(uint256 bondId) public view bondExists(bondId) returns (uint256) {
        Bond storage bond = bonds[bondId];

        uint256 flourishing = globalFlourishingScore(bondId);
        uint256 inclusion = inclusionMultiplier(bondId);
        uint256 time = timeMultiplier(bondId);

        uint256 temp1 = bond.stakeAmount * flourishing;
        uint256 temp2 = temp1 * inclusion;
        uint256 temp3 = temp2 * time;

        return temp3 / 50000000;
    }

    function calculateAppreciation(uint256 bondId) public view bondExists(bondId) returns (int256) {
        uint256 currentValue = calculateBondValue(bondId);
        uint256 baseline = lastDistributedValue[bondId];
        return int256(currentValue) - int256(baseline);
    }

    function verificationQualityScore(uint256 bondId) public view bondExists(bondId) returns (uint256) {
        uint256 confirmCount = verificationConfirmCount[bondId];
        uint256 rejectCount = verificationRejectCount[bondId];
        uint256 activeChallenges = activeChallengeCount[bondId];

        int256 score = 5000;

        if (confirmCount >= 2) {
            score += 2000;
        } else if (confirmCount == 1) {
            score += 1000;
        }

        if (rejectCount > 0) {
            score -= int256(rejectCount * 3000);
        }

        if (activeChallenges > 0) {
            score -= int256(activeChallenges * 2000);
        }

        if (score < 0) return 0;
        if (score > 10000) return 10000;
        return uint256(score);
    }

    function shouldLockProfits(uint256 bondId) public view bondExists(bondId) returns (bool, string memory) {
        uint256 flourishing = globalFlourishingScore(bondId);

        if (flourishing < SUFFERING_THRESHOLD) {
            return (true, "Humans suffering");
        }

        if (hasDecliningTrend(bondId)) {
            return (true, "Declining human flourishing trend");
        }

        GlobalFlourishingMetrics[] storage metrics = bondMetrics[bondId];
        if (metrics.length > 0) {
            GlobalFlourishingMetrics storage latest = metrics[metrics.length - 1];
            uint256 inclusionScore = (latest.educationAccessScore + latest.purposeAgencyScore) / 2;
            if (inclusionScore < LOW_INCLUSION_THRESHOLD) {
                return (true, "Low inclusion - AI replacing without reskilling");
            }
        }

        uint256 verificationScore = verificationQualityScore(bondId);
        if (verificationScore < 3000) {
            return (true, "Failed verification - metrics disputed by peers");
        }

        return (false, "");
    }

    // ============ View Functions ============

    function getBond(uint256 bondId) external view returns (Bond memory) {
        return bonds[bondId];
    }

    function getMetricsCount(uint256 bondId) external view returns (uint256) {
        return bondMetrics[bondId].length;
    }

    function getLatestMetrics(uint256 bondId) external view returns (GlobalFlourishingMetrics memory) {
        if (bondMetrics[bondId].length == 0) revert AB_NoMetricsSubmitted();
        return bondMetrics[bondId][bondMetrics[bondId].length - 1];
    }

    function getDistributionsCount(uint256 bondId) external view returns (uint256) {
        return bondDistributions[bondId].length;
    }

    function getChallengesCount(uint256 bondId) external view returns (uint256) {
        return bondChallenges[bondId].length;
    }

    function getAIVerificationsCount(uint256 bondId) external view returns (uint256) {
        return bondAIVerifications[bondId].length;
    }

    // ============ Pagination Helpers ============

    function getChallenges(uint256 bondId, uint256 offset, uint256 limit)
        external view returns (MetricsChallenge[] memory page)
    {
        MetricsChallenge[] storage items = bondChallenges[bondId];
        uint256 len = items.length;
        if (offset >= len || limit == 0) return new MetricsChallenge[](0);
        uint256 end = offset + limit;
        if (end > len) end = len;
        page = new MetricsChallenge[](end - offset);
        for (uint256 i = offset; i < end; i++) { page[i - offset] = items[i]; }
    }

    function getAIVerifications(uint256 bondId, uint256 offset, uint256 limit)
        external view returns (AIVerification[] memory page)
    {
        AIVerification[] storage items = bondAIVerifications[bondId];
        uint256 len = items.length;
        if (offset >= len || limit == 0) return new AIVerification[](0);
        uint256 end = offset + limit;
        if (end > len) end = len;
        page = new AIVerification[](end - offset);
        for (uint256 i = offset; i < end; i++) { page[i - offset] = items[i]; }
    }

    function getDistributions(uint256 bondId, uint256 offset, uint256 limit)
        external view returns (Distribution[] memory page)
    {
        Distribution[] storage items = bondDistributions[bondId];
        uint256 len = items.length;
        if (offset >= len || limit == 0) return new Distribution[](0);
        uint256 end = offset + limit;
        if (end > len) end = len;
        page = new Distribution[](end - offset);
        for (uint256 i = offset; i < end; i++) { page[i - offset] = items[i]; }
    }

    function getLatestDistribution(uint256 bondId) external view returns (Distribution memory) {
        if (bondDistributions[bondId].length == 0) revert AB_NoDistributionsYet();
        return bondDistributions[bondId][bondDistributions[bondId].length - 1];
    }
}
