/**
 * MessageStore — CRUD for `chat_messages` plus the server-side
 * sequence assigner described in tech-spec §4.
 *
 * Key properties:
 * - Every insert assigns `seq = MAX(seq)+1` for the channel inside a
 *   transaction that also bumps `chat_channels.last_message_at`.
 * - Concurrent writes serialize through the SQLite write lock; the
 *   `UNIQUE(channel_id, seq)` constraint is a belt-and-braces guard.
 * - `clientMessageId` is an optional idempotency key stored in
 *   `metadata.clientMessageId`; duplicate inserts surface the
 *   persisted row instead of erroring.
 *
 * @module services/chat-v2/sqlite/message.store
 */
import { type ChatContentType, type ChatMessageCursorPayload, type ChatMessageRow, type ChatSenderType } from '../types.js';
import type { ChatDatabase } from './chat-db.js';
/**
 * Encode a pagination cursor to an opaque base64url string.
 *
 * @param payload - Cursor fields
 * @returns Opaque base64url-encoded JSON string
 */
export declare function encodeCursor(payload: ChatMessageCursorPayload): string;
/**
 * Decode an opaque pagination cursor.
 *
 * @param cursor - The cursor string (or null/undefined)
 * @returns The decoded payload, or null if input is missing
 * @throws {ChatError} `invalid_cursor` (400) if the cursor fails to parse or
 *   doesn't contain the expected fields.
 */
