@namespace("chat.1")

protocol local {
  import idl "github.com/keybase/client/go/protocol/gregor1" as gregor1;
  import idl "github.com/keybase/client/go/protocol/keybase1" as keybase1;
  import idl "common.avdl";

  record MessageText {
    string body;
  }

  record MessageConversationMetadata {
    string conversationTitle;
  }

  record MessageEdit {
    MessageID messageID;
    string body;
  }

  record MessageDelete {
    array<MessageID> messageIDs;
  }

  record MessageHeadline {
    string headline;
  }

  record AssetMetadataImage {
    int width;
    int height;
  }

  record AssetMetadataVideo {
    int width;
    int height;
    int durationMs;
  }

  record AssetMetadataAudio {
    int durationMs;
  }

  @go("nostring")
  enum AssetMetadataType {
    NONE_0,
    IMAGE_1,
    VIDEO_2,
    AUDIO_3
  }

  variant AssetMetadata switch (AssetMetadataType assetType) {
    case IMAGE: AssetMetadataImage;
    case VIDEO: AssetMetadataVideo;
    case AUDIO: AssetMetadataAudio;
  }

  @go("nostring")
  enum AssetTag {
    PRIMARY_0
  }

  record Asset {
    string filename;           // original filename of the object
    string region;             // storage region name
    string endpoint;           // storage endpoint
    string bucket;             // storage bucket
    string path;               // path to the object in bucket
    int size;                  // size of the object
    string mimeType;           // mime type of the object
    Hash encHash;              // hash of ciphertext object
    bytes key;                 // encryption key
    bytes verifyKey;           // signature verification key
    string title;              // title of the asset (defaults to filename if not provided)
    bytes nonce;               // encryption nonce
    AssetMetadata metadata;    // type-specific metadata
    AssetTag tag;              // for multiple previews, a tag to differentiate
  }

  record MessageAttachment {
    Asset object;                // the primary attachment object (can be empty)
    union {null, Asset} preview; // the (optional) preview of object    (V1)
    array<Asset> previews;       // the previews of object              (V2)
    bytes metadata;              // generic metadata (msgpack)
    boolean uploaded;            // true if assets have been uploaded   (V2)
  }

  record MessageAttachmentUploaded {
    MessageID messageID;
    Asset object;                // the primary attachment object
    array<Asset> previews;       // the previews of object
    bytes metadata;              // generic metadata (msgpack)
  }

  variant MessageBody switch (MessageType messageType) {
    case TEXT: MessageText;
    case ATTACHMENT: MessageAttachment;
    case EDIT: MessageEdit;
    case DELETE: MessageDelete;
    case METADATA: MessageConversationMetadata;
    case HEADLINE: MessageHeadline;
    case ATTACHMENTUPLOADED: MessageAttachmentUploaded;
  }

  enum OutboxStateType {
    SENDING_0,
    ERROR_1
  }

  enum OutboxErrorType {
    MISC_0,
    OFFLINE_1,
    IDENTIFY_2,
    TOOLONG_3
  }

  record OutboxStateError {
    string message;
    OutboxErrorType typ;
  }

  variant OutboxState switch (OutboxStateType state) {
    case SENDING: int; // # of attempts
    case ERROR: OutboxStateError; // error
  }

  record OutboxRecord {
    OutboxState state;
    OutboxID outboxID;
    ConversationID convID;
    gregor1.Time ctime;
    @lint("ignore")
    MessagePlaintext Msg;
    keybase1.TLFIdentifyBehavior identifyBehavior;
  }

  record Inbox {
    InboxVers version;
    array<Conversation> convsUnverified;
    array<ConversationLocal> convs;
    union { null, Pagination } pagination;
  }

  enum HeaderPlaintextVersion {
    V1_1,
    V2_2,
    V3_3,
    V4_4,
    V5_5,
    V6_6,
    V7_7,
    V8_8,
    V9_9,
    V10_10
  }

  record HeaderPlaintextMetaInfo {
    boolean crit; // whether it is critical to support this message
  }

  record HeaderPlaintextUnsupported {
    HeaderPlaintextMetaInfo mi;
  }

