import { LogSink, LogLevel, Context } from '@rocicorp/logger';

/** The values that can be represented in JSON */
type JSONValue =
  | null
  | string
  | boolean
  | number
  | Array<JSONValue>
  | JSONObject;

/**
 * A JSON object. This is a map from strings to JSON values or `undefined`. We
 * allow `undefined` values as a convenience... but beware that the `undefined`
 * values do not round trip to the server. For example:
 *
 * ```
 * // Time t1
 * await tx.set('a', {a: undefined});
 *
 * // time passes, in a new transaction
 * const v = await tx.get('a');
 * console.log(v); // either {a: undefined} or {}
 * ```
 */
type JSONObject = {[key: string]: JSONValue | undefined};

/** Like {@link JSONValue} but deeply readonly */
type ReadonlyJSONValue =
  | null
  | string
  | boolean
  | number
  | ReadonlyArray<ReadonlyJSONValue>
  | ReadonlyJSONObject;

/** Like {@link JSONObject} but deeply readonly */
type ReadonlyJSONObject = {
  readonly [key: string]: ReadonlyJSONValue | undefined;
};
/**
 * The ID describing a client.
 */
type ClientID = string;

/**
 * When using indexes the key is a tuple of the secondary key and the primary
 * key.
 */
type IndexKey = readonly [secondary: string, primary: string];

/**
 * Options for {@link ReadTransaction.scan | scan}
 */
type ScanOptions = ScanIndexOptions | ScanNoIndexOptions;
/**
 * Options for {@link ReadTransaction.scan | scan} when scanning over the entire key
 * space.
 */
type ScanNoIndexOptions = {
    /** Only include keys starting with `prefix`. */
    prefix?: string | undefined;
    /** Only include up to `limit` results. */
    limit?: number | undefined;
    /** When provided the scan starts at this key. */
    start?: {
        key: string;
        /** Whether the `key` is exclusive or inclusive. */
        exclusive?: boolean | undefined;
    } | undefined;
};
/**
 * Options for {@link ReadTransaction.scan | scan} when scanning over an index. When
 * scanning over and index you need to provide the `indexName` and the `start`
 * `key` is now a tuple consisting of secondary and primary key
 */
type ScanIndexOptions = {
    /** Only include results starting with the *secondary* keys starting with `prefix`. */
    prefix?: string | undefined;
    /** Only include up to `limit` results. */
    limit?: number | undefined;
    /** Do a {@link ReadTransaction.scan | scan} over a named index. The `indexName` is
     * the name of an index defined when creating the {@link Replicache} instance using
     * {@link ReplicacheOptions.indexes}. */
    indexName: string;
    /** When provided the scan starts at this key. */
    start?: {
        key: ScanOptionIndexedStartKey;
        /** Whether the `key` is exclusive or inclusive. */
        exclusive?: boolean | undefined;
    } | undefined;
};
/**
 * The key to start scanning at.
 *
 * If you are scanning the primary index (i.e., you did not specify
 * `indexName`), then pass a single string for this field, which is the key in
 * the primary index to scan at.
 *
 * If you are scanning a secondary index (i.e., you specified `indexName`), then
 * use the tuple form. In that case, `secondary` is the secondary key to start
 * scanning at, and `primary` (if any) is the primary key to start scanning at.
 */
type ScanOptionIndexedStartKey = readonly [secondary: string, primary?: string | undefined] | string;

type ScanKey = string | IndexKey;
interface ScanResult<K extends ScanKey, V> extends AsyncIterable<V> {
    /** The default AsyncIterable. This is the same as {@link values}. */
    [Symbol.asyncIterator](): AsyncIterableIteratorToArray<V>;
    /** Async iterator over the values of the {@link ReadTransaction.scan | scan} call. */
    values(): AsyncIterableIteratorToArray<V>;
    /**
     * Async iterator over the keys of the {@link ReadTransaction.scan | scan}
     * call. If the {@link ReadTransaction.scan | scan} is over an index the key
     * is a tuple of `[secondaryKey: string, primaryKey]`
     */
    keys(): AsyncIterableIteratorToArray<K>;
    /**
     * Async iterator over the entries of the {@link ReadTransaction.scan | scan}
     * call. An entry is a tuple of key values. If the
     * {@link ReadTransaction.scan | scan} is over an index the key is a tuple of
     * `[secondaryKey: string, primaryKey]`
     */
    entries(): AsyncIterableIteratorToArray<readonly [K, V]>;
    /** Returns all the values as an array. Same as `values().toArray()` */
    toArray(): Promise<V[]>;
}
/**
 * An interface that adds a {@link toArray} method to `AsyncIterableIterator`.
 *
 * Usage:
 *
 * ```ts
 * const keys: string[] = await rep.scan().keys().toArray();
 * ```
 */
