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

import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title RewardProgramV3
/// @notice Sponsor-funded flourishing rewards. STRICTLY separate from bond principal.
///         Never marketed as "yield." A program can never promise more than its
///         funded reserve.
/// @dev Owner = MultisigGovernanceV3.
contract RewardProgramV3 {
    using SafeERC20 for IERC20;
    // ---------------------------------------------------------------------
    // Types
    // ---------------------------------------------------------------------

    struct Program {
        address sponsor;
        address asset; // 0 = native
        uint256 fundedAmount; // total ever funded
        uint256 committedAmount; // reserved for pending claims
        uint256 paidAmount; // total paid out
        uint256 maxLiability; // <= fundedAmount at any time
        uint64 startsAt;
        uint64 endsAt;
        bytes32 metricSchemaId; // hash of the metric schema
        bytes32 verifierPolicyId; // hash of the independent-verifier policy
        bytes32 rewardFormulaId; // hash of the reward formula
        bytes32 privacyClass;
        bool cancelled;
    }

    // ---------------------------------------------------------------------
    // Storage
    // ---------------------------------------------------------------------

    mapping(uint256 => Program) public programs;
    uint256 public nextProgramId = 1;

    /// @dev bondId → programId → committed reward amount
    mapping(uint256 => mapping(uint256 => uint256)) public committedFor;
    /// @notice Rewards and returned program funds awaiting a beneficiary claim.
    /// @dev asset => beneficiary => amount; address(0) denotes native currency.
    mapping(address => mapping(address => uint256)) public claimableRewards;
    /// @notice Total of all funded program reserves and issued-but-unclaimed
    ///         rewards for an asset. Direct transfers are deliberately absent.
    mapping(address => uint256) public assetLiabilities;

    address public owner;
    address public authorizedIssuer; // bond factory or governance-controlled dispatcher
    uint256 private _reentrancyStatus = 1;

    // ---------------------------------------------------------------------
    // Errors + Events
    // ---------------------------------------------------------------------

    error NotOwner();
    error NotAuthorizedIssuer();
    error ProgramCancelled();
    error ProgramNotStarted();
    error ProgramEnded();
    error OverFunded();
    error OverCommitted();
    error NothingToWithdraw();
    error ZeroAddress();
    error TransferFailed();
    error Reentrancy();
    error UnsupportedAssetTransfer();

    event ProgramCreatedV3(
        uint256 indexed programId, address indexed sponsor, address indexed asset, uint256 maxLiability
    );
    event ProgramFundedV3(uint256 indexed programId, address indexed sponsor, uint256 amount);
    event RewardCommittedV3(uint256 indexed programId, uint256 indexed bondId, uint256 amount);
    event RewardReleasedV3(uint256 indexed programId, uint256 indexed bondId, uint256 amount);
    event RewardIssuedV3(
        uint256 indexed programId, uint256 indexed bondId, address indexed beneficiary, uint256 amount
    );
    event ProgramCancelledV3(uint256 indexed programId);
    event UnusedFundsWithdrawnV3(uint256 indexed programId, address indexed to, uint256 amount);
    event AuthorizedIssuerSetV3(address indexed previous, address indexed current);
    event RewardClaimedV3(address indexed beneficiary, address indexed asset, uint256 amount);

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier onlyIssuer() {
        if (msg.sender != authorizedIssuer) revert NotAuthorizedIssuer();
        _;
    }

    modifier nonReentrant() {
        if (_reentrancyStatus != 1) revert Reentrancy();
        _reentrancyStatus = 2;
        _;
        _reentrancyStatus = 1;
    }

    constructor(address _owner) {
        if (_owner == address(0)) revert ZeroAddress();
        owner = _owner;
    }

    function setAuthorizedIssuer(address issuer) external onlyOwner nonReentrant {
        emit AuthorizedIssuerSetV3(authorizedIssuer, issuer);
        authorizedIssuer = issuer;
    }

    // ---------------------------------------------------------------------
    // Program lifecycle
    // ---------------------------------------------------------------------

    function createProgram(
        address asset,
        uint256 maxLiability,
        uint64 startsAt,
        uint64 endsAt,
        bytes32 metricSchemaId,
        bytes32 verifierPolicyId,
        bytes32 rewardFormulaId,
        bytes32 privacyClass
    ) external nonReentrant returns (uint256 programId) {
        programId = nextProgramId++;
        programs[programId] = Program({
            sponsor: msg.sender,
            asset: asset,
            fundedAmount: 0,
            committedAmount: 0,
            paidAmount: 0,
            maxLiability: maxLiability,
            startsAt: startsAt,
            endsAt: endsAt,
            metricSchemaId: metricSchemaId,
            verifierPolicyId: verifierPolicyId,
            rewardFormulaId: rewardFormulaId,
            privacyClass: privacyClass,
            cancelled: false
        });
        emit ProgramCreatedV3(programId, msg.sender, asset, maxLiability);
    }

    function fundNative(uint256 programId) external payable nonReentrant {
        Program storage p = programs[programId];
        if (p.cancelled) revert ProgramCancelled();
        if (p.asset != address(0)) revert TransferFailed();
        p.fundedAmount += msg.value;
        assetLiabilities[address(0)] += msg.value;
        if (p.fundedAmount > p.maxLiability) revert OverFunded();
        emit ProgramFundedV3(programId, msg.sender, msg.value);
    }

    function fundERC20(uint256 programId, uint256 amount) external nonReentrant {
        Program storage p = programs[programId];
        if (p.cancelled) revert ProgramCancelled();
        if (p.asset == address(0)) revert TransferFailed();
        uint256 updatedFunding = p.fundedAmount + amount;
        if (updatedFunding > p.maxLiability) revert OverFunded();
        p.fundedAmount = updatedFunding;
        assetLiabilities[p.asset] += amount;
        // Reserve program capacity before the untrusted token callback. A
        // failed exact pull reverts this reservation atomically.
        // slither-disable-next-line reentrancy-eth
        _pullExact(p.asset, amount);
        emit ProgramFundedV3(programId, msg.sender, amount);
    }

    /// @notice Reserve reward for a specific bond. Only the authorized issuer
    ///         (bond factory) may call. Cannot exceed unallocated reserve.
    function commit(uint256 programId, uint256 bondId, uint256 amount) external onlyIssuer nonReentrant {
        Program storage p = programs[programId];
        if (p.cancelled) revert ProgramCancelled();
        if (block.timestamp < p.startsAt) revert ProgramNotStarted();
        if (block.timestamp >= p.endsAt) revert ProgramEnded();
        uint256 available = p.fundedAmount - p.committedAmount - p.paidAmount;
        if (amount > available) revert OverCommitted();
        p.committedAmount += amount;
        committedFor[bondId][programId] += amount;
        emit RewardCommittedV3(programId, bondId, amount);
    }

    function release(uint256 programId, uint256 bondId, uint256 amount) external onlyIssuer nonReentrant {
        Program storage p = programs[programId];
        uint256 c = committedFor[bondId][programId];
        if (amount > c) revert OverCommitted();
        p.committedAmount -= amount;
        committedFor[bondId][programId] = c - amount;
        emit RewardReleasedV3(programId, bondId, amount);
    }

    /// @notice Called by the bond contract after authenticated evidence + oracle
    ///         attestation. Credits the reward for beneficiary-initiated withdrawal.
    function issue(uint256 programId, uint256 bondId, address beneficiary, uint256 amount)
        external
        onlyIssuer
        nonReentrant
    {
        if (beneficiary == address(0)) revert ZeroAddress();
        Program storage p = programs[programId];
        uint256 c = committedFor[bondId][programId];
        if (amount > c) revert OverCommitted();
        p.committedAmount -= amount;
        p.paidAmount += amount;
        committedFor[bondId][programId] = c - amount;
        claimableRewards[p.asset][beneficiary] += amount;
        emit RewardIssuedV3(programId, bondId, beneficiary, amount);
    }

    function cancelProgram(uint256 programId) external nonReentrant {
        Program storage p = programs[programId];
        if (msg.sender != p.sponsor && msg.sender != owner) revert NotOwner();
        p.cancelled = true;
        emit ProgramCancelledV3(programId);
    }

    /// @notice Sponsor withdraws only the unused portion after end OR on cancel.
    function withdrawUnused(uint256 programId, address to) external nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        Program storage p = programs[programId];
        if (msg.sender != p.sponsor) revert NotOwner();
        bool ended = p.cancelled || block.timestamp >= p.endsAt;
        if (!ended) revert ProgramEnded();
        uint256 unused = p.fundedAmount - p.committedAmount - p.paidAmount;
        if (unused == 0) revert NothingToWithdraw();
        p.fundedAmount -= unused;
        claimableRewards[p.asset][to] += unused;
        emit UnusedFundsWithdrawnV3(programId, to, unused);
    }

    /// @notice Withdraw credits for the calling beneficiary only.
    /// @dev The recipient is fixed to msg.sender, so program accounting never
    ///      performs an arbitrary native-currency send.
    function claimReward(address asset) external nonReentrant {
        uint256 amount = claimableRewards[asset][msg.sender];
        if (amount == 0) revert NothingToWithdraw();
        claimableRewards[asset][msg.sender] = 0;
        assetLiabilities[asset] -= amount;
        _transferToCaller(asset, amount);
        emit RewardClaimedV3(msg.sender, asset, amount);
    }

    /// @notice Raw held balance for `asset`; direct/forced transfers are not
    ///         liabilities and therefore appear only as surplus.
    function assets(address asset) public view returns (uint256) {
        return asset == address(0) ? address(this).balance : IERC20(asset).balanceOf(address(this));
    }

    function liabilities(address asset) public view returns (uint256) {
        return assetLiabilities[asset];
    }

    function isSolvent(address asset) external view returns (bool) {
        return assets(asset) >= assetLiabilities[asset];
    }

    function surplus(address asset) external view returns (uint256) {
        uint256 held = assets(asset);
        uint256 owed = assetLiabilities[asset];
        return held > owed ? held - owed : 0;
    }

    // ---------------------------------------------------------------------
    // Exact transfers
    // ---------------------------------------------------------------------

    function _pullExact(address asset, uint256 amount) private {
        // slither-disable-next-line reentrancy-balance
        uint256 beforeBalance = IERC20(asset).balanceOf(address(this));
        IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
        uint256 afterBalance = IERC20(asset).balanceOf(address(this));
        if (afterBalance < beforeBalance || afterBalance - beforeBalance != amount) {
            revert UnsupportedAssetTransfer();
        }
    }

    function _transferToCaller(address asset, uint256 amount) private {
        if (asset == address(0)) {
            (bool ok,) = payable(msg.sender).call{ value: amount }("");
            if (!ok) revert TransferFailed();
        } else {
            // slither-disable-next-line reentrancy-balance
            uint256 senderBefore = IERC20(asset).balanceOf(address(this));
            // slither-disable-next-line reentrancy-balance
            uint256 recipientBefore = IERC20(asset).balanceOf(msg.sender);
            IERC20(asset).safeTransfer(msg.sender, amount);
            uint256 senderAfter = IERC20(asset).balanceOf(address(this));
            uint256 recipientAfter = IERC20(asset).balanceOf(msg.sender);
            if (
                senderAfter > senderBefore || senderBefore - senderAfter != amount || recipientAfter < recipientBefore
                    || recipientAfter - recipientBefore != amount
            ) {
                revert UnsupportedAssetTransfer();
            }
        }
    }
}
