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

import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol";
import { PullPaymentsV3 } from "../bonds/PullPaymentsV3.sol";

/// @title TaskEscrowV3
/// @notice Exact-receipt task escrow. Every terminal outcome becomes a pull
/// payment, so no participant can be trapped by an unavailable counterparty.
contract TaskEscrowV3 is PullPaymentsV3 {
    enum TaskState {
        None,
        Open,
        Assigned,
        Submitted,
        Approved,
        Disputed,
        Refunded,
        Settled,
        ForceRefunded
    }

    struct Task {
        address creator;
        address assignee;
        address asset; // 0 = native
        uint256 amount;
        uint64 createdAt;
        uint64 assigneeDeadline;
        uint64 submitDeadline;
        uint64 disputeDeadline; // submit-review deadline
        uint64 decisionDeadline; // set when a dispute is opened
        TaskState state;
        bytes32 disputeCommitment;
        uint16 feeBpsSnapshot;
        address treasurySnapshot;
    }

    uint256 public constant MAX_BIDS = 32;
    mapping(uint256 => Task) public tasks;
    mapping(uint256 => address[]) internal _bidders;
    mapping(uint256 => mapping(address => bool)) public isBidder;
    mapping(address => uint256) public assetLiabilities;
    uint256 public nextTaskId = 1;

    address public owner;
    address public treasury;
    uint256 public protocolFeeBps; // <= 500 (5%)
    /// @notice Maximum face value of one task, immutable for the pilot.
    uint256 public immutable maxTaskAmount;
    /// @notice Maximum liabilities per asset. Assets are not numerically
    /// comparable, so each native/ERC-20 asset has this independent cap.
    uint256 public immutable maxAggregateLiabilityPerAsset;
    /// @notice Time after a dispute before anyone may execute its default.
    uint64 public immutable disputeDecisionTimeout;

    error NotOwner();
    error ZeroAddress();
    error BadState(TaskState);
    error DeadlineNotReached();
    error DeadlinePassed();
    error TooManyBids();
    error DuplicateBidder();
    error InvalidBidder();
    error NotParticipant();
    error FeeTooHigh();
    error TaskCapExceeded();
    error AggregateCapExceeded();
    error InsufficientSurplus();

    event TaskCreatedV3(uint256 indexed taskId, address indexed creator, address indexed asset, uint256 amount);
    event TaskBidPlacedV3(uint256 indexed taskId, address indexed bidder);
    event TaskAcceptedV3(uint256 indexed taskId, address indexed assignee);
    event TaskSubmittedV3(uint256 indexed taskId);
    event TaskApprovedV3(uint256 indexed taskId, uint256 paid, uint256 fee);
    event TaskDisputedV3(uint256 indexed taskId, bytes32 commitment, uint64 decisionDeadline);
    event TaskRefundedV3(uint256 indexed taskId, uint256 amount);
    event TaskForceRefundedV3(uint256 indexed taskId, address indexed by);
    event SubmittedDefaultedV3(uint256 indexed taskId, address indexed by);
    event DisputeDefaultedV3(uint256 indexed taskId, address indexed by);
    event ProtocolFeeSetV3(uint256 previous, uint256 current);
    event TreasurySetV3(address indexed previous, address indexed current);
    event SurplusSkimmedV3(address indexed asset, address indexed to, uint256 amount);

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

    constructor(
        address _owner,
        address _treasury,
        uint256 _feeBps,
        uint256 _maxTaskAmount,
        uint256 _maxAggregateLiabilityPerAsset,
        uint64 _disputeDecisionTimeout
    ) {
        if (_owner == address(0) || _treasury == address(0)) revert ZeroAddress();
        if (_feeBps > 500) revert FeeTooHigh();
        if (_maxTaskAmount == 0 || _maxAggregateLiabilityPerAsset == 0 || _disputeDecisionTimeout == 0) {
            revert ZeroAmount();
        }
        if (_maxTaskAmount > _maxAggregateLiabilityPerAsset) revert TaskCapExceeded();
        owner = _owner;
        treasury = _treasury;
        protocolFeeBps = _feeBps;
        maxTaskAmount = _maxTaskAmount;
        maxAggregateLiabilityPerAsset = _maxAggregateLiabilityPerAsset;
        disputeDecisionTimeout = _disputeDecisionTimeout;
    }

    function setProtocolFee(uint256 bps) external onlyOwner nonReentrantPullPayment {
        if (bps > 500) revert FeeTooHigh();
        emit ProtocolFeeSetV3(protocolFeeBps, bps);
        protocolFeeBps = bps;
    }

    function setTreasury(address t) external onlyOwner nonReentrantPullPayment {
        if (t == address(0)) revert ZeroAddress();
        emit TreasurySetV3(treasury, t);
        treasury = t;
    }

    // ---------------------------------------------------------------------
    // Creation and bidding
    // ---------------------------------------------------------------------

    function createTaskNative(uint64 assigneeDeadline, uint64 submitDeadline, uint64 disputeDeadline)
        external
        payable
        nonReentrantPullPayment
        returns (uint256 taskId)
    {
        if (msg.value == 0) revert ZeroAmount();
        _checkCreation(address(0), msg.value, assigneeDeadline, submitDeadline, disputeDeadline);
        assetLiabilities[address(0)] += msg.value;
        taskId = _recordTask(address(0), msg.value, assigneeDeadline, submitDeadline, disputeDeadline);
    }

    /// @notice Creates an ERC-20 task only after the escrow receives exactly
    /// the stated amount. Fee-on-transfer/non-delivery tokens are rejected.
    function createTaskERC20(
        address asset,
        uint256 amount,
        uint64 assigneeDeadline,
        uint64 submitDeadline,
        uint64 disputeDeadline
    ) external nonReentrantPullPayment returns (uint256 taskId) {
        if (asset == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        _checkCreation(asset, amount, assigneeDeadline, submitDeadline, disputeDeadline);
        // Reserve aggregate capacity and task state before the untrusted token
        // callback. The shared pull-payment lock also rejects every lifecycle
        // entry point during this window; a failed pull rolls this reservation
        // back atomically.
        assetLiabilities[asset] += amount;
        taskId = _recordTask(asset, amount, assigneeDeadline, submitDeadline, disputeDeadline);
        // slither-disable-next-line reentrancy-eth
        _pullExact(asset, amount);
    }

    function placeBid(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (t.state != TaskState.Open) revert BadState(t.state);
        if (block.timestamp >= t.assigneeDeadline) revert DeadlinePassed();
        if (msg.sender == t.creator) revert InvalidBidder();
        if (isBidder[taskId][msg.sender]) revert DuplicateBidder();
        if (_bidders[taskId].length >= MAX_BIDS) revert TooManyBids();
        isBidder[taskId][msg.sender] = true;
        _bidders[taskId].push(msg.sender);
        emit TaskBidPlacedV3(taskId, msg.sender);
    }

    function acceptBid(uint256 taskId, address bidder) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (msg.sender != t.creator) revert NotParticipant();
        if (t.state != TaskState.Open) revert BadState(t.state);
        if (block.timestamp >= t.assigneeDeadline) revert DeadlinePassed();
        if (bidder == address(0) || !isBidder[taskId][bidder]) revert InvalidBidder();
        t.assignee = bidder;
        t.state = TaskState.Assigned;
        emit TaskAcceptedV3(taskId, bidder);
    }

    // ---------------------------------------------------------------------
    // Task lifecycle
    // ---------------------------------------------------------------------

    function submitWork(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (msg.sender != t.assignee) revert NotParticipant();
        if (t.state != TaskState.Assigned) revert BadState(t.state);
        if (block.timestamp >= t.submitDeadline) revert DeadlinePassed();
        t.state = TaskState.Submitted;
        emit TaskSubmittedV3(taskId);
    }

    function approve(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (msg.sender != t.creator) revert NotParticipant();
        if (t.state != TaskState.Submitted) revert BadState(t.state);
        if (block.timestamp >= t.disputeDeadline) revert DeadlinePassed();
        _approve(t, taskId);
    }

    function dispute(uint256 taskId, bytes32 commitment) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (msg.sender != t.creator && msg.sender != t.assignee) revert NotParticipant();
        if (t.state != TaskState.Submitted) revert BadState(t.state);
        if (block.timestamp >= t.disputeDeadline) revert DeadlinePassed();
        t.state = TaskState.Disputed;
        t.disputeCommitment = commitment;
        t.decisionDeadline = uint64(block.timestamp + disputeDecisionTimeout);
        emit TaskDisputedV3(taskId, commitment, t.decisionDeadline);
    }

    /// @notice A submitted task defaults to approval after the review window.
    function defaultApproveSubmitted(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (t.state != TaskState.Submitted) revert BadState(t.state);
        if (block.timestamp < t.disputeDeadline) revert DeadlineNotReached();
        _approve(t, taskId);
        emit SubmittedDefaultedV3(taskId, msg.sender);
    }

    /// @notice Permissionless refund if a task never acquires an assignee or
    /// if its assignee misses the submission deadline.
    function forceRefund(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (t.state == TaskState.Open) {
            if (block.timestamp < t.assigneeDeadline) revert DeadlineNotReached();
        } else if (t.state == TaskState.Assigned) {
            if (block.timestamp < t.submitDeadline) revert DeadlineNotReached();
        } else {
            revert BadState(t.state);
        }
        _refund(t, taskId, TaskState.ForceRefunded, msg.sender);
    }

    /// @notice Owner-directed split during the decision period. A missed
    /// decision cannot strand funds because `defaultResolveDispute` is open.
    function resolveDispute(uint256 taskId, uint256 payToAssignee) external onlyOwner nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (t.state != TaskState.Disputed) revert BadState(t.state);
        if (block.timestamp >= t.decisionDeadline) revert DeadlinePassed();
        if (payToAssignee > t.amount) revert InvalidBidder();
        uint256 refundToCreator = t.amount - payToAssignee;
        t.state = TaskState.Settled;
        if (payToAssignee > 0) _credit(t.asset, t.assignee, payToAssignee);
        if (refundToCreator > 0) _credit(t.asset, t.creator, refundToCreator);
        emit TaskRefundedV3(taskId, refundToCreator);
    }

    /// @notice Default result for an unresolved dispute: return the escrow to
    /// the creator. It is permissionless and uses a pull credit.
    function defaultResolveDispute(uint256 taskId) external nonReentrantPullPayment {
        Task storage t = tasks[taskId];
        if (t.state != TaskState.Disputed) revert BadState(t.state);
        if (block.timestamp < t.decisionDeadline) revert DeadlineNotReached();
        _refund(t, taskId, TaskState.Settled, msg.sender);
        emit DisputeDefaultedV3(taskId, msg.sender);
    }

    /// @notice Only forced/direct transfers may be removed by the owner; all
    /// task principal and unclaimed credits remain protected liabilities.
    function skimSurplus(address asset, address to, uint256 amount) external onlyOwner nonReentrantPullPayment {
        if (to == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (amount > surplus(asset)) revert InsufficientSurplus();
        if (asset == address(0)) {
            (bool ok,) = payable(to).call{ value: amount }("");
            if (!ok) revert TransferFailed();
        } else {
            _pushExact(asset, to, amount);
        }
        emit SurplusSkimmedV3(asset, to, amount);
    }

    // ---------------------------------------------------------------------
    // Asset accounting views
    // ---------------------------------------------------------------------

    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) public view returns (uint256) {
        uint256 held = assets(asset);
        uint256 owed = assetLiabilities[asset];
        return held > owed ? held - owed : 0;
    }

    function biddersCount(uint256 taskId) external view returns (uint256) {
        return _bidders[taskId].length;
    }

    function bidderAt(uint256 taskId, uint256 i) external view returns (address) {
        return _bidders[taskId][i];
    }

    function _afterCreditWithdrawal(address asset, uint256 amount) internal override {
        assetLiabilities[asset] -= amount;
    }

    function _checkCreation(
        address asset,
        uint256 amount,
        uint64 assigneeDeadline,
        uint64 submitDeadline,
        uint64 disputeDeadline
    ) private view {
        if (
            assigneeDeadline <= block.timestamp || submitDeadline <= assigneeDeadline
                || disputeDeadline <= submitDeadline
        ) revert DeadlinePassed();
        if (amount > maxTaskAmount) revert TaskCapExceeded();
        if (assetLiabilities[asset] + amount > maxAggregateLiabilityPerAsset) revert AggregateCapExceeded();
    }

    function _recordTask(
        address asset,
        uint256 amount,
        uint64 assigneeDeadline,
        uint64 submitDeadline,
        uint64 disputeDeadline
    ) private returns (uint256 taskId) {
        taskId = nextTaskId++;
        tasks[taskId] = Task({
            creator: msg.sender,
            assignee: address(0),
            asset: asset,
            amount: amount,
            createdAt: uint64(block.timestamp),
            assigneeDeadline: assigneeDeadline,
            submitDeadline: submitDeadline,
            disputeDeadline: disputeDeadline,
            decisionDeadline: 0,
            state: TaskState.Open,
            disputeCommitment: bytes32(0),
            feeBpsSnapshot: uint16(protocolFeeBps),
            treasurySnapshot: treasury
        });
        emit TaskCreatedV3(taskId, msg.sender, asset, amount);
    }

    function _approve(Task storage t, uint256 taskId) private {
        // Avoid an intermediate overflow for a valid task amount at the
        // configured cap. Rounding remains down, so `pay + fee == amount`.
        uint256 fee = Math.mulDiv(t.amount, t.feeBpsSnapshot, 10_000);
        uint256 pay = t.amount - fee;
        t.state = TaskState.Approved;
        if (pay > 0) _credit(t.asset, t.assignee, pay);
        if (fee > 0) _credit(t.asset, t.treasurySnapshot, fee);
        emit TaskApprovedV3(taskId, pay, fee);
    }

    function _refund(Task storage t, uint256 taskId, TaskState terminalState, address by) private {
        t.state = terminalState;
        _credit(t.asset, t.creator, t.amount);
        emit TaskForceRefundedV3(taskId, by);
    }
}
