// File: contracts/Gateway.sol



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

import "./Escrow.sol";
import "./Dispute.sol";
import "./Agent.sol";
import "./interfaces/IGateway.sol";
import "./libraries/TransferHelper.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

contract Gateway is Ownable, Escrow, Dispute, Agent, IGateway {
    // External token address, should be able to reset this by an owner
    address public token;

    mapping(uint256 => address[]) public reviewers;
    mapping(uint256 => address[]) public pickedAgents;

    constructor(address _token) {
        require(
            _token != address(0),
            "Invalid Token Address"
        );
        token = _token;
    }
    
    function resetTokenAddress(address _newTokenAddress) external onlyOwner {
        require(
            _newTokenAddress != address(0) && _newTokenAddress != address(this),
            "Invalid Token Address"
        );
        token = _newTokenAddress;
    }

    /* 
    * @param _escrowDisputableTime (Epoch time in seconds) - After this time, a customer can make a dispute case
    * @param _escrowWithdrawableTime (Epoch time in seconds) - After this time, a merchant can withdraw funds from an escrow contract
    */
    function purchase(
        uint256 _productId,
        address _merchantAddress,
        uint256 _amount,
        uint256 _escrowDisputableTime,
        uint256 _escrowWithdrawableTime
    ) public {
        _purchase(
            token,
            _msgSender(),
            _productId,
            _merchantAddress,
            _amount,
            _escrowDisputableTime,
            _escrowWithdrawableTime
        );
    }

    function withdraw(uint256 _escrowId) public {
        _withdraw(token, _msgSender(), _escrowId);
    }

    function startDispute(uint256 _escrowId) public {
        require(
            escrows[_escrowId].status == DEFAULT,
            "Escrow status must be on the DEFAULT status"
        );
        require(
            _msgSender() == escrows[_escrowId].buyerAddress,
            "Caller is not buyer"
        );
        require(
            escrows[_escrowId].escrowDisputableTime <= block.timestamp,
            "Please wait until the disputable time"
        );
        require(
            escrows[_escrowId].escrowWithdrawableTime >= block.timestamp,
            "Disputable time was passed already"
        );

        escrows[_escrowId].status = DISPUTE;
        _dispute(_msgSender(), _escrowId);
    }

    // Call this function to get credits as an Agent, should call approve function of Token contract before calling this function
    function participate() external {
        Agent memory agent = agents[_msgSender()];
        require(
            agent.status == 0 || // not exists
                agent.status == _LOST || // Agent submitted and lost
                    agent.status == _INIT, // Agent submitted and won
            "Wrong status"
        );
        require(
            TransferHelper.balanceOf(token, _msgSender()) >=
                agentPaticipateAmount,
            "Not correct amount"
        );

        if (agent.participationCount != 0 && agent.score < criteriaScore) {
            revert(
                "Your agent score is too low, so can't participate any more"
            );
        }

        if (agent.participationCount == 0) {
            agents[_msgSender()] = Agent(
                initialAgentScore,
                0,
                agentPaticipateAmount,
                0,
                _WAITING
            );
        } else {
            agents[_msgSender()] = Agent(
                agent.score,
                agent.participationCount,
                agent.accumulatedAmount + agentPaticipateAmount,
                0,
                _WAITING
            );
        }
        TransferHelper.safeTransferFrom(
            token,
            _msgSender(),
            address(this),
            agentPaticipateAmount
        );

        emit AgentParticipated(_msgSender());
    }

    function pickDispute(uint256 _disputeId)
        external
    {
        require(
            agents[_msgSender()].status == _WAITING,
            "Agent is not in waiting state"
        );
        require(
            agents[_msgSender()].score >= criteriaScore,
            "Low agent score"
        );
        require(disputes[_disputeId].escrowId != 0, "Invalid dispute id");
         require(disputes[_disputeId].applied_agents_count < disputeReviewGroupCount ||
            (disputes[_disputeId].createdAt + maxReviewDelay) < block.timestamp, "max applied agents exceed");
        require(
            disputes[_disputeId].status == INIT ||
                disputes[_disputeId].status == WAITING ||
                disputes[_disputeId].status == REVIEW,
            "Dispute is not in init nor in waiting status"
        );
        pickedAgents[_disputeId].push(_msgSender());
        disputes[_disputeId].status = REVIEW;
        agents[_msgSender()].status = REVIEW;
        agents[_msgSender()].assignedDisputeId = _disputeId;
        disputes[_disputeId].applied_agents_count +=1;
        emit AssignAgent(_msgSender(), _disputeId);
    }

    function _setAgent(
        Agent storage agent,
        uint256 status,
        uint256 score,
        uint256 assignedDisputeId
    ) internal {
        agent.status = status;
        if (status == _LOST) 
            agent.score -= score;
        else 
            agent.score += score;
        agent.assignedDisputeId = assignedDisputeId;
    }

    function _setDispute(
        Dispute storage dispute,
        uint256 status
    ) internal {
        dispute.status = status;
        if(status == FAIL){
            dispute.disapprovedCount += 1;
        }else if(status == WIN){
            dispute.approvedCount += 1;
        }else if( status == INIT){
            dispute.approvedCount = 0;
            dispute.disapprovedCount = 0;
        }
        dispute.updatedAt = block.timestamp;
    }

    // Need to have MTO transfered beforehand
    function submit(uint256 _disputeId, uint256 _decision) external {
        Dispute storage dispute = disputes[_disputeId];
        Agent storage agent;
        require(
            agents[_msgSender()].score >= criteriaScore,
            "Too low score as an Agent"
        );        
        require(
            agents[_msgSender()].status == _REVIEW,
            "Agent status should be review"
        );
        require(
            agents[_msgSender()].assignedDisputeId == _disputeId,
            "disputeID is not assigned"
        );
        require(
            disputes[agents[_msgSender()].assignedDisputeId].escrowId != 0,
            "DisputeID is not valid"
        );
        require(
            _decision == _APPROVED || _decision == _DISAPPROVED,
            "Invalid decision value"
        );

        agents[_msgSender()].participationCount += 1;
        if (
            _decision == _APPROVED &&
            dispute.approvedCount + 1 >= disputeReviewConsensusCount
        ) {
            _setAgent(agents[_msgSender()], _EARNED, scoreUp, 0);

            _setDispute(dispute, WIN);
            emit DisputeApproved(_disputeId);

            escrows[dispute.escrowId].status = REFUNDED; // REFUNDABLE; In case not returing the funds back to a customer in this function
            // Transfer the funds to a customer for chargeback as a dipsute case got approved
            TransferHelper.safeTransfer(
                token,
                escrows[dispute.escrowId].buyerAddress,
                escrows[dispute.escrowId].amount
            );
            emit Refunded(
                escrows[dispute.escrowId].buyerAddress,
                dispute.escrowId,
                escrows[dispute.escrowId].amount
            );

            for (uint256 i = 0; i < reviewers[_disputeId].length; i++) {
                agent = agents[reviewers[_disputeId][i]];
                if (agent.status == _APPROVED) {
                    _setAgent(agent, _EARNED, scoreUp, 0);
                } else if (agent.status == _DISAPPROVED) {
                    _setAgent(agent, _LOST, scoreDown, 0);
                }
            }
            for (uint256 i = 0; i < pickedAgents[_disputeId].length; i++) {
                agent = agents[pickedAgents[_disputeId][i]];
                _setAgent(agent, _WAITING, 0, 0);
            }

        } else if (
            _decision == _DISAPPROVED &&
            dispute.disapprovedCount + 1 >= disputeReviewConsensusCount
        ) {
            _setAgent(agents[_msgSender()], _EARNED, scoreUp, 0);
            _setDispute(dispute, FAIL);
            emit DisputeDisapproved(_disputeId);

            escrows[dispute.escrowId].status = COMPLETED; // DEFAULT; In case not returing the funds to a merchant in this function

            for (uint256 i = 0; i < reviewers[_disputeId].length; i++) {
                agent = agents[reviewers[_disputeId][i]];
                if (agent.status == _DISAPPROVED) {
                    _setAgent(agent, _EARNED, scoreUp, 0);
                } else if (agent.status == _APPROVED) {
                    _setAgent(agent, _LOST, scoreDown, 0);
                }
            }
            for (uint256 i = 0; i < pickedAgents[_disputeId].length; i++) {
                agent = agents[pickedAgents[_disputeId][i]];
                _setAgent(agent, _WAITING, 0, 0);
            }
            // Transfer the funds to a merchant for selling the product as a dipsute case(by a customer) got disapproved
            TransferHelper.safeTransfer(
                token,
                escrows[dispute.escrowId].merchantAddress,
                escrows[dispute.escrowId].amount
            );
            emit Withdraw(
                escrows[dispute.escrowId].merchantAddress,
                dispute.escrowId,
                escrows[dispute.escrowId].amount
            );
        } else if (
            _decision == _APPROVED &&
            dispute.approvedCount + 1 < disputeReviewConsensusCount &&
            (dispute.approvedCount + dispute.disapprovedCount + 1) >=
            disputeReviewGroupCount
        ) {
            _setAgent(agents[_msgSender()], _LOST, scoreDown, 0);
            _setDispute(dispute, INIT);

            for (uint256 i = 0; i < reviewers[_disputeId].length; i++) {
                _setAgent(
                    agents[reviewers[_disputeId][i]],
                    _LOST,
                    scoreDown,
                    0
                );
            }
            for (uint256 i = 0; i < pickedAgents[_disputeId].length; i++) {                
                _setAgent(agents[pickedAgents[_disputeId][i]], _WAITING, 0, 0);
            }
        } else if (
            _decision == _DISAPPROVED &&
            dispute.disapprovedCount + 1 < disputeReviewConsensusCount &&
            (dispute.approvedCount + dispute.disapprovedCount + 1) >=
            disputeReviewGroupCount
        ) {
            _setAgent(agents[_msgSender()], _LOST, scoreDown, 0);
            _setDispute(dispute, INIT);
            for (uint256 i = 0; i < reviewers[_disputeId].length; i++) {
                _setAgent(
                    agents[reviewers[_disputeId][i]],
                    _LOST,
                    scoreDown,
                    0
                );
            }
            for (uint256 i = 0; i < pickedAgents[_disputeId].length; i++) {                
                _setAgent(agents[pickedAgents[_disputeId][i]], _WAITING, 0, 0);
            }
        } else {            
            _setAgent(agents[_msgSender()], _decision, 0, 0);

            _removeFromPickedAgents(_disputeId, _msgSender());
            reviewers[_disputeId].push(_msgSender());

            dispute.status = WAITING;
            dispute.updatedAt = block.timestamp;

            if (_decision == _APPROVED) dispute.approvedCount += 1;
            else if (_decision == _DISAPPROVED) dispute.disapprovedCount += 1;
        }

        if (
            agents[_msgSender()].score < criteriaScore &&
            agents[_msgSender()].status != _BAN
        ) {
            agents[_msgSender()].status = _BAN;
        }

        emit SubmittedDispute(_msgSender(), _disputeId, _decision);
    }

    function agentWithdraw() external {
        require(
            agents[_msgSender()].status == _EARNED,
            "Cannot withdraw unearned tokens"
        );

        agents[_msgSender()].status = _INIT;
        TransferHelper.safeTransfer(token, _msgSender(), disputeBonusAmount);

        emit AgentWithdraw(_msgSender(), disputeBonusAmount);
    }

    function adminWithdrawToken(uint256 _amount) external onlyOwner {
        require(
            TransferHelper.balanceOf(token, address(this)) > _amount,
            "Not enough balance"
        );
        TransferHelper.safeTransfer(token, _msgSender(), _amount);
    }

    function getMerchantReputation(address _merchantAddress)
        public
        view
        returns (uint256)
    {
        require(_merchantAddress != address(0), "Invalid Merchant Address");

        if (currentEscrowId == 0) {
            return 0;
        }

        uint256 Es = 0; //total number of escrow for merchant address
        uint256 Ds = 0; //total win dispute againts merchant address
        uint256 Er = 0; //(escrow success ratio) = (Es - Ds)/Es

        uint256 Esa = 0; //total escrowed amount
        uint256 Dsa = 0; //total disputed amount
        uint256 Ar = 0; // (Amount ratio) = (Esa - Dsa) / Esa

        for (uint256 i = 1; i <= currentEscrowId; i++) {
            if (
                escrows[i].merchantAddress == _merchantAddress &&
                escrows[i].status != DISPUTE
            ) {
                if (
                    escrows[i].status != DEFAULT ||
                    escrows[i].escrowWithdrawableTime < block.timestamp
                ) {
                    Es = Es + 1;
                    Esa = Esa + escrows[i].amount;

                    if (
                        escrows[i].status == REFUNDABLE ||
                        escrows[i].status == REFUNDED
                    ) {
                        Ds = Ds + 1;
                        Dsa = Dsa + escrows[i].amount;
                    }
                }
            }
        }

        if (Es == 0) {
            return 0;
        }

        Er = ((60 * (Es - Ds)) / Es); //60 percent reputation for successful escrows
        Ar = ((40 * (Esa - Dsa)) / Esa); ////60 percent reputation for successful escrows Amount
        uint256 total = Er + Ar;
        return total;
    }

    function applyADM(uint256 _disputeId, uint256 _decision)
        external
        onlyOwner
    {
        Dispute storage dispute = disputes[_disputeId];
        Escrow storage escrow = escrows[dispute.escrowId];
        require(dispute.status == INIT, "Dispute is not in init state");
        require(
            _decision == _APPROVED || _decision == _DISAPPROVED,
            "Invalid decision value"
        );
        //APPROVE
        if (_decision == _APPROVED) {
            dispute.status = WIN;
            dispute.updatedAt = block.timestamp;
            emit DisputeApproved(_disputeId);

            escrow.status = REFUNDED;
            TransferHelper.safeTransfer(
                token,
                escrow.buyerAddress,
                escrow.amount
            );
            emit Refunded(escrow.buyerAddress, dispute.escrowId, escrow.amount);
        } else {
            //FAIL
            dispute.status = FAIL;
            dispute.updatedAt = block.timestamp;
            emit DisputeDisapproved(_disputeId);

            escrow.status = COMPLETED;

            TransferHelper.safeTransfer(
                token,
                escrow.merchantAddress,
                escrow.amount
            );

            emit Withdraw(
                escrow.merchantAddress,
                dispute.escrowId,
                escrow.amount
            );
        }
    }

    // TODO 1: fee, 2: reset global variables 3: auto assign system

    function _removeFromPickedAgents(uint256 disputeId, address agent) internal {
        address[] storage agents = pickedAgents[disputeId];
        uint i = _findIndex(agents, agent);
        _removeByIndex(agents, i);
    }

    function _removeByIndex(address[] storage agents, uint index) internal {
        if (index >= agents.length) return;

        agents[index] = agents[agents.length - 1];
        agents.pop();
    }

    function _findIndex(address[] storage agents, address submitter) internal view returns(uint) {
        uint i = 0;
        while (agents[i] != submitter) {
            i++;
        }
        return i;
    }
}

