import { Injectable } from '@nestjs/common';
import { and, eq, isNull, sql } from 'drizzle-orm';
import { DbService, Tx } from '@/common/db/db.service';
import { documentSequences, warehouses } from '@/db/schema';
import { requireRequestContext } from '@/common/context/request-context';

export type DocType = 'ASN' | 'GRN' | 'RCR' | 'ORD' | 'WAVE' | 'TASK' | 'LPN' | 'SHP' | 'LOAD' | 'GP' | 'INV' | 'ADJ' | 'CNT' | 'RMA' | 'CRN' | 'CTN' | 'CON' | 'DN';

const DEFAULT_PREFIX: Record<DocType, string> = {
  ASN: 'ASN', GRN: 'GRN', RCR: 'RCR', ORD: 'SO', WAVE: 'WV', TASK: 'T', LPN: 'LP', SHP: 'SH', LOAD: 'LD', GP: 'GP',
  INV: 'INV', ADJ: 'ADJ', CNT: 'CC', RMA: 'RMA', CRN: 'CRN', CTN: 'CT', CON: 'CON', DN: 'DN',
};

/** Operational numbers (tasks, LPNs, cartons) are high volume; they are taken in their own short transaction and may have gaps. */
const AUTONOMOUS: ReadonlySet<DocType> = new Set<DocType>(['TASK', 'LPN', 'CTN']);

/**
 * Per-tenant (optionally per-warehouse) document numbers.
 * Business documents (ASN, GRN, orders, invoices ...) are gap-free: the
 * UPDATE ... RETURNING on the sequence row stays inside the caller's transaction.
 * Operational numbers (TASK, LPN, CTN) are allocated in a separate short
 * transaction so a long wave release or receipt does not serialise every RF user
 * on one row lock; a rolled-back caller simply leaves a gap.
 */
@Injectable()
export class NumberingService {
  constructor(private readonly db: DbService) {}

  async next(docType: DocType, tx: Tx, warehouseId: string | null = null, tenantIdOverride?: string): Promise<string> {
    const tenantId = tenantIdOverride ?? requireRequestContext().tenantId;
    if (!tenantId) throw new Error('NumberingService requires a tenant');
    if (AUTONOMOUS.has(docType)) {
      return this.db.run((own) => this.allocate(docType, own, warehouseId, tenantId), { tenantId, bypassRls: false });
    }
    return this.allocate(docType, tx, warehouseId, tenantId);
  }

  private async allocate(docType: DocType, tx: Tx, warehouseId: string | null, tenantId: string): Promise<string> {
    const whereClause = warehouseId
      ? and(eq(documentSequences.tenantId, tenantId), eq(documentSequences.warehouseId, warehouseId), eq(documentSequences.docType, docType))
      : and(eq(documentSequences.tenantId, tenantId), isNull(documentSequences.warehouseId), eq(documentSequences.docType, docType));

    let [row] = await tx
      .update(documentSequences)
      .set({ nextNumber: sql`${documentSequences.nextNumber} + 1` })
      .where(whereClause)
      .returning();

    if (!row) {
      // first use: create the sequence row (prefix includes warehouse code when scoped)
      let prefix = DEFAULT_PREFIX[docType];
      if (warehouseId) {
        const [wh] = await tx.select({ code: warehouses.code }).from(warehouses).where(eq(warehouses.id, warehouseId)).limit(1);
        if (wh) prefix = `${prefix}-${wh.code}`;
      }
      [row] = await tx
        .insert(documentSequences)
        .values({ tenantId, warehouseId, docType, prefix, nextNumber: 2, padding: 6 })
        .onConflictDoUpdate({
          target: [documentSequences.tenantId, documentSequences.warehouseId, documentSequences.docType],
          set: { nextNumber: sql`${documentSequences.nextNumber} + 1` },
        })
        .returning();
    }

    const n = row.nextNumber - 1;
    return `${row.prefix}-${String(n).padStart(row.padding, '0')}`;
  }
}
