import { Prisma, type PrismaClient, isPrismaKnownError } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";

// Minimal structural logger so this stays decoupled from the concrete Logger
// (and lets tests pass a capturing logger).
type ErrorLogger = { error: (message: string, fields?: Record<string, unknown>) => void };

// Prisma connectivity / infrastructure error codes — engine- and
// connection-level failures, not query- or validation-level ones. When the
// database is unreachable, Prisma 6.x throws a PrismaClientKnownRequestError
// carrying one of these codes (e.g. P1001 "Can't reach database server").
const INFRASTRUCTURE_PRISMA_CODES = new Set([
  "P1001", // Can't reach database server
  "P1002", // Database server reached but timed out
  "P1008", // Operations timed out
  "P1017", // Server has closed the connection
]);

/**
 * True when `error` is a Prisma infrastructure/connectivity failure (DB
 * unreachable, timed out, connection dropped) rather than a query- or
 * validation-level error.
 *
 * These errors carry internal infrastructure detail (e.g. the database
 * hostname) in their `.message`, so they must never be surfaced to API
 * clients — callers should let them propagate to the generic 5xx handler
 * (which both scrubs the message and is retryable by the SDK) instead of
 * folding `.message` into a client-facing error.
 */
export function isInfrastructureError(error: unknown): boolean {
  if (
    error instanceof Prisma.PrismaClientInitializationError ||
    error instanceof Prisma.PrismaClientRustPanicError ||
    error instanceof Prisma.PrismaClientUnknownRequestError
  ) {
    return true;
  }

  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    return INFRASTRUCTURE_PRISMA_CODES.has(error.code);
  }

  return false;
}

// One-shot marker so a single infra error is logged exactly once: the client
// extension (statement level) tags it, and the $transaction-boundary loggers
// skip a tagged error rather than logging the same failure a second time.
const INFRA_ERROR_LOGGED: unique symbol = Symbol("prismaInfraErrorLogged");

function markInfraErrorLogged(error: unknown): void {
  if (typeof error !== "object" || error === null) {
    return;
  }
  try {
    // Non-enumerable so error-spreads/serializers can't copy the marker onto a
    // different error; try/catch so a frozen error object can't make this throw
    // and mask the original error as it propagates out of the catch.
    Object.defineProperty(error, INFRA_ERROR_LOGGED, {
      value: true,
      enumerable: false,
      configurable: true,
      writable: true,
    });
  } catch {
    // best-effort: a sealed/frozen error simply won't be deduped.
  }
}

export function infraErrorAlreadyLogged(error: unknown): boolean {
  return (
    typeof error === "object" &&
    error !== null &&
    (error as Record<symbol, unknown>)[INFRA_ERROR_LOGGED] === true
  );
}

// Logs infrastructure failures (P1xxx-class, see isInfrastructureError) and
// rethrows the ORIGINAL error: callers branch on error.code, and this fires
// per-statement inside transactions, so converting it would break that.
export function captureInfrastructureErrors<T extends PrismaClient>(
  client: T,
  log: ErrorLogger = logger
): T {
  return client.$extends({
    name: "infrastructure-error-capture",
    query: {
      $allOperations: async ({ model, operation, args, query }) => {
        try {
          return await query(args);
        } catch (error) {
          if (isInfrastructureError(error)) {
            log.error("prisma infrastructure error", {
              model,
              operation,
              code: error instanceof Prisma.PrismaClientKnownRequestError ? error.code : undefined,
              meta: error instanceof Prisma.PrismaClientKnownRequestError ? error.meta : undefined,
              message: error instanceof Error ? error.message : String(error),
              stack: error instanceof Error ? error.stack : undefined,
            });
            markInfraErrorLogged(error);
          }

          throw error;
        }
      },
    },
  }) as unknown as T;
}

// Logs infrastructure errors that reach the $transaction boundary WITHOUT a
// Prisma error code (e.g. PrismaClientInitializationError). Coded errors there
// are already logged by transac()'s callback, and errors that bubbled up from a
// statement were already logged (and tagged) by the client extension — both are
// skipped here to avoid double-logging. Returns whether it logged.
export function logTransactionInfrastructureError(
  error: unknown,
  log: ErrorLogger = logger
): boolean {
  if (
    !isInfrastructureError(error) ||
    isPrismaKnownError(error) ||
    infraErrorAlreadyLogged(error)
  ) {
    return false;
  }

  log.error("prisma.$transaction infrastructure error", {
    message: error instanceof Error ? error.message : String(error),
    name: error instanceof Error ? error.name : undefined,
    stack: error instanceof Error ? error.stack : undefined,
  });

  return true;
}

// Replaces a Prisma infrastructure error's message (which carries the DB
// hostname) with a generic one before it reaches an API client. Any other
// error's message is returned unchanged. Status codes/headers are unaffected.
export function clientSafeErrorMessage(error: Error): string {
  return isInfrastructureError(error) ? "Internal Server Error" : error.message;
}