// File: contracts/Escrow.sol



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

import "./interfaces/IEscrow.sol";
import "./libraries/TransferHelper.sol";

abstract contract Escrow is IEscrow {
    // Escrow Status
    uint256 public currentEscrowId;
    uint8 constant DEFAULT = 1;
    uint8 constant DISPUTE = 2;
    uint8 constant REFUNDABLE = 3;
    uint8 constant COMPLETED = 4;
    uint8 constant REFUNDED = 5;

    mapping(uint256 => Escrow) public escrows;

    // _escrowDisputableTime(Epoch time in seconds) - After this time, a customer can make a dispute case
    // _escrowWithdrawableTime(Epoch time in seconds) - After this time, a merchant can withdraw funds from an escrow contract
    function _purchase(
        address _token,
        address _currentCaller,
        uint256 _productId,
        address _merchantAddress,
        uint256 _amount,
        uint256 _escrowDisputableTime,
        uint256 _escrowWithdrawableTime
    ) internal {
        require(_merchantAddress != address(0), "Invalid Merchant Address");
        require(_amount > 0, "Amount should be bigger than zero");
        require(
            TransferHelper.balanceOf(_token, _currentCaller) >= _amount,
            "You don't have enough token amount"
        );
        require(
            TransferHelper.allowance(_token, _currentCaller, address(this)) >=
                _amount,
            "You should approve token transfer to this contract first"
        );
        require(
            _escrowDisputableTime > block.timestamp,
            "Disputable time should be later than current time"
        );
        require(
            _escrowWithdrawableTime > _escrowDisputableTime,
            "Withdraw Time should be later than Disputable time"
        );

        escrows[currentEscrowId + 1] = Escrow(
            _productId,
            _currentCaller,
            _merchantAddress,
            _amount,
            _escrowWithdrawableTime,
            _escrowDisputableTime,
            DEFAULT,
            block.timestamp
        );
        currentEscrowId = currentEscrowId + 1;
        TransferHelper.safeTransferFrom(
            _token,
            _currentCaller,
            address(this),
            _amount
        );
        emit Escrowed(_currentCaller, _productId, _amount, currentEscrowId);
    }
    function _withdraw(
        address _token,
        address _currentCaller,
        uint256 _escrowId
    ) public {
        require(
            escrows[_escrowId].status == DEFAULT ||
                escrows[_escrowId].status == REFUNDABLE,
            "Invalid Status"
        );
        require(
            block.timestamp > escrows[_escrowId].escrowWithdrawableTime,
            "Escrowd time has not passed yet"
        );
        require(
            _currentCaller == escrows[_escrowId].buyerAddress ||
                _currentCaller == escrows[_escrowId].merchantAddress,
            "Caller is neither Buyer nor Merchant"
        );
        /*
        require(
            TransferHelper.balanceOf(_token, address(this)) >=
                escrows[_escrowId].amount,
            "Contract doesn't have enough funds"
        );
        */
        if (
            escrows[_escrowId].status == DEFAULT &&
            escrows[_escrowId].buyerAddress == _currentCaller
        ) {
            revert("Buyer cannot withdraw in default status");
        }
        if (
            escrows[_escrowId].status == REFUNDABLE &&
            escrows[_escrowId].merchantAddress == _currentCaller
        ) {
            revert("Merchant cannot withdraw in refund status");
        }
        if (
            escrows[_escrowId].status == REFUNDABLE &&
            escrows[_escrowId].buyerAddress == _currentCaller
        ) {
            // Transfers tokens to buyer
            TransferHelper.safeTransfer(
                _token,
                escrows[_escrowId].buyerAddress,
                escrows[_escrowId].amount
            );
            // Update the escrow status as REFUNDED
            escrows[_escrowId].status = REFUNDED;
            // emit Withdraw(_currentCaller, _escrowId, escrows[_escrowId].amount);
            emit Refunded(_currentCaller, _escrowId, escrows[_escrowId].amount);
        } else if (
            escrows[_escrowId].status == DEFAULT &&
            escrows[_escrowId].merchantAddress == _currentCaller
        ) {
            // Update the escrow status as COMPLETED
            escrows[_escrowId].status = COMPLETED;
            // Transfers tokens to merchant
            TransferHelper.safeTransfer(
                _token,
                escrows[_escrowId].merchantAddress,
                escrows[_escrowId].amount
            );
            emit Withdraw(_currentCaller, _escrowId, escrows[_escrowId].amount);
        }
    }
}