interface AsyncIterableIteratorToArray<V> extends AsyncIterableIterator<V> {
    toArray(): Promise<V[]>;
}

type TransactionEnvironment = 'client' | 'server';
type TransactionLocation = TransactionEnvironment;
type TransactionReason = 'initial' | 'rebase' | 'authoritative';
/**
 * Basic deep readonly type. It works for {@link JSONValue}.
 */
type DeepReadonly<T> = T extends null | boolean | string | number | undefined ? T : DeepReadonlyObject<T>;
type DeepReadonlyObject<T> = {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
};
/**
 * ReadTransactions are used with {@link Replicache.query} and
 * {@link Replicache.subscribe} and allows read operations on the
 * database.
 */
interface ReadTransaction {
    readonly clientID: ClientID;
    /** @deprecated Use {@link ReadTransaction.location} instead. */
    readonly environment: TransactionLocation;
    readonly location: TransactionLocation;
    /**
     * Get a single value from the database. If the `key` is not present this
     * returns `undefined`.
     *
     * Important: The returned JSON is readonly and should not be modified. This
     * is only enforced statically by TypeScript and there are no runtime checks
     * for performance reasons. If you mutate the return value you will get
     * undefined behavior.
     */
    get(key: string): Promise<ReadonlyJSONValue | undefined>;
    get<T extends JSONValue>(key: string): Promise<DeepReadonly<T> | undefined>;
    /** Determines if a single `key` is present in the database. */
    has(key: string): Promise<boolean>;
    /** Whether the database is empty. */
    isEmpty(): Promise<boolean>;
    /**
     * Gets many values from the database. This returns a {@link ScanResult} which
     * implements `AsyncIterable`. It also has methods to iterate over the
     * {@link ScanResult.keys | keys} and {@link ScanResult.entries | entries}.
     *
     * If `options` has an `indexName`, then this does a scan over an index with
     * that name. A scan over an index uses a tuple for the key consisting of
     * `[secondary: string, primary: string]`.
     *
     * If the {@link ScanResult} is used after the `ReadTransaction` has been closed
     * it will throw a {@link TransactionClosedError}.
     *
     * Important: The returned JSON is readonly and should not be modified. This
     * is only enforced statically by TypeScript and there are no runtime checks
     * for performance reasons. If you mutate the return value you will get
     * undefined behavior.
     */
    scan(options: ScanIndexOptions): ScanResult<IndexKey, ReadonlyJSONValue>;
    scan(options?: ScanNoIndexOptions): ScanResult<string, ReadonlyJSONValue>;
    scan(options?: ScanOptions): ScanResult<IndexKey | string, ReadonlyJSONValue>;
    scan<V extends ReadonlyJSONValue>(options: ScanIndexOptions): ScanResult<IndexKey, DeepReadonly<V>>;
    scan<V extends ReadonlyJSONValue>(options?: ScanNoIndexOptions): ScanResult<string, DeepReadonly<V>>;
    scan<V extends ReadonlyJSONValue>(options?: ScanOptions): ScanResult<IndexKey | string, DeepReadonly<V>>;
}
/**
 * WriteTransactions are used with *mutators* which are registered using
 * {@link ReplicacheOptions.mutators} and allows read and write operations on the
 * database.
 */
interface WriteTransaction$1 extends ReadTransaction {
    /**
     * The ID of the mutation that is being applied.
     */
    readonly mutationID: number;
    /**
     * The reason for the transaction. This can be `initial`, `rebase` or `authoriative`.
     */
    readonly reason: TransactionReason;
    /**
     * Sets a single `value` in the database. The value will be frozen (using
     * `Object.freeze`) in debug mode.
     */
    set(key: string, value: ReadonlyJSONValue): Promise<void>;
    /**
     * @deprecated Use {@link WriteTransaction.set} instead.
     */
    put(key: string, value: ReadonlyJSONValue): Promise<void>;
    /**
     * Removes a `key` and its value from the database. Returns `true` if there was a
     * `key` to remove.
     */
    del(key: string): Promise<boolean>;
}
type MaybePromise<T> = T | Promise<T>;
type MutatorReturn<T extends ReadonlyJSONValue = ReadonlyJSONValue> = MaybePromise<T | void>;

/**
 * `AuthData` must include a `userID` which is unique stable identifier
 * for the user.
 * `AuthData` has a size limit of 6 KB.
 * `AuthData` is passed via {@link WriteTransaction.auth} to mutators
 * when they are run on the server, which can use it to supplement
 * mutator args and to authorize the mutation.
 */
type AuthData = ReadonlyJSONObject & {readonly userID: string};

