import { Awaitable, Fn, AnyFunction, RetryOnErrorConfig } from '@andrew_l/toolkit';
import { ClientSession, ClientSessionOptions } from 'mongodb';

type OnMongoSessionCommittedResult<T> = {
    /**
     * Executes the provided function upon transaction commit.
     *
     * Returns `T` if the transaction is committed and the function completes successfully.
     *
     * Returns `undefined` if the transaction is explicitly aborted or ends without committing.
     *
     * Rejects if the function throws an error.
     */
    promise: Promise<T | undefined>;
    cancel: () => void;
};
/**
 * Executes the provided function upon transaction commit.
 *
 * Returns `T` if the transaction is committed and the function completes successfully.
 *
 * Returns `false` if the transaction ends without committing.
 *
 * Rejects if the function throws an error.
 *
 * @example
 * const { promise } = onTransactionCommitted(async () => {
 *   console.info('Transaction committed successfully!');
 *   return Math.random(); // Random value generated after commit
 * });
 *
 * promise.then(result => {
 *   if (result !== false) {
 *     console.info('Handler result:', result); // e.g., Handler result: 0.07576196837476501
 *   }
 * });
 *
 * @group Hooks
 */
declare function onMongoSessionCommitted<T>(fn: () => Awaitable<T>): OnMongoSessionCommittedResult<T>;
declare function onMongoSessionCommitted<T>(session: ClientSession, fn: () => Awaitable<T>): OnMongoSessionCommittedResult<T>;

/**
 * Returns the current transaction session if executed within `withMongoTransaction()` otherwise returns `null`
 *
 * @example
 * async function createAlert() {
 *   const session = useMongoSession();
 *
 *   await db.alerts.insertOne(
 *     { title: 'Order Created' },
 *     { session: session ?? undefined }
 *   );
 * }
 *
 * @group Hooks
 */
declare function useMongoSession(): ClientSession | null;

interface TransactionEffect {
    /**
     * Specifies when the transaction effect should run:
     *
     * `pre` -  execute immediately
     *
     *  `post` - execute before transaction commit
     *
     * @default: "pre"
     */
    flush: 'pre' | 'post';
    /**
     * Setup effect function. You can return a cleanup callback to be used as a rollback.
     */
    setup: EffectCallback;
    /**
     * Cleanup function.
     */
    cleanup?: EffectCleanup;
    /**
     * Useful for debugging execution logs.
     */
    name?: string;
    dependencies?: readonly any[];
}
type EffectCallback = () => Awaitable<EffectCleanup | void>;
type EffectCleanup = () => Awaitable<void>;
type OnCommittedCallback = () => Awaitable<void>;
type OnRollbackCallback = () => Awaitable<void>;

type UseTransactionEffectOptions = Partial<Pick<TransactionEffect, 'name' | 'flush' | 'dependencies'>>;
/**
 * Executes a transactional effect with cleanup on error or rollback.
 *
 * Ensures the `callback` function is executed only once per transaction, even during retries.
 * On errors or dependency changes, the cleanup logic is invoked before re-execution to maintain consistency.
 *
 * @param setup A function defining the transactional effect. It is guaranteed to run once per transaction
 *              and may be re-executed after cleanup if dependencies change.
 *
 * @example
 * const confirmOrder = withMongoTransaction({
 *   connection: () => mongoose.connection.getClient(),
 *   async fn(session) {
 *     // Register an alert as a transactional effect
 *     await useTransactionEffect(async () => {
 *       const alertId = await alertService.create({
 *         title: `Order Confirmed: ${orderId}`,
 *       });
 *
 *       // Define cleanup logic to remove the alert on rollback
 *       return () => alertService.removeById(alertId);
 *     });
 *
 *     // Simulate order processing (e.g., database updates)
 *     await db
 *       .collection('orders')
 *       .updateOne({ orderId }, { $set: { status: 'confirmed' } }, { session });
 *
 *     // Simulate an error to test rollback
 *     throw new Error('Simulated transaction failure');
 *   },
 * });
 *
 * @group Hooks
 */
declare function useTransactionEffect(setup: TransactionEffect['setup'], options?: UseTransactionEffectOptions): Promise<void>;

/**
 * Registers a callback to be executed upon transaction commitment, with support
 * for dependency-based updates.
 *
 * This function is used within a transaction scope to perform specific actions
 * when a transaction is committed. If dependencies are provided, the callback
 * is re-registered only if the dependencies have changed. Otherwise, the
 * callback is registered unconditionally.
 *
 * @param {OnCommittedCallback} callback - The function to be executed upon
 *   transaction commitment.
 * @param {readonly any[]} [dependencies=[]] - An optional array of dependencies
 *   to determine if the callback should be re-registered. If the dependencies
 *   differ from the previously registered ones, the callback is updated.
 * @returns {Fn} A cleanup function to cancel event listener.
 *
 * @example
 * // Basic usage without dependencies
 * onCommitted(() => {
 *   console.log('Transaction committed!');
 * });
 *
 * @example
 * // Using dependencies
 * count++;
 * onCommitted(() => {
 *   console.log(`Commit #${count}`);
 * }, [count]);
 *
 * @example
 * //  Cancel by request
 * const cancel = onCommitted(() => {
 *   console.log('This will run only once!');
 * });
 *
 * if (orderReceived) {
 *   cancel(); // Prevents onCommitted from running
 * }
 *
 * @group Hooks
 */
