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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title AereAgentBond — slashable bond for autonomous AI agents
 * @notice The agentic economy needs accountable agents. A wallet address
 *         claiming to be "OpenAI's research agent" or "an audited TPM-bound
 *         inference node" is a claim, not a property. AereAgentBond turns
 *         the claim into a financial commitment.
 *
 *         An operator (the human / org behind an agent) posts a bond in AERE.
 *         A registry of slashing oracles — entities the protocol has
 *         committed to listen to — can slash the bond when they observe
 *         provable misbehaviour (model-attestation perjury, refusal of
 *         observation challenges, AERE402 payment fraud, etc.).
 *
 *         Slashed AERE goes to AereSink — directly into the burn engine.
 *         No discretion, no recovery, no "we'll give it back if you fix it".
 *         The cost of a slash is a real economic loss, not a corporate
 *         knuckle-rap.
 *
 *         Operators can WITHDRAW a bond, but only after a cooldown
 *         (default 7 days, immutable) during which the bond is still
 *         slashable. This prevents bad actors from withdrawing the moment
 *         they detect they're about to get caught.
 *
 *         AGENT IDENTITY:
 *           This contract does NOT establish who "the agent" is — it just
 *           keys on (operator address, agentId). The agentId can be the
 *           hash of an OpenAI API key fingerprint, an AereAgent registry
 *           id, an EAS attestation id, or any external identifier. Read
 *           consistency is the consumer's problem.
 *
 *         IMMUTABILITY:
 *           - WITHDRAWAL_COOLDOWN constant.
 *           - SINK + AERE token addresses immutable at deploy.
 *           - Slashing oracles registered immutably at deploy. Cannot add
 *             new oracles post-deploy. Phase 2 will use a multi-sig
 *             quorum across oracles; Phase 1 supports a single Foundation
 *             oracle for fastest iteration.
 *
 *         GOTCHA — cooldown semantics:
 *           When an operator calls `requestWithdraw()`, their bond is
 *           "marked for withdrawal". The bond is STILL slashable until
 *           the cooldown elapses. `withdraw()` then transfers the
 *           remaining (post-any-slashes) bond to the operator. Anyone
 *           can call `withdraw()` for an operator whose cooldown has
 *           elapsed (defensive: prevents griefing where operator never
 *           calls withdraw).
 */
