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

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

/// @dev Matches Succinct's canonical SP1 verifier / SP1VerifierGateway ABI:
///      verifyProof RETURNS NOTHING and REVERTS on an invalid proof. (An
///      earlier version of this interface declared `returns (bool)`, which is
///      wrong for the real gateway — decoding a bool from the gateway's empty
///      returndata reverts even for VALID proofs, so submitProof could never
///      succeed on-chain. Fixed to void + try/catch below.)
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereZKScreen — Privacy-Pools-style ZK compliance anchor
 * @notice Anchors zero-knowledge proofs that a user passed an off-chain
 *         compliance screen WITHOUT revealing the source data. Pattern:
 *         user runs a screening program (sanctions check, residency
 *         attestation, accredited-investor proof, etc.) inside an SP1
 *         zkVM; the resulting proof is verified on-chain and the user's
 *         address is marked as "screened" against `programVKey`.
 *
 *         dApps then gate access by reading
 *         `isCleared(user, programVKey, minTimestamp)` — no PII ever
 *         touches the chain.
 *
 *         GOVERNANCE:
 *           - Foundation registers `programVKey` values (each represents a
 *             distinct compliance program: OFAC screen, accredited-investor
 *             check, EU MiCA-eligible jurisdiction proof, etc.).
 *           - Each program binds a Foundation-set `authorizedRoot` — see
 *             ROOT BINDING below.
 *           - Foundation can set a per-program `expirySeconds`, rotate the
 *             authorizedRoot (`setAuthorizedRoot`), and revoke an individual
 *             clearance (`revoke`).
 *
 *         ROOT BINDING (critical soundness property):
 *           The proof commits an `extraDataHash` interpreted as the allowlist
 *           Merkle root the screen was checked against. Because the PROVER
 *           supplies that root inside the zkVM, a naive design would let anyone
 *           prove membership in a root of THEIR OWN making (an "allowlist"
 *           containing only themselves) — a total soundness break for the
 *           compliance claim. This contract closes that: each program binds a
 *           Foundation-set `authorizedRoot`, and `submitProof` REQUIRES the
 *           proof's `extraDataHash == authorizedRoot`. The Foundation rotates
 *           the root via `setAuthorizedRoot` as the real allowlist changes and
 *           can void an individual clearance via `revoke`.
 *
 *         CONSUMER CONTRACT: dApps MUST call `isCleared` with a real
 *           `minTimestamp` (e.g. `block.timestamp - freshnessWindow`), never
 *           0 — otherwise they accept arbitrarily old attestations. Treat
 *           clearance as NECESSARY-not-sufficient and AND it with a live
 *           sanctions check for sanctions-sensitive flows.
 *
 *         IMMUTABILITY:
 *           - SP1_VERIFIER is fixed at deploy
 *           - programVKey + expirySeconds are immutable PER-PROGRAM after
 *             registration. authorizedRoot is rotatable by the owner.
 *
 *         PUBLIC-VALUES CONVENTION:
 *           bytes representing `abi.encode(uint256 chainId, address user, uint256 attestedAt, bytes32 extraDataHash)`.
 */
