import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { TransactionService, type TransactionOperation } from "./transaction.service";
import { getErrorMessage } from "@bff/core/utils/error.util";

export interface DistributedStep<TId extends string = string, TResult = unknown> {
  id: TId;
  description: string;
  execute: () => 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;
  error?: string;
  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"]>
    >;
  };

/**
 * 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 {
      // Execute steps sequentially
      for (const step of steps) {
        this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
          description: step.description,
          critical: step.critical,
        });

        try {
          const stepStartTime = Date.now();
          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: getErrorMessage(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: getErrorMessage(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: getErrorMessage(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,
      rollbackDatabaseOnExternalFailure = true,
      ...distributedOptions
    } = options;

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

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

    try {
      let databaseResult: TDatabaseResult | null = null;
      let externalResult: DistributedTransactionResult<TStepResults> | null = null;

      if (databaseFirst) {
        // Execute database operations first
        this.logger.debug(`Executing database operations [${transactionId}]`);
        const dbTransactionResult = await this.transactionService.executeTransaction(
          databaseOperation,
          {
            description: `${options.description} - Database Operations`,
            timeout: options.timeout,
          }
        );

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

        databaseResult = dbTransactionResult.data!;

        // Execute external operations
        this.logger.debug(`Executing external operations [${transactionId}]`);
        externalResult = await this.executeDistributedTransaction<TSteps, TStepResults>(
          externalSteps,
          {
            ...distributedOptions,
            description: distributedOptions.description || "External operations",
          }
        );

        if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
          // Note: Database transaction already committed, so we can't rollback automatically
          // This is a limitation of this approach - consider using saga pattern for true rollback
          this.logger.error(
            `External operations failed but database already committed [${transactionId}]`,
            {
              externalError: externalResult.error,
            }
          );
        }
      } else {
        // Execute external operations first
        this.logger.debug(`Executing external operations [${transactionId}]`);
        externalResult = await this.executeDistributedTransaction<TSteps, TStepResults>(
          externalSteps,
          {
            ...distributedOptions,
            description: distributedOptions.description || "External operations",
          }
        );

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

        // Execute database operations
        this.logger.debug(`Executing database operations [${transactionId}]`);
        const dbTransactionResult = await this.transactionService.executeTransaction(
          databaseOperation,
          {
            description: `${options.description} - Database Operations`,
            timeout: options.timeout,
          }
        );

        if (!dbTransactionResult.success) {
          // Rollback external operations
          const executedExternalSteps = Object.keys(externalResult.stepResults);
          await this.executeRollbacks(externalSteps, executedExternalSteps, transactionId);
          throw new Error(dbTransactionResult.error || "Database transaction failed");
        }

        databaseResult = dbTransactionResult.data!;
      }

      const duration = Date.now() - startTime;

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

      return {
        success: true,
        data: databaseResult,
        duration,
        stepsExecuted: externalResult?.stepsExecuted || 0,
        stepsRolledBack: 0,
        stepResults: (externalResult?.stepResults ?? ({} as StepResultMap<TSteps>)) as TStepResults,
        failedSteps: externalResult?.failedSteps || [],
      };
    } catch (error) {
      const duration = Date.now() - startTime;

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

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

  private async executeStepWithTimeout<TResult>(
    step: DistributedStep<string, TResult>,
    timeout: number
  ): Promise<TResult> {
    return (await Promise.race([
      step.execute(),
      new Promise<never>((_, reject) => {
        setTimeout(() => {
          reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
        }, timeout);
      }),
    ])) as TResult;
  }

  private async executeRollbacks<TSteps extends readonly DistributedStep[]>(
    steps: TSteps,
    executedSteps: readonly string[],
    transactionId: string
  ): Promise<number> {
    this.logger.warn(`Executing rollbacks for ${executedSteps.length} steps [${transactionId}]`);

    let rollbacksExecuted = 0;

    // Execute rollbacks in reverse order (LIFO)
    for (let i = executedSteps.length - 1; i >= 0; i--) {
      const stepId = executedSteps[i];
      const step = steps.find(s => s.id === stepId);

      if (step?.rollback) {
        try {
          this.logger.debug(`Executing rollback for step: ${stepId} [${transactionId}]`);
          await step.rollback();
          rollbacksExecuted++;
          this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
        } catch (rollbackError) {
          this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
            error: getErrorMessage(rollbackError),
          });
          // Continue with other rollbacks even if one fails
        }
      }
    }

    this.logger.log(`Completed ${rollbacksExecuted} rollbacks [${transactionId}]`);
    return rollbacksExecuted;
  }

  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;

    // attempt counter represents completed attempts; loop until success or attempts exhausted
    while (attempt < totalAttempts) {
      try {
        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: getErrorMessage(error),
        });
      }
    }

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

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