import { PrismaClient, Prisma } from '@prisma/client';

export { Prisma };

/**
 * The Prisma client singleton.
 *
 * Next.js recreates modules on every hot reload in development, which would
 * otherwise open a new connection pool per edit until PostgreSQL refuses more
 * connections. Caching on `globalThis` survives reload; production gets a
 * single instance for the process lifetime either way.
 */
const globalForPrisma = globalThis as unknown as { __ilearnedPrisma?: PrismaClient };

function createClient(): PrismaClient {
  return new PrismaClient({
    log:
      process.env.NODE_ENV === 'development'
        ? [{ emit: 'event', level: 'warn' }, { emit: 'event', level: 'error' }]
        : [{ emit: 'event', level: 'error' }],
  });
}

export const prisma: PrismaClient = globalForPrisma.__ilearnedPrisma ?? createClient();

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.__ilearnedPrisma = prisma;
}

/**
 * A transaction-scoped client. Every service that writes more than one row
 * takes this type rather than the concrete client, so it can be composed into
 * a larger transaction by its caller.
 */
export type PrismaTransaction = Omit<
  PrismaClient,
  '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
>;

/** Either the root client or an open transaction. Repositories accept this. */
export type Db = PrismaClient | PrismaTransaction;

export interface TransactionOptions {
  /** Milliseconds Prisma waits for a connection before failing. */
  maxWait?: number;
  /** Milliseconds the transaction may run before rollback. */
  timeout?: number;
  isolationLevel?: Prisma.TransactionIsolationLevel;
}

/**
 * Runs `work` inside a database transaction.
 *
 * SPEC.md §35 requires the multi-record critical operations — converting an
 * applicant to a student, finalizing enrollment, posting a payment with its
 * allocation, submitting grades — to be atomic. Crucially, the audit entry and
 * the status-history row are written inside the same transaction as the change
 * they describe, so a partial failure cannot leave history claiming something
 * that did not happen.
 *
 * The default isolation level is Serializable for these operations: a
 * concurrent enrollment finalization must not be able to allocate the same
 * student number or oversubscribe the same section.
 */
export async function withTransaction<T>(
  work: (tx: PrismaTransaction) => Promise<T>,
  options: TransactionOptions = {},
  client: PrismaClient = prisma,
): Promise<T> {
  return client.$transaction(work, {
    maxWait: options.maxWait ?? 5_000,
    timeout: options.timeout ?? 15_000,
    isolationLevel: options.isolationLevel ?? Prisma.TransactionIsolationLevel.Serializable,
  });
}

/**
 * Whether an error is a Prisma unique-constraint violation on a given field.
 * Used to turn a race on a unique index into a clean domain ConflictError
 * rather than a 500.
 */
export function isUniqueConstraintError(error: unknown, field?: string): boolean {
  if (!(error instanceof Prisma.PrismaClientKnownRequestError)) return false;
  if (error.code !== 'P2002') return false;
  if (!field) return true;
  const target = error.meta?.target;
  const fields = Array.isArray(target) ? target : typeof target === 'string' ? [target] : [];
  return fields.includes(field);
}

/** Whether an error is a Prisma "record not found" error. */
export function isNotFoundError(error: unknown): boolean {
  return error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025';
}

/**
 * Whether an error came from one of the append-only triggers installed by the
 * integrity migration. Surfacing this distinctly makes an attempted tamper
 * obvious in logs rather than looking like a generic database fault.
 */
export function isAppendOnlyViolation(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  return message.includes('append-only');
}
