import { Injectable, Inject, InternalServerErrorException } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { TransactionService, type TransactionOperation } from "./transaction.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { RollbackCoordinator } from "./rollback-coordinator.js";

export interface DistributedStep<TId extends string = string, TResult = unknown> {
  id: TId;
  description: string;
  execute: (signal?: AbortSignal) => Promise<TResult>;
  rollback?: () => Promise<void>;
  critical?: boolean; // If true, failure stops entire transaction
  retryable?: boolean; // If true, step can be retried on failure
}

export interface DistributedTransactionOptions {
  description: string;
  timeout?: number;
  maxRetries?: number;
  continueOnNonCriticalFailure?: boolean;
}

export interface DistributedTransactionResult<
  TData = unknown,
  TStepResults extends StepResultMap = StepResultMap,
> {
  success: boolean;
  data?: TData | undefined;
  error?: string | undefined;
  duration: number;
  stepsExecuted: number;
  stepsRolledBack: number;
  stepResults: TStepResults;
  failedSteps: string[];
}

export type StepResultMap<TSteps extends readonly DistributedStep[] = readonly DistributedStep[]> =
  {
    [K in TSteps[number]["id"]]?: Awaited<
      ReturnType<Extract<TSteps[number], { id: K }>["execute"]>
    >;
  };

interface HybridTransactionContext<TDatabaseResult, TSteps extends readonly DistributedStep[]> {
  databaseOperation: TransactionOperation<TDatabaseResult>;
  externalSteps: TSteps;
  options: Omit<
    DistributedTransactionOptions & { rollbackDatabaseOnExternalFailure?: boolean },
    "databaseFirst"
  >;
  transactionId: string;
}

interface HybridExecutionResult<TDatabaseResult, TStepResults extends StepResultMap> {
  databaseResult: TDatabaseResult;
  externalResult: DistributedTransactionResult<TStepResults>;
}

/**
 * Service for managing distributed transactions across multiple external systems
 * Provides coordination between database operations and external API calls
 */
