// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.23;

import {AerePQCThreshold} from "./AerePQCThreshold.sol";

/// @title  AereThresholdAccount
/// @notice A non-custodial ERC-4337 (v0.7) smart account whose owner is a t-of-n POST-QUANTUM
///         committee: a user operation is authorized iff at least `threshold` DISTINCT committee
///         members each supply a valid NIST PQC signature (Falcon / ML-DSA / SLH-DSA) over the
///         account's domain-separated challenge, every leg verified in full, on-chain, by AERE's
///         LIVE precompiles (0x0AE1..0x0AE4). No single party's key can move funds; there is no
///         admin, owner override, or recovery backdoor. Built for non-custodial custody (e.g. a
///         crypto-card neobank) where quantum-durable, distributed key control is required.
///
/// @dev    Two independent, DOMAIN-SEPARATED authorization paths, each with its own replay
///         barrier (so a signature made for one path can never be replayed on the other):
///           1. ERC-4337 `validateUserOp` — challenge binds `userOpHash` (whose EntryPoint nonce
///              is the replay barrier). It reads only this account's own storage and staticcalls the
///              precompiles, so it satisfies the bundler's validation storage rules. It does not
///              mutate persistent account storage during validation, but per ERC-4337 it MAY emit a
///              LOG and, when the EntryPoint requests it, transfer the missing prefund to the
///              EntryPoint; it is therefore NOT declared `view`.
///           2. Direct self-relay `executeThreshold` — no bundler required; challenge binds a
///              per-account `execNonce` + the exact call, which increments on success.
///         The committee (scheme, threshold, member public keys) is stored in THIS account's own
///         storage, set once by the factory, immutable thereafter. Application/account layer only;
///         AERE consensus remains classical ECDSA QBFT.
contract AereThresholdAccount {
    using AerePQCThreshold for uint8;

    // ─── config (set once by the factory) ─────────────────────────────────────
    address public immutable factory;
    address public entryPoint;
    uint8 public scheme;      // 1=Falcon-512, 2=Falcon-1024, 3=ML-DSA-44, 4=SLH-DSA-128s
    uint8 public threshold;   // t
    uint8 public size;        // n
    bytes32 public membersHash; // keccak256(abi.encode(pubKeys)) — pins the exact key set
    bytes[] internal _pubKeys;

    uint64 public execNonce;  // replay barrier for the direct self-relay path

    /// @notice ERC-4337 v0.7 PackedUserOperation shape (matches AereEntryPointV2).
    struct PackedUserOperation {
        address sender;
        uint256 nonce;
        bytes initCode;
        bytes callData;
        bytes32 accountGasLimits;
        uint256 preVerificationGas;
        bytes32 gasFees;
        bytes paymasterAndData;
        bytes signature;
    }

    /// @notice One authorizing leg: a committee member index + that member's PQC signature over
    ///         the account challenge, in the scheme's on-chain envelope.
    struct Leg {
        uint8 memberIndex;
        bytes signature;
    }

    /// @notice ERC-4337 signature-validation-failed sentinel (validationData != 0).
    uint256 internal constant SIG_VALIDATION_FAILED = 1;

    // Distinct domain separators — a signature for one path never verifies on the other.
    bytes32 public constant USEROP_DOMAIN = keccak256("AereThresholdAccount.v1.userop");
    bytes32 public constant EXEC_DOMAIN = keccak256("AereThresholdAccount.v1.exec");

    event Initialized(address indexed entryPoint, uint8 scheme, uint8 threshold, uint8 size, bytes32 membersHash);
    event Executed(uint64 indexed execNonce, address indexed target, uint256 value, uint8 signers);
    event UserOpAuthorized(bytes32 indexed userOpHash, uint8 signers);

    error Unauthorized();
    error AlreadyInitialized();
    error BadParams();
    error InvalidPubKey(uint8 memberIndex);
    error DuplicatePubKey(uint8 memberIndex);
    error BelowThreshold(uint256 got, uint256 need);
    error CallFailed(bytes returnData);

    constructor() {
        factory = msg.sender;
    }

    /// @notice One-shot initializer, callable only by the deploying factory.
    /// @param _entryPoint the ERC-4337 EntryPoint the account trusts for the UserOp path.
    /// @param _scheme     the committee's shared NIST PQC scheme (1..4).
    /// @param _threshold  t: minimum distinct valid signers to authorize (1 <= t <= n).
    /// @param pubKeys     the n member public keys (each validated for length + header).
    function initialize(address _entryPoint, uint8 _scheme, uint8 _threshold, bytes[] calldata pubKeys) external {
        if (msg.sender != factory) revert Unauthorized();
        if (entryPoint != address(0) || size != 0) revert AlreadyInitialized();
        uint256 n = pubKeys.length;
        if (_entryPoint == address(0) || n < 1 || n > 255 || _threshold < 1 || _threshold > n) revert BadParams();

        // Enforce DISTINCT committee keys. Distinctness is counted by member INDEX at authorization
        // time (the `seen` bitmask in _countMem), so if the SAME public key occupied two indices, a
        // single keyholder could fill multiple "distinct member" slots and reach `threshold` alone -
        // silently collapsing the t-of-n non-custodial guarantee (the whole point of this account).
        // Reject any duplicate here, at the one place the committee is ever set. Keys are hashed once
        // (n keccaks) then compared pairwise on the 32-byte hashes; n <= 255, a one-time init cost.
        bytes32[] memory keyHashes = new bytes32[](n);
        for (uint256 i = 0; i < n; i++) {
            if (!AerePQCThreshold.validPubKey(_scheme, pubKeys[i])) revert InvalidPubKey(uint8(i));
            bytes32 kh = keccak256(pubKeys[i]);
            for (uint256 j = 0; j < i; j++) {
                if (keyHashes[j] == kh) revert DuplicatePubKey(uint8(i));
            }
            keyHashes[i] = kh;
            _pubKeys.push(pubKeys[i]);
        }
        entryPoint = _entryPoint;
        scheme = _scheme;
        threshold = _threshold;
        size = uint8(n);
        membersHash = keccak256(abi.encode(pubKeys));
        emit Initialized(_entryPoint, _scheme, _threshold, uint8(n), membersHash);
    }

    // ─── ERC-4337 path (bundler) ──────────────────────────────────────────────

    /// @notice Called by the EntryPoint to validate a user operation. Returns 0 iff >= threshold
    ///         DISTINCT committee members validly PQC-signed this account's userOp challenge;
    ///         SIG_VALIDATION_FAILED otherwise. Never reverts on a bad OR non-decodable signature
    ///         blob (both return SIG_VALIDATION_FAILED). Does not mutate persistent account storage
    ///         during validation; per ERC-4337 it may emit a LOG and may transfer the requested
    ///         prefund to the EntryPoint (so it is not `view`).
    function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
        external
        returns (uint256 validationData)
    {
        if (msg.sender != entryPoint) revert Unauthorized();

        bytes32 challenge = userOpChallenge(userOpHash);
        (bool ok, uint256 signers) = _countThreshold(challenge, userOp.signature);
        if (!ok) return SIG_VALIDATION_FAILED;

        emit UserOpAuthorized(userOpHash, uint8(signers));
        if (missingAccountFunds > 0) {
            (bool sent, ) = payable(entryPoint).call{value: missingAccountFunds}("");
            (sent); // EntryPoint reverts if underfunded
        }
        return 0;
    }

    /// @notice Dispatch a call after the EntryPoint has validated the UserOp.
    function executeFromEntryPoint(address target, uint256 value, bytes calldata data) external {
        if (msg.sender != entryPoint) revert Unauthorized();
        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) revert CallFailed(ret);
    }

    // ─── Direct self-relay path (no bundler) ──────────────────────────────────

    /// @notice Authorize and execute a single call directly, without a bundler: `legs` must carry
    ///         >= threshold distinct valid PQC signatures over the exec challenge binding the
    ///         current `execNonce` and the exact call. Reverts below threshold. `execNonce`
    ///         increments on success so the same legs can never be replayed.
    function executeThreshold(address target, uint256 value, bytes calldata data, Leg[] calldata legs)
        external
        returns (uint64 usedNonce)
    {
        usedNonce = execNonce;
        bytes32 challenge = execChallenge(usedNonce, target, value, data);
        (bool ok, uint256 signers) = _countThresholdLegs(challenge, legs);
        if (!ok) revert BelowThreshold(signers, threshold);

        execNonce = usedNonce + 1; // effects before interaction
        emit Executed(usedNonce, target, value, uint8(signers));
        (bool s, bytes memory ret) = target.call{value: value}(data);
        if (!s) revert CallFailed(ret);
    }

    // ─── threshold counting (shared, VIEW) ────────────────────────────────────

    /// @dev Count DISTINCT valid signers over `challenge` from an abi-encoded Leg[] blob. Only a
    ///      leg that is in-range, not-yet-seen, and verifies is counted; malformed/duplicate/
    ///      invalid legs are ignored (an attacker can never inflate the distinct-valid count).
    function _countThreshold(bytes32 challenge, bytes calldata sigBlob) internal view returns (bool, uint256) {
        // AUD-CONTRACT-1: `abi.decode` REVERTS on a non-decodable blob (empty, truncated, bad
        // offsets). ERC-4337 v0.7 requires signature errors to be signalled by return value, not by
        // a revert escaping validateUserOp. Decode via an external self-call wrapped in try/catch so
        // a malformed blob yields (false, 0) => SIG_VALIDATION_FAILED, mirroring the fail-soft
        // `AerePQCTxAccount._buildInput`. `decodeLegs` is `pure`, so this compiles to a STATICCALL to
        // self and stays view-safe for bundler simulation (the decode touches no storage).
        try this.decodeLegs(sigBlob) returns (Leg[] memory legs) {
            return _countMem(challenge, legs);
        } catch {
            return (false, 0);
        }
    }

    /// @notice External ABI-decode helper for `_countThreshold`'s try/catch. Pure (reads only its
    ///         calldata argument); reverts iff `sigBlob` is not a well-formed `Leg[]` encoding.
    /// @dev    Exposed only so the internal counter can CATCH a non-decodable signature blob and
    ///         return SIG_VALIDATION_FAILED instead of reverting; it carries no authority and mutates
    ///         nothing.
    function decodeLegs(bytes calldata sigBlob) external pure returns (Leg[] memory) {
        return abi.decode(sigBlob, (Leg[]));
    }

    function _countThresholdLegs(bytes32 challenge, Leg[] calldata legs) internal view returns (bool, uint256) {
        // copy calldata legs to memory once for the shared counter
        Leg[] memory m = new Leg[](legs.length);
        for (uint256 i = 0; i < legs.length; i++) {
            m[i] = Leg({memberIndex: legs[i].memberIndex, signature: legs[i].signature});
        }
        return _countMem(challenge, m);
    }

    function _countMem(bytes32 challenge, Leg[] memory legs) internal view returns (bool, uint256) {
        uint256 seen = 0;
        uint256 valid = 0;
        uint8 n = size;
        uint8 s = scheme;
        for (uint256 i = 0; i < legs.length; i++) {
            uint8 idx = legs[i].memberIndex;
            if (idx >= n) continue;
            uint256 bit = uint256(1) << idx;
            if (seen & bit != 0) continue; // never double-count a member
            if (AerePQCThreshold.verify(s, _pubKeys[idx], challenge, legs[i].signature)) {
                seen |= bit;
                valid += 1;
            }
        }
        return (valid >= threshold, valid);
    }

    // ─── challenge helpers (off-chain signers query these) ────────────────────

    /// @notice The exact 32-byte challenge each member must PQC-sign to authorize a UserOp.
    function userOpChallenge(bytes32 userOpHash) public view returns (bytes32) {
        return keccak256(abi.encode(USEROP_DOMAIN, block.chainid, address(this), userOpHash));
    }

    /// @notice The exact 32-byte challenge each member must PQC-sign to authorize a direct call
    ///         at `nonce` (bind the current `execNonce`).
    function execChallenge(uint64 nonce, address target, uint256 value, bytes calldata data)
        public
        view
        returns (bytes32)
    {
        return keccak256(abi.encode(EXEC_DOMAIN, block.chainid, address(this), nonce, target, value, keccak256(data)));
    }

    // ─── views ────────────────────────────────────────────────────────────────

    function pubKeyAt(uint8 memberIndex) external view returns (bytes memory) {
        return _pubKeys[memberIndex];
    }

    function committee() external view returns (uint8 _scheme, uint8 _threshold, uint8 _size, bytes32 _membersHash) {
        return (scheme, threshold, size, membersHash);
    }

    receive() external payable {}
}