  // HeaderPlaintextV1 is version 1 of HeaderPlaintext.
  // The fields here cannot change.  To modify,
  // create a new record type with a new version.
  // CORE-4540: Update note about how compatibility works.
  record HeaderPlaintextV1 {
    ConversationIDTriple conv;
    string tlfName;
    boolean tlfPublic;
    MessageType messageType;
    array<MessagePreviousPointer> prev;
    gregor1.UID sender;
    gregor1.DeviceID senderDevice;
    // MessageBoxed.V1: Hash of the encrypted body ciphertext.
    // MessageBoxed.V2: Hash of encrypted body (.v || .n || .e)
    //                  Where V is a big-endian int32
    Hash bodyHash;
    union { null, OutboxInfo } outboxInfo;
    union { null, OutboxID } outboxID;
    // MessageBoxed.V1: Signature over the serialized HeaderPlaintextV1 (with headerSignature set to null).
    // MessageBoxed.V2: Null (because the header is signencrypted outside)
    union {null, SignatureInfo} headerSignature;
  }

  // HeaderPlaintext is a variant container for all the
  // versions of HeaderPlaintext.
  variant HeaderPlaintext switch (HeaderPlaintextVersion version) {
    case V1 : HeaderPlaintextV1;
    case V2 : HeaderPlaintextUnsupported;
    case V3 : HeaderPlaintextUnsupported;
    case V4 : HeaderPlaintextUnsupported;
    case V5 : HeaderPlaintextUnsupported;
    case V6 : HeaderPlaintextUnsupported;
    case V7 : HeaderPlaintextUnsupported;
    case V8 : HeaderPlaintextUnsupported;
    case V9 : HeaderPlaintextUnsupported;
    case V10: HeaderPlaintextUnsupported;
  }

  enum BodyPlaintextVersion {
    V1_1,
    V2_2,
    V3_3,
    V4_4,
    V5_5,
    V6_6,
    V7_7,
    V8_8,
    V9_9,
    V10_10
  }

  record BodyPlaintextMetaInfo {
    boolean crit; // whether it's critical to support this message
  }

  // Every future BodyPlaintextVX needs to be a superset of this structure.
  record BodyPlaintextUnsupported {
    BodyPlaintextMetaInfo mi;
  }

  // BodyPlaintextV1 is version 1 of BodyPlaintext.
  // The fields here cannot change.  To modify,
  // create a new record type with a new version.
  record BodyPlaintextV1 {
    MessageBody messageBody;
  }

  // BodyPlaintext is a variant container for all the
  // versions of BodyPlaintext.
  variant BodyPlaintext switch (BodyPlaintextVersion version) {
    case V1: BodyPlaintextV1;
    case V2 : BodyPlaintextUnsupported;
    case V3 : BodyPlaintextUnsupported;
    case V4 : BodyPlaintextUnsupported;
    case V5 : BodyPlaintextUnsupported;
    case V6 : BodyPlaintextUnsupported;
    case V7 : BodyPlaintextUnsupported;
    case V8 : BodyPlaintextUnsupported;
    case V9 : BodyPlaintextUnsupported;
    case V10: BodyPlaintextUnsupported;
  }

  record MessagePlaintext {
    MessageClientHeader clientHeader;
    MessageBody messageBody;
  }

  enum MessageUnboxedState {
    VALID_1,
    ERROR_2,
    OUTBOX_3
  }

  record MessageUnboxedValid {
    MessageClientHeaderVerified clientHeader;
    MessageServerHeader serverHeader;
    MessageBody messageBody;
    string senderUsername;
    string senderDeviceName;
    string senderDeviceType;
    Hash bodyHash;

    // MessageBoxed.V1: Hash of the encrypted header ciphertext.
    // MessageBoxed.V2: Hash of MessageBoxed.headerSealed (.v || .n || .b)
    //                  Where V is a big-endian int32
    Hash headerHash;

    // TOOD Maybe get rid of this field in favor of verificationKey.
    //      If so, bump-nuke the caches in storage_blockengine and any other persistent
    //      storage of MessageUnboxedValid.
    // MessageBoxed.V1: Header signature. Included for the verification key.
    // MessageBoxed.V2: Null
    union {null, SignatureInfo} headerSignature;

