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

/**
 * @title AereAgentMemoryVault — portable, user-owned AI agent memory
 * @notice Today every AI assistant rebuilds your context from scratch.
 *         Your ChatGPT history, your Claude preferences, the tone your
 *         trading bot has learned about you — none of it travels. Switch
 *         provider, the memory dies. The vendor owns the relationship.
 *
 *         AereAgentMemoryVault flips that ownership. The USER (not the
 *         provider) is the namespace owner. Agents read and write to
 *         per-user slots under EXPLICIT user permission. When the user
 *         switches provider, the new agent gets read access to the same
 *         history with one signature. Privacy is preserved because the
 *         memory blobs themselves are ENCRYPTED off-chain — only
 *         commitments + access-control live on chain.
 *
 *         MODEL:
 *
 *           1. Each user has a private namespace (keyed by their address).
 *              Within it, named "slots" hold versioned memory entries.
 *
 *           2. The user grants Permission to an agent address. A Permission
 *              specifies (agent, slot, write?, validUntil). Multiple
 *              permissions can coexist (read-only "memory consumer" agent
 *              + read/write "memory builder" agent).
 *
 *           3. An authorised agent writes an entry: a content hash + an
 *              opaque pointer (ipfs:// to the encrypted blob). The chain
 *              stores ONLY the commitment. Encryption keys are off-chain
 *              and exchanged out-of-band (e.g. via ECIES to the agent's
 *              registered key, or via a passkey-bound user device).
 *
 *           4. The user can REVOKE any permission at any time. The
 *              revocation is immediate; an agent's next write attempt
 *              reverts. Past entries written under that permission stay
 *              in history (the chain remembers; only the agent's ability
 *              to read goes away — the user can rotate keys to actually
 *              cut access).
 *
 *           5. PORTABILITY: when a user grants a fresh agent read access
 *              to ALL their slots, that agent immediately sees the full
 *              memory tree. The chain emits enough events for an indexer
 *              to reconstruct the canonical slot → latest-entry map.
 *
 *         WHAT THE CHAIN DOES NOT DO:
 *           - Does NOT decrypt anything. Keys live with the user / agent.
 *           - Does NOT enforce schema. Slots are just bytes32 names.
 *           - Does NOT charge fees per write. (Gas is the only cost.)
 *
 *         IMMUTABILITY:
 *           - No admin, no Foundation override. The user is the only
 *             authority over their own namespace.
 *           - Permissions table is append-only via events; the latest
 *             permission state per (user, agent, slot) is the source of
 *             truth.
 */
contract AereAgentMemoryVault {

    /* ================================ types ================================ */

    struct Permission {
        bool    canRead;
        bool    canWrite;
        uint64  validUntil;     // 0 = no expiry
        bool    exists;         // true after first grant; never reset
    }

    struct Entry {
        bytes32 contentHash;    // SHA256 of the encrypted blob
        string  blobUri;        // ipfs:// or https:// (encrypted)
        address writer;
        uint64  version;
        uint64  writtenAt;
    }

    /* ================================ storage ================================ */

    /// @notice user → agent → slot → Permission. Per-slot grain; wildcard
    ///         per (user, agent) NOT supported — agents must enumerate the
    ///         exact slots they're given access to.
    mapping(address => mapping(address => mapping(bytes32 => Permission))) internal _perm;

    /// @notice user → slot → next version counter. Starts at 1.
    mapping(address => mapping(bytes32 => uint64)) public nextVersion;

    /// @notice user → slot → latest Entry. Indexers can reconstruct full
    ///         history from events; the chain keeps only the head.
    mapping(address => mapping(bytes32 => Entry)) internal _head;

    /* ================================ events ================================ */

    event PermissionSet(
        address indexed user,
        address indexed agent,
        bytes32 indexed slot,
        bool canRead,
        bool canWrite,
        uint64 validUntil
    );

    event PermissionRevoked(address indexed user, address indexed agent, bytes32 indexed slot);

    event EntryWritten(
        address indexed user,
        bytes32 indexed slot,
        address indexed agent,
        uint64 version,
        bytes32 contentHash,
        string blobUri
    );

    /* ================================ errors ================================ */

    error NotWriter();
    error PermissionExpired();
    error NoSuchPermission();
    error EmptyHash();
    error InvalidValidity();

    /* =========================== user-side permission ========================= */

    /// @notice User grants (or updates) a per-slot permission to an agent.
    ///         msg.sender == user. validUntil = 0 means no expiry.
    function grant(
        address agent,
        bytes32 slot,
        bool canRead,
        bool canWrite,
        uint64 validUntil
    ) external {
        if (validUntil != 0 && validUntil <= block.timestamp) revert InvalidValidity();
        Permission storage p = _perm[msg.sender][agent][slot];
        p.canRead = canRead;
        p.canWrite = canWrite;
        p.validUntil = validUntil;
        p.exists = true;
        emit PermissionSet(msg.sender, agent, slot, canRead, canWrite, validUntil);
    }

    /// @notice User revokes a permission. Immediate effect on writes.
    function revoke(address agent, bytes32 slot) external {
        Permission storage p = _perm[msg.sender][agent][slot];
        if (!p.exists) revert NoSuchPermission();
        p.canRead = false;
        p.canWrite = false;
        // We KEEP `exists` true so subsequent grants don't accidentally
        // look like brand-new attestations; just zero the rights.
        emit PermissionRevoked(msg.sender, agent, slot);
    }

    /* =============================== writes =============================== */

    /// @notice Agent writes a memory entry for `user` at `slot`. msg.sender
    ///         must have an unexpired canWrite permission.
    function write(
        address user,
        bytes32 slot,
        bytes32 contentHash,
        string calldata blobUri
    ) external returns (uint64 version) {
        if (contentHash == bytes32(0)) revert EmptyHash();
        Permission storage p = _perm[user][msg.sender][slot];
        if (!p.canWrite) revert NotWriter();
        if (p.validUntil != 0 && block.timestamp > p.validUntil) revert PermissionExpired();

        version = ++nextVersion[user][slot];
        _head[user][slot] = Entry({
            contentHash: contentHash,
            blobUri: blobUri,
            writer: msg.sender,
            version: version,
            writtenAt: uint64(block.timestamp)
        });
        emit EntryWritten(user, slot, msg.sender, version, contentHash, blobUri);
    }

    /* ================================ views ================================ */

    function permissionOf(address user, address agent, bytes32 slot) external view returns (
        bool canRead, bool canWrite, uint64 validUntil, bool exists
    ) {
        Permission storage p = _perm[user][agent][slot];
        return (p.canRead, p.canWrite, p.validUntil, p.exists);
    }

    /// @notice True if `agent` can currently read this slot (canRead +
    ///         not expired). Used by off-chain clients before requesting
    ///         the encrypted blob from the user's key-exchange service.
    function canReadNow(address user, address agent, bytes32 slot) external view returns (bool) {
        Permission storage p = _perm[user][agent][slot];
        if (!p.canRead) return false;
        if (p.validUntil != 0 && block.timestamp > p.validUntil) return false;
        return true;
    }

    function headOf(address user, bytes32 slot) external view returns (
        bytes32 contentHash, string memory blobUri, address writer, uint64 version, uint64 writtenAt
    ) {
        Entry storage e = _head[user][slot];
        return (e.contentHash, e.blobUri, e.writer, e.version, e.writtenAt);
    }
}