contract AereAgentBond is ReentrancyGuard {

    /* ================================ config ================================ */

    IERC20 public immutable AERE;             // bond token
    address public immutable SINK;            // AereSink for slashed-AERE forward
    uint256 public constant WITHDRAWAL_COOLDOWN = 7 days;

    /// @notice immutable set of slashing oracles. Each can slash any bond.
    ///         Phase 1: a single Foundation oracle. Phase 2: multi-sig quorum
    ///         across N oracles (would require redeploy, by design).
    mapping(address => bool) public isOracle;
    address[] public oracleList;

    /* ================================= state ================================= */

    struct Bond {
        uint256 amount;            // current AERE bonded (post any slashes)
        uint64  requestedAt;       // 0 if no withdrawal requested; else unix seconds
        uint256 lifetimeSlashed;   // cumulative AERE slashed (informational; never resets)
        bool    exists;
    }

    /// @notice (operator, agentId) → bond
    mapping(address => mapping(bytes32 => Bond)) internal _bonds;

    /// @notice cumulative AERE forwarded to sink (informational).
    uint256 public totalSlashedToSink;

    /// AUDIT FIX (HIGH #21): operator-level cumulative slash across all
    /// agentIds. Rotating agentId cannot wipe this — reputation reads it
    /// to apply a residual penalty on the operator's new agents.
    mapping(address => uint256) public operatorLifetimeSlashed;

    /* ================================= events ================================= */

    event BondPosted(address indexed operator, bytes32 indexed agentId, uint256 amount, uint256 newTotal);
    event BondToppedUp(address indexed operator, bytes32 indexed agentId, uint256 added, uint256 newTotal);
    event WithdrawalRequested(address indexed operator, bytes32 indexed agentId, uint64 readyAt);
    event WithdrawalCancelled(address indexed operator, bytes32 indexed agentId); // AUDIT FIX (MED #36)
    event Withdrawn(address indexed operator, bytes32 indexed agentId, address indexed recipient, uint256 amount);
    event Slashed(
        address indexed operator,
        bytes32 indexed agentId,
        address indexed oracle,
        uint256 amount,
        bytes32 reasonHash,
        string reasonUri
    );

    /* ================================= errors ================================= */

    error NotOracle();
    error BondNotFound();
    error WithdrawalAlreadyRequested();
    error WithdrawalNotRequested();
    error WithdrawalCooldownOpen(uint256 nowTs, uint256 readyAt);
    error ZeroAmount();
    error SlashAmountExceedsBond(uint256 amount, uint256 have);
    error SlashExceedsCallCap(uint256 amount, uint256 cap);
    error SlashCooldownActive(uint64 nowTs, uint64 earliest);
    error TransferFailed();

    /* =============================== modifiers ============================== */

    modifier onlyOracle() {
        if (!isOracle[msg.sender]) revert NotOracle();
        _;
    }

    /* ============================== constructor ============================= */

    /// AUDIT FIX (HIGH #18): per-call slash cap (25% of bond) + per-(operator,agentId)
    ///                       slash cooldown (24h) to limit single-key oracle blast.
    uint16  public constant MAX_SLASH_BPS_PER_CALL = 2500; // 25%
    uint64  public constant SLASH_COOLDOWN = 24 hours;
    mapping(address => mapping(bytes32 => uint64)) public lastSlashAt;

    constructor(IERC20 aere, address sink, address[] memory oracles) {
        require(address(aere) != address(0), "zero-aere");
        require(sink != address(0), "zero-sink");
        require(oracles.length > 0, "no-oracles"); // AUDIT FIX (MED #33)
        AERE = aere;
        SINK = sink;
        for (uint256 i = 0; i < oracles.length; i++) {
            require(oracles[i] != address(0), "zero-oracle");
            require(!isOracle[oracles[i]], "dup-oracle");
            isOracle[oracles[i]] = true;
            oracleList.push(oracles[i]);
        }
    }

    /* ================================= write ================================= */

    /// @notice Post or top-up a bond for (msg.sender, agentId). Pulls AERE
    ///         from the caller. Top-up clears any pending withdrawal request
    ///         — re-committing the bond.
    function postBond(bytes32 agentId, uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        if (!AERE.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();

        Bond storage b = _bonds[msg.sender][agentId];
        bool isNew = !b.exists;
        b.exists = true;
        b.amount += amount;
        // Re-committing the bond cancels any in-flight withdrawal request.
        if (b.requestedAt != 0) {
            b.requestedAt = 0;
            emit WithdrawalCancelled(msg.sender, agentId); // AUDIT FIX (MED #36)
        }

        if (isNew) {
            emit BondPosted(msg.sender, agentId, amount, b.amount);
        } else {
            emit BondToppedUp(msg.sender, agentId, amount, b.amount);
        }
    }

    /// @notice Operator requests withdrawal. Bond stays slashable for
    ///         WITHDRAWAL_COOLDOWN. Re-requesting before cooldown elapses
    ///         is forbidden (would extend exposure of slashers who already
    ///         observed the original request).
    function requestWithdraw(bytes32 agentId) external {
        Bond storage b = _bonds[msg.sender][agentId];
        if (!b.exists || b.amount == 0) revert BondNotFound();
        if (b.requestedAt != 0) revert WithdrawalAlreadyRequested();
        b.requestedAt = uint64(block.timestamp);
        emit WithdrawalRequested(msg.sender, agentId, uint64(block.timestamp + WITHDRAWAL_COOLDOWN));
    }

    /// @notice Permissionless withdraw once cooldown has elapsed. Transfers
    ///         the (post-slash) remaining bond to the operator.
    function withdraw(address operator, bytes32 agentId) external nonReentrant {
        Bond storage b = _bonds[operator][agentId];
        if (!b.exists || b.amount == 0) revert BondNotFound();
        if (b.requestedAt == 0) revert WithdrawalNotRequested();
        uint256 readyAt = uint256(b.requestedAt) + WITHDRAWAL_COOLDOWN;
        if (block.timestamp < readyAt) revert WithdrawalCooldownOpen(block.timestamp, readyAt);

        uint256 amount = b.amount;
        b.amount = 0;
        b.requestedAt = 0;

        if (!AERE.transfer(operator, amount)) revert TransferFailed();
        emit Withdrawn(operator, agentId, operator, amount);
    }

    /// @notice Slash a bond. Only registered oracles can call. Slashed AERE
    ///         forwarded immediately to AereSink — no holding period, no
    ///         "release to whitelist" route. Burn engine eats it.
    /// @param reasonHash   on-chain hash of the violation evidence (e.g.
    ///                     keccak256 of the AereAIProof attestation that
    ///                     contradicts the agent's claim, or the AERE402
    ///                     dispute proof).
    /// @param reasonUri    optional human-readable evidence pointer
    ///                     (ipfs://… or https://…). Stored as event arg only.
    function slash(
        address operator,
        bytes32 agentId,
        uint256 amount,
        bytes32 reasonHash,
        string calldata reasonUri
    ) external nonReentrant onlyOracle {
        if (amount == 0) revert ZeroAmount();
        Bond storage b = _bonds[operator][agentId];
        if (!b.exists || b.amount == 0) revert BondNotFound();
        if (amount > b.amount) revert SlashAmountExceedsBond(amount, b.amount);
        // AUDIT FIX (HIGH #18): per-call cap (25%) limits blast radius of a
        // compromised single-oracle key.
        uint256 cap = (b.amount * MAX_SLASH_BPS_PER_CALL) / 10_000;
        if (cap == 0) cap = b.amount; // tiny dust bonds — slash all (still ≤ b.amount)
        if (amount > cap) revert SlashExceedsCallCap(amount, cap);
        // Cooldown between slashes per (operator, agentId).
        uint64 lastTs = lastSlashAt[operator][agentId];
        if (lastTs != 0 && block.timestamp < uint256(lastTs) + SLASH_COOLDOWN) {
            revert SlashCooldownActive(uint64(block.timestamp), lastTs + uint64(SLASH_COOLDOWN));
        }
        lastSlashAt[operator][agentId] = uint64(block.timestamp);

        b.amount -= amount;
        b.lifetimeSlashed += amount;
        operatorLifetimeSlashed[operator] += amount; // AUDIT FIX (HIGH #21)
        totalSlashedToSink += amount;

        // Forward slashed AERE to sink via approve + flush. The sink
        // requires transferFrom semantics from msg.sender.
        AERE.approve(SINK, 0);
        AERE.approve(SINK, amount);
        (bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", address(AERE), amount));
        AERE.approve(SINK, 0);
        if (!ok) revert TransferFailed();

        emit Slashed(operator, agentId, msg.sender, amount, reasonHash, reasonUri);
    }

    /* ================================= views ================================= */

    function bondOf(address operator, bytes32 agentId) external view returns (
        uint256 amount, uint64 requestedAt, uint256 lifetimeSlashed, bool exists
    ) {
        Bond storage b = _bonds[operator][agentId];
        return (b.amount, b.requestedAt, b.lifetimeSlashed, b.exists);
    }

    /// @notice Convenience: true if the bond is currently >= `minBond` and
    ///         not requested for withdrawal. Consumers (AERE402 facilitator,
    ///         AereAIProof verifier) should require this before honoring
    ///         agent claims.
    function isBonded(address operator, bytes32 agentId, uint256 minBond) external view returns (bool) {
        Bond storage b = _bonds[operator][agentId];
        return b.exists && b.amount >= minBond && b.requestedAt == 0;
    }

    function oracles() external view returns (address[] memory) {
        return oracleList;
    }
}
