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

import { GameType } from "../libraries/DisputeTypes.sol";
import { GameStatus } from "../libraries/DisputeTypes.sol";
import { SafeCall } from "../libraries/SafeCall.sol";

import { IDisputeGame } from "./IDisputeGame.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";

/**
 * @title BondManager
 * @notice The Bond Manager serves as an escrow for permissionless output proposal bonds.
 */
contract BondManager {
    /**
     * @notice The Bond Type
     */
    struct Bond {
        address owner;
        uint256 expiration;
        bytes32 id;
        uint256 amount;
    }

    /**
     * @notice Mapping from bondId to bond.
     */
    mapping(bytes32 => Bond) public bonds;

    /**
     * @notice BondPosted is emitted when a bond is posted.
     * @param bondId is the id of the bond.
     * @param owner is the address that owns the bond.
     * @param expiration is the time at which the bond expires.
     * @param amount is the amount of the bond.
     */
    event BondPosted(bytes32 bondId, address owner, uint256 expiration, uint256 amount);

    /**
     * @notice BondSeized is emitted when a bond is seized.
     * @param bondId is the id of the bond.
     * @param owner is the address that owns the bond.
     * @param seizer is the address that seized the bond.
     * @param amount is the amount of the bond.
     */
    event BondSeized(bytes32 bondId, address owner, address seizer, uint256 amount);

    /**
     * @notice BondReclaimed is emitted when a bond is reclaimed by the owner.
     * @param bondId is the id of the bond.
     * @param claiment is the address that reclaimed the bond.
     * @param amount is the amount of the bond.
     */
    event BondReclaimed(bytes32 bondId, address claiment, uint256 amount);

    /**
     * @notice The permissioned dispute game factory.
     * @dev Used to verify the status of bonds.
     */
    IDisputeGameFactory public immutable DISPUTE_GAME_FACTORY;

    /**
     * @notice Amount of gas used to transfer ether when splitting the bond.
     *         This is a reasonable amount of gas for a transfer, even to a smart contract.
     *         The number of participants is bound of by the block gas limit.
     */
    uint256 private constant TRANSFER_GAS = 30_000;

    /**
     * @notice Instantiates the bond maanger with the registered dispute game factory.
     * @param _disputeGameFactory is the dispute game factory.
     */
    constructor(IDisputeGameFactory _disputeGameFactory) {
        DISPUTE_GAME_FACTORY = _disputeGameFactory;
    }

    /**
     * @notice Post a bond with a given id and owner.
     * @dev This function will revert if the provided bondId is already in use.
     * @param _bondId is the id of the bond.
     * @param _bondOwner is the address that owns the bond.
     * @param _minClaimHold is the minimum amount of time the owner
     *        must wait before reclaiming their bond.
     */
    function post(
        bytes32 _bondId,
        address _bondOwner,
        uint256 _minClaimHold
    ) external payable {
        require(bonds[_bondId].owner == address(0), "BondManager: BondId already posted.");
        require(_bondOwner != address(0), "BondManager: Owner cannot be the zero address.");
        require(msg.value > 0, "BondManager: Value must be non-zero.");

        uint256 expiration = _minClaimHold + block.timestamp;
        bonds[_bondId] = Bond({
            owner: _bondOwner,
            expiration: expiration,
            id: _bondId,
            amount: msg.value
        });

        emit BondPosted(_bondId, _bondOwner, expiration, msg.value);
    }

    /**
     * @notice Seizes the bond with the given id.
     * @dev This function will revert if there is no bond at the given id.
     * @param _bondId is the id of the bond.
     */
    function seize(bytes32 _bondId) external {
        Bond memory b = bonds[_bondId];
        require(b.owner != address(0), "BondManager: The bond does not exist.");
        require(b.expiration >= block.timestamp, "BondManager: Bond expired.");

        IDisputeGame caller = IDisputeGame(msg.sender);
        IDisputeGame game = DISPUTE_GAME_FACTORY.games(
            GameType.ATTESTATION,
            caller.rootClaim(),
            caller.extraData()
        );
        require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
        require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");

        delete bonds[_bondId];

        emit BondSeized(_bondId, b.owner, msg.sender, b.amount);

        bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
        require(success, "BondManager: Failed to send Ether.");
    }

    /**
     * @notice Seizes the bond with the given id and distributes it to recipients.
     * @dev This function will revert if there is no bond at the given id.
     * @param _bondId is the id of the bond.
     * @param _claimRecipients is a set of addresses to split the bond amongst.
     */
    function seizeAndSplit(bytes32 _bondId, address[] calldata _claimRecipients) external {
        Bond memory b = bonds[_bondId];
        require(b.owner != address(0), "BondManager: The bond does not exist.");
        require(b.expiration >= block.timestamp, "BondManager: Bond expired.");

        IDisputeGame caller = IDisputeGame(msg.sender);
        IDisputeGame game = DISPUTE_GAME_FACTORY.games(
            GameType.ATTESTATION,
            caller.rootClaim(),
            caller.extraData()
        );
        require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
        require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");

        delete bonds[_bondId];

        emit BondSeized(_bondId, b.owner, msg.sender, b.amount);

        uint256 len = _claimRecipients.length;
        uint256 proportionalAmount = b.amount / len;
        // Send the proportional amount to each recipient. Do not revert if a send fails as that
        // will prevent other recipients from receiving their share.
        for (uint256 i; i < len; i++) {
            SafeCall.send({
                _target: payable(_claimRecipients[i]),
                _gas: TRANSFER_GAS,
                _value: proportionalAmount
            });
        }
    }

    /**
     * @notice Reclaims the bond of the bond owner.
     * @dev This function will revert if there is no bond at the given id.
     * @param _bondId is the id of the bond.
     */
    function reclaim(bytes32 _bondId) external {
        Bond memory b = bonds[_bondId];
        require(b.owner == msg.sender, "BondManager: Unauthorized claimant.");
        require(b.expiration <= block.timestamp, "BondManager: Bond isn't claimable yet.");

        delete bonds[_bondId];

        emit BondReclaimed(_bondId, msg.sender, b.amount);

        bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
        require(success, "BondManager: Failed to send Ether.");
    }
}