    // MessageBoxed.V1: Null
    // MessageBoxed.V2: The verification key used to unbox.
    //                  See MessageBoxed.verifyKey
    union {null, bytes} verificationKey;

    // Whether the message was sent by a device that is now revoked.
    // We aren't sure whether the device was revoked when the message was sent.
    // Re-evaluated when unboxed or pulled from the cache.
    union {null, gregor1.Time} senderDeviceRevokedAt;
  }

  enum MessageUnboxedErrorType {
    MISC_0,
    BADVERSION_CRITICAL_1,
    BADVERSION_2,
    IDENTIFY_3
  }

  record MessageUnboxedError {
    MessageUnboxedErrorType errType;
    string errMsg;
    MessageID messageID;
    MessageType messageType;
    gregor1.Time ctime;
  }

  variant MessageUnboxed switch (MessageUnboxedState state) {
    case VALID: MessageUnboxedValid;
    case ERROR: MessageUnboxedError;
    case OUTBOX: OutboxRecord;
  }

  // This causes fetching to return N items, where N = IdeallyGetUnreadPlus +
  // Unread, if AtLeast <= N <= AtMost, or one of the bounds if there are too
  // many / too few unread items. i.e. (derived from chris's comment)
  // collar(AtLeast, (IdeallyGetUnreadPlus + Unread), AtMost)
  //
  // By definition, one could use a same non-zero number for both AtLeast and
  // AtMost to precisely control the number of items returned.
  record UnreadFirstNumLimit {
    @lint("ignore")
    int NumRead;

    @lint("ignore")
    int AtLeast;
    @lint("ignore")
    int AtMost;
  }

  record ConversationInfoLocal {
    ConversationID id;
    ConversationIDTriple triple;

    string tlfName;
    string topicName;
    TLFVisibility visibility;
    ConversationStatus status;

    // Lists of usernames, always complete, optionally sorted by activity.
    array<string> writerNames;
    array<string> readerNames;

    union { null, ConversationFinalizeInfo } finalizeInfo;
  }

  enum ConversationErrorType {
    MISC_0,
    MISSINGINFO_1,
    SELFREKEYNEEDED_2,
    OTHERREKEYNEEDED_3,
    IDENTIFY_4,
    LOCALMAXMESSAGENOTFOUND_5 // special use-case internally
  }

  record ConversationErrorLocal {
    ConversationErrorType typ;
    string message;
    Conversation remoteConv;
    boolean permanent;
    string unverifiedTLFName;
    // Only set if typ is for rekeying.
    union { null, ConversationErrorRekey} rekeyInfo;
  }

  record ConversationErrorRekey {
    // All of this stuff is server trust. Don't use it to send messages.
    string tlfName;
    boolean tlfPublic;
    // Users who could rekey this conv.
    array<string> rekeyers;
    // Lists of usernames in the conv. Untrusted.
    array<string> writerNames;
    array<string> readerNames;
  }

  // ConversationLocal, whenever present, has a valid `identifyFailures` field that
  // faithfully represent identify result. If identify information is not
  // availabe, we should use a different type.
  record ConversationLocal {
    union { null, ConversationErrorLocal } error;
    ConversationInfoLocal info;
    ConversationReaderInfo readerInfo;
    array<ConversationMetadata> supersedes;
    array<ConversationMetadata> supersededBy;

    array<MessageUnboxed> maxMessages; // the latest message for each message type

    // Whether this conversation has no content-ful messages.
    boolean isEmpty;