export declare function decodeCursor(cursor: string | null | undefined): ChatMessageCursorPayload | null;
/** Options for inserting a message. */
export interface MessageInsertInput {
    channelId: string;
    senderType: ChatSenderType;
    senderId: string;
    content: string;
    contentType?: ChatContentType;
    /** Optional metadata object. Will be JSON-stringified. */
    metadata?: Record<string, unknown>;
    /** Stable client id for idempotent retries; folded into metadata. */
    clientMessageId?: string;
    /**
     * Phase A (SEALED §3.2) — array of mention IDs (member or team)
     * referenced inline in `content`. Stored as a JSON-encoded string in
     * the `mentions` column. Empty array or omitted → null in storage.
     */
    mentions?: string[];
    /**
     * Phase A (SEALED §3.2) — Slack-style thread root. When set, this
     * message is a reply within the thread rooted at `threadId`. Omitted
     * for top-level channel messages.
     */
    threadId?: string;
    /** Override the insert clock (tests). */
    nowMs?: number;
    /** Override the generated id (tests / deterministic callers). */
    id?: string;
}
/** Result of inserting a message — includes whether this was a dedupe hit. */
export interface MessageInsertResult {
    row: ChatMessageRow;
    /** True when the returned row pre-existed under the given clientMessageId. */
    deduped: boolean;
}
/** Options for listing messages with cursor pagination. */
export interface MessageListOptions {
    cursor?: string | null;
    /** 1..100, default 50. */
    limit?: number;
    /** `backward` (default) loads older; `forward` loads newer. */
    direction?: 'backward' | 'forward';
}
/** SQLite-backed store for chat messages. */
export declare class MessageStore {
    private readonly db;
    /** Cap on a single page size. */
    static readonly MAX_LIMIT = 100;
    /** Default page size when caller didn't specify. */
    static readonly DEFAULT_LIMIT = 50;
    constructor(db: ChatDatabase);
    /**
     * Insert a message into the channel.
     *
     * Performs sequence assignment (`MAX(seq)+1`) and idempotency dedupe in a
     * single transaction. If a row with the same `(channel_id, clientMessageId)`
     * already exists, returns it with `deduped=true` instead of inserting.
     *
     * Also updates the parent channel's `last_message_at`.
     *
     * @param input - Insert payload
     * @returns The persisted row plus a dedupe flag
     * @throws {ChatError} `channel_not_found` (404) if the channel is missing
     *   (or archived and we want to refuse).
     */
    insert(input: MessageInsertInput): MessageInsertResult;
    /**
     * Look up a message by its id.
     *
     * @param id - The message id
     * @returns The row, or null
     */
    getById(id: string): ChatMessageRow | null;
    /**
     * List messages for a channel with cursor pagination.
     *
     * @param channelId - The channel id to read
     * @param options - Listing options (cursor, limit, direction)
     * @returns An ordered array of rows and cursors for next/prev pages
     */
    listByChannel(channelId: string, options?: MessageListOptions): {
        rows: ChatMessageRow[];
        nextCursor: string | null;
        prevCursor: string | null;
    };
    /** Execute the paginated SELECT for one page of messages. */
    private queryPage;
    /** Compute next/prev cursors based on the page result. */
    private buildCursors;
    /**
     * Aggregate Slack-style thread reply counts for a channel in a single
     * query.
     *
     * Replies carry `thread_id = <root message id>`; root messages have
     * `thread_id IS NULL`. This groups every reply by its root and returns
     * one entry per thread with the reply count and the timestamp (ms) of
     * the most recent reply. The caller attaches these to the matching
     * root-message DTOs.
     *
     * Kept as ONE aggregate query per page (O(rows-with-threads)) rather
     * than a per-message lookup, so a page render stays a fixed two-query
     * cost regardless of page size.
     *
     * @param channelId - The channel id to aggregate over
     * @returns Map of `rootMessageId` to `{ replyCount, lastReplyAtMs }`
     */
    threadReplySummary(channelId: string): Map<string, {
        replyCount: number;
        lastReplyAtMs: number;
    }>;
    /**
     * Return the current `MAX(seq)` for a channel, or 0 when empty.
     *
     * @param channelId - The channel id
     * @returns The largest seq, or 0
     */
    getLastSeq(channelId: string): number;
    /** Count messages for a channel. Used by tests and metrics. */
    count(channelId: string): number;
    /**
     * Count all messages across every channel in the database.
     *
     * Onboarding v3 (B1) — used by the cold-start detector to decide whether
     * a fresh OSS install should auto-launch onboarding. The "any chat at all
     * means user has been here before" semantics is intentional: the detector
     * combines this with the per-project `firstLaunchedAt` marker to defend
     * against the case where chat-v2 is wiped accidentally on an existing
     * install (a non-zero count short-circuits onboarding before the marker
     * is even consulted).
     *
     * Single COUNT(*) over the index — O(rows) in the worst case but
     * negligible at install time when the table is empty (the only time the
     * caller actually cares about the result).
     *
     * @returns Total number of persisted chat messages
     */
    countAll(): number;
    /**
     * Phase 6.0b — delete every message in a channel while leaving the
     * channel row in place. Used by the legacy `clearConversation`
     * controller route. Not transactional with channel state; callers
     * that need atomicity should wrap in their own transaction.
     *
     * @param channelId - The channel whose messages to remove
     * @returns Number of rows deleted
     */
    deleteAllByChannel(channelId: string): number;
    /**
     * Merge a JSON patch into an existing message's metadata column.
     * Implementation of Phase 6.0 of the unified-chat-message-store spec
     * — replaces the legacy `ChatService.updateMessageMetadata` so all
     * mutation flows through the canonical store. SQLite's `json_patch`
     * built-in handles the merge atomically.
     *
     * @param id - Message id to update
     * @param patch - Partial metadata object to merge in (shallow merge)
     * @returns The post-update row, or null if the message doesn't exist
     */
    updateMetadata(id: string, patch: Record<string, unknown>): ChatMessageRow | null;
    /**
     * Find messages with `metadata.slackDeliveryStatus = 'pending'` AND a
     * non-empty `metadata.slackChannelId`, created within the past
     * `maxAgeMs`. Used by `NotifyReconciliationService` to retry Slack
     * deliveries that failed mid-flight.
     *
     * The `slackDeliveryStatus` field is set by callers (legacy code and
     * Phase 3 Slack writes) — this method is the canonical replacement
     * for `ChatService.getMessagesWithPendingSlackDelivery`.
     *
     * @param maxAgeMs - Lookback window in milliseconds
     * @param nowMs - Optional clock override for tests
     * @returns Up to `MAX_LIMIT` matching rows, newest first
     */
    findPendingSlackDelivery(maxAgeMs: number, nowMs?: number): ChatMessageRow[];
}
//# sourceMappingURL=message.store.d.ts.map