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

/**
 * @title VaultfireTaskEscrow
 * @author Vaultfire Protocol
 * @notice On-chain task escrow for the Vaultfire Task Marketplace. Agents post tasks
 *         funded with USDC, other registered agents bid, and settlement is handled
 *         automatically on approval — or via governance in the event of a dispute.
 * @dev Designed to be deployed on Base, Avalanche, Arbitrum, and Polygon.
 *      Works alongside ERC8004IdentityRegistry (identity) and
 *      AIPartnershipBondsV2 (trust bonds) but does not call AIPartnershipBondsV2
 *      directly; it only needs the registry for agent verification.
 *
 *      Self-contained — no OpenZeppelin imports. Manual ReentrancyGuard included.
 */

// ---------------------------------------------------------------------------
//  Minimal interfaces
// ---------------------------------------------------------------------------

/// @notice Minimal ERC-20 interface used for USDC interactions.
interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

/// @notice Minimal interface for the ERC-8004 Identity Registry.
///         An agent is "registered" when isAgentActive returns true.
interface IERC8004IdentityRegistry {
    /// @dev Returns true if `agent` has an active registration.
    function isAgentActive(address agent) external view returns (bool);
}

// ---------------------------------------------------------------------------
//  Contract
// ---------------------------------------------------------------------------

contract VaultfireTaskEscrow {

    // -----------------------------------------------------------------------
    //  Types
    // -----------------------------------------------------------------------

    /// @notice All possible lifecycle states for a Task.
    enum TaskStatus {
        Created,       // Task created, accepting bids
        Bidding,       // At least one bid has been placed (alias to Created in some flows)
        Assigned,      // Poster accepted a bid; worker assigned
        WorkSubmitted, // Assignee submitted deliverable URI
        Completed,     // Poster approved work; USDC released
        Disputed,      // Either party raised a dispute
        Cancelled      // Poster cancelled before any bid was accepted
    }

    /// @notice Core task record stored on-chain.
    struct Task {
        uint256 id;
        address poster;
        string  description;
        uint256 reward;          // total USDC locked (18-dec equivalent; USDC is 6-dec — stored as-is)
        uint256 deadline;        // unix timestamp by which work must be submitted
        TaskStatus status;
        address assignee;
        string  deliverableURI;
        uint256 createdAt;
    }

    /// @notice A single bid placed by a registered agent.
    struct Bid {
        address bidder;
        string  proposal;
        uint256 timestamp;
    }

    // -----------------------------------------------------------------------
    //  Constants
    // -----------------------------------------------------------------------

    /// @notice Protocol fee in basis points (100 = 1 %).
    uint256 public constant FEE_BPS = 100;

    /// @notice Denominator for basis-point math.
    uint256 private constant BPS_DENOM = 10_000;

    // -----------------------------------------------------------------------
    //  Immutables
    // -----------------------------------------------------------------------

    /// @notice ERC-8004 Identity Registry used to verify agent registration.
    IERC8004IdentityRegistry public immutable identityRegistry;

    /// @notice MultisigGovernance address — the only account that can resolve disputes.
    address public immutable governance;

    /// @notice USDC token contract used for all escrow payments.
    IERC20 public immutable usdc;

    /// @notice Address that receives the 1 % protocol fee on every settlement.
    address public immutable feeRecipient;

    // -----------------------------------------------------------------------
    //  State
    // -----------------------------------------------------------------------

    /// @dev Incrementing task counter. Starts at 1 so 0 is never a valid task id.
    uint256 private _taskCounter;

    /// @dev Primary task storage.
    mapping(uint256 => Task) private _tasks;

    /// @dev Bids per task.
    mapping(uint256 => Bid[]) private _bids;

    // -----------------------------------------------------------------------
    //  Reentrancy guard (manual — no OpenZeppelin dependency)
    // -----------------------------------------------------------------------

    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED     = 2;
    uint256 private _reentrancyStatus = _NOT_ENTERED;

    modifier nonReentrant() {
        require(_reentrancyStatus != _ENTERED, "VaultfireTaskEscrow: reentrant call");
        _reentrancyStatus = _ENTERED;
        _;
        _reentrancyStatus = _NOT_ENTERED;
    }

    // -----------------------------------------------------------------------
    //  Events
    // -----------------------------------------------------------------------

    /// @notice Emitted when a new task is created and USDC deposited into escrow.
    event TaskCreated(
        uint256 indexed taskId,
        address indexed poster,
        uint256 reward,
        uint256 deadline,
        string description
    );

    /// @notice Emitted when a registered agent submits a bid.
    event BidPlaced(
        uint256 indexed taskId,
        address indexed bidder,
        uint256 bidIndex,
        string proposal
    );

    /// @notice Emitted when the poster accepts a bid and assigns the task.
    event BidAccepted(
        uint256 indexed taskId,
        address indexed assignee,
        uint256 bidIndex
    );

    /// @notice Emitted when the assignee submits their deliverable.
    event WorkSubmitted(
        uint256 indexed taskId,
        address indexed assignee,
        string deliverableURI
    );

    /// @notice Emitted when the poster approves the work and USDC is released.
    event WorkApproved(
        uint256 indexed taskId,
        address indexed assignee,
        uint256 workerAmount,
        uint256 feeAmount
    );

    /// @notice Emitted when either party raises a dispute.
    event TaskDisputed(
        uint256 indexed taskId,
        address indexed disputedBy,
        string reason
    );

    /// @notice Emitted when governance resolves a dispute.
    event DisputeResolved(
        uint256 indexed taskId,
        address indexed resolvedBy,
        address indexed payTo,
        uint256 amount
    );

    /// @notice Emitted when a poster cancels a task with no accepted bid.
    event TaskCancelled(
        uint256 indexed taskId,
        address indexed poster,
        uint256 refundAmount
    );

    // -----------------------------------------------------------------------
    //  Constructor
    // -----------------------------------------------------------------------

    /**
     * @notice Deploy the VaultfireTaskEscrow.
     * @param identityRegistry_ Address of the ERC-8004 Identity Registry.
     * @param governance_        Address of MultisigGovernance (dispute resolver).
     * @param usdc_              Address of the USDC ERC-20 token.
     * @param feeRecipient_      Address that receives the 1 % protocol fee.
     */
    constructor(
        address identityRegistry_,
        address governance_,
        address usdc_,
        address feeRecipient_
    ) {
        require(identityRegistry_ != address(0), "VaultfireTaskEscrow: zero identityRegistry");
        require(governance_        != address(0), "VaultfireTaskEscrow: zero governance");
        require(usdc_              != address(0), "VaultfireTaskEscrow: zero usdc");
        require(feeRecipient_      != address(0), "VaultfireTaskEscrow: zero feeRecipient");

        identityRegistry = IERC8004IdentityRegistry(identityRegistry_);
        governance        = governance_;
        usdc              = IERC20(usdc_);
        feeRecipient      = feeRecipient_;
    }

    // -----------------------------------------------------------------------
    //  Modifiers
    // -----------------------------------------------------------------------

    /// @dev Reverts if the caller is not a registered agent in the identity registry.
    modifier onlyRegisteredAgent() {
        require(
            identityRegistry.isAgentActive(msg.sender),
            "VaultfireTaskEscrow: caller not a registered agent"
        );
        _;
    }

    /// @dev Reverts if `taskId` does not correspond to a live task.
    modifier taskExists(uint256 taskId) {
        require(taskId > 0 && taskId <= _taskCounter, "VaultfireTaskEscrow: task does not exist");
        _;
    }

    // -----------------------------------------------------------------------
    //  Core functions
    // -----------------------------------------------------------------------

    /**
     * @notice Create a new task and lock the USDC reward in escrow.
     * @dev Caller must be a registered agent and must have approved this contract
     *      to spend `reward` USDC beforehand.
     * @param description Human-readable task description.
     * @param reward      Amount of USDC (in token's native decimals) to lock as reward.
     * @param deadline    Unix timestamp by which the assignee must submit work.
     * @return taskId     The newly created task's identifier.
     */
    function createTask(
        string calldata description,
        uint256 reward,
        uint256 deadline
    )
        external
        nonReentrant
        onlyRegisteredAgent
        returns (uint256 taskId)
    {
        require(bytes(description).length > 0, "VaultfireTaskEscrow: empty description");
        require(reward > 0,                    "VaultfireTaskEscrow: reward must be > 0");
        require(deadline > block.timestamp,    "VaultfireTaskEscrow: deadline in the past");

        // Pull USDC from poster into escrow
        bool ok = usdc.transferFrom(msg.sender, address(this), reward);
        require(ok, "VaultfireTaskEscrow: USDC transferFrom failed");

        unchecked { taskId = ++_taskCounter; }

        _tasks[taskId] = Task({
            id:             taskId,
            poster:         msg.sender,
            description:    description,
            reward:         reward,
            deadline:       deadline,
            status:         TaskStatus.Created,
            assignee:       address(0),
            deliverableURI: "",
            createdAt:      block.timestamp
        });

        emit TaskCreated(taskId, msg.sender, reward, deadline, description);
    }

    /**
     * @notice Place a bid on an open task.
     * @dev Only registered agents may bid. Posters cannot bid on their own task.
     *      Bids are accepted while the task is in Created or Bidding status.
     * @param taskId   The task to bid on.
     * @param proposal A text proposal describing how the bidder will complete the task.
     */
    function bidOnTask(uint256 taskId, string calldata proposal)
        external
        nonReentrant
        onlyRegisteredAgent
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(
            task.status == TaskStatus.Created || task.status == TaskStatus.Bidding,
            "VaultfireTaskEscrow: task not open for bids"
        );
        require(task.poster != msg.sender,   "VaultfireTaskEscrow: poster cannot bid on own task");
        require(block.timestamp < task.deadline, "VaultfireTaskEscrow: task deadline passed");
        require(bytes(proposal).length > 0,  "VaultfireTaskEscrow: empty proposal");

        uint256 bidIndex = _bids[taskId].length;

        _bids[taskId].push(Bid({
            bidder:    msg.sender,
            proposal:  proposal,
            timestamp: block.timestamp
        }));

        // Advance to Bidding state if still in Created
        if (task.status == TaskStatus.Created) {
            task.status = TaskStatus.Bidding;
        }

        emit BidPlaced(taskId, msg.sender, bidIndex, proposal);
    }

    /**
     * @notice Accept a bid and assign the task to the chosen bidder.
     * @dev Only the poster may call this. Task must be in Bidding status.
     * @param taskId   The task to assign.
     * @param bidIndex Index into the bids array for this task.
     */
    function acceptBid(uint256 taskId, uint256 bidIndex)
        external
        nonReentrant
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(task.poster == msg.sender, "VaultfireTaskEscrow: only poster can accept bids");
        require(task.status == TaskStatus.Bidding, "VaultfireTaskEscrow: no bids to accept");
        require(bidIndex < _bids[taskId].length, "VaultfireTaskEscrow: invalid bid index");
        require(block.timestamp < task.deadline,  "VaultfireTaskEscrow: deadline passed");

        Bid storage chosen = _bids[taskId][bidIndex];

        task.assignee = chosen.bidder;
        task.status   = TaskStatus.Assigned;

        emit BidAccepted(taskId, chosen.bidder, bidIndex);
    }

    /**
     * @notice Submit the completed work deliverable.
     * @dev Only the task assignee may call this before the deadline.
     * @param taskId        The assigned task.
     * @param deliverableURI URI pointing to the work deliverable (e.g. IPFS CID).
     */
    function submitWork(uint256 taskId, string calldata deliverableURI)
        external
        nonReentrant
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(task.assignee == msg.sender,      "VaultfireTaskEscrow: only assignee can submit work");
        require(task.status == TaskStatus.Assigned, "VaultfireTaskEscrow: task not in Assigned state");
        require(block.timestamp <= task.deadline,  "VaultfireTaskEscrow: deadline passed");
        require(bytes(deliverableURI).length > 0,  "VaultfireTaskEscrow: empty deliverableURI");

        task.deliverableURI = deliverableURI;
        task.status          = TaskStatus.WorkSubmitted;

        emit WorkSubmitted(taskId, msg.sender, deliverableURI);
    }

    /**
     * @notice Approve the submitted work and release USDC to the worker.
     * @dev Only the poster may call this. Releases `reward * 99%` to the assignee
     *      and `reward * 1%` to the protocol feeRecipient.
     * @param taskId The task whose work is being approved.
     */
    function approveWork(uint256 taskId)
        external
        nonReentrant
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(task.poster == msg.sender,               "VaultfireTaskEscrow: only poster can approve work");
        require(task.status == TaskStatus.WorkSubmitted, "VaultfireTaskEscrow: work not yet submitted");

        task.status = TaskStatus.Completed;

        uint256 fee          = (task.reward * FEE_BPS) / BPS_DENOM;
        uint256 workerAmount = task.reward - fee;

        // Transfer to worker
        bool ok1 = usdc.transfer(task.assignee, workerAmount);
        require(ok1, "VaultfireTaskEscrow: worker transfer failed");

        // Transfer fee
        bool ok2 = usdc.transfer(feeRecipient, fee);
        require(ok2, "VaultfireTaskEscrow: fee transfer failed");

        emit WorkApproved(taskId, task.assignee, workerAmount, fee);
    }

    /**
     * @notice Raise a dispute on a task.
     * @dev Either the poster or the assignee may dispute a task that is in
     *      Assigned or WorkSubmitted status.
     * @param taskId The task to dispute.
     * @param reason A human-readable explanation for the dispute.
     */
    function disputeTask(uint256 taskId, string calldata reason)
        external
        nonReentrant
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(
            msg.sender == task.poster || msg.sender == task.assignee,
            "VaultfireTaskEscrow: only poster or assignee can dispute"
        );
        require(
            task.status == TaskStatus.Assigned || task.status == TaskStatus.WorkSubmitted,
            "VaultfireTaskEscrow: task cannot be disputed in current state"
        );
        require(bytes(reason).length > 0, "VaultfireTaskEscrow: empty dispute reason");

        task.status = TaskStatus.Disputed;

        emit TaskDisputed(taskId, msg.sender, reason);
    }

    /**
     * @notice Resolve a disputed task by directing the escrowed USDC to a given address.
     * @dev Only MultisigGovernance may call this. `payTo` must be either the poster
     *      or the assignee. The full reward (no fee deducted) is sent to the winner;
     *      governance may choose to send to either party at its discretion.
     * @param taskId The disputed task.
     * @param payTo  Address to receive the escrowed USDC (poster or assignee).
     */
    function resolveDispute(uint256 taskId, address payTo)
        external
        nonReentrant
        taskExists(taskId)
    {
        require(msg.sender == governance, "VaultfireTaskEscrow: only governance can resolve disputes");

        Task storage task = _tasks[taskId];

        require(task.status == TaskStatus.Disputed, "VaultfireTaskEscrow: task is not disputed");
        require(
            payTo == task.poster || payTo == task.assignee,
            "VaultfireTaskEscrow: payTo must be poster or assignee"
        );

        task.status = TaskStatus.Completed;

        uint256 amount = task.reward;

        bool ok = usdc.transfer(payTo, amount);
        require(ok, "VaultfireTaskEscrow: dispute resolution transfer failed");

        emit DisputeResolved(taskId, msg.sender, payTo, amount);
    }

    /**
     * @notice Cancel a task and refund the poster's USDC.
     * @dev The poster may cancel only while no bid has been accepted
     *      (i.e. status is Created or Bidding). Once a bid is accepted the
     *      poster must either approve work or raise a dispute.
     * @param taskId The task to cancel.
     */
    function cancelTask(uint256 taskId)
        external
        nonReentrant
        taskExists(taskId)
    {
        Task storage task = _tasks[taskId];

        require(task.poster == msg.sender, "VaultfireTaskEscrow: only poster can cancel");
        require(
            task.status == TaskStatus.Created || task.status == TaskStatus.Bidding,
            "VaultfireTaskEscrow: cannot cancel after bid is accepted"
        );

        task.status = TaskStatus.Cancelled;

        uint256 refund = task.reward;

        bool ok = usdc.transfer(task.poster, refund);
        require(ok, "VaultfireTaskEscrow: refund transfer failed");

        emit TaskCancelled(taskId, msg.sender, refund);
    }

    // -----------------------------------------------------------------------
    //  View functions
    // -----------------------------------------------------------------------

    /**
     * @notice Retrieve the full Task record for a given task ID.
     * @param taskId The task to query.
     * @return task  The Task struct.
     */
    function getTask(uint256 taskId)
        external
        view
        taskExists(taskId)
        returns (Task memory task)
    {
        task = _tasks[taskId];
    }

    /**
     * @notice Retrieve all bids placed on a task.
     * @param taskId The task to query.
     * @return bids  Array of Bid structs ordered by placement time.
     */
    function getTaskBids(uint256 taskId)
        external
        view
        taskExists(taskId)
        returns (Bid[] memory bids)
    {
        bids = _bids[taskId];
    }

    /**
     * @notice Returns the total number of tasks ever created (including cancelled ones).
     * @return count Current task counter value.
     */
    function taskCount() external view returns (uint256 count) {
        count = _taskCounter;
    }
}