// File: contracts/Dispute.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./interfaces/IDispute.sol";
import "./interfaces/IEscrow.sol";

abstract contract Dispute is IDispute {
    // Dispute Status
    uint256 public currentDisputeId;
    uint8 constant INIT = 1;
    uint8 constant WAITING = 2;
    uint8 constant REVIEW = 3;
    uint8 constant WIN = 4;
    uint8 constant FAIL = 5;

    mapping(uint256 => Dispute) public disputes;

    function _dispute(address msgSender, uint256 _escrowId) internal {
        disputes[currentDisputeId + 1] = Dispute(
            _escrowId,
            0, // approvedCount
            0, // disapprovedCount
            INIT,// status
            0, // applied_agents_count
            block.timestamp,
            block.timestamp
        );
        currentDisputeId = currentDisputeId + 1;
        emit Disputed(msgSender, currentDisputeId, _escrowId);
    }

    function _setDispute(
        Dispute storage dispute,
        uint8 status,
        uint256 approvedCount,
        uint256 updatedAt
    ) internal {
        dispute.status = status;
        dispute.approvedCount += approvedCount;
        dispute.updatedAt = updatedAt;
    }
}

// File: contracts/Agent.sol



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

import "./interfaces/IAgent.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Agent is IAgent, Ownable {
    // Agent related params, should be able to reset this by an owner
    uint256 public initialAgentScore = 100;
    uint256 public criteriaScore = 70;
    uint256 public disputeBonusAmount = 10 * (10**18);
    uint256 public scoreUp = 10;
    uint256 public scoreDown = 10;
    uint256 public disputeReviewGroupCount = 5;//  maximum required agent to resolve a dispute
    uint256 public disputeReviewConsensusCount = 3;
    uint256 public agentPaticipateAmount = 5 * (10**18);
    uint public maxReviewDelay = 20 seconds; // maximum period to resolve a dispute
    // Agent Status
    uint256 constant _INIT = 1;
    uint256 constant _WAITING = 2;
    uint256 constant _REVIEW = 3;
    uint256 constant _APPROVED = 4;
    uint256 constant _DISAPPROVED = 5;
    uint256 constant _EARNED = 6;
    uint256 constant _LOST = 7;
    uint256 constant _BAN = 8;

    mapping(address => Agent) public agents;

    function resetInitialAgentScore(uint256 _newInitialAgentScore)
        external
        onlyOwner
    {
        require(_newInitialAgentScore > 0, "Invalid value");
        initialAgentScore = _newInitialAgentScore;
    }
    function resetMaxReviewDelay(uint256 newMaxReviewDelay) external onlyOwner{
        require(newMaxReviewDelay > 0, "Invalid value");
        maxReviewDelay = newMaxReviewDelay;
    }
    function resetCriteriaScore(uint256 _newCriteriaScore) external onlyOwner {
        require(_newCriteriaScore >= 0, "Invalid value");
        criteriaScore = _newCriteriaScore;
    }

    function resetDisputeBonusAmount(uint256 _newDisputeBonusAmount)
        external
        onlyOwner
    {
        require(_newDisputeBonusAmount >= 0, "Invalid value");
        disputeBonusAmount = _newDisputeBonusAmount * (10**18);
    }

    function resetScoreUp(uint256 _newScoreUp) external onlyOwner {
        require(_newScoreUp >= 0, "Invalid value");
        scoreUp = _newScoreUp;
    }

    function resetScoreDown(uint256 _newScoreDown) external onlyOwner {
        require(_newScoreDown >= 0, "Invalid value");
        scoreDown = _newScoreDown;
    }

    function resetDisputeReviewGroupCount(uint256 _newDisputeReviewGroupCount)
        external
        onlyOwner
    {
        require(_newDisputeReviewGroupCount > 0, "Invalid value");
        require(
            _newDisputeReviewGroupCount >= disputeReviewConsensusCount,
            "Should be larger number than the Consensus count"
        );
        disputeReviewGroupCount = _newDisputeReviewGroupCount;
    }

    function resetDisputeReviewConsensusCount(
        uint256 _newDisputeReviewConsensusCount
    ) external onlyOwner {
        require(_newDisputeReviewConsensusCount > 0, "Invalid value");
        require(
            _newDisputeReviewConsensusCount <= disputeReviewGroupCount,
            "Should be smaller number than the Group count"
        );
        disputeReviewConsensusCount = _newDisputeReviewConsensusCount;
    }

    function resetAgentPaticipateAmount(uint256 _newAgentPaticipateAmount)
        external
        onlyOwner
    {
        require(_newAgentPaticipateAmount > 0, "Invalid value");
        agentPaticipateAmount = _newAgentPaticipateAmount * (10**18);
    }
}

