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

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

/**
 * @title AereHybridAuthorizer, a crypto-agility CONSUMER over AereCryptoRegistry (chain 2800)
 *
 * @notice AereCryptoRegistry (0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5) turns AERE's five
 *         live native crypto precompiles into a governed table
 *
 *              algorithmId -> { verifier, wire-format, status, gas, successor }
 *
 *         A registry has value only when something CONSUMES it. This contract is that
 *         consumer. Instead of pinning a scheme, an account/app authorizes against
 *         "whatever scheme is currently ACTIVE": every check first calls
 *         {AereCryptoRegistry.resolveActive} to follow the successor chain to the live
 *         ACTIVE row, then verifies through the registry's routed verifier.
 *
 *         The payoff is a ZERO-REDEPLOY migration. When the Foundation (owner of the
 *         registry, later the Timelock) retires a scheme with the standard swap
 *
 *              1. addAlgorithm(newScheme)        // register the successor, ACTIVE
 *              2. setSuccessor(oldId, newId)     // point old -> new
 *              3. setStatus(oldId, DEPRECATED|REVOKED)
 *
 *         every consumer that routes through this authorizer starts accepting the NEW
 *         scheme and rejecting the OLD one on the very next block, with no code change and
 *         no redeploy on this contract or on any dApp that calls it.
 *
 * @dev    HONEST SCOPE. Application-layer authorization only. It changes NOTHING about AERE
 *         consensus: blocks are still produced and signed by Besu QBFT validators with
 *         classical ECDSA / secp256k1. All post-quantum verification is delegated to the
 *         SAME live native precompiles the registry routes to (Falcon-512/1024, ML-DSA-44,
 *         SLH-DSA-128s). This is NOT post-quantum consensus. No funds are ever held: there
 *         is no payable function, no selfdestruct, and no custody of any kind. The
 *         per-account "spend"/preference here is a routing choice, not money.
 *
 * @dev    POLICY. This authorizer only ever accepts an ACTIVE scheme. {resolveActive}
 *         returns the first ACTIVE row on the successor chain, so:
 *           - A REVOKED id is NEVER used: it is skipped in favour of its ACTIVE successor,
 *             or, if it has none, resolveActive reverts and the fail-closed check returns
 *             false. A signature made under a revoked scheme's key can never authorize.
 *           - A DEPRECATED id with an ACTIVE successor transparently routes to the
 *             successor (a legacy signature under the deprecated key stops authorizing).
 *           - A DEPRECATED-or-REVOKED id with NO ACTIVE successor authorizes nothing.
 *         This is stricter than {AereCryptoRegistry.verify} (which still verifies a
 *         DEPRECATED row for legacy support) precisely because this surface is about
 *         "the currently recommended scheme", not legacy verification.
 *
 * @dev    FAIL-CLOSED VIEWS. {checkAuthorized} / {checkAuthorizedFor} never revert: any
 *         registry revert (unknown id, dead-end successor chain, cycle) is caught and
 *         reported as `false`. The state-changing {authorize} lets a genuine registry
 *         revert surface (so a caller learns WHY), and reverts {AuthorizationFailed} when
 *         the routed verifier simply returns false.
 */
interface IAereCryptoRegistry {
    /// @notice Follow the successor chain to the first ACTIVE id. Reverts on unknown id,
    ///         a dead-end with no ACTIVE row, or a cycle.
    function resolveActive(uint256 algorithmId) external view returns (uint256);