contract AereZKScreen is Ownable {

    address public immutable SP1_VERIFIER;

    struct Program {
        bool registered;
        uint32 expirySeconds;     // 0 = no expiry
        bytes32 programVKey;
        bytes32 authorizedRoot;   // the ONLY allowlist root proofs may attest to
        string description;
    }

    /// @notice programVKey → Program
    mapping(bytes32 => Program) public programs;

    /// @notice user → programVKey → last cleared timestamp
    mapping(address => mapping(bytes32 => uint256)) public clearedAt;

    /// @notice user → programVKey → revocation watermark; a clearance whose
    ///         attestation timestamp is <= this value is void.
    mapping(address => mapping(bytes32 => uint256)) public revokedAt;

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

    event ProgramRegistered(bytes32 indexed programVKey, uint32 expirySeconds, bytes32 authorizedRoot, string description);
    event AuthorizedRootUpdated(bytes32 indexed programVKey, bytes32 oldRoot, bytes32 newRoot);
    event Screened(address indexed user, bytes32 indexed programVKey, uint256 attestedAt, bytes32 extraDataHash);
    event Revoked(address indexed user, bytes32 indexed programVKey, uint256 at);

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

    error UnknownProgram();
    error InvalidProof();
    error WrongChain(uint256 inProof, uint256 onChain);
    error UserMismatch(address inProof, address msgSender);
    error AttestationInFuture();
    error AlreadyRegistered();
    error ZeroAddress();
    error ZeroRoot();
    error BadPublicValuesLength(uint256 got);
    error StaleProof(uint256 newAttestedAt, uint256 currentClearedAt);
    error UnauthorizedRoot(bytes32 inProof, bytes32 authorized);

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

    constructor(address sp1Verifier) {
        if (sp1Verifier == address(0)) revert ZeroAddress();
        SP1_VERIFIER = sp1Verifier;
    }

    /* ---------------------------- governance -------------------------------- */

    function registerProgram(
        bytes32 programVKey,
        uint32 expirySeconds,
        bytes32 authorizedRoot,
        string calldata description
    ) external onlyOwner {
        if (programs[programVKey].registered) revert AlreadyRegistered();
        if (authorizedRoot == bytes32(0)) revert ZeroRoot();
        programs[programVKey] = Program({
            registered: true,
            expirySeconds: expirySeconds,
            programVKey: programVKey,
            authorizedRoot: authorizedRoot,
            description: description
        });
        emit ProgramRegistered(programVKey, expirySeconds, authorizedRoot, description);
    }

    /// @notice Rotate the allowlist root a program accepts (e.g. when the
    ///         approved set changes). Does NOT retroactively void existing
    ///         clearances — use `revoke` for that.
    function setAuthorizedRoot(bytes32 programVKey, bytes32 newRoot) external onlyOwner {
        Program storage p = programs[programVKey];
        if (!p.registered) revert UnknownProgram();
        if (newRoot == bytes32(0)) revert ZeroRoot();
        bytes32 old = p.authorizedRoot;
        p.authorizedRoot = newRoot;
        emit AuthorizedRootUpdated(programVKey, old, newRoot);
    }

    /// @notice Void a user's clearance for a program (de-listed, compromised,
    ///         or newly sanctioned). `isCleared` returns false until the user
    ///         submits a fresh proof with `attestedAt` strictly after this call.
    function revoke(address user, bytes32 programVKey) external onlyOwner {
        revokedAt[user][programVKey] = block.timestamp;
        emit Revoked(user, programVKey, block.timestamp);
    }

    /* ------------------------------ proof submit ---------------------------- */

    /// @notice Submit a fresh SP1 proof attesting that `msg.sender` passed
    ///         the program identified by `programVKey`. publicValues MUST
    ///         decode to (chainId, user, attestedAt, extraDataHash) per spec,
    ///         and extraDataHash MUST equal the program's authorizedRoot.
    function submitProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external {
        Program memory p = programs[programVKey];
        if (!p.registered) revert UnknownProgram();
        // Exact length check prevents trailing-bytes decode spoof.
        // 4 × 32 = 128 bytes for (uint256, address, uint256, bytes32).
        if (publicValues.length != 128) revert BadPublicValuesLength(publicValues.length);

        // Real SP1 gateway reverts on an invalid proof and returns nothing on
        // success. Convert its revert into our typed InvalidProof error.
        try ISp1Verifier(SP1_VERIFIER).verifyProof(programVKey, publicValues, proof) {
            // proof verified — did not revert
        } catch {
            revert InvalidProof();
        }

        (uint256 chainId, address user, uint256 attestedAt, bytes32 extraDataHash) =
            abi.decode(publicValues, (uint256, address, uint256, bytes32));

        if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
        if (user != msg.sender) revert UserMismatch(user, msg.sender);
        // CRITICAL soundness gate: the proof must attest to the
        // Foundation-authorized allowlist root, not one the prover chose.
        // Without this, anyone can self-clear by proving membership in an
        // allowlist of their own making.
        if (extraDataHash != p.authorizedRoot) revert UnauthorizedRoot(extraDataHash, p.authorizedRoot);
        if (attestedAt > block.timestamp) revert AttestationInFuture();
        // Prevent stale-proof replay from DOWNGRADING a fresher clearance.
        uint256 currentTs = clearedAt[user][programVKey];
        if (attestedAt <= currentTs) revert StaleProof(attestedAt, currentTs);

        clearedAt[user][programVKey] = attestedAt;
        emit Screened(user, programVKey, attestedAt, extraDataHash);
    }

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

    /// @notice True iff `user` has a valid, non-revoked anchor for `programVKey`
    ///         whose attestation timestamp is at least `minTimestamp` AND (if
    ///         expirySeconds > 0) not older than expirySeconds.
    function isCleared(address user, bytes32 programVKey, uint256 minTimestamp) external view returns (bool) {
        Program memory p = programs[programVKey];
        if (!p.registered) return false;
        uint256 ts = clearedAt[user][programVKey];
        if (ts == 0) return false;
        if (ts < minTimestamp) return false;
        if (ts <= revokedAt[user][programVKey]) return false; // revoked
        if (p.expirySeconds > 0 && block.timestamp > ts + uint256(p.expirySeconds)) return false;
        return true;
    }

    function clearanceAge(address user, bytes32 programVKey) external view returns (uint256) {
        uint256 ts = clearedAt[user][programVKey];
        if (ts == 0) return type(uint256).max;
        return block.timestamp - ts;
    }
}
