import {
  formatApplicationReference,
  formatDocumentRequestNumber,
  formatPaymentNumber,
  formatStudentNumber,
} from '@i-learned/core';

import type { Db, PrismaTransaction } from './client';

/**
 * Human-readable reference numbers (ASSUMPTIONS.md C5, D1).
 *
 * A PostgreSQL sequence cannot restart per year without DDL, and cannot
 * participate in the caller's transaction the way this must: a student number
 * has to be allocated in the *same* transaction that finalizes the enrollment,
 * so a rollback does not burn a number and leave a visible gap in the register.
 *
 * `reference_sequence` is therefore a counter row per (name, scope). The
 * atomic `update … increment` takes a row lock for the duration of the caller's
 * transaction, which serializes concurrent allocations.
 */

export type SequenceName =
  | 'APPLICATION'
  | 'STUDENT_NUMBER'
  | 'PAYMENT'
  | 'DOCUMENT_REQUEST'
  | 'ASSESSMENT'
  | 'COR';

/**
 * Reserves the next value for a sequence.
 *
 * Must be called with `tx` inside a transaction. Passing the root client works
 * but gives up the rollback guarantee, so services should never do it.
 */
export async function nextSequenceValue(
  tx: Db,
  institutionId: string,
  name: SequenceName,
  scope: string,
): Promise<number> {
  const existing = await tx.referenceSequence.findUnique({
    where: { institutionId_name_scope: { institutionId, name, scope } },
    select: { id: true },
  });

  if (!existing) {
    // First use of this (name, scope). A concurrent caller may win the race,
    // in which case the unique constraint rejects this insert and the update
    // path below handles it.
    try {
      const created = await tx.referenceSequence.create({
        data: { institutionId, name, scope, value: 1 },
        select: { value: true },
      });
      return created.value;
    } catch {
      // Fall through to the increment path.
    }
  }

  const updated = await tx.referenceSequence.update({
    where: { institutionId_name_scope: { institutionId, name, scope } },
    data: { value: { increment: 1 } },
    select: { value: true },
  });

  return updated.value;
}

/** `APP-2026-000417` */
export async function nextApplicationReference(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
): Promise<string> {
  const sequence = await nextSequenceValue(tx, institutionId, 'APPLICATION', String(year));
  return formatApplicationReference(year, sequence);
}

/** `2026-00184` — allocated only at enrollment finalization. */
export async function nextStudentNumber(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
): Promise<string> {
  const sequence = await nextSequenceValue(tx, institutionId, 'STUDENT_NUMBER', String(year));
  return formatStudentNumber(year, sequence);
}

/** `PMT-2026-0000512` — internal reference, not a BIR receipt number. */
export async function nextPaymentNumber(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
): Promise<string> {
  const sequence = await nextSequenceValue(tx, institutionId, 'PAYMENT', String(year));
  return formatPaymentNumber(year, sequence);
}

/** `DR-2026-000042` */
export async function nextDocumentRequestNumber(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
): Promise<string> {
  const sequence = await nextSequenceValue(tx, institutionId, 'DOCUMENT_REQUEST', String(year));
  return formatDocumentRequestNumber(year, sequence);
}

/** `ASMT-2026-1-000184` — scoped per term so the numbering resets sensibly. */
export async function nextAssessmentNumber(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
  termSequence: number,
): Promise<string> {
  const scope = `${year}-${termSequence}`;
  const sequence = await nextSequenceValue(tx, institutionId, 'ASSESSMENT', scope);
  return `ASMT-${scope}-${String(sequence).padStart(6, '0')}`;
}

/** `COR-2026-1-000184` */
export async function nextCorNumber(
  tx: PrismaTransaction,
  institutionId: string,
  year: number,
  termSequence: number,
): Promise<string> {
  const scope = `${year}-${termSequence}`;
  const sequence = await nextSequenceValue(tx, institutionId, 'COR', scope);
  return `COR-${scope}-${String(sequence).padStart(6, '0')}`;
}