    // This field, if null or empty, indicates identify succeeded without any
    // break.
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  record NonblockFetchRes {
    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  record ThreadView {
    array<MessageUnboxed> messages;
    union { null, Pagination } pagination;
  }

  GetThreadLocalRes getThreadLocal(ConversationID conversationID, union { null, GetThreadQuery} query, union { null, Pagination } pagination, keybase1.TLFIdentifyBehavior identifyBehavior);
  record GetThreadQuery {
    boolean markAsRead;
    array<MessageType> messageTypes;
    boolean disableResolveSupersedes;

    union { null, gregor1.Time } before;
    union { null, gregor1.Time } after;
  }
  record GetThreadLocalRes {
    ThreadView thread;

    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  GetThreadLocalRes getCachedThread(ConversationID conversationID, union { null, GetThreadQuery} query, union { null, Pagination } pagination, keybase1.TLFIdentifyBehavior identifyBehavior);

  NonblockFetchRes getThreadNonblock(int sessionID, ConversationID conversationID, union { null, GetThreadQuery} query, union { null, Pagination } pagination, keybase1.TLFIdentifyBehavior identifyBehavior); 

  record GetInboxLocalRes {
    array<Conversation> conversationsUnverified;
    union { null, Pagination } pagination;

    boolean offline;
    array<RateLimit> rateLimits;
    // This only contains identify failures if a TLF name is specified in arg.
    // It doesn't contain identifyFailures otherwise, since GetInboxLocal
    // doesn't run identify for each conversation in the inbox.
    // GetInboxAndUnboxLocal should be used if identify failures are needed.
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  GetInboxAndUnboxLocalRes getInboxAndUnboxLocal(union { null, GetInboxLocalQuery} query, union { null, Pagination } pagination, keybase1.TLFIdentifyBehavior identifyBehavior);
  record GetInboxLocalQuery {
    // Local analog of common:GetInboxQuery

    union { null, string } tlfName;
    union { null, string } topicName;

    array<ConversationID> convIDs;
    union { null, TopicType } topicType;
    union { null, TLFVisibility } tlfVisibility;
    union { null, gregor1.Time } before;
    union { null, gregor1.Time } after;
    union { null, boolean } oneChatTypePerTLF;

    // If left empty, default is to show all.
    array<ConversationStatus> status;

    boolean unreadOnly;
    boolean readOnly;
    boolean computeActiveList;
  }
  record GetInboxAndUnboxLocalRes {
    array<ConversationLocal> conversations;
    union { null, Pagination } pagination;

    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  NonblockFetchRes getInboxNonblockLocal(int sessionID, union { null, int } maxUnbox, union { null, GetInboxLocalQuery} query, union { null, Pagination } pagination, keybase1.TLFIdentifyBehavior identifyBehavior);

  PostLocalRes postLocal(ConversationID conversationID, MessagePlaintext msg, keybase1.TLFIdentifyBehavior identifyBehavior);
  record PostLocalRes {
    array<RateLimit> rateLimits;
    MessageID messageID;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  PostLocalNonblockRes postLocalNonblock(ConversationID conversationID, MessagePlaintext msg, MessageID clientPrev, keybase1.TLFIdentifyBehavior identifyBehavior);
  record PostLocalNonblockRes {
    array<RateLimit> rateLimits;
    OutboxID outboxID;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  PostLocalNonblockRes postTextNonblock(ConversationID conversationID, ConversationIDTriple conv, string tlfName, boolean tlfPublic, string body, MessageID clientPrev, keybase1.TLFIdentifyBehavior identifyBehavior);
  PostLocalNonblockRes postDeleteNonblock(ConversationID conversationID, ConversationIDTriple conv, string tlfName, boolean tlfPublic, MessageID supersedes,MessageID clientPrev, keybase1.TLFIdentifyBehavior identifyBehavior);
  PostLocalNonblockRes postEditNonblock(ConversationID conversationID, ConversationIDTriple conv, string tlfName, boolean tlfPublic, MessageID supersedes, string body, MessageID clientPrev, keybase1.TLFIdentifyBehavior identifyBehavior);


  @lint("ignore")
  SetConversationStatusLocalRes SetConversationStatusLocal(ConversationID conversationID, ConversationStatus status, keybase1.TLFIdentifyBehavior identifyBehavior);
  record SetConversationStatusLocalRes {
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  NewConversationLocalRes newConversationLocal(string tlfName, TopicType topicType, TLFVisibility tlfVisibility, union { null, string } topicName, keybase1.TLFIdentifyBehavior identifyBehavior);
  record NewConversationLocalRes {
    ConversationLocal conv;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }


  // if since is given, limit is ignored
  GetInboxSummaryForCLILocalRes getInboxSummaryForCLILocal(GetInboxSummaryForCLILocalQuery query);
  record GetInboxSummaryForCLILocalQuery {
    TopicType topicType;
    string after;
    string before;
    TLFVisibility visibility;

    // If left empty, default is to show all.
    array<ConversationStatus> status;

    boolean unreadFirst;
    UnreadFirstNumLimit unreadFirstLimit;
    int activitySortedLimit;
  }
  record GetInboxSummaryForCLILocalRes {
    array<ConversationLocal> conversations;
    boolean offline;
    array<RateLimit> rateLimits;
  }


  GetConversationForCLILocalRes getConversationForCLILocal(GetConversationForCLILocalQuery query);
  record GetConversationForCLILocalQuery {
    boolean markAsRead;
    @lint("ignore")
    array<MessageType> MessageTypes;

    @lint("ignore")
    union { null, string } Since;

    UnreadFirstNumLimit limit;

    @lint("ignore")
    ConversationLocal conv;
  }
  record GetConversationForCLILocalRes {
    ConversationLocal conversation;
    array<MessageUnboxed> messages;
    boolean offline;
    array<RateLimit> rateLimits;
  }

  // Get messages by ID.
  @lint("ignore")
  GetMessagesLocalRes GetMessagesLocal(ConversationID conversationID, array<MessageID> messageIDs, boolean disableResolveSupersedes, keybase1.TLFIdentifyBehavior identifyBehavior);
  record GetMessagesLocalRes {
    array<MessageUnboxed> messages;

    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  // LocalSource is a stream attachment source.
  record LocalSource {
    keybase1.Stream source;
    string filename;
    int size;
  }

  // Post an attachment from stream source to conversationID.
  PostLocalRes postAttachmentLocal(int sessionID, ConversationID conversationID, MessageClientHeader clientHeader, LocalSource attachment, union { null, MakePreviewRes} preview, string title, bytes metadata, keybase1.TLFIdentifyBehavior identifyBehavior);

  // LocalFileSource is a file attachment source.  Filename must be readable
  // by the service for the duration of the attachment upload.
  record LocalFileSource {
    string filename;
  }

  // Post an attachment from file source to conversationID.
  PostLocalRes postFileAttachmentLocal(int sessionID, ConversationID conversationID, MessageClientHeader clientHeader, LocalFileSource attachment, union { null, MakePreviewRes } preview, string title, bytes metadata, keybase1.TLFIdentifyBehavior identifyBehavior);

  record DownloadAttachmentLocalRes {
    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  // Download an attachment from a message into sink stream.
  @lint("ignore")
  DownloadAttachmentLocalRes DownloadAttachmentLocal(int sessionID, ConversationID conversationID, MessageID messageID, keybase1.Stream sink, boolean preview, keybase1.TLFIdentifyBehavior identifyBehavior);

  // Download an attachment from a message into a local file.
  // Filename must be writable by the service.
  @lint("ignore")
  DownloadAttachmentLocalRes DownloadFileAttachmentLocal(int sessionID, ConversationID conversationID, MessageID messageID, string filename, boolean preview, keybase1.TLFIdentifyBehavior identifyBehavior);
 
  record MakePreviewRes {
    string mimeType;        // this will always be populated 
    union {null, string} filename;    // will exist if service is able to make a preview 
    union {null, AssetMetadata} metadata; // will exist if service is able to make a preview
    union {null, AssetMetadata} baseMetadata; // will exist if service is able to get base metadata
  } 
  MakePreviewRes makePreview(int sessionID, LocalFileSource attachment, string outputDir);

  @lint("ignore")
  void CancelPost(OutboxID outboxID);
  @lint("ignore")
  void RetryPost(OutboxID outboxID);

  record MarkAsReadLocalRes {
     boolean offline;
     array<RateLimit> rateLimits;
  }
  MarkAsReadLocalRes markAsReadLocal(int sessionID, ConversationID conversationID, MessageID msgID);

  record FindConversationsLocalRes {
    array<ConversationLocal> conversations;

    boolean offline;
    array<RateLimit> rateLimits;
    array<keybase1.TLFIdentifyFailure> identifyFailures;
  }

  FindConversationsLocalRes findConversationsLocal(string tlfName, TLFVisibility visibility, TopicType topicType, string topicName, union { null, bool } oneChatPerTLF, keybase1.TLFIdentifyBehavior identifyBehavior);

}