declare function onCommitted(callback: OnCommittedCallback, dependencies?: readonly any[]): Fn;

/**
 * Registers a callback to be executed upon transaction rollback, with support
 * for dependency-based updates.
 *
 * This function is used within a transaction scope to perform specific actions
 * when a transaction is rolled back. If dependencies are provided, the callback
 * is re-registered only if the dependencies have changed. Otherwise, the
 * callback is registered unconditionally.
 *
 * @param {OnRollbackCallback} callback - The function to be executed upon
 *   transaction rollback.
 * @param {readonly any[]} [dependencies=[]] - An optional array of dependencies
 *   to determine if the callback should be re-registered. If the dependencies
 *   differ from the previously registered ones, the callback is updated.
 * @returns {Fn} A cleanup function to cancel event listener.
 *
 * @example
 * // Basic usage without dependencies
 * onRollback(() => {
 *   console.log('Transaction rolled back!');
 * });
 *
 * @example
 * // Using dependencies
 * count++;
 * onRollback(() => {
 *   console.log(`Rollback detected, flag is ${flag}`);
 * }, [count]);
 *
 * @example
 * // Cancel by request
 * const cancel = onRollback(() => {
 *   console.log('This will run only once on rollback!');
 * });
 *
 * if (orderReceived) {
 *   cancel(); // Prevents onRollback from running
 * }
 *
 * @group Hooks
 */
declare function onRollback(callback: OnRollbackCallback, dependencies?: readonly any[]): Fn;

interface MongoClientLike {
    startSession(options: Record<string, any>): ClientSessionLike;
}
interface ClientSessionLike {
    withTransaction(fn: AnyFunction): Promise<any>;
    endSession(): Promise<void>;
}
type ConnectionValue = MongoClientLike | (() => Awaitable<MongoClientLike>);
type Callback<T, K = any, Args extends Array<any> = any[]> = (this: K, session: ClientSession, ...args: Args) => Awaitable<T>;
interface WithMongoTransactionOptions<T, K = any, Args extends Array<any> = any[]> {
    /**
     * Mongodb connection getter
     */
    connection: ConnectionValue;
    /**
     * Transaction session options
     *
     * @default: {
     *   defaultTransactionOptions: {
     *     readPreference: 'primary',
     *     readConcern: { level: 'local' },
     *     writeConcern: { w: 'majority' },
     *   }
     * }
     */
    sessionOptions?: ClientSessionOptions;
    /**
     * Configures a timeoutMS expiry for the entire withTransactionCallback.
     *
     * @remarks
     * - The remaining timeout will not be applied to callback operations that do not use the ClientSession.
     * - Overriding timeoutMS for operations executed using the explicit session inside the provided callback will result in a client-side error.
     */
    timeoutMS?: number;
    /**
     * Transaction function that will be executed
     *
     * ⚠️ Possible several times!
     */
    fn: Callback<T, K, Args>;
}
type WithMongoTransactionWrapped<T, K = any, Args extends Array<any> = any[]> = (this: K, ...args: Args) => Promise<T>;
/**
 * Runs a provided callback within a transaction, retrying either the commitTransaction operation or entire transaction as needed (and when the error permits) to better ensure that the transaction can complete successfully.
 *
 * Passes the session as the function's first argument or via `useMongoSession()` hook
 *
 * @example
 * const executeTransaction = withMongoTransaction({
 *   connection: () => mongoose.connection.getClient(),
 *   async fn() {
 *     const session = useMongoSession();
 *     const orders = mongoose.connection.collection('orders');
 *
 *     const { modifiedCount } = await orders.updateMany(
 *       { status: 'pending' },
 *       { $set: { status: 'confirmed' } },
 *       { session },
 *     );
 *   },
 * });
 *
 * @group Main
 */
declare function withMongoTransaction<T, K = any, Args extends Array<any> = any[]>(options: WithMongoTransactionOptions<T, K, Args>): WithMongoTransactionWrapped<T, K, Args>;
/**
 * Runs a provided callback within a transaction, retrying either the commitTransaction operation or entire transaction as needed (and when the error permits) to better ensure that the transaction can complete successfully.
 *
 * Passes the session as the function's first argument or via `useMongoSession()` hook
 *
 * @example
 * const executeTransaction = withMongoTransaction(mongoose.connection.getClient(), async () => {
 *   const session = useMongoSession();
 *   const orders = mongoose.connection.collection('orders');
 *
 *   const { modifiedCount } = await orders.updateMany(
 *     { status: 'pending' },
 *     { $set: { status: 'confirmed' } },
 *     { session },
 *   );
 * });
 */