/** Environment variables. */
type Env = {readonly [key: string]: string};

interface WriteTransaction extends WriteTransaction$1 {
  /**
   * When a mutator is run on the server, the `AuthData` for the connection
   * that pushed the mutation (i.e. the `AuthData` returned by the
   * {@link ReflectServerOptions.authHandler} when it authenticated the
   * connection).  Always undefined on the client. This can be used to implement
   * fine-grained server-side authorization of mutations.
   */
  readonly auth?: AuthData | undefined;

  /**
   * When a mutator is run on the server, `Env` contains environment variables.
   */
  readonly env?: Env | undefined;
}

type MutatorDefs = {
  [key: string]: (
    tx: WriteTransaction,
    // Not sure how to not use any here...
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    args?: any,
  ) => MutatorReturn;
};

/**
 * An `AuthHandler` should validate that the user authenticated by `auth` is
 * authorized to access the room with `roomID`. By 'access' we mean create or
 * connect to the room with `roomID`.
 * @return A promise which resolves to `AuthData` for the user if authentication
 * and authorization is successful. If authentication fails you can return
 * `null`. Exceptions and promise rejections are treated as authentication
 * failures.  The returned `AuthData` is passed via
 * {@link WriteTransaction.auth} to mutators when they are run on the server,
 * and can be used to implement fine-grained server-side authorization of
 * mutations.
 */
type AuthHandler = (auth: string, roomID: string, env: Env) => MaybePromise<AuthData | null>;

/**
 * @deprecated Use {@link ClientDisconnectHandler} instead.
 *
 */
type DisconnectHandler = ClientDisconnectHandler;
/**
 * A `ClientDisconnectHandler` can modify room state in response to a client
 * disconnecting from the room.  These changes will be synced to all clients of
 * the room just like mutator changes. `write.clientID` will be the id of the
 * disconnected client. `write.mutationID` will be -1.
 */
type ClientDisconnectHandler = (write: WriteTransaction) => Promise<void>;

interface DatadogLogSinkOptions {
    apiKey?: string | undefined;
    source?: string | undefined;
    service?: string | undefined;
    host?: string | undefined;
    version?: string | undefined;
    interval?: number | undefined;
    baseURL?: URL | undefined;
}
declare class DatadogLogSink implements LogSink {
    #private;
    constructor(options: DatadogLogSinkOptions);
    log(level: LogLevel, context: Context | undefined, ...args: unknown[]): void;
    flush(): Promise<void>;
}

declare function createWorkerDatadogLogSink(opts: WorkerDatadogLogSinkOptions): DatadogLogSink;
type WorkerDatadogLogSinkOptions = {
    apiKey: string;
    service?: string | undefined;
    host?: string | undefined;
    version?: string | undefined;
};

/**
 * A `ClientDeleteHandler` can modify room state in response to a client being
 * garbage collected from the room. A client gets "deleted" when it can no
 * longer reconnect. These changes will be synced to all clients of the room
 * just like mutator changes. `write.clientID` will be the id of the deleted
 * client. `write.mutationID` will be -1.
 */
type ClientDeleteHandler = (write: WriteTransaction) => Promise<void>;

/**
 * The `RoomStartHandler` is invoked when the room is started, before
 * any connections are accepted. This is useful for initializing or migrating
 * room state.
 *
 * Note that rooms may be shutdown when idle and restarted when connections
 * resume, upon which the `RoomStartHandler` will be invoked again. It can
 * therefore conceivably be used for running logic when a room resumes after
 * being idle; however, there are no guarantees of timing with respect to idleness.
 *
 * A succeeding RoomStartHandler (i.e. no error thrown) is guaranteed to be
 * invoked exactly once during the lifetime of a room (from start to shutdown).
 *
 * If the RoomStartHandler throws an error, it will be retried on the next
 * connection attempt. Connections will continue to fail until the RoomStartHandler
 * succeeds.
 *
 * As the transaction is not associated with any client, `write.clientID`
 * will be empty and `write.mutationID` will be -1.
 */
type RoomStartHandler = (write: WriteTransaction, roomID: string) => Promise<void>;

