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

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

/**
 * @title AereComputeMarketV2 - DePIN compute coordination + settlement rail
 * @notice The ON-CHAIN coordination and settlement layer for a decentralized
 *         compute marketplace (DePIN for AI and verifiable compute). Providers
 *         stake native AERE and advertise capacity; requesters escrow AERE for a
 *         job; the provider runs the work OFF-CHAIN and submits a result; the
 *         requester confirms (or a timeout auto-confirms) and payment releases.
 *         A dispute window lets the requester challenge, with Foundation
 *         arbitration and provider-stake slashing.
 *
 *         WHAT V2 ADDS OVER V1 (0xf0c8178a5d9feb0f70C5f184e79edeEDaddcF350):
 *         1. Self-contained jobs. `postJob` takes the FULL workload spec as
 *            bytes and emits it in the JobPosted event, so ANY provider daemon
 *            watching the chain learns exactly what to compute with no off-chain
 *            channel. The contract stores only keccak256(spec) as the anchor.
 *         2. Verifiable determinism. `submitResult` takes the FULL result bytes
 *            and emits them; the contract stores keccak256(result). The requester
 *            (or anyone) re-runs the deterministic workload from the on-chain
 *            spec and checks keccak256(localResult) == the on-chain resultHash.
 *         3. A distinct Disputed status. Once a job is disputed it can no longer
 *            be re-submitted by the provider to dodge arbitration (a V1 flaw);
 *            only the Foundation can resolve it.
 *         4. Bootstrap-configurable MIN_STAKE (constructor immutable) so the
 *            stake floor is a governance parameter rather than a hardcode.
 *
 *         SCOPE (honest): this contract is the trust and settlement RAIL. The
 *         actual GPU/compute supply is off-chain and requires real hardware
 *         providers. The rail is fully operational (anyone can stake and run the
 *         reference provider daemon to earn), but large-scale supply is adoption,
 *         exactly like validator decentralization. No new token: staking, escrow
 *         and payment are all in native AERE.
 */