    /// @notice Fail-closed routed signature verification (returns false, never reverts,
    ///         for unknown/hash-only/UNKNOWN/REVOKED/malformed inputs).
    function verify(uint256 algorithmId, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
        external
        view
        returns (bool);

    /// @notice True iff `id` is a signature scheme safe to adopt for NEW work (ACTIVE).
    function isUsable(uint256 id) external view returns (bool);
}

contract AereHybridAuthorizer is Ownable {
    // ==========================================================================
    //                              Immutable wiring
    // ==========================================================================

    /// @notice The governed crypto-agility registry this authorizer consumes.
    IAereCryptoRegistry public immutable registry;

    // ==========================================================================
    //                                  Storage
    // ==========================================================================

    /**
     * @notice The app-wide DEFAULT algorithm id used for any account that has not chosen a
     *         personal preference. Owner-governed. Stored as a stable reference id: every
     *         check re-resolves it through {resolveActive}, so it auto-follows a registry
     *         swap even before the owner calls {migrate}. {migrate} simply crystallizes it
     *         to the resolved successor.
     */
    uint256 public defaultAlgorithmId;

    /// @dev Per-account preference; 0 means "use the app default" (the SAFE DEFAULT).
    mapping(address => uint256) private _preferred;

    // Authorization ledger (a state-changing proof surface; holds no value).
    /// @notice Number of successful {authorize} calls recorded.
    uint256 public authorizationCount;
    /// @notice keccak256(pubKey || messageHash) => was this (key, message) ever authorized.
    mapping(bytes32 => bool) private _authorizedProof;
    /// @notice The ACTIVE algorithm id used by the most recent successful authorization.
    uint256 public lastResolvedId;
    /// @notice The message hash of the most recent successful authorization.
    bytes32 public lastMessageHash;

    // ==========================================================================
    //                                  Events
    // ==========================================================================

    event DefaultAlgorithmSet(uint256 indexed id);
    event DefaultMigrated(uint256 indexed oldId, uint256 indexed newId);
    event PreferredAlgorithmSet(address indexed account, uint256 indexed id);
    event Authorized(
        address indexed caller, uint256 indexed requestedId, uint256 indexed resolvedId, bytes32 messageHash
    );

    // ==========================================================================
    //                                  Errors
    // ==========================================================================

    error ZeroRegistry();
    error UnusableAlgorithm(uint256 id); // chosen/default id does not resolve to a usable ACTIVE signature scheme
    error NotCurrentDefault(uint256 got, uint256 current); // migrate(oldId) with oldId != defaultAlgorithmId
    error AuthorizationFailed(uint256 requestedId, uint256 resolvedId);

    // ==========================================================================
    //                                Constructor
    // ==========================================================================

    /**
     * @param registry_           the AereCryptoRegistry to route through (non-zero).
     * @param defaultAlgorithmId_ the initial app default. It MUST resolve, via the
     *                            registry, to a usable ACTIVE signature scheme right now,
     *                            so the contract can never be born pointing at an unusable
     *                            or hash-only default (fail-fast "safe default").
     */
    constructor(address registry_, uint256 defaultAlgorithmId_) {
        if (registry_ == address(0)) revert ZeroRegistry();
        registry = IAereCryptoRegistry(registry_);
        // Fail-fast: the initial default must be a usable ACTIVE signature scheme.
        if (!_resolvesToUsable(defaultAlgorithmId_)) revert UnusableAlgorithm(defaultAlgorithmId_);
        defaultAlgorithmId = defaultAlgorithmId_;
        emit DefaultAlgorithmSet(defaultAlgorithmId_);
    }

    // ==========================================================================
    //                          Owner: default governance
    // ==========================================================================

    /**
     * @notice Set the app-wide default algorithm id. Owner only. The id must resolve to a
     *         usable ACTIVE signature scheme (guards against pointing the default at a
     *         revoked, dead-ended, or hash-only scheme).
     */
    function setDefaultAlgorithm(uint256 id) external onlyOwner {
        if (!_resolvesToUsable(id)) revert UnusableAlgorithm(id);
        defaultAlgorithmId = id;
        emit DefaultAlgorithmSet(id);
    }

    /**
     * @notice Owner helper: crystallize the app default onto the ACTIVE successor of
     *         `oldId` after a registry swap. `oldId` must equal the current
     *         {defaultAlgorithmId} (a guard so a stale call cannot silently retarget it).
     *         Because {authorize}/{checkAuthorized} already re-resolve on every call, this
     *         is an OPTIMIZATION/CLARITY step (shorter successor walk, fresh views), not a
     *         correctness requirement: consumers are already migrated the moment the
     *         registry swap lands. Reverts (from {resolveActive}) if the chain dead-ends or
     *         cycles, so the default can never be moved onto a non-ACTIVE row.
     * @return newId the ACTIVE id the default now points at (== oldId if already ACTIVE).
     */
    function migrate(uint256 oldId) external onlyOwner returns (uint256 newId) {
        if (defaultAlgorithmId != oldId) revert NotCurrentDefault(oldId, defaultAlgorithmId);
        newId = registry.resolveActive(oldId); // reverts on unknown/dead-end/cycle
        if (newId != oldId) {
            defaultAlgorithmId = newId;
            emit DefaultMigrated(oldId, newId);
        }
    }

    // ==========================================================================
    //                        Account: preference (self-service)
    // ==========================================================================

    /**
     * @notice Choose a personal preferred algorithm id for msg.sender. Pass 0 to clear the
     *         preference and fall back to the app default. A non-zero id must resolve to a
     *         usable ACTIVE signature scheme at set time.
     * @dev    The stored value is a stable reference: like the default, it is re-resolved
     *         through {resolveActive} on every check, so a later registry swap migrates a
     *         preferring account automatically too.
     */
    function setPreferredAlgorithm(uint256 id) external {
        if (id != 0 && !_resolvesToUsable(id)) revert UnusableAlgorithm(id);
        _preferred[msg.sender] = id;
        emit PreferredAlgorithmSet(msg.sender, id);
    }

    /// @notice The raw stored preference for `account` (0 == none / use default).
    function preferredAlgorithmOf(address account) external view returns (uint256) {
        return _preferred[account];
    }

    /**
     * @notice The effective (pre-resolution) algorithm id for `account`: its preference if
     *         set, else the app default. This is the STABLE reference id, before the
     *         successor chain is walked.
     */
    function algorithmFor(address account) public view returns (uint256) {
        uint256 p = _preferred[account];
        return p == 0 ? defaultAlgorithmId : p;
    }

    /**
     * @notice The ACTIVE algorithm id `account` currently authorizes under, after resolving
     *         the successor chain. Reverts (from {resolveActive}) if the chain dead-ends or
     *         cycles with no ACTIVE row.
     */
    function resolvedAlgorithmFor(address account) external view returns (uint256) {
        return registry.resolveActive(algorithmFor(account));
    }

    // ==========================================================================
    //                          Authorization (fail-closed views)
    // ==========================================================================

    /**
     * @notice Fail-closed check: is `signature` a valid signature over `messageHash` by
     *         `pubKey` under the currently-ACTIVE scheme reachable from `algorithmId`?
     *         Resolves the successor chain, then verifies through the registry's routed
     *         verifier. Returns FALSE (never reverts) for an unknown id, a dead-end or
     *         cyclic successor chain, a malformed input, or a failed verification.
     */
    function checkAuthorized(
        uint256 algorithmId,
        bytes calldata pubKey,
        bytes32 messageHash,
        bytes calldata signature
    ) public view returns (bool) {
        uint256 active;
        try registry.resolveActive(algorithmId) returns (uint256 a) {
            active = a;
        } catch {
            return false; // unknown id, dead-end, or cycle -> not authorized
        }
        try registry.verify(active, pubKey, messageHash, signature) returns (bool ok) {
            return ok;
        } catch {
            return false;
        }
    }

    /**
     * @notice Fail-closed check under `account`'s effective algorithm (its preference, else
     *         the app default). Same semantics as {checkAuthorized}.
     */
    function checkAuthorizedFor(
        address account,
        bytes calldata pubKey,
        bytes32 messageHash,
        bytes calldata signature
    ) external view returns (bool) {
        return checkAuthorized(algorithmFor(account), pubKey, messageHash, signature);
    }

    /// @notice Whether (pubKey, messageHash) was ever recorded by a successful {authorize}.
    function isProofAuthorized(bytes calldata pubKey, bytes32 messageHash) external view returns (bool) {
        return _authorizedProof[keccak256(abi.encodePacked(pubKey, messageHash))];
    }

    // ==========================================================================
    //                     Authorization (state-changing, recorded)
    // ==========================================================================

    /**
     * @notice State-changing authorization under the ACTIVE scheme reachable from
     *         `algorithmId`. Reverts if the successor chain has no ACTIVE row (surfacing
     *         the registry's reason) or {AuthorizationFailed} if the routed verifier
     *         returns false. On success it records the (pubKey, messageHash) proof, bumps
     *         {authorizationCount}, and emits {Authorized}.
     * @return resolvedId the ACTIVE id the signature was verified under.
     */
    function authorize(
        uint256 algorithmId,
        bytes calldata pubKey,
        bytes32 messageHash,
        bytes calldata signature
    ) external returns (uint256 resolvedId) {
        return _authorize(algorithmId, pubKey, messageHash, signature);
    }

    /**
     * @notice State-changing authorization under msg.sender's effective algorithm (its
     *         preference, else the app default). See {authorize}.
     */
    function authorizeWithPreferred(bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
        external
        returns (uint256 resolvedId)
    {
        return _authorize(algorithmFor(msg.sender), pubKey, messageHash, signature);
    }

    function _authorize(
        uint256 algorithmId,
        bytes calldata pubKey,
        bytes32 messageHash,
        bytes calldata signature
    ) internal returns (uint256 resolvedId) {
        resolvedId = registry.resolveActive(algorithmId); // reverts on unknown/dead-end/cycle
        bool ok = registry.verify(resolvedId, pubKey, messageHash, signature);
        if (!ok) revert AuthorizationFailed(algorithmId, resolvedId);

        _authorizedProof[keccak256(abi.encodePacked(pubKey, messageHash))] = true;
        authorizationCount += 1;
        lastResolvedId = resolvedId;
        lastMessageHash = messageHash;
        emit Authorized(msg.sender, algorithmId, resolvedId, messageHash);
    }

    // ==========================================================================
    //                                  Internal
    // ==========================================================================

    /// @dev True iff `id` resolves (via the successor chain) to a usable ACTIVE signature
    ///      scheme. Fail-closed: a revert in resolveActive (unknown/dead-end/cycle) is
    ///      treated as "not usable".
    function _resolvesToUsable(uint256 id) internal view returns (bool) {
        try registry.resolveActive(id) returns (uint256 active) {
            return registry.isUsable(active);
        } catch {
            return false;
        }
    }
}
