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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @dev The AereAgentDID surface this facilitator consumes. The DID is the single
///      authorization oracle: one call to `authorize` enforces the Falcon ROOT
///      lifecycle, the session scope, the cumulative spend cap, the secp256k1 session
///      signature (ecrecover) and per-session anti-replay, and records the spend, or
///      reverts. `getSession` is used only to resolve which agent (Falcon root) funds
///      the payment and to guard self-deal. `keyRegistry` lets this contract locate the
///      registry so it can read the current key owner (the agent controller).
interface IAereAgentDID {
    function authorize(uint256 sessionId, bytes32 scope, bytes32 actionHash, uint256 amount, bytes calldata sig)
        external
        returns (bool);

    function getSession(uint256 sessionId)
        external
        view
        returns (
            uint256 rootKeyId,
            address sessionAddr,
            bytes32 scopeHash,
            uint256 spendCap,
            uint256 spent,
            uint64 issuedBlock,
            uint64 expiry,
            uint64 actionNonce,
            bool revoked
        );

    function isSessionValid(uint256 sessionId) external view returns (bool);

    function agentExists(uint256 rootKeyId) external view returns (bool);

    function keyRegistry() external view returns (address);
}

/// @dev The one AerePQCKeyRegistry read this facilitator needs: the current owner of a
///      key record. That owner is the agent's controller (the party that proved Falcon
///      possession at registration). ownerOf returns the owner for ACTIVE, ROTATED and
///      REVOKED records alike, so an operator can always withdraw an agent's idle vault
///      even after rotating or revoking its Falcon root; only session-authorized SPENDING
///      halts on a rotated/revoked root.
interface IAerePQCKeyRegistryLite {
    function ownerOf(uint256 keyId) external view returns (address);
}

/**
 * @title AERE402FacilitatorPQC — HTTP 402 agentic settlement rooted in a post-quantum identity
 *
 * @notice A settlement facilitator for machine (agent-to-service) payments whose spending
 *         authority is rooted in a quantum-durable Falcon key, not a bare secp256k1 signer.
 *
 *         WHAT IS DIFFERENT FROM AERE402Facilitator / V2. The classic facilitators debit an
 *         AereAgent registry and authorize each payment with an EIP-712 signature from the
 *         agent's rotatable secp256k1 signer (AereAgent.signerOf). Here the payer is an
 *         AereAgentDID: an agent whose ROOT authority is a Falcon key held in
 *         AerePQCKeyRegistry, and whose day-to-day work is signed by cheap, short-lived,
 *         revocable secp256k1 SESSION keys. A payment is authorized by the agent's DID
 *         SESSION signature, and the whole payment path inherits the DID's guarantees:
 *
 *           - ROOT LIFECYCLE ENFORCED. A session is valid only while its Falcon root is
 *             still ACTIVE in the registry. Rotating or revoking the Falcon root instantly
 *             halts every downstream payment under it — this facilitator delegates the
 *             check to AereAgentDID.authorize, which fails closed the moment the root is no
 *             longer ACTIVE Falcon. Spending authority lives in the quantum-durable key.
 *           - CHEAP HOT PATH. Per payment, the agent signs a raw secp256k1 signature over
 *             the DID action digest; verification is a single ecrecover (~3k gas). The
 *             Falcon key is a cold, high-value authority that only ever issues sessions; it
 *             is never exposed to the hot payment path.
 *           - PER-SESSION SCOPE + SPEND CAP. Each session is locked to an opaque scope tag
 *             and a cumulative spend cap. This facilitator passes the caller-supplied scope
 *             to the DID (which reverts on a mismatch) and passes the payment amount as the
 *             session spend (which reverts if the cap would be exceeded). A leaked session
 *             key is contained by its scope, cap, expiry and the on-chain revocation list.
 *
 *         PREPAID VAULT. This contract holds a prepaid balance per (agent, token), keyed by
 *         the agent's Falcon root key id (the DID agent id). Anyone may `topUp` a registered
 *         agent's vault; only the agent's controller (the current registry owner of the root
 *         key) may `withdraw` idle balance or `setPaused`. On `settle`, the debited amount is
 *         split: (amount - fee) to the payee and a 25-bps protocol fee to AereSink (the
 *         flywheel), exactly as the classic facilitators, with the same best-effort flush.
 *
 *         SETTLE FLOW per call:
 *           1. Provider answers a request with HTTP 402 + an AERE402-PQC quote
 *              { facilitator, sessionId, scope, token, payee, amount, resourceId, deadline }.
 *           2. The agent signs the DID action digest for its session with the session key
 *              (raw secp256k1) and returns the signature.
 *           3. Provider calls settle(...) with the quote + signature. This contract:
 *                a. Checks the payment deadline, msg.sender == payee, amount, self-deal, and
 *                   the agent's pause flag.
 *                b. Binds the payment terms into an actionHash committed to THIS facilitator.
 *                c. Delegates authorization to AereAgentDID.authorize — the single call that
 *                   enforces root lifecycle + scope + spend cap + session signature +
 *                   anti-replay, and records the spend.
 *                d. Debits the agent's prepaid vault and splits amount -> payee + sink.
 *           4. Provider serves the response now that settlement is final.
 *
 *         HONEST SCOPE. Application-layer identity + settlement. This does NOT change AERE
 *         consensus (blocks are still Besu QBFT with classical ECDSA). Falcon verification
 *         is performed by AERE's live native precompile through AerePQCKeyRegistry, reached
 *         via the DID. This contract custodies prepaid ERC-20 balances only; it mints no
 *         token and has no owner.
 *
 *         IMMUTABILITY. AereAgentDID address, AerePQCKeyRegistry address (read from the DID
 *         at construction), SINK address and the 25-bps fee are all pinned. No admin can
 *         move funds, change the fee, or repoint the DID.
 */
