// 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 AERE402Facilitator — HTTP 402 agentic settlement
 * @notice Service providers (web APIs, MCP servers, agent-to-agent endpoints)
 *         exchange HTTP 402 (Payment Required) for an EIP-712 signed
 *         payment authorisation from an AereAgent. The provider then
 *         submits the authorisation to this contract; the facilitator
 *         debits the agent's prepaid balance (via the AereAgent registry),
 *         credits the provider, and routes a 25-bps protocol fee to
 *         AereSink (the flywheel).
 *
 *         CORE FLOW per call:
 *           1. Provider responds to a request with HTTP 402 +
 *              X-AERE402-Quote: { facilitator, token, amount, resourceId,
 *              providerAddress, nonce, deadline }.
 *           2. Agent signs the authorisation (EIP-712 typed data) using
 *              its registered signer key.
 *           3. Agent retries the request with X-AERE402-Sig: <hex>.
 *           4. Provider POSTs the (auth + signature) to this contract via
 *              `settle(...)`. The facilitator:
 *                a. Validates signature against AereAgent.signerOf(agentId).
 *                b. Checks nonce hasn't been spent + deadline.
 *                c. Splits amount: (amount - fee) → provider, fee → sink.
 *                d. Calls AereAgent.debit(agentId, token, address(this), amount).
 *                e. Forwards the splits in one tx.
 *           5. Provider serves the response now that settlement is final.
 *
 *         IMMUTABILITY:
 *           - AereAgent address fixed in constructor.
 *           - SINK address fixed.
 *           - 25-bps fee constant.
 *           - DOMAIN_SEPARATOR pinned.
 *
 *         REPLAY-PROTECTION:
 *           - Per-(agentId, nonce) bitmap of consumed nonces.
 *           - Provider chooses nonces; convention: monotonic per provider-
 *             agent pair.
 */
contract AERE402Facilitator 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 agentId → nonce → consumed
    mapping(uint256 => 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();
        if (consumed[agentId][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();

        consumed[agentId][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).
            // ROUND-3 FIX: zero-then-set approve (USDT-style tokens revert
            // on non-zero→non-zero); also 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);
    }
}
