import { InternalServerErrorException } from "@nestjs/common";
import type { Logger } from "nestjs-pino";
import { extractErrorMessage } from "./error.util.js";

/**
 * Operation criticality levels determine error handling behavior
 */
export enum OperationCriticality {
  /** Throw error - workflow cannot continue without this operation */
  CRITICAL = "critical",

  /** Log error, return fallback - continue in degraded mode */
  DEGRADABLE = "degradable",

  /** Log warning, return fallback - expected to fail sometimes */
  OPTIONAL = "optional",

  /** Debug log only - non-essential operation */
  SILENT = "silent",
}

/**
 * Options for safe operation execution
 */
export interface SafeOperationOptions<T> {
  /** How critical is this operation? Determines error handling behavior */
  criticality: OperationCriticality;

  /** Value to return if operation fails (required for non-CRITICAL) */
  fallback?: T;

  /** Context description for logging */
  context: string;

  /** Logger instance for error reporting */
  logger: Logger;

  /** Additional metadata for logging */
  metadata?: Record<string, unknown>;

  /** Exception types to rethrow as-is (only applicable when criticality is CRITICAL) */
  rethrow?: Array<abstract new (...args: never[]) => Error>;

  /** Custom message for InternalServerErrorException wrapper (only applicable when criticality is CRITICAL) */
  fallbackMessage?: string;
}

/**
 * Result of a safe operation
 */
export interface SafeOperationResult<T> {
  /** Whether the operation succeeded */
  success: boolean;

  /** The result value (from operation or fallback) */
  value: T;

  /** Error message if operation failed */
  error?: string;
}

/**
 * Execute an operation with criticality-based error handling
 *
 * @example
 * // Critical operation - throws on error
 * const user = await safeOperation(
 *   () => userService.findById(id),
 *   {
 *     criticality: OperationCriticality.CRITICAL,
 *     context: 'Load user for authentication',
 *     logger: this.logger,
 *   }
 * );
 *
 * @example
 * // Optional operation - returns fallback on error
 * const orders = await safeOperation(
 *   () => orderService.getOrders(userId),
 *   {
 *     criticality: OperationCriticality.OPTIONAL,
 *     fallback: [],
 *     context: 'Load orders for dashboard',
 *     logger: this.logger,
 *   }
 * );
 */
export async function safeOperation<T>(
  executor: () => Promise<T>,
  options: SafeOperationOptions<T>
): Promise<T> {
  const {
    criticality,
    fallback,
    context,
    logger,
    metadata = {},
    rethrow,
    fallbackMessage,
  } = options;

  try {
    return await executor();
  } catch (error) {
    const errorMessage = extractErrorMessage(error);
    const logPayload = {
      context,
      error: errorMessage,
      criticality,
      ...metadata,
    };

    switch (criticality) {
      case OperationCriticality.CRITICAL:
        logger.error(logPayload, `Critical operation failed: ${context}`);
        if (rethrow) {
          for (const ExceptionType of rethrow) {
            if (error instanceof ExceptionType) {
              throw error;
            }
          }
          throw new InternalServerErrorException(fallbackMessage ?? `${context} failed`);
        }
        throw error;

      case OperationCriticality.DEGRADABLE:
        logger.error(logPayload, `Degradable operation failed: ${context}`);
        if (fallback === undefined) {
          throw new Error(`Fallback required for degradable operation: ${context}`);
        }
        return fallback;

      case OperationCriticality.OPTIONAL:
        logger.warn(logPayload, `Optional operation failed: ${context}`);
        if (fallback === undefined) {
          throw new Error(`Fallback required for optional operation: ${context}`);
        }
        return fallback;

      case OperationCriticality.SILENT:
        logger.debug(logPayload, `Silent operation failed: ${context}`);
        if (fallback === undefined) {
          throw new Error(`Fallback required for silent operation: ${context}`);
        }
        return fallback;

      default:
        throw error;
    }
  }
}

/**
 * Execute an operation and return a result object with success/failure info
 *
 * Useful when you need to know if the operation failed but still want
 * to continue with a fallback value.
 *
 * @example
 * const result = await safeOperationWithResult(
 *   () => externalService.fetchData(),
 *   {
 *     criticality: OperationCriticality.OPTIONAL,
 *     fallback: null,
 *     context: 'Fetch external data',
 *     logger: this.logger,
 *   }
 * );
 *
 * if (!result.success) {
 *   // Handle degraded state
 * }
 */
export async function safeOperationWithResult<T>(
  executor: () => Promise<T>,
  options: SafeOperationOptions<T>
): Promise<SafeOperationResult<T>> {
  try {
    const value = await executor();
    return { success: true, value };
  } catch (error) {
    const errorMessage = extractErrorMessage(error);

    // If critical, still throw
    if (options.criticality === OperationCriticality.CRITICAL) {
      const result = await safeOperation(executor, options).catch((e: Error) => {
        throw e;
      });
      return { success: true, value: result };
    }

    // For non-critical, get the fallback value
    const fallbackValue = await safeOperation(executor, options).catch(() => options.fallback as T);

    return {
      success: false,
      value: fallbackValue,
      error: errorMessage,
    };
  }
}

/**
 * Create a pre-configured safe operation executor for a specific context
 *
 * @example
 * const safeLoadOrders = createSafeExecutor(
 *   OperationCriticality.OPTIONAL,
 *   [],
 *   'Load orders',
 *   this.logger
 * );
 *
 * const orders = await safeLoadOrders(() => this.orderService.getOrders(userId));
 */
export function createSafeExecutor<T>(
  criticality: OperationCriticality,
  fallback: T,
  context: string,
  logger: Logger
): (executor: () => Promise<T>, metadata?: Record<string, unknown>) => Promise<T> {
  return async (executor: () => Promise<T>, metadata?: Record<string, unknown>) => {
    const options: SafeOperationOptions<T> = {
      criticality,
      fallback,
      context,
      logger,
    };
    if (metadata !== undefined) {
      options.metadata = metadata;
    }
    return safeOperation(executor, options);
  };
}
