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

/**
 * @title AereTravelRuleHashRegistry — on-chain anchor for FATF Travel Rule
 *                                      message exchanges (Notabene-compatible)
 * @notice The FATF Travel Rule requires VASPs to exchange identity payloads
 *         for transfers above a jurisdiction-set threshold (typically
 *         USD 1,000 for the US, EUR 1,000 for the EU MiCA regime). The
 *         payloads themselves are confidential and exchanged off-chain via
 *         a message bus like Notabene IVMS-101.
 *
 *         This contract is the on-chain ANCHOR for the off-chain exchange.
 *         The originating VASP commits a keccak256 hash of the IVMS-101
 *         payload before the transaction goes through, and the beneficiary
 *         VASP later verifies that hash matches what was exchanged. The
 *         on-chain anchor:
 *           1. Proves the payload existed at a specific block.
 *           2. Cannot be retroactively altered (commitments are
 *              append-only).
 *           3. Is auditable by regulators without revealing the payload.
 *
 *         No payload data is stored on chain. No identity is revealed.
 *
 * @dev    PHASE 1 — minimal viable Travel Rule anchor.
 *           - `commit(payloadHash, beneficiaryVasp, threshold)` permissionless
 *             callable by the originator VASP. The VASP self-identifies via
 *             the msg.sender; consumers can query "did VASP X commit to a
 *             payload referencing this txid?" via the events index.
 *           - `acknowledge(commitmentId)` callable by the beneficiary VASP
 *             to mark receipt + verification of the off-chain payload.
 *           - `dispute(commitmentId, reason)` callable by the beneficiary
 *             if the payload doesn't match the on-chain hash.
 *
 *         NO admin. NO Foundation interception. NO custody.
 *
 *         INTEGRATIONS:
 *           - Notabene SDK: the JS SDK on each VASP calls
 *             contract.commit(hash, recipientVasp, threshold) automatically
 *             when the user initiates a > $1k transfer.
 *           - Compliance dashboards: index Commitments + Acknowledgements
 *             + Disputes by VASP address for audit trail.
 *
 *         AERE Foundation Seychelles is registered as a VASP and uses this
 *         registry for its own institutional flows (e.g. via the
 *         SettlementHub for BUIDL/USDY/OUSG receipts).
 */
contract AereTravelRuleHashRegistry {

    enum State { Committed, Acknowledged, Disputed }

    struct Commitment {
        bytes32 payloadHash;
        address originatorVasp;
        address beneficiaryVasp;
        uint256 threshold;       // jurisdiction threshold in USD units (cents)
        uint64 committedAt;
        uint64 statusUpdatedAt;
        State state;
        string disputeReason;
    }

    /// @notice commitmentId → Commitment
    mapping(uint256 => Commitment) public commitments;
    uint256 public nextCommitmentId;

    /// @notice Per-VASP commitment index (for audit retrieval).
    mapping(address => uint256[]) public vaspCommitments;

    event Committed(uint256 indexed id, address indexed originator, address indexed beneficiary, bytes32 payloadHash, uint256 threshold);
    event Acknowledged(uint256 indexed id, address indexed beneficiary);
    event Disputed(uint256 indexed id, address indexed beneficiary, string reason);

    error UnknownCommitment();
    error NotBeneficiary();
    error AlreadyAcknowledged();
    error AlreadyDisputed();
    error ZeroAddress();

    function commit(bytes32 payloadHash, address beneficiaryVasp, uint256 threshold)
        external
        returns (uint256 id)
    {
        if (beneficiaryVasp == address(0)) revert ZeroAddress();
        id = nextCommitmentId++;
        commitments[id] = Commitment({
            payloadHash: payloadHash,
            originatorVasp: msg.sender,
            beneficiaryVasp: beneficiaryVasp,
            threshold: threshold,
            committedAt: uint64(block.timestamp),
            statusUpdatedAt: uint64(block.timestamp),
            state: State.Committed,
            disputeReason: ""
        });
        vaspCommitments[msg.sender].push(id);
        vaspCommitments[beneficiaryVasp].push(id);
        emit Committed(id, msg.sender, beneficiaryVasp, payloadHash, threshold);
    }

    function acknowledge(uint256 id) external {
        Commitment storage c = commitments[id];
        if (c.committedAt == 0) revert UnknownCommitment();
        if (msg.sender != c.beneficiaryVasp) revert NotBeneficiary();
        if (c.state == State.Acknowledged) revert AlreadyAcknowledged();
        if (c.state == State.Disputed) revert AlreadyDisputed();
        c.state = State.Acknowledged;
        c.statusUpdatedAt = uint64(block.timestamp);
        emit Acknowledged(id, msg.sender);
    }

    function dispute(uint256 id, string calldata reason) external {
        Commitment storage c = commitments[id];
        if (c.committedAt == 0) revert UnknownCommitment();
        if (msg.sender != c.beneficiaryVasp) revert NotBeneficiary();
        if (c.state == State.Disputed) revert AlreadyDisputed();
        c.state = State.Disputed;
        c.disputeReason = reason;
        c.statusUpdatedAt = uint64(block.timestamp);
        emit Disputed(id, msg.sender, reason);
    }

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

    function vaspCommitmentCount(address vasp) external view returns (uint256) {
        return vaspCommitments[vasp].length;
    }
}