@Injectable()
export class DistributedTransactionService {
  constructor(
    private readonly transactionService: TransactionService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Execute a distributed transaction with multiple steps across different systems
   *
   * @example
   * ```typescript
   * const result = await this.distributedTransactionService.executeDistributedTransaction([
   *   {
   *     id: 'sf_status_update',
   *     description: 'Update Salesforce order status to Activating',
   *     execute: async () => {
   *       return await this.salesforceService.updateOrder({
   *         Id: sfOrderId,
   *         Status: 'Activating'
   *       });
   *     },
   *     rollback: async () => {
   *       await this.salesforceService.updateOrder({
   *         Id: sfOrderId,
   *         Status: 'Draft'
   *       });
   *     },
   *     critical: true
   *   },
   *   {
   *     id: 'whmcs_create_order',
   *     description: 'Create order in WHMCS',
   *     execute: async () => {
   *       return await this.whmcsOrderService.createOrder(orderData);
   *     },
   *     rollback: async () => {
   *       if (stepResults.whmcs_create_order?.orderId) {
   *         await this.whmcsOrderService.cancelOrder(stepResults.whmcs_create_order.orderId);
   *       }
   *     },
   *     critical: true
   *   }
   * ], {
   *   description: 'Order fulfillment workflow',
   *   timeout: 120000
   * });
   * ```
   */
  async executeDistributedTransaction<
    TSteps extends readonly DistributedStep[],
    TStepResults extends StepResultMap<TSteps> = StepResultMap<TSteps>,
  >(
    steps: TSteps,
    options: DistributedTransactionOptions
  ): Promise<DistributedTransactionResult<TStepResults>> {
    const {
      description,
      timeout = 120000, // 2 minutes default for distributed operations
      maxRetries: configuredMaxRetries = 1,
      continueOnNonCriticalFailure = false,
    } = options;
    const maxRetries = Math.max(0, Math.floor(configuredMaxRetries ?? 0));

    const transactionId = this.generateTransactionId();
    const startTime = Date.now();

    this.logger.log(`Starting distributed transaction [${transactionId}]`, {
      description,
      stepsCount: steps.length,
      timeout,
    });

    const stepResults: StepResultMap<TSteps> = {} as StepResultMap<TSteps>;
    const executedSteps: string[] = [];
    const failedSteps: string[] = [];

    try {
      for (const step of steps) {
        this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
          description: step.description,
          critical: step.critical,
        });

        try {
          const stepStartTime = Date.now();
          // eslint-disable-next-line no-await-in-loop -- Steps must execute sequentially as later steps may depend on earlier step results
          const result = await this.executeStepWithRetry(step, timeout, maxRetries, transactionId);
          const stepDuration = Date.now() - stepStartTime;

          const key = step.id as keyof StepResultMap<TSteps>;
          Reflect.set(stepResults, key, result);
          executedSteps.push(step.id);

          this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
            duration: stepDuration,
          });
        } catch (stepError) {
          const errorInstance =
            stepError instanceof Error ? stepError : new Error(String(stepError));
          failedSteps.push(step.id);

          this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
            error: extractErrorMessage(errorInstance),
            critical: step.critical,
            retryable: step.retryable,
          });

          // If it's a critical step, stop the entire transaction
          if (step.critical) {
            throw stepError;
          }

          // If we're not continuing on non-critical failures, stop
          if (!continueOnNonCriticalFailure) {
            throw stepError;
          }

          // Otherwise, log and continue
          this.logger.warn(
            `Continuing despite non-critical step failure: ${step.id} [${transactionId}]`
          );
        }
      }

      const duration = Date.now() - startTime;

      this.logger.log(`Distributed transaction completed successfully [${transactionId}]`, {
        description,
        duration,
        stepsExecuted: executedSteps.length,
        failedSteps: failedSteps.length,
      });

      return {
        success: true,
        data: stepResults as TStepResults,
        duration,
        stepsExecuted: executedSteps.length,
        stepsRolledBack: 0,
        stepResults: stepResults as TStepResults,
        failedSteps,
      } satisfies DistributedTransactionResult<TStepResults>;
    } catch (error) {
      const duration = Date.now() - startTime;

      this.logger.error(`Distributed transaction failed [${transactionId}]`, {
        description,
        error: extractErrorMessage(error),
        duration,
        stepsExecuted: executedSteps.length,
        failedSteps: failedSteps.length,
      });

      // Execute rollbacks for completed steps
      const rollbacksExecuted = await this.executeRollbacks(steps, executedSteps, transactionId);

      return {
        success: false,
        error: extractErrorMessage(error),
        duration,
        stepsExecuted: executedSteps.length,
        stepsRolledBack: rollbacksExecuted,
        stepResults: stepResults as TStepResults,
        failedSteps,
      } satisfies DistributedTransactionResult<TStepResults>;
    }
  }

  /**
   * Execute a hybrid transaction that combines database operations with external system calls
   */
  async executeHybridTransaction<
    TDatabaseResult,
    TSteps extends readonly DistributedStep[],
    TStepResults extends StepResultMap<TSteps> = StepResultMap<TSteps>,
  >(
    databaseOperation: TransactionOperation<TDatabaseResult>,
    externalSteps: TSteps,
    options: DistributedTransactionOptions & {
      databaseFirst?: boolean;
      rollbackDatabaseOnExternalFailure?: boolean;
    }
  ): Promise<DistributedTransactionResult<TDatabaseResult, TStepResults>> {
    const { databaseFirst = true, ...restOptions } = options;
    const transactionId = this.generateTransactionId();
    const startTime = Date.now();

    this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
      description: options.description,
      databaseFirst,
      externalStepsCount: externalSteps.length,
    });

    const context: HybridTransactionContext<TDatabaseResult, TSteps> = {
      databaseOperation,
      externalSteps,
      options: restOptions,
      transactionId,
    };

    try {
      const { databaseResult, externalResult } = databaseFirst
        ? await this.executeDatabaseFirst<TDatabaseResult, TSteps, TStepResults>(context)
        : await this.executeExternalFirst<TDatabaseResult, TSteps, TStepResults>(context);

      return this.buildHybridSuccessResult<TDatabaseResult, TSteps, TStepResults>(
        startTime,
        options.description,
        transactionId,
        { databaseResult, externalResult }
      );
    } catch (error) {
      return this.buildHybridFailureResult<TDatabaseResult, TStepResults>(
        startTime,
        options.description,
        transactionId,
        error
      );
    }
  }

  private async executeDatabaseFirst<
    TDatabaseResult,
    TSteps extends readonly DistributedStep[],
    TStepResults extends StepResultMap<TSteps>,
  >(
    context: HybridTransactionContext<TDatabaseResult, TSteps>
  ): Promise<HybridExecutionResult<TDatabaseResult, TStepResults>> {
    const { databaseOperation, externalSteps, options, transactionId } = context;
    const rollbackDatabaseOnExternalFailure = options.rollbackDatabaseOnExternalFailure ?? true;

    this.logger.debug(`Executing database operations [${transactionId}]`);
    const databaseResult = await this.executeDatabaseOperation(
      databaseOperation,
      options.description,
      options.timeout
    );

    this.logger.debug(`Executing external operations [${transactionId}]`);
    const externalResult = await this.executeDistributedTransaction<TSteps, TStepResults>(
      externalSteps,
      this.buildDistributedOptions(options)
    );

    if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
      this.logger.error(
        `External operations failed but database already committed [${transactionId}]`,
        { externalError: externalResult.error }
      );
    }

    return { databaseResult, externalResult };
  }

  private async executeExternalFirst<
    TDatabaseResult,
    TSteps extends readonly DistributedStep[],
    TStepResults extends StepResultMap<TSteps>,
  >(
    context: HybridTransactionContext<TDatabaseResult, TSteps>
  ): Promise<HybridExecutionResult<TDatabaseResult, TStepResults>> {
    const { databaseOperation, externalSteps, options, transactionId } = context;

    this.logger.debug(`Executing external operations [${transactionId}]`);
    const externalResult = await this.executeDistributedTransaction<TSteps, TStepResults>(
      externalSteps,
      this.buildDistributedOptions(options)
    );

    if (!externalResult.success) {
      throw new InternalServerErrorException(externalResult.error || "External operations failed");
    }

    this.logger.debug(`Executing database operations [${transactionId}]`);
    try {
      const databaseResult = await this.executeDatabaseOperation(
        databaseOperation,
        options.description,
        options.timeout
      );
      return { databaseResult, externalResult };
    } catch (dbError) {
      const executedExternalSteps = Object.keys(externalResult.stepResults);
      await this.executeRollbacks(externalSteps, executedExternalSteps, transactionId);
      throw dbError;
    }
  }

  private async executeDatabaseOperation<TDatabaseResult>(
    operation: TransactionOperation<TDatabaseResult>,
    description: string,
    timeout?: number
  ): Promise<TDatabaseResult> {
    const result = await this.transactionService.executeTransaction(operation, {
      description: `${description} - Database Operations`,
      ...(timeout !== undefined && { timeout }),
    });

    if (!result.success) {
      throw new InternalServerErrorException(result.error || "Database transaction failed");
    }

    return result.data!;
  }

  private buildDistributedOptions(
    options: HybridTransactionContext<unknown, readonly DistributedStep[]>["options"]
  ): DistributedTransactionOptions {
    const result: DistributedTransactionOptions = {
      description: options.description || "External operations",
    };
    if (options.timeout !== undefined) result.timeout = options.timeout;
    if (options.maxRetries !== undefined) result.maxRetries = options.maxRetries;
    if (options.continueOnNonCriticalFailure !== undefined) {
      result.continueOnNonCriticalFailure = options.continueOnNonCriticalFailure;
    }
    return result;
  }

  private buildHybridSuccessResult<
    TDatabaseResult,
    TSteps extends readonly DistributedStep[],
    TStepResults extends StepResultMap<TSteps>,
  >(
    startTime: number,
    description: string,
    transactionId: string,
    results: HybridExecutionResult<TDatabaseResult, TStepResults>
  ): DistributedTransactionResult<TDatabaseResult, TStepResults> {
    const duration = Date.now() - startTime;
    const { databaseResult, externalResult } = results;

    this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
      description,
      duration,
    });

    return {
      success: true,
      data: databaseResult,
      duration,
      stepsExecuted: externalResult?.stepsExecuted || 0,
      stepsRolledBack: 0,
      stepResults: (externalResult?.stepResults ?? {}) as TStepResults,
      failedSteps: externalResult?.failedSteps || [],
    };
  }

  private buildHybridFailureResult<TDatabaseResult, TStepResults extends StepResultMap>(
    startTime: number,
    description: string,
    transactionId: string,
    error: unknown
  ): DistributedTransactionResult<TDatabaseResult, TStepResults> {
    const duration = Date.now() - startTime;

    this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
      description,
      error: extractErrorMessage(error),
      duration,
    });

    return {
      success: false,
      error: extractErrorMessage(error),
      duration,
      stepsExecuted: 0,
      stepsRolledBack: 0,
      stepResults: {} as TStepResults,
      failedSteps: [],
    };
  }

  private async executeStepWithTimeout<TResult>(
    step: DistributedStep<string, TResult>,
    timeout: number
  ): Promise<TResult> {
    const controller = new AbortController();
    let timer: ReturnType<typeof setTimeout> | null = null;
    try {
      const timeoutPromise = new Promise<never>((_, reject) => {
        timer = setTimeout(() => {
          controller.abort();
          reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
        }, timeout);
      });

      return (await Promise.race([step.execute(controller.signal), timeoutPromise])) as TResult;
    } finally {
      if (timer) {
        clearTimeout(timer);
      }
    }
  }

  private async executeRollbacks<TSteps extends readonly DistributedStep[]>(
    steps: TSteps,
    executedSteps: readonly string[],
    transactionId: string
  ): Promise<number> {
    // Build rollback actions only for executed steps that have a rollback function
    const actions = executedSteps
      .map(stepId => {
        const step = steps.find(s => s.id === stepId);
        if (!step?.rollback) return null;
        return {
          execute: step.rollback,
          label: `step: ${stepId}`,
        };
      })
      .filter((a): a is NonNullable<typeof a> => a !== null);

    return RollbackCoordinator.executeInReverse(actions, transactionId, this.logger);
  }

  private async executeStepWithRetry<TResult>(
    step: DistributedStep<string, TResult>,
    timeout: number,
    maxRetries: number,
    transactionId: string
  ): Promise<TResult> {
    const totalAttempts = step.retryable ? Math.max(1, maxRetries + 1) : 1;
    let attempt = 0;

    while (attempt < totalAttempts) {
      try {
        // eslint-disable-next-line no-await-in-loop -- Retries must be sequential; we only continue after previous attempt fails
        return await this.executeStepWithTimeout(step, timeout);
      } catch (error) {
        attempt++;
        if (attempt >= totalAttempts) {
          throw error;
        }

        this.logger.warn(`Retrying step: ${step.id} [${transactionId}]`, {
          attempt: attempt + 1,
          maxAttempts: totalAttempts,
          error: extractErrorMessage(error),
        });
      }
    }

    throw new InternalServerErrorException(
      `Step ${step.id} failed after ${totalAttempts} attempts`
    );
  }

  private generateTransactionId(): string {
    return `dtx_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
  }
}