type DatadogMetricsOptions = {
    apiKey: string;
    service?: string | undefined;
};
interface ReflectServerOptions<MD extends MutatorDefs> {
    mutators: MD;
    authHandler?: AuthHandler | undefined;
    roomStartHandler?: RoomStartHandler | undefined;
    /**
     * @deprecated Use {@link onClientDisconnect} instead.
     */
    disconnectHandler?: DisconnectHandler | undefined;
    onClientDisconnect?: ClientDisconnectHandler | undefined;
    onClientDelete?: ClientDeleteHandler | undefined;
    /**
     * Where to send logs. By default logs are sent to `console.log`.
     */
    logSinks?: LogSink[] | undefined;
    /**
     * The level to log at. By default the level is 'info'.
     */
    logLevel?: LogLevel | undefined;
    /**
     * Options for reporting metrics to Datadog. By default metrics are sent nowhere.
     */
    datadogMetricsOptions?: DatadogMetricsOptions | undefined;
    /**
     * If `true`, outgoing network messages are sent before the writes they
     * reflect are confirmed to be durable. This enables lower latency but can
     * result in clients losing some mutations in the case of an untimely server
     * restart.
     *
     * Default is `false`.
     */
    allowUnconfirmedWrites?: boolean | undefined;
    /**
     * The max number of mutations that will be processed per turn.
     * Lowering this limit can prevent busy rooms from experiencing "overloaded"
     * exceptions at the cost of peer-to-peer latency.
     *
     * Default is `66` when `allowUnconfirmedWrites` is `false`, or `16` when
     * `allowUnconfirmedWrites` is `true`.
     */
    maxMutationsPerTurn?: number | undefined;
}
interface ReflectServerBaseEnv {
    roomDO: DurableObjectNamespace;
    authDO: DurableObjectNamespace;
    REFLECT_API_KEY: string;
}
type DurableObjectCtor<Env> = new (state: DurableObjectState, env: Env) => DurableObject;
/**
 * Creates the different parts of a reflect server.
 * @param makeOptions Function for creating the options for the server.
 *     IMPORTANT: Do not cache the return value from this function (or any of
 *     its parts, ie a log sink) across invocations. You should return a brand
 *     new instance each time this function is called.
 *     TODO: Add reference to CF bug.
 */
declare function createReflectServer<Env extends ReflectServerBaseEnv, MD extends MutatorDefs>(makeOptions: (env: Env) => ReflectServerOptions<MD>): {
    worker: ExportedHandler<Env>;
    RoomDO: DurableObjectCtor<Env>;
    AuthDO: DurableObjectCtor<Env>;
};

type BuildableOptionsEnv = LogFilterEnv & LogLevelEnv & DataDogLogEnv & DataDogMetricsEnv;
declare function newOptionsBuilder<Env, MD extends MutatorDefs>(base: OptionsMaker<Env, MD>): OptionsBuilder<Env, MD>;
declare function defaultConsoleLogSink<Env, MD extends MutatorDefs>(): OptionsAdder<Env, MD>;
type LogFilterEnv = {
    DISABLE_LOG_FILTERING?: string;
};
declare function logFilter<Env extends LogFilterEnv, MD extends MutatorDefs>(include: LogPredicate): OptionsAdder<Env, MD>;
type LogLevelEnv = {
    LOG_LEVEL?: string;
};
declare function logLevel<Env extends LogLevelEnv, MD extends MutatorDefs>(defaultLogLevel?: LogLevel): OptionsAdder<Env, MD>;
type DataDogLogEnv = {
    DATADOG_LOGS_API_KEY?: string;
    DATADOG_SERVICE_LABEL?: string;
};
declare function datadogLogging<Env extends DataDogLogEnv, MD extends MutatorDefs>(defaultServiceLabel: string, host?: string): OptionsAdder<Env, MD>;
type DataDogMetricsEnv = {
    DATADOG_METRICS_API_KEY?: string;
    DATADOG_SERVICE_LABEL?: string;
};
declare function datadogMetrics<Env extends DataDogMetricsEnv, MD extends MutatorDefs>(defaultServiceLabel: string, tags?: Record<string, string>): OptionsAdder<Env, MD>;
type LogPredicate = (level: LogLevel, context: Context | undefined, ...args: unknown[]) => boolean;
type OptionsMaker<Env, MD extends MutatorDefs> = (env: Env) => ReflectServerOptions<MD>;
type OptionsAdder<Env, MD extends MutatorDefs> = (options: ReflectServerOptions<MD>, env: Env) => ReflectServerOptions<MD>;
declare class OptionsBuilder<Env, MD extends MutatorDefs> {
    #private;
    constructor(base: OptionsMaker<Env, MD>);
    add(adder: OptionsAdder<Env, MD>): OptionsBuilder<Env, MD>;
    build(): OptionsMaker<Env, MD>;
}

export { AuthHandler, BuildableOptionsEnv, ClientDisconnectHandler, DisconnectHandler, ReflectServerBaseEnv, ReflectServerOptions, WorkerDatadogLogSinkOptions, createReflectServer, createWorkerDatadogLogSink, datadogLogging, datadogMetrics, defaultConsoleLogSink, logFilter, logLevel, newOptionsBuilder };