// File: contracts/interfaces/IGateway.sol



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

interface IGateway {}

// File: contracts/libraries/TransferHelper.sol



// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.6.0;

import "../IERC20.sol";

// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
library TransferHelper {
    function safeApprove(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0x095ea7b3, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::safeApprove: approve failed"
        );
    }

    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0xa9059cbb, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::safeTransfer: transfer failed"
        );
    }

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(0x23b872dd, from, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "TransferHelper::transferFrom: transferFrom failed"
        );
    }

    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(
            success,
            "TransferHelper::safeTransferETH: ETH transfer failed"
        );
    }

    function allowance(
        address token,
        address owner,
        address spender
    ) internal view returns (uint256) {
        return IERC20(token).allowance(owner, spender);
    }

    function balanceOf(address token, address account)
        internal
        view
        returns (uint256)
    {
        return IERC20(token).balanceOf(account);
    }
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: contracts/interfaces/IEscrow.sol



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

interface IEscrow {
    struct Escrow {
        uint256 productId;
        address buyerAddress;
        address merchantAddress;
        uint256 amount;
        uint256 escrowWithdrawableTime;
        uint256 escrowDisputableTime;
        uint256 status;
        uint256 createdAt;
    }
    event Escrowed(
        address indexed _from,
        uint256 indexed _productID,
        uint256 _amount,
        uint256 indexed _escrowId
    );
    event Withdraw(
        address indexed _withdrawer,
        uint256 indexed _escrowId,
        uint256 _amount
    );
    event Refunded(
        address indexed _withdrawer,
        uint256 indexed _escrowId,
        uint256 _amount
    );
}

