import { Document, Model } from 'mongoose';

interface QueryOptions {
    q?: string;
    page?: number;
    limit?: number;
    sort?: string;
    select?: string;
    populate?: string;
    lean?: boolean;
    [key: string]: any;
}
interface PaginationResult<T> {
    docs: T[];
    totalDocs: number;
    limit: number;
    page: number;
    totalPages: number;
    hasNextPage: boolean;
    hasPrevPage: boolean;
}
interface CursorOptions {
    q?: string;
    limit?: number;
    cursor?: string | null;
    cursorField?: string;
    direction?: 'asc' | 'desc';
    select?: string;
    populate?: string;
    lean?: boolean;
    [key: string]: any;
}
interface CursorResult<T> {
    docs: T[];
    limit: number;
    nextCursor: string | null;
    hasNextPage: boolean;
}
interface PopulateConfig {
    path: string;
    select?: string;
}
type SearchMode = 'regex' | 'text';
declare class QueryToolkit<T extends Document> {
    private readonly model;
    private searchFields;
    private filterableFields;
    private selectableFields;
    private populatableFields;
    private defaultLimit;
    private maxLimit;
    private searchMode;
    private leanByDefault;
    private splitCommaValues;
    private presets;
    constructor(model: Model<T>, options?: {
        searchFields?: string[];
        filterableFields?: string[];
        selectableFields?: string[];
        populatableFields?: string[];
        defaultLimit?: number;
        maxLimit?: number;
        searchMode?: SearchMode;
        lean?: boolean;
        splitCommaValues?: boolean;
    });
    /**
     * Escapes regex special characters to prevent regex injection and
     * catastrophic backtracking (ReDoS) from user-supplied search terms.
     */
    private escapeRegex;
    private buildSearchQuery;
    private buildFilterQuery;
    /**
     * Translates a single filter value into a safe Mongo query fragment:
     * - arrays become `$in` (multi-value filter); comma-separated strings also
     *   become `$in` only when `splitCommaValues` is enabled (off by default, so
     *   values that legitimately contain commas keep exact-match semantics)
     * - objects are treated as operator filters, keeping only whitelisted
     *   operators (gte, lte, ne, in, ...) and dropping anything unrecognized
     *   to block NoSQL operator injection ($where, $function, raw $-keys, ...)
     * - primitives become exact-match
     * Returns `undefined` when nothing safe could be derived.
     */
    private buildFilterValue;
    /**
     * Splits an array or comma-separated string into a deduped list of safe
     * primitive values (used by `$in`/`$nin` and comma-value filters).
     */
    private toMultiValue;
    private buildOperatorFilter;
    private isPrimitive;
    /**
     * Coerces and clamps pagination input. Query-string params arrive as
     * strings, so values are normalized to integers, page is forced to >= 1,
     * and limit is bounded to [1, maxLimit] to prevent unbounded scans.
     */
    private normalizePagination;
    private normalizeLimit;
    private parseSortString;
    private buildSelectQuery;
    /**
     * Parses the populate string into populate configs, optionally with
     * per-path field selection.
     *
     * Grammar (deterministic — `;` always separates paths, `,` separates the
     * selected fields of a single path):
     * - `profile,posts`                  → populate both paths (legacy, no `:`)
     * - `profile:name,avatar`            → populate `profile` selecting name+avatar
     * - `profile:name;posts:title,body`  → multiple paths, each with selection
     * - `profile;posts:title`            → mix paths with and without selection
     */
    private buildPopulateFields;
    private addPopulateConfig;
    /**
     * Assembles the base match query shared by every read method from the
     * search term and the whitelisted filter options.
     */
    private buildBaseQuery;
    /** Returns the filter fields of an options object, excluding reserved keys. */
    private extractFilters;
    /** Reads a possibly dotted path (e.g. `profile.score`) off a document. */
    private getValueByPath;
    /** Encodes a cursor field value into a string that round-trips losslessly. */
    private encodeCursor;
    private applyCommonModifiers;
    findWithOptions(options?: QueryOptions): Promise<PaginationResult<T>>;
    /**
     * Cursor-based (keyset) pagination. Scales to large collections because it
     * avoids the growing `skip` cost of offset pagination. Pass the `nextCursor`
     * from the previous result back in as `cursor` to fetch the following page.
     */
    findWithCursor(options?: CursorOptions): Promise<CursorResult<T>>;
    /**
     * Returns a single document matching the search/filter options, or null.
     * Supports select, populate and lean; pagination/sort options are ignored.
     */
    findOne(options?: QueryOptions): Promise<T | null>;
    /**
     * Returns true if at least one document matches the search/filter options.
     */
    exists(options?: QueryOptions): Promise<boolean>;
    countWithOptions(options?: QueryOptions): Promise<number>;
    definePreset(name: string, options: QueryOptions): void;
    getPreset(name: string): QueryOptions | undefined;
    hasPreset(name: string): boolean;
    deletePreset(name: string): boolean;
    listPresets(): string[];
    /**
     * Looks up a preset and merges it with overrides. Overrides take precedence;
     * when both preset and override hold a plain object for the same key (e.g. an
     * operator filter `{ gte: 10 }`), the two objects are merged rather than the
     * preset's value being wholly replaced — so `{ gte: 10 }` + `{ lte: 100 }`
     * yields `{ gte: 10, lte: 100 }`.
     */
    private resolvePreset;
    private isPlainObject;
    findWithPreset(presetName: string, overrides?: QueryOptions): Promise<PaginationResult<T>>;
    countWithPreset(presetName: string, overrides?: QueryOptions): Promise<number>;
}

export { type CursorOptions, type CursorResult, type PaginationResult, type PopulateConfig, type QueryOptions, QueryToolkit, type SearchMode };