contract AERE402FacilitatorPQC is ReentrancyGuard {
    /* ------------------------------- immutable ------------------------------- */

    /// @notice The AereAgentDID that roots authorization in a Falcon key and owns the
    ///         session model. The sole authorization oracle for `settle`.
    IAereAgentDID public immutable DID;

    /// @notice The AerePQCKeyRegistry the DID roots into (read from DID at construction).
    ///         Used only to resolve an agent's current controller (registry key owner).
    IAerePQCKeyRegistryLite public immutable KEY_REGISTRY;

    /// @notice AereSink (the flywheel) that receives the protocol fee.
    address public immutable SINK;

    /// @notice 0.25% protocol fee, identical to the classic facilitators.
    uint16 public constant PROTOCOL_FEE_BPS = 25;

    /// @notice Domain tag binding a payment's terms to THIS facilitator inside the DID
    ///         action commitment. The session key signs the DID action digest over this
    ///         actionHash, so the same session action can never be replayed against a
    ///         different facilitator or with different terms.
    bytes32 public constant PAYMENT_DOMAIN = keccak256("AERE402FacilitatorPQC.v1.payment");

    /* --------------------------------- state -------------------------------- */

    /// @notice rootKeyId (DID agent id) => token => prepaid balance.
    mapping(uint256 => mapping(address => uint256)) public balances;

    /// @notice rootKeyId => paused. When true, `settle` reverts for this agent. Controlled
    ///         by the agent's controller as a cheap incident-response switch (independent of,
    ///         and in addition to, per-session revocation and root rotation/revocation).
    mapping(uint256 => bool) public paused;

    /// @notice token => failed-flush fee residual custodied by this contract but NOT owed to
    ///         any agent vault. It grows ONLY when a `settle` fee flush to the SINK fails
    ///         best-effort (the fee tokens then sit idle here), and shrinks only when
    ///         `flushResidual` successfully forwards them. It is the EXACT and only pool
    ///         `flushResidual` may move; prepaid vault balances are never reachable from it.
    ///         This is what keeps the vault-solvency invariant true:
    ///             tokenBalance(this) >= sum(balances[*][token]) + residual[token].
    mapping(address => uint256) public residual;

    /* --------------------------------- events ------------------------------- */

    event SettledPQC(
        uint256 indexed rootKeyId,
        uint256 indexed sessionId,
        address indexed payee,
        address token,
        uint256 amount,
        uint256 protocolFee,
        bytes32 resourceId
    );
    event TopUp(uint256 indexed rootKeyId, address indexed token, address indexed funder, uint256 amount);
    event Withdrawn(uint256 indexed rootKeyId, address indexed token, address indexed recipient, uint256 amount);
    event PausedSet(uint256 indexed rootKeyId, bool paused, address by);
    /// @notice A `settle` fee flush to the SINK failed; `amount` of fee is now tracked residual.
    event ResidualAccrued(address indexed token, uint256 amount);
    /// @notice `flushResidual` forwarded `amount` of tracked residual to the SINK.
    event ResidualFlushed(address indexed token, address indexed by, uint256 amount);

    /* --------------------------------- errors ------------------------------- */

    error ZeroAddress();
    error ZeroAmount();
    error DeadlinePassed();
    error PayeeMismatch();
    error SelfDealForbidden(); // payee == the session's own key
    error AgentPaused();
    error UnknownAgent();
    error NotController();
    error SessionAuthFailed(); // DID.authorize did not confirm the payment
    error InsufficientBalance(uint256 have, uint256 want);
    error InsufficientResidual(uint256 have, uint256 want); // flushResidual bounded to tracked residual
    error TransferFailed();

    /* ------------------------------- constructor ---------------------------- */

    constructor(address did, address sink) {
        if (did == address(0) || sink == address(0)) revert ZeroAddress();
        DID = IAereAgentDID(did);
        address reg = IAereAgentDID(did).keyRegistry();
        if (reg == address(0)) revert ZeroAddress();
        KEY_REGISTRY = IAerePQCKeyRegistryLite(reg);
        SINK = sink;
    }

    /* ------------------------------- funding --------------------------------- */

    /// @notice Fund an agent's prepaid vault. Permissionless: anyone may top up a
    ///         registered agent (the operator, a sponsor, a treasury). Pulls `amount` of
    ///         `token` from msg.sender.
    function topUp(uint256 rootKeyId, address token, uint256 amount) external nonReentrant {
        if (!DID.agentExists(rootKeyId)) revert UnknownAgent();
        if (amount == 0) revert ZeroAmount();
        if (!IERC20(token).transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
        balances[rootKeyId][token] += amount;
        emit TopUp(rootKeyId, token, msg.sender, amount);
    }

    /// @notice Withdraw idle vault balance. Only the agent's controller (the current
    ///         registry owner of the Falcon root key) may withdraw. A rotated or revoked
    ///         root does NOT strand funds: the registry still reports the same owner, so
    ///         the operator can always recover the prepaid balance.
    function withdraw(uint256 rootKeyId, address token, uint256 amount, address recipient) external nonReentrant {
        if (recipient == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        _requireController(rootKeyId);
        uint256 bal = balances[rootKeyId][token];
        if (bal < amount) revert InsufficientBalance(bal, amount);
        unchecked {
            balances[rootKeyId][token] = bal - amount;
        }
        if (!IERC20(token).transfer(recipient, amount)) revert TransferFailed();
        emit Withdrawn(rootKeyId, token, recipient, amount);
    }

    /// @notice Pause or unpause an agent's settlement path. Only the agent's controller.
    ///         A cheap ECDSA incident switch that halts all `settle` calls for this agent
    ///         without touching the DID or the Falcon root.
    function setPaused(uint256 rootKeyId, bool value) external {
        _requireController(rootKeyId);
        paused[rootKeyId] = value;
        emit PausedSet(rootKeyId, value, msg.sender);
    }

    /* --------------------------------- settle -------------------------------- */

    /// @notice Settle a machine payment authorized by an AereAgentDID session.
    /// @dev    msg.sender MUST equal `payee`: a third party cannot redirect a leaked
    ///         session signature to an attacker-controlled payee, and the payee's identity
    ///         is bound into the authorization commitment either way. All session/root
    ///         authorization (root lifecycle, scope, spend cap, secp256k1 session signature,
    ///         anti-replay) is enforced by the single DID.authorize call; this function adds
    ///         the payment-deadline, self-deal, pause and prepaid-balance layers.
    /// @param sessionId  the DID session authorizing the payment.
    /// @param scope      the scope tag for this payment (must equal the session's scopeHash).
    /// @param token      the settlement ERC-20.
    /// @param payee      the service provider being paid (== msg.sender).
    /// @param amount     gross payment (fee is taken from this); charged against the session cap.
    /// @param resourceId opaque resource tag (e.g. keccak256("GET /v1/inference")).
    /// @param deadline   unix seconds after which the payment auth is stale.
    /// @param sessionSig 65-byte secp256k1 signature by the session key over the DID action
    ///                   digest for (sessionId, scope, paymentActionHash, amount, actionNonce).
    function settle(
        uint256 sessionId,
        bytes32 scope,
        address token,
        address payee,
        uint256 amount,
        bytes32 resourceId,
        uint256 deadline,
        bytes calldata sessionSig
    ) external nonReentrant {
        if (block.timestamp > deadline) revert DeadlinePassed();
        if (msg.sender != payee) revert PayeeMismatch();
        if (payee == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        // Resolve the funding agent (Falcon root) and the session key, for the self-deal
        // guard and the vault debit. getSession reverts on an unknown session.
        (uint256 rootKeyId, address sessionAddr,,,,,,,) = DID.getSession(sessionId);

        // A compromised session key cannot drain the agent by paying itself.
        if (payee == sessionAddr) revert SelfDealForbidden();
        if (paused[rootKeyId]) revert AgentPaused();

        // Bind the payment terms to THIS facilitator. The session key's signature (checked
        // inside DID.authorize) is over the DID action digest, which commits to sessionId,
        // scope, this actionHash, the amount and the session's own actionNonce — so every
        // term below is authorized and this authorization cannot be replayed elsewhere.
        bytes32 actionHash = _paymentActionHash(token, payee, resourceId, deadline);

        // The single authorization gate: enforces root lifecycle + scope + spend cap +
        // session signature + anti-replay, and records the spend, or reverts.
        bool ok = DID.authorize(sessionId, scope, actionHash, amount, sessionSig);
        if (!ok) revert SessionAuthFailed();

        // Debit the prepaid vault (keyed by the Falcon root). Checks-effects before any
        // token transfer; the whole tx (including the DID spend record) reverts if short.
        uint256 bal = balances[rootKeyId][token];
        if (bal < amount) revert InsufficientBalance(bal, amount);
        unchecked {
            balances[rootKeyId][token] = bal - amount;
        }

        uint256 fee = (amount * PROTOCOL_FEE_BPS) / 10_000;
        uint256 toPayee = amount - fee;

        if (toPayee > 0) {
            if (!IERC20(token).transfer(payee, toPayee)) revert TransferFailed();
        }
        if (fee > 0) {
            // Best-effort forward to the sink; never gate the user flow on sink success. On
            // failure the fee tokens stay idle in this facilitator, so we RECORD them as
            // tracked residual — the ONLY pool flushResidual is ever allowed to move. Without
            // this accounting, an unbounded flushResidual could push prepaid vault balances to
            // the sink and strand user funds; here every token flushResidual can touch is
            // matched one-for-one by a residual credit, so vault balances stay solvent.
            if (!_tryFlushToSink(token, fee)) {
                residual[token] += fee;
                emit ResidualAccrued(token, fee);
            }
        }

        emit SettledPQC(rootKeyId, sessionId, payee, token, amount, fee, resourceId);
    }

    /* ------------------------------- residual flush -------------------------- */

    /// @notice Anyone can re-attempt the sink flush for a FAILED-FLUSH fee residual.
    /// @dev    HARD BOUND: `amount` may not exceed `residual[token]`, the tokens this contract
    ///         holds that are NOT owed to any agent vault. This function can therefore never
    ///         move prepaid vault balances to the sink — the very drain that an unbounded,
    ///         unauthenticated flush would allow. The residual is debited FIRST
    ///         (checks-effects-interactions); a failed re-flush reverts the whole call, so the
    ///         residual is preserved for a later retry.
    function flushResidual(address token, uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        uint256 r = residual[token];
        if (amount > r) revert InsufficientResidual(r, amount);
        unchecked {
            residual[token] = r - amount;
        }
        if (!_tryFlushToSink(token, amount)) revert TransferFailed();
        emit ResidualFlushed(token, msg.sender, amount);
    }

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

    /// @notice The actionHash a session key must commit to (inside the DID action digest)
    ///         to authorize a payment with these exact terms on THIS facilitator. Off-chain
    ///         signers compute this, then sign DID.actionDigest(sessionId, scope, actionHash,
    ///         amount, actionNonce) with the session key.
    function paymentActionHash(address token, address payee, bytes32 resourceId, uint256 deadline)
        external
        view
        returns (bytes32)
    {
        return _paymentActionHash(token, payee, resourceId, deadline);
    }

    /// @notice The agent's current controller (registry owner of the Falcon root key), or
    ///         address(0) if the agent does not exist / the key has no owner.
    function controllerOf(uint256 rootKeyId) public view returns (address) {
        if (!DID.agentExists(rootKeyId)) return address(0);
        return KEY_REGISTRY.ownerOf(rootKeyId);
    }

    /// @notice True iff `account` is the agent's current controller.
    function isController(uint256 rootKeyId, address account) external view returns (bool) {
        return account != address(0) && controllerOf(rootKeyId) == account;
    }

    /// @notice Convenience: the DID's live validity for a session (root ACTIVE Falcon,
    ///         not revoked, not expired). A settle for an invalid session always reverts.
    function isSessionValid(uint256 sessionId) external view returns (bool) {
        return DID.isSessionValid(sessionId);
    }

    /* -------------------------------- internal ------------------------------- */

    function _paymentActionHash(address token, address payee, bytes32 resourceId, uint256 deadline)
        internal
        view
        returns (bytes32)
    {
        return keccak256(abi.encode(PAYMENT_DOMAIN, block.chainid, address(this), token, payee, resourceId, deadline));
    }

    function _requireController(uint256 rootKeyId) internal view {
        if (!DID.agentExists(rootKeyId)) revert UnknownAgent();
        address controller = KEY_REGISTRY.ownerOf(rootKeyId);
        if (controller == address(0) || msg.sender != controller) revert NotController();
    }

    /// @dev Best-effort forward of `amount` of `token` to the SINK via its flush(token,amount)
    ///      surface. Zero-then-set approve (USDT-style tokens revert on non-zero -> non-zero);
    ///      re-zero after so no lingering allowance survives a silent sink failure. Returns
    ///      whether the sink call reported success; callers decide how to account for a miss.
    function _tryFlushToSink(address token, uint256 amount) internal returns (bool ok) {
        IERC20(token).approve(SINK, 0);
        IERC20(token).approve(SINK, amount);
        (ok,) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", token, amount));
        IERC20(token).approve(SINK, 0);
    }
}