declare function withMongoTransaction<T, K = any, Args extends Array<any> = any[]>(connection: ConnectionValue, fn: Callback<T, K, Args>, options?: Omit<WithMongoTransactionOptions<any>, 'fn' | 'connection'>): WithMongoTransactionWrapped<T, K, Args>;

interface WithTransactionOptions extends Partial<RetryOnErrorConfig> {
}
/**
 * Wraps a function with transaction context, enabling retry logic and transactional effects.
 *
 * The wrapped function may be executed multiple times (up to `maxRetriesNumber`) to ensure
 * all side effects complete successfully. If the retries are exhausted without success,
 * registered cleanup functions will be executed to undo any applied effects.
 *
 * This utility is useful for managing transactional side effects, such as
 * updates to external systems, and ensures proper cleanup in case of failure.
 *
 * Additionally, this enables hooks like `useTransactionEffect()`, which allows
 * defining effects with automatic rollback mechanisms.
 *
 * @param fn - The target function to wrap with transaction handling.
 * @param [options] - Configuration options for the transaction handling.
 * @param [options.beforeRetryCallback] - An optional callback to execute before each retry attempt.
 * @param [options.shouldRetryBasedOnError] - A predicate to determine if a retry should occur based on the thrown error. Defaults to always retry.
 * @param [options.maxRetriesNumber=5] - The maximum number of retries before failing the transaction. Defaults to 5.
 * @param [options.delayFactor=0] - A multiplier for the delay between retries. Default is 0 (no exponential backoff).
 * @param [options.delayMaxMs=1000] - The maximum delay between retries, in milliseconds. Defaults to 1000 ms.
 * @param [options.delayMinMs=100] - The minimum delay between retries, in milliseconds. Defaults to 100 ms.
 *
 * @example
 * const confirmOrder = withTransaction(async (orderId) => {
 *   // Register Alert
 *   await useTransactionEffect(async () => {
 *     const alertId = await alertService.create({
 *       title: 'New Order: ' + orderId,
 *     });
 *
 *     return () => alertService.removeById(alertId); // Cleanup in case of failure
 *   });
 *
 *   // Update Statistics
 *   await useTransactionEffect(async () => {
 *     await statService.increment('orders_amount', 1);
 *
 *     return () => statService.decrement('orders_amount', 1); // Cleanup in case of failure
 *   });
 *
 *   // Simulate failure to trigger rollback
 *   throw new Error('Cancel transaction.');
 * });
 *
 * @group Main
 */
declare function withTransaction<T, K = any, Args extends Array<any> = any[]>(fn: (this: K, ...args: Args) => T, { beforeRetryCallback, shouldRetryBasedOnError, maxAttempts, maxRetriesNumber, delayFactor, delayMaxMs, delayMinMs, }?: WithTransactionOptions): (this: K, ...args: Args) => Promise<Awaited<T>>;

interface TransactionControlled<T, K = any, Args extends Array<any> = any[]> {
    run: (this: K, ...args: Args) => Promise<void>;
    commit: () => Promise<void>;
    rollback: () => Promise<void>;
    result: Readonly<T | undefined>;
    error: Readonly<Error | undefined>;
    active: boolean;
}
/**
 * Wraps a function and returns a `TransactionControlled` interface, allowing manual control
 * over transaction commit and rollback operations.
 *
 * This provides finer-grained control over the transaction lifecycle, enabling users to
 * explicitly commit or rollback a transaction based on custom logic. It's especially useful
 * in scenarios where transactional state or conditions need to be externally determined.
 *
 * @example
 * const t = withTransactionControlled(async (userId) => {
 *   await useTransactionEffect(async () => {
 *     await db.users.updateById(userId, { premium: true });
 *
 *     return () => db.users.updateById(userId, { premium: false })
 *   });
 *
 *   const user = await db.users.findById(userId);
 *
 *   return user;
 * });
 *
 *
 * await t.run();
 *
 * // Remove premium when no subscriptions
 * if (t.result.activeSubscriptions > 0) {
 *   await t.commit();
 * } else {
 *   await t.rollback();
 * }
 *
 * @group Main
 */
declare function withTransactionControlled<T, K = any, Args extends Array<any> = any[]>(fn: (this: K, ...args: Args) => T): TransactionControlled<Awaited<T>, K, Args>;

export { type OnMongoSessionCommittedResult, type TransactionControlled, type UseTransactionEffectOptions, type WithMongoTransactionOptions, type WithTransactionOptions, onCommitted, onMongoSessionCommitted, onRollback, useMongoSession, useTransactionEffect, withMongoTransaction, withTransaction, withTransactionControlled };
