// src/services/db/errors.db.ts
import type { Logger } from 'pino';
import { DatabaseError as ProcessingDatabaseError } from '../processingErrors';

/**
 * Base class for custom repository-level errors to ensure they have a status property.
 */
export class RepositoryError extends Error {
  public status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = this.constructor.name;
    this.status = status;
    // This is necessary to make `instanceof` work correctly with transpiled TS classes
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

/**
 * Thrown when a unique constraint is violated (e.g., trying to register an existing email).
 * Corresponds to PostgreSQL error code '23505'.
 */
export class UniqueConstraintError extends RepositoryError {
  constructor(message = 'The record already exists.') {
    super(message, 409); // 409 Conflict
  }
}

/**
 * Thrown when a foreign key constraint is violated (e.g., trying to reference a non-existent record).
 * Corresponds to PostgreSQL error code '23503'.
 */
export class ForeignKeyConstraintError extends RepositoryError {
  constructor(message = 'The referenced record does not exist.') {
    super(message, 400); // 400 Bad Request
  }
}

/**
 * Thrown when a 'not null' constraint is violated.
 * Corresponds to PostgreSQL error code '23502'.
 */
export class NotNullConstraintError extends RepositoryError {
  constructor(message = 'A required field was left null.') {
    super(message, 400); // 400 Bad Request
  }
}

/**
 * Thrown when a 'check' constraint is violated.
 * Corresponds to PostgreSQL error code '23514'.
 */
export class CheckConstraintError extends RepositoryError {
  constructor(message = 'A check constraint was violated.') {
    super(message, 400); // 400 Bad Request
  }
}

/**
 * Thrown when a value has an invalid text representation for its data type (e.g., 'abc' for an integer).
 * Corresponds to PostgreSQL error code '22P02'.
 */
export class InvalidTextRepresentationError extends RepositoryError {
  constructor(message = 'A value has an invalid format for its data type.') {
    super(message, 400); // 400 Bad Request
  }
}

/**
 * Thrown when a numeric value is out of range for its data type (e.g., too large for an integer).
 * Corresponds to PostgreSQL error code '22003'.
 */
export class NumericValueOutOfRangeError extends RepositoryError {
  constructor(message = 'A numeric value is out of the allowed range.') {
    super(message, 400); // 400 Bad Request
  }
}

/**
 * Thrown when a specific record is not found in the database.
 */
export class NotFoundError extends RepositoryError {
  constructor(message = 'The requested resource was not found.') {
    super(message, 404); // 404 Not Found
  }
}

/**
 * Thrown when the user does not have permission to access the resource.
 */
export class ForbiddenError extends RepositoryError {
  constructor(message = 'Access denied.') {
    super(message, 403); // 403 Forbidden
    this.name = 'ForbiddenError';
  }
}

/**
 * Defines the structure for a single validation issue, often from a library like Zod.
 */
export interface ValidationIssue {
  path: (string | number)[];
  message: string;
  [key: string]: unknown; // Allow other properties that might exist on the error object
}

/**
 * Thrown when request validation fails (e.g., missing body fields or invalid params).
 */
export class ValidationError extends RepositoryError {
  public validationErrors: ValidationIssue[];

  constructor(errors: ValidationIssue[], message = 'The request data is invalid.') {
    super(message, 400); // 400 Bad Request
    this.name = 'ValidationError';
    this.validationErrors = errors;
  }
}

export class FileUploadError extends Error {
  public status = 400;
  constructor(message: string) {
    super(message);
    this.name = 'FileUploadError';
  }
}

export interface HandleDbErrorOptions {
  entityName?: string;
  uniqueMessage?: string;
  fkMessage?: string;
  notNullMessage?: string;
  checkMessage?: string;
  invalidTextMessage?: string;
  numericOutOfRangeMessage?: string;
  defaultMessage?: string;
}

/**
 * A type guard to check if an error object is a PostgreSQL error with a code.
 */
function isPostgresError(
  error: unknown,
): error is { code: string; constraint?: string; detail?: string } {
  return typeof error === 'object' && error !== null && 'code' in error;
}

/**
 * Centralized error handler for database repositories.
 * Logs the error and throws appropriate custom errors based on PostgreSQL error codes.
 */
export function handleDbError(
  error: unknown,
  logger: Logger,
  logMessage: string,
  logContext: Record<string, unknown>,
  options: HandleDbErrorOptions = {},
): never {
  // If it's already a known domain error (like NotFoundError thrown manually), rethrow it.
  if (error instanceof RepositoryError) {
    throw error;
  }

  if (isPostgresError(error)) {
    const { code, constraint, detail } = error;
    const enhancedLogContext = { err: error, code, constraint, detail, ...logContext };

    // Log the detailed error first
    logger.error(enhancedLogContext, logMessage);

    // Now, throw the appropriate custom error
    switch (code) {
      case '23505': // unique_violation
        throw new UniqueConstraintError(options.uniqueMessage);
      case '23503': // foreign_key_violation
        throw new ForeignKeyConstraintError(options.fkMessage);
      case '23502': // not_null_violation
        throw new NotNullConstraintError(options.notNullMessage);
      case '23514': // check_violation
        throw new CheckConstraintError(options.checkMessage);
      case '22P02': // invalid_text_representation
        throw new InvalidTextRepresentationError(options.invalidTextMessage);
      case '22003': // numeric_value_out_of_range
        throw new NumericValueOutOfRangeError(options.numericOutOfRangeMessage);
      default:
        // If it's a PG error but not one we handle specifically, fall through to the generic error.
        break;
    }
  } else {
    // Log the error if it wasn't a recognized Postgres error
    logger.error({ err: error, ...logContext }, logMessage);
  }

  // Fallback generic error
  // Use the consistent DatabaseError from the processing errors module for the fallback.
  const errorMessage = options.defaultMessage || `Failed to perform operation on ${options.entityName || 'database'}.`;
  throw new ProcessingDatabaseError(errorMessage);
}
