// 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";
import { ReentrancyGuard } from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";

/// @title MutualAidPoolV3
/// @notice Discretionary mutual aid, not insurance. Contributions are held
///         until a capped aid allocation is made. Unsolicited transfers are
///         surplus and cannot enlarge the owner-controlled payout budget.
contract MutualAidPoolV3 is ReentrancyGuard {
    using SafeERC20 for IERC20;

    struct Payout {
        address to;
        uint256 amount;
        bytes32 reasonHash;
        uint64 paidAt;
    }

    address public owner;
    address public immutable asset; // 0 = native
    uint256 public totalContributed;
    uint256 public totalPaid;
    uint256 public immutable maxSinglePayout;
    uint256 public immutable maxTotalPaid;

    Payout[] public payouts;

    error NotOwner();
    error ZeroAddress();
    error ZeroAmount();
    error PayoutTooLarge();
    error PoolCapExceeded();
    error InsufficientAvailableFunds();
    error InsufficientSurplus();
    error TransferFailed();
    error UnsupportedAssetTransfer();

    event ContributedV3(address indexed from, uint256 amount);
    event MutualAidPaidV3(address indexed to, uint256 amount, bytes32 reasonHash);
    event SurplusSkimmedV3(address indexed asset, address indexed to, uint256 amount);

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

    constructor(address _owner, address _asset, uint256 _maxSingle, uint256 _maxTotal) {
        if (_owner == address(0)) revert ZeroAddress();
        if (_maxSingle > _maxTotal) revert PayoutTooLarge();
        owner = _owner;
        asset = _asset;
        maxSinglePayout = _maxSingle;
        maxTotalPaid = _maxTotal;
    }

    receive() external payable nonReentrant {
        if (asset != address(0)) revert TransferFailed();
        if (msg.value == 0) revert ZeroAmount();
        totalContributed += msg.value;
        emit ContributedV3(msg.sender, msg.value);
    }

    function contributeERC20(uint256 amount) external nonReentrant {
        if (asset == address(0)) revert TransferFailed();
        if (amount == 0) revert ZeroAmount();
        // 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();
        }
        totalContributed += amount;
        emit ContributedV3(msg.sender, amount);
    }

    function pay(address to, uint256 amount, bytes32 reasonHash) external onlyOwner nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (amount > maxSinglePayout) revert PayoutTooLarge();
        if (totalPaid + amount > maxTotalPaid) revert PoolCapExceeded();
        if (amount > liabilities()) revert InsufficientAvailableFunds();

        totalPaid += amount;
        payouts.push(Payout({ to: to, amount: amount, reasonHash: reasonHash, paidAt: uint64(block.timestamp) }));
        _pushExact(to, amount);
        emit MutualAidPaidV3(to, amount, reasonHash);
    }

    /// @notice Withdraws only a direct/forced-transfer surplus. It cannot use
    ///         contributed funds that remain allocated to the aid pool.
    function skimSurplus(address to, uint256 amount) external onlyOwner nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (amount > surplus()) revert InsufficientSurplus();
        _pushExact(to, amount);
        emit SurplusSkimmedV3(asset, to, amount);
    }

    function assets() public view returns (uint256) {
        return asset == address(0) ? address(this).balance : IERC20(asset).balanceOf(address(this));
    }

    /// @notice Legal contributions not yet allocated to a beneficiary.
    function liabilities() public view returns (uint256) {
        return totalContributed - totalPaid;
    }

    function isSolvent() external view returns (bool) {
        return assets() >= liabilities();
    }

    function surplus() public view returns (uint256) {
        uint256 held = assets();
        uint256 owed = liabilities();
        return held > owed ? held - owed : 0;
    }

    function payoutCount() external view returns (uint256) {
        return payouts.length;
    }

    function _pushExact(address to, uint256 amount) private {
        if (asset == address(0)) {
            (bool ok,) = payable(to).call{ value: amount }("");
            if (!ok) revert TransferFailed();
            return;
        }

        // slither-disable-next-line reentrancy-balance
        uint256 senderBefore = IERC20(asset).balanceOf(address(this));
        // slither-disable-next-line reentrancy-balance
        uint256 recipientBefore = IERC20(asset).balanceOf(to);
        IERC20(asset).safeTransfer(to, amount);
        uint256 senderAfter = IERC20(asset).balanceOf(address(this));
        uint256 recipientAfter = IERC20(asset).balanceOf(to);
        if (
            senderAfter > senderBefore || senderBefore - senderAfter != amount || recipientAfter < recipientBefore
                || recipientAfter - recipientBefore != amount
        ) {
            revert UnsupportedAssetTransfer();
        }
    }
}
