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

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

interface IAereAgent {
    function debit(uint256 agentId, address token, address payee, uint256 amount) external;
    function signerOf(uint256 agentId) external view returns (address);
}

interface IAereSinkFacilitator {
    function flush(address token, uint256 amount) external;
}

/**
 * @title AERE402FacilitatorV2 — HTTP 402 agentic settlement (F13 fix)
 * @notice Bug-fix redeploy of AERE402Facilitator
 *         (0xbA6e6700D629a5E3C885778a42885a944CA84E56).
 *
 *         FINDING F13 (liveness): V1's replay key was `consumed[agentId][nonce]`
 *         — a SINGLE nonce namespace shared by every provider settling against
 *         an agent. But the documented convention is a "monotonic nonce per
 *         provider-agent pair", so two independent providers naturally pick
 *         overlapping nonce sequences (both start at 1, 2, ...). When provider A
 *         settles (agentId, nonce=1), provider B's fully valid, agent-signed
 *         (agentId, nonce=1) toward a DIFFERENT payee reverts
 *         NonceAlreadyConsumed. No theft — a legitimate settlement is wrongly
 *         blocked (denial of settlement).
 *
 *         FIX: the replay key is now `consumed[agentId][payee][nonce]`. Each
 *         (agent, payee) pair owns an independent nonce namespace, matching the
 *         per-provider convention. True replay — the SAME (agentId, payee,
 *         nonce) — still reverts NonceAlreadyConsumed, so replay protection is
 *         unchanged in strength; only the false collision across distinct
 *         payees is removed.
 *
 *         Everything else is byte-for-byte identical to V1: EIP-712 typed data,
 *         signer check via AereAgent.signerOf, msg.sender == payee guard,
 *         self-deal block, 25-bps fee to the sink, and the best-effort sink
 *         flush. The EIP-712 domain `name` is intentionally unchanged
 *         ("AERE402Facilitator") — the new deployment is domain-separated by
 *         its `verifyingContract` address, so existing signer SDKs work by
 *         swapping only the address.
 *
 *         CROSS-WIRING: AereAgent.debit is gated to the immutable
 *         AERE402_FACILITATOR, so this V2 facilitator is paired with an
 *         AereAgentV2 whose AERE402_FACILITATOR points here. See AereAgentV2.
 *
 *         IMMUTABILITY: AereAgent (registry) address, SINK, 25-bps fee, and
 *         DOMAIN_SEPARATOR all pinned in the constructor, exactly as V1.
 */
contract AERE402FacilitatorV2 is ReentrancyGuard {
    using ECDSA for bytes32;

    /* ------------------------------- immutable ------------------------------- */

    address public immutable AGENT_REGISTRY;
    address public immutable SINK;
    uint16 public constant PROTOCOL_FEE_BPS = 25; // 0.25%

    bytes32 public immutable DOMAIN_SEPARATOR;
    bytes32 public constant AUTH_TYPEHASH = keccak256(
        "PaymentAuth(uint256 agentId,address token,address payee,uint256 amount,bytes32 resourceId,uint256 nonce,uint256 deadline)"
    );

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

    /// @notice F13 FIX: replay key is per (agentId, payee, nonce) — each
    ///         (agent, payee) pair owns an independent nonce namespace.
    mapping(uint256 => mapping(address => mapping(uint256 => bool))) public consumed;

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

    event Settled(
        uint256 indexed agentId,
        address indexed payee,
        address indexed token,
        uint256 amount,
        uint256 protocolFee,
        bytes32 resourceId,
        uint256 nonce
    );

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

    error ZeroAddress();
    error InvalidSignature();
    error NonceAlreadyConsumed();
    error DeadlinePassed();
    error PayeeMismatch();
    error ZeroAmount();
    error TransferFailed();
    error SelfDealForbidden();   // payee == agent's signer key

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

    constructor(address agentRegistry, address sink) {
        if (agentRegistry == address(0) || sink == address(0)) revert ZeroAddress();
        AGENT_REGISTRY = agentRegistry;
        SINK = sink;
        DOMAIN_SEPARATOR = keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("AERE402Facilitator"),
            keccak256("1"),
            block.chainid,
            address(this)
        ));
    }

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

    /// @notice Provider settles a payment auth from an agent.
    /// @dev    msg.sender MUST equal the `payee` in the auth; this prevents a
    ///         third party from replaying someone else's auth toward an
    ///         attacker-controlled payee even if the agent's signature leaks.
    function settle(
        uint256 agentId,
        address token,
        address payee,
        uint256 amount,
        bytes32 resourceId,
        uint256 nonce,
        uint256 deadline,
        bytes calldata signature
    ) external nonReentrant {
        if (block.timestamp > deadline) revert DeadlinePassed();
        if (msg.sender != payee) revert PayeeMismatch();
        if (amount == 0) revert ZeroAmount();
        // F13 FIX: check the per-(agentId, payee, nonce) namespace.
        if (consumed[agentId][payee][nonce]) revert NonceAlreadyConsumed();

        bytes32 structHash = keccak256(abi.encode(
            AUTH_TYPEHASH,
            agentId,
            token,
            payee,
            amount,
            resourceId,
            nonce,
            deadline
        ));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        address signer = digest.recover(signature);
        address expected = IAereAgent(AGENT_REGISTRY).signerOf(agentId);
        if (signer == address(0) || signer != expected) revert InvalidSignature();

        // Block self-deal: a compromised signer key cannot drain the agent
        // balance by paying itself. The signer MUST sign a payment to a
        // different address — typically an actual service provider.
        if (payee == expected) revert SelfDealForbidden();

        // F13 FIX: consume the per-(agentId, payee, nonce) slot.
        consumed[agentId][payee][nonce] = true;

        // Pull the full amount from the agent via the registry. The registry
        // sends the AERE to address(this) (the facilitator); we then split
        // into payee + sink.
        IAereAgent(AGENT_REGISTRY).debit(agentId, token, address(this), amount);

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

        if (toPayee > 0) {
            bool ok = IERC20(token).transfer(payee, toPayee);
            if (!ok) revert TransferFailed();
        }
        if (fee > 0) {
            // Forward to sink for flywheel; if the sink doesn't accept, the fee
            // stays in the facilitator as residual (sweepable via flushResidual).
            // Zero-then-set approve (USDT-style tokens revert on non-zero->non-
            // zero); re-zero after the call so no lingering allowance survives a
            // silent sink failure.
            IERC20(token).approve(SINK, 0);
            IERC20(token).approve(SINK, fee);
            (bool sinkOk, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", token, fee));
            sinkOk; // intentional: don't gate user flow on sink success
            IERC20(token).approve(SINK, 0);
        }

        emit Settled(agentId, payee, token, amount, fee, resourceId, nonce);
    }

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

    /// @notice Anyone can re-attempt the sink flush for a residual balance.
    function flushResidual(address token, uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        IERC20(token).approve(SINK, 0);
        IERC20(token).approve(SINK, amount);
        (bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", token, amount));
        if (!ok) {
            IERC20(token).approve(SINK, 0);
            revert TransferFailed();
        }
        IERC20(token).approve(SINK, 0);
    }
}