// File: contracts/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.3.2 (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

// File: contracts/interfaces/IDispute.sol



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

interface IDispute {
    struct Dispute {
        uint256 escrowId;
        uint256 approvedCount; // (default: 0)
        uint256 disapprovedCount; // (default: 0)
        uint256 status; // (default: 0)  1: init, 2: waiting, 3: review, 4: win, 4: fail
        uint256 applied_agents_count;
        uint256 createdAt;
        uint256 updatedAt;
    }
    event Disputed(
        address indexed _from,
        uint256 indexed _disputeId,
        uint256 indexed _escrowId
    );
    event SubmittedDispute(
        address indexed _agentAddress,
        uint256 indexed _disputeId,
        uint256 indexed _decision
    );

    event DisputeApproved(uint256 indexed _disputeId);
    event DisputeDisapproved(uint256 indexed _disputeId);
}

// File: contracts/interfaces/IAgent.sol



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

interface IAgent {
    struct Agent {
        uint256 score; // (default: initial_agent_score)
        uint256 participationCount;
        uint256 accumulatedAmount;
        uint256 assignedDisputeId;
        uint256 status;
    }
    event AgentParticipated(address indexed _agentAddress);
    event AssignAgent(
        address indexed _agentAddress,
        uint256 indexed _disputeId
    );
    event AgentWithdraw(address indexed _withdrawer, uint256 _amount);
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}