contract AereComputeMarketV2 is Ownable, ReentrancyGuard {

    uint256 public immutable MIN_STAKE;                // stake floor to be a provider (set at deploy)
    uint64  public constant  CONFIRM_WINDOW = 3 days;  // requester confirm/dispute window
    uint16  public constant  SLASH_BPS = 5000;         // 50% of stake slashed on a lost dispute

    address public immutable FOUNDATION;               // dispute arbiter

    struct Provider {
        bool    active;
        uint256 stake;
        uint32  pricePerUnit;    // informational, AERE-wei per compute unit
        uint32  activeJobs;      // outstanding claimed/submitted/disputed jobs
        string  endpoint;        // provider API endpoint (off-chain, optional)
    }

    enum Status { None, Open, Claimed, Submitted, Disputed, Paid, Refunded, Slashed }

    struct Job {
        address requester;
        address provider;
        uint256 payment;         // escrowed AERE
        bytes32 specHash;        // keccak256 of the workload spec (spec bytes emitted in event)
        bytes32 resultHash;      // keccak256 of the result (result bytes emitted in event)
        Status  status;
        uint64  submittedAt;
    }

    mapping(address => Provider) public providers;
    mapping(uint256 => Job) public jobs;
    uint256 public jobCount;
    uint256 public slashPool;    // slashed stake, sweepable by Foundation to the sink/treasury

    event ProviderRegistered(address indexed provider, uint256 stake, uint32 pricePerUnit, string endpoint);
    event ProviderDeregistered(address indexed provider, uint256 stakeReturned);
    // spec bytes are emitted (non-indexed) so any watcher can reconstruct the workload.
    event JobPosted(uint256 indexed id, address indexed requester, uint256 payment, bytes32 specHash, bytes spec);
    event JobClaimed(uint256 indexed id, address indexed provider);
    // result bytes are emitted so the requester can verify determinism against specHash off-chain.
    event JobSubmitted(uint256 indexed id, bytes32 resultHash, bytes result);
    event JobPaid(uint256 indexed id, address indexed provider, uint256 payment);
    event JobRefunded(uint256 indexed id, address indexed requester, uint256 payment);
    event JobDisputed(uint256 indexed id);
    event DisputeResolved(uint256 indexed id, bool providerWon);

    error BadStake();
    error NotProvider();
    error HasActiveJobs();
    error ZeroPayment();
    error WrongStatus();
    error NotRequester();
    error NotJobProvider();
    error WindowOpen();
    error WindowClosed();
    error ZeroAddress();
    error EmptySpec();

    constructor(address foundation, uint256 minStake) {
        if (foundation == address(0)) revert ZeroAddress();
        if (minStake == 0) revert BadStake();
        FOUNDATION = foundation;
        MIN_STAKE = minStake;
    }

    modifier onlyFoundation() {
        if (msg.sender != FOUNDATION) revert("not foundation");
        _;
    }

    /* ------------------------------ providers ------------------------------- */

    function registerProvider(uint32 pricePerUnit, string calldata endpoint) external payable {
        Provider storage p = providers[msg.sender];
        if (p.stake + msg.value < MIN_STAKE) revert BadStake();
        p.active = true;
        p.stake += msg.value;
        p.pricePerUnit = pricePerUnit;
        p.endpoint = endpoint;
        emit ProviderRegistered(msg.sender, p.stake, pricePerUnit, endpoint);
    }

    function deregisterProvider() external nonReentrant {
        Provider storage p = providers[msg.sender];
        if (!p.active) revert NotProvider();
        if (p.activeJobs != 0) revert HasActiveJobs();
        uint256 amt = p.stake;
        p.active = false;
        p.stake = 0;
        (bool ok, ) = msg.sender.call{value: amt}("");
        require(ok, "xfer");
        emit ProviderDeregistered(msg.sender, amt);
    }

    /* -------------------------------- jobs ---------------------------------- */

    /// @notice Post a job with its FULL workload spec. The spec bytes are emitted
    ///         so any provider daemon can reconstruct and run it deterministically.
    function postJob(bytes calldata spec) external payable returns (uint256 id) {
        if (msg.value == 0) revert ZeroPayment();
        if (spec.length == 0) revert EmptySpec();
        bytes32 specHash = keccak256(spec);
        id = jobCount++;
        jobs[id] = Job({
            requester: msg.sender, provider: address(0), payment: msg.value,
            specHash: specHash, resultHash: bytes32(0), status: Status.Open, submittedAt: 0
        });
        emit JobPosted(id, msg.sender, msg.value, specHash, spec);
    }

    /// @notice Requester cancels an unclaimed job and reclaims the escrow.
    function cancelJob(uint256 id) external nonReentrant {
        Job storage j = jobs[id];
        if (j.requester != msg.sender) revert NotRequester();
        if (j.status != Status.Open) revert WrongStatus();
        j.status = Status.Refunded;
        uint256 amt = j.payment;
        j.payment = 0;
        (bool ok, ) = msg.sender.call{value: amt}("");
        require(ok, "xfer");
        emit JobRefunded(id, msg.sender, amt);
    }

    function claimJob(uint256 id) external {
        Job storage j = jobs[id];
        if (j.status != Status.Open) revert WrongStatus();
        Provider storage p = providers[msg.sender];
        if (!p.active || p.stake < MIN_STAKE) revert NotProvider();
        j.provider = msg.sender;
        j.status = Status.Claimed;
        p.activeJobs += 1;
        emit JobClaimed(id, msg.sender);
    }

    /// @notice Provider submits the FULL result bytes. keccak256(result) is stored
    ///         on-chain as the verifiable anchor; the bytes are emitted for re-run.
    function submitResult(uint256 id, bytes calldata result) external {
        Job storage j = jobs[id];
        if (j.provider != msg.sender) revert NotJobProvider();
        if (j.status != Status.Claimed) revert WrongStatus();
        bytes32 resultHash = keccak256(result);
        j.resultHash = resultHash;
        j.status = Status.Submitted;
        j.submittedAt = uint64(block.timestamp);
        emit JobSubmitted(id, resultHash, result);
    }

    /// @notice Requester accepts the result, pay the provider immediately.
    function confirmJob(uint256 id) external nonReentrant {
        Job storage j = jobs[id];
        if (j.requester != msg.sender) revert NotRequester();
        if (j.status != Status.Submitted) revert WrongStatus();
        _pay(id, j);
    }

    /// @notice After the confirm window with no dispute, anyone can settle, pay provider.
    function autoConfirm(uint256 id) external nonReentrant {
        Job storage j = jobs[id];
        if (j.status != Status.Submitted) revert WrongStatus();
        if (block.timestamp < j.submittedAt + CONFIRM_WINDOW) revert WindowOpen();
        _pay(id, j);
    }

    function _pay(uint256 id, Job storage j) internal {
        j.status = Status.Paid;
        uint256 amt = j.payment;
        j.payment = 0;
        providers[j.provider].activeJobs -= 1;
        (bool ok, ) = j.provider.call{value: amt}("");
        require(ok, "xfer");
        emit JobPaid(id, j.provider, amt);
    }

    /* ------------------------------ disputes -------------------------------- */

    /// @notice Requester challenges a submitted result within the confirm window.
    ///         The job moves to Disputed and can no longer be re-submitted; only
    ///         the Foundation can resolve it.
    function disputeJob(uint256 id) external {
        Job storage j = jobs[id];
        if (j.requester != msg.sender) revert NotRequester();
        if (j.status != Status.Submitted) revert WrongStatus();
        if (block.timestamp >= j.submittedAt + CONFIRM_WINDOW) revert WindowClosed();
        j.status = Status.Disputed;
        emit JobDisputed(id);
    }

    /// @notice Foundation arbitrates a disputed job. providerWon -> provider paid;
    ///         else requester refunded AND provider stake slashed by SLASH_BPS.
    function resolveDispute(uint256 id, bool providerWon) external onlyFoundation nonReentrant {
        Job storage j = jobs[id];
        if (j.status != Status.Disputed) revert WrongStatus();
        Provider storage p = providers[j.provider];
        p.activeJobs -= 1;
        uint256 amt = j.payment;
        j.payment = 0;
        if (providerWon) {
            j.status = Status.Paid;
            (bool ok, ) = j.provider.call{value: amt}("");
            require(ok, "xfer");
            emit JobPaid(id, j.provider, amt);
        } else {
            j.status = Status.Slashed;
            uint256 slash = (p.stake * SLASH_BPS) / 10_000;
            p.stake -= slash;
            slashPool += slash;
            (bool ok, ) = j.requester.call{value: amt}("");
            require(ok, "xfer");
            emit JobRefunded(id, j.requester, amt);
        }
        emit DisputeResolved(id, providerWon);
    }

    function sweepSlashPool(address to) external onlyFoundation nonReentrant {
        if (to == address(0)) revert ZeroAddress();
        uint256 amt = slashPool;
        slashPool = 0;
        (bool ok, ) = to.call{value: amt}("");
        require(ok, "xfer");
    }

    /* -------------------------------- views --------------------------------- */

    /// @notice Convenience getter for off-chain daemons.
    function getJob(uint256 id) external view returns (Job memory) {
        return jobs[id];
    }
}
