// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IOMNLNotaryRegistry} from "./interfaces/IOMNLNotaryRegistry.sol";

/**
 * @title ReserveCommitmentStore
 * @notice Stores attested reserve R per line with TTL, merkle evidence root, versioning, primary + mirror,
 *         and optional threshold ECDSA attestation for commits.
 */
contract ReserveCommitmentStore is AccessControl {
    bytes32 public constant RESERVE_COMMITTER_ROLE = keccak256("RESERVE_COMMITTER_ROLE");
    bytes32 public constant ATTESTATION_ADMIN_ROLE = keccak256("ATTESTATION_ADMIN_ROLE");

    /// @notice EIP-191 structured preimage prefix for attested commits (see commitReserveAttested).
    bytes32 public constant ATTESTATION_TYPEHASH =
        keccak256("OMNLReserveCommit(uint256 chainId,address store,bytes32 lineId,uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 nonce)");

    struct Commitment {
        uint256 R;
        uint256 validUntil;
        bytes32 evidenceHash;
        bytes32 merkleRoot;
        uint256 version;
    }

    address public mirrorReceiver;
    IOMNLNotaryRegistry public notaryRegistry;
    bool public requireNotarizedEvidence;
    bytes32 public defaultJurisdictionId;
    bytes32 public defaultMatrixControlId;

    mapping(bytes32 lineId => Commitment) private _commitments;
    mapping(bytes32 lineId => uint256) public lineAttestationNonce;

    mapping(address => bool) public isAttestationSigner;
    uint256 public attestationThreshold;

    event ReserveCommitted(
        bytes32 indexed lineId,
        uint256 R,
        uint256 validUntil,
        bytes32 evidenceHash,
        bytes32 merkleRoot,
        uint256 version,
        address indexed by
    );
    event MirrorReceiverUpdated(address indexed oldReceiver, address indexed newReceiver);
    event AttestationSignersUpdated(uint256 threshold);
    event AttestationSignerSet(address indexed signer, bool active);
    event NotaryGateConfigured(
        address indexed notaryRegistry,
        bool requireNotarizedEvidence,
        bytes32 defaultJurisdictionId,
        bytes32 defaultMatrixControlId
    );

    constructor(address admin) {
        require(admin != address(0), "ReserveCommitmentStore: zero admin");
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(RESERVE_COMMITTER_ROLE, admin);
        _grantRole(ATTESTATION_ADMIN_ROLE, admin);
    }

    function setMirrorReceiver(address receiver) external onlyRole(DEFAULT_ADMIN_ROLE) {
        address old = mirrorReceiver;
        mirrorReceiver = receiver;
        emit MirrorReceiverUpdated(old, receiver);
    }

    function setAttestationSigner(address signer, bool active) external onlyRole(ATTESTATION_ADMIN_ROLE) {
        isAttestationSigner[signer] = active;
        emit AttestationSignerSet(signer, active);
    }

    function setAttestationThreshold(uint256 threshold) external onlyRole(ATTESTATION_ADMIN_ROLE) {
        attestationThreshold = threshold;
        emit AttestationSignersUpdated(threshold);
    }

    function configureNotaryGate(
        address registry,
        bool required,
        bytes32 jurisdictionId,
        bytes32 matrixControlId
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        notaryRegistry = IOMNLNotaryRegistry(registry);
        requireNotarizedEvidence = required;
        defaultJurisdictionId = jurisdictionId;
        defaultMatrixControlId = matrixControlId;
        emit NotaryGateConfigured(registry, required, jurisdictionId, matrixControlId);
    }

    /// @notice Primary chain (or designated committer) updates reserves.
    function commitReserve(
        bytes32 lineId,
        uint256 R,
        uint256 validUntil,
        bytes32 evidenceHash,
        bytes32 merkleRoot
    ) external onlyRole(RESERVE_COMMITTER_ROLE) {
        _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender);
    }

    /// @notice Threshold ECDSA attestation (any EOA may submit once enough distinct signers included).
    function commitReserveAttested(
        bytes32 lineId,
        uint256 R,
        uint256 validUntil,
        bytes32 evidenceHash,
        bytes32 merkleRoot,
        uint256 nonce,
        bytes[] calldata signatures
    ) external {
        require(attestationThreshold > 0, "ReserveCommitmentStore: attestation off");
        require(nonce == lineAttestationNonce[lineId], "ReserveCommitmentStore: nonce");

        bytes32 digest = keccak256(
            abi.encode(
                ATTESTATION_TYPEHASH,
                block.chainid,
                address(this),
                lineId,
                R,
                validUntil,
                evidenceHash,
                merkleRoot,
                nonce
            )
        );
        bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest));

        uint256 n = signatures.length;
        require(n >= attestationThreshold, "ReserveCommitmentStore: sig count");
        address[] memory seen = new address[](n);
        uint256 uniq = 0;

        for (uint256 i = 0; i < n; i++) {
            address recovered = ECDSA.recover(ethSignedMessageHash, signatures[i]);
            require(isAttestationSigner[recovered], "ReserveCommitmentStore: not signer");
            for (uint256 j = 0; j < uniq; j++) {
                require(seen[j] != recovered, "ReserveCommitmentStore: duplicate signer");
            }
            seen[uniq] = recovered;
            unchecked {
                ++uniq;
            }
        }
        require(uniq >= attestationThreshold, "ReserveCommitmentStore: threshold");

        lineAttestationNonce[lineId] = nonce + 1;
        _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender);
    }

    function _commit(
        bytes32 lineId,
        uint256 R,
        uint256 validUntil,
        bytes32 evidenceHash,
        bytes32 merkleRoot,
        address by
    ) internal {
        if (requireNotarizedEvidence) {
            require(address(notaryRegistry) != address(0), "ReserveCommitmentStore: notary unset");
            require(
                notaryRegistry.isNotarized(defaultJurisdictionId, defaultMatrixControlId, evidenceHash),
                "ReserveCommitmentStore: evidence not notarized"
            );
        }
        Commitment storage c = _commitments[lineId];
        unchecked {
            c.version += 1;
        }
        c.R = R;
        c.validUntil = validUntil;
        c.evidenceHash = evidenceHash;
        c.merkleRoot = merkleRoot;
        emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, c.version, by);
    }

    /// @notice Applied by OMNLMirrorReceiver after CCIP validation (monotonic version).
    function applyMirrorCommit(
        bytes32 lineId,
        uint256 R,
        uint256 validUntil,
        bytes32 evidenceHash,
        bytes32 merkleRoot,
        uint256 version
    ) external {
        require(msg.sender == mirrorReceiver, "ReserveCommitmentStore: only mirror");
        Commitment storage c = _commitments[lineId];
        require(version > c.version, "ReserveCommitmentStore: version");
        c.version = version;
        c.R = R;
        c.validUntil = validUntil;
        c.evidenceHash = evidenceHash;
        c.merkleRoot = merkleRoot;
        emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, version, msg.sender);
    }

    function getCommitment(bytes32 lineId) external view returns (Commitment memory) {
        return _commitments[lineId];
    }
}
