import prisma from "../config/database";
import { Prisma } from "@prisma/client";
import type { PrismaClient } from "@prisma/client";

// Prisma transaction client (omit methods not available inside $transaction)
type TxClient = Omit<
  PrismaClient,
  "$connect" | "$disconnect" | "$on" | "$transaction" | "$extends"
>;

// Default patterns (backward compatible with existing numbers)
const DEFAULT_OFFER_PATTERN = "{YYYY}/{PREFIX}/{NNN}";
const DEFAULT_ORDER_PATTERN = "{YY}{CODE}{NNNN}";
const DEFAULT_INVOICE_PATTERN = "{YY}{CODE}{NNNN}";
const DEFAULT_ISSUED_ORDER_PATTERN = "{YY}{CODE}{NNNN}";
const DEFAULT_PROJECT_PATTERN = "{YY}{CODE}{NNNN}";

/**
 * Apply a numbering pattern template.
 * Placeholders: {YYYY}, {YY}, {PREFIX}, {CODE}, {N+} (padding = count of N's)
 */
export function applyPattern(
  pattern: string,
  vars: { year: number; prefix: string; code: string; seq: number },
): string {
  const yyyy = String(vars.year);
  const yy = yyyy.slice(-2);

  return pattern.replace(/\{(\w+)\}/g, (match, key: string) => {
    if (key === "YYYY") return yyyy;
    if (key === "YY") return yy;
    if (key === "PREFIX") return vars.prefix;
    if (key === "CODE") return vars.code;
    if (/^N+$/.test(key)) return String(vars.seq).padStart(key.length, "0");
    return match;
  });
}

async function getSettings() {
  return prisma.company_settings.findFirst({
    select: {
      quotation_prefix: true,
      order_type_code: true,
      invoice_type_code: true,
      offer_number_pattern: true,
      order_number_pattern: true,
      invoice_number_pattern: true,
      issued_order_number_pattern: true,
      issued_order_type_code: true,
      project_number_pattern: true,
      project_type_code: true,
    },
  });
}

/**
 * Atomically get the next sequence number for a given type and year.
 * Uses SELECT ... FOR UPDATE inside a transaction to prevent races.
 * If `tx` is provided, the increment happens inside the caller's transaction
 * (no nested transaction is created).
 */
export async function getNextSequence(
  type: string,
  year: number,
  tx?: TxClient,
): Promise<number> {
  // Read-modify-write under a row lock. The very first allocation of a
  // (type, year) pair has no row to lock yet, so two concurrent callers can
  // both see zero rows and both attempt the INSERT — the @@unique([type,year])
  // constraint then raises P2002 on the loser. `allowInsertRetry` lets us catch
  // that one case and fall back to the locked read-then-update path, so the
  // first-of-year race returns the next sequence instead of a 500.
  const exec = async (
    client: TxClient,
    allowInsertRetry: boolean,
  ): Promise<number> => {
    const existing = await client.$queryRaw<
      Array<{ id: number; last_number: number }>
    >`
      SELECT id, last_number FROM number_sequences
      WHERE \`type\` = ${type} AND \`year\` = ${year}
      FOR UPDATE
    `;

    if (existing.length === 0) {
      try {
        await client.$executeRaw`
          INSERT INTO number_sequences (\`type\`, \`year\`, \`last_number\`)
          VALUES (${type}, ${year}, 1)
        `;
        return 1;
      } catch (err) {
        // Concurrent first-of-year insert won the race. Retry the locked
        // read-then-update exactly once; the row now exists so this resolves.
        if (
          allowInsertRetry &&
          err instanceof Prisma.PrismaClientKnownRequestError &&
          err.code === "P2002"
        ) {
          return exec(client, false);
        }
        throw err;
      }
    }

    const next = existing[0].last_number + 1;
    await client.$executeRaw`
      UPDATE number_sequences
      SET \`last_number\` = ${next}
      WHERE id = ${existing[0].id}
    `;
    return next;
  };

  if (tx) {
    return exec(tx, true);
  }
  return prisma.$transaction((client) => exec(client, true));
}

/**
 * Preview the next sequence number without consuming it.
 */
async function previewNextSequence(
  type: string,
  year: number,
): Promise<number> {
  const existing = await prisma.$queryRaw<Array<{ last_number: number }>>`
    SELECT last_number FROM number_sequences
    WHERE \`type\` = ${type} AND \`year\` = ${year}
  `;

  if (existing.length === 0) {
    return 1;
  }

  return existing[0].last_number + 1;
}

/**
 * Release a sequence number back to the pool.
 * Only decrements if the deleted entity held the current highest number,
 * preventing gaps while avoiding duplicate re-allocation.
 */
async function releaseSequence(
  type: string,
  year: number,
  deletedNumber?: string,
) {
  if (!deletedNumber) return;

  // Settings are read-only here and don't need to be inside the lock window.
  const settings = await getSettings();
  const pattern =
    type === "shared"
      ? settings?.order_number_pattern || DEFAULT_ORDER_PATTERN
      : type === "invoice"
        ? settings?.invoice_number_pattern || DEFAULT_INVOICE_PATTERN
        : type === "issued_order"
          ? settings?.issued_order_number_pattern ||
            DEFAULT_ISSUED_ORDER_PATTERN
          : type === "project"
            ? settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN
            : settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
  const code =
    type === "shared"
      ? settings?.order_type_code || "71"
      : type === "invoice"
        ? settings?.invoice_type_code || "81"
        : type === "issued_order"
          ? settings?.issued_order_type_code || "72"
          : type === "project"
            ? settings?.project_type_code || "73"
            : "";
  const prefix = type === "offer" ? settings?.quotation_prefix || "NA" : "";

  // Lock the sequence row for the duration of the decrement so a concurrent
  // getNextSequence (which also takes FOR UPDATE) can't read-modify-write the
  // same row in between — that interleaving could otherwise lose a decrement or
  // decrement an in-use number (gap/dup).
  await prisma.$transaction(async (tx) => {
    const existing = await tx.$queryRaw<Array<{ last_number: number }>>`
      SELECT last_number FROM number_sequences
      WHERE \`type\` = ${type} AND \`year\` = ${year}
      FOR UPDATE
    `;
    if (existing.length === 0) return;

    const highestNumber = applyPattern(pattern, {
      year,
      prefix,
      code,
      seq: existing[0].last_number,
    });

    if (deletedNumber === highestNumber && existing[0].last_number > 0) {
      await tx.$executeRaw`
        UPDATE number_sequences
        SET \`last_number\` = ${existing[0].last_number - 1}
        WHERE \`type\` = ${type} AND \`year\` = ${year}
      `;
    }
  });
}

/** Verify a shared number is not already used by an order or project. */
export async function isSharedNumberTaken(number: string): Promise<boolean> {
  const [existingOrder, existingProject] = await Promise.all([
    // select id only — the orders row carries the PO attachment blob, which an
    // existence check must never pull.
    prisma.orders.findFirst({
      where: { order_number: number },
      select: { id: true },
    }),
    prisma.projects.findFirst({
      where: { project_number: number },
      select: { id: true },
    }),
  ]);
  return !!(existingOrder || existingProject);
}

/** Verify an invoice number is not already used. */
async function isInvoiceNumberTaken(number: string): Promise<boolean> {
  const existing = await prisma.invoices.findFirst({
    where: { invoice_number: number },
  });
  return !!existing;
}

/** Verify an issued-order (PO) number is not already used. Pass the tx client
 *  when the check must be part of a transaction (otherwise it runs on a
 *  separate connection and is only a best-effort pre-check). */
export async function isIssuedOrderNumberTaken(
  number: string,
  client: Prisma.TransactionClient | PrismaClient = prisma,
): Promise<boolean> {
  const existing = await client.issued_orders.findFirst({
    where: { po_number: number },
    select: { id: true },
  });
  return !!existing;
}

/** Verify an offer/quotation number is not already used. Pass the tx client
 *  when the check must be part of a transaction (otherwise it runs on a
 *  separate connection and is only a best-effort pre-check). */
export async function isOfferNumberTaken(
  number: string,
  client: Prisma.TransactionClient | PrismaClient = prisma,
): Promise<boolean> {
  const existing = await client.quotations.findFirst({
    where: { quotation_number: number },
    select: { id: true },
  });
  return !!existing;
}

/** Verify an order number is not already used. */
export async function isOrderNumberTaken(number: string): Promise<boolean> {
  // select id only — the orders row carries the PO attachment blob, which an
  // existence check must never pull.
  const existing = await prisma.orders.findFirst({
    where: { order_number: number },
    select: { id: true },
  });
  return !!existing;
}

/** Verify a project number is not already used. */
export async function isProjectNumberTaken(number: string): Promise<boolean> {
  const existing = await prisma.projects.findFirst({
    where: { project_number: number },
  });
  return !!existing;
}

/**
 * Next offer/quotation number (consumes sequence).
 * Verifies uniqueness against the quotations table; retries if taken.
 * Pass `tx` when calling inside an existing Prisma transaction.
 */
export async function generateOfferNumber(tx?: TxClient): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
  const prefix = settings?.quotation_prefix || "NA";
  const year = new Date().getFullYear();

  for (let attempt = 0; attempt < 100; attempt++) {
    const seq = await getNextSequence("offer", year, tx);
    const number = applyPattern(pattern, { year, prefix, code: "", seq });
    if (!(await isOfferNumberTaken(number))) {
      return number;
    }
  }

  throw new Error("Nepodařilo se vygenerovat jedinečné číslo nabídky");
}

/**
 * Preview next offer/quotation number (does NOT consume sequence).
 */
export async function previewOfferNumber(): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
  const prefix = settings?.quotation_prefix || "NA";
  const year = new Date().getFullYear();

  // Mirror generateOfferNumber's collision check (without consuming the
  // sequence). The number_sequences counter can lag behind real data, so the
  // raw next sequence may already be taken — advance past any number already
  // used by a quotation so the preview matches what generateOfferNumber assigns
  // instead of showing a stale, already-used number.
  let seq = await previewNextSequence("offer", year);
  for (let attempt = 0; attempt < 1000; attempt++) {
    const number = applyPattern(pattern, { year, prefix, code: "", seq });
    if (!(await isOfferNumberTaken(number))) return number;
    seq++;
  }
  return applyPattern(pattern, { year, prefix, code: "", seq });
}

/**
 * Shared number for orders and projects (consumes sequence).
 * Verifies uniqueness against both orders and projects tables; retries if taken.
 * Pass `tx` when calling inside an existing Prisma transaction.
 */
export async function generateSharedNumber(tx?: TxClient): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.order_number_pattern || DEFAULT_ORDER_PATTERN;
  const code = settings?.order_type_code || "71";
  const year = new Date().getFullYear();

  for (let attempt = 0; attempt < 100; attempt++) {
    const seq = await getNextSequence("shared", year, tx);
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isSharedNumberTaken(number))) {
      return number;
    }
  }

  throw new Error(
    "Nepodařilo se vygenerovat jedinečné číslo objednávky/projekt",
  );
}

/**
 * Preview shared number for orders and projects (does NOT consume sequence).
 */
export async function previewSharedNumber(): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.order_number_pattern || DEFAULT_ORDER_PATTERN;
  const code = settings?.order_type_code || "71";
  const year = new Date().getFullYear();

  // Mirror generateSharedNumber's collision check (without consuming the
  // sequence). The number_sequences counter can lag behind real data — e.g.
  // when orders/projects were imported or seeded without bumping it — so the
  // raw next sequence may already be taken. Advance past any number already
  // used by an order or project, so the preview matches what generateSharedNumber
  // would actually assign instead of showing a stale, already-used number.
  let seq = await previewNextSequence("shared", year);
  for (let attempt = 0; attempt < 1000; attempt++) {
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isSharedNumberTaken(number))) return number;
    seq++;
  }
  return applyPattern(pattern, { year, prefix: "", code, seq });
}

/**
 * Next project number (consumes the dedicated `project` sequence).
 * Decoupled from the `shared` order sequence so a project (standalone OR
 * auto-created from an order) gets its own number from a separate pool.
 * Verifies uniqueness against the projects table; retries if taken.
 * Pass `tx` when calling inside an existing Prisma transaction.
 */
export async function generateProjectNumber(tx?: TxClient): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN;
  const code = settings?.project_type_code || "73";
  const year = new Date().getFullYear();

  for (let attempt = 0; attempt < 100; attempt++) {
    const seq = await getNextSequence("project", year, tx);
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isProjectNumberTaken(number))) {
      return number;
    }
  }

  throw new Error("Nepodařilo se vygenerovat jedinečné číslo projektu");
}

/**
 * Preview next project number (does NOT consume sequence).
 */
export async function previewProjectNumber(): Promise<string> {
  const settings = await getSettings();
  const pattern = settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN;
  const code = settings?.project_type_code || "73";
  const year = new Date().getFullYear();

  // Mirror generateProjectNumber's collision check (without consuming the
  // sequence). The number_sequences counter can lag behind real data — e.g.
  // when projects were imported or seeded without bumping it — so the raw next
  // sequence may already be taken. Advance past any number already used by a
  // project, so the preview matches what generateProjectNumber would actually
  // assign instead of showing a stale, already-used number.
  let seq = await previewNextSequence("project", year);
  for (let attempt = 0; attempt < 1000; attempt++) {
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isProjectNumberTaken(number))) return number;
    seq++;
  }
  return applyPattern(pattern, { year, prefix: "", code, seq });
}

/**
 * Next invoice number (consumes sequence).
 * Verifies uniqueness against the invoices table; retries if taken.
 * Pass `tx` when calling inside an existing Prisma transaction.
 */
export async function generateInvoiceNumber(
  _year?: number,
  tx?: TxClient,
): Promise<{ number: string; next_number: string }> {
  const settings = await getSettings();
  const pattern = settings?.invoice_number_pattern || DEFAULT_INVOICE_PATTERN;
  const code = settings?.invoice_type_code || "81";
  const year = _year || new Date().getFullYear();

  for (let attempt = 0; attempt < 100; attempt++) {
    const seq = await getNextSequence("invoice", year, tx);
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isInvoiceNumberTaken(number))) {
      return { number, next_number: number };
    }
  }

  throw new Error("Nepodařilo se vygenerovat jedinečné číslo faktury");
}

/**
 * Preview next invoice number (does NOT consume sequence).
 */
export async function previewInvoiceNumber(
  _year?: number,
): Promise<{ number: string; next_number: string }> {
  const settings = await getSettings();
  const pattern = settings?.invoice_number_pattern || DEFAULT_INVOICE_PATTERN;
  const code = settings?.invoice_type_code || "81";
  const year = _year || new Date().getFullYear();

  // Mirror generateInvoiceNumber's collision check (without consuming the
  // sequence). The number_sequences counter can lag behind real data, so the
  // raw next sequence may already be taken — advance past any number already
  // used by an invoice so the preview matches what generateInvoiceNumber
  // would actually assign instead of showing a stale, already-used number.
  let seq = await previewNextSequence("invoice", year);
  for (let attempt = 0; attempt < 1000; attempt++) {
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isInvoiceNumberTaken(number)))
      return { number, next_number: number };
    seq++;
  }
  const number = applyPattern(pattern, { year, prefix: "", code, seq });
  return { number, next_number: number };
}

/**
 * Next issued-order (PO) number (consumes sequence).
 * Verifies uniqueness against the issued_orders table; retries if taken.
 * Pass `tx` when calling inside an existing Prisma transaction.
 */
export async function generateIssuedOrderNumber(
  _year?: number,
  tx?: TxClient,
): Promise<{ number: string; next_number: string }> {
  const settings = await getSettings();
  const pattern =
    settings?.issued_order_number_pattern || DEFAULT_ISSUED_ORDER_PATTERN;
  const code = settings?.issued_order_type_code || "72";
  const year = _year || new Date().getFullYear();

  for (let attempt = 0; attempt < 100; attempt++) {
    const seq = await getNextSequence("issued_order", year, tx);
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isIssuedOrderNumberTaken(number))) {
      return { number, next_number: number };
    }
  }
  throw new Error("Nepodařilo se vygenerovat jedinečné číslo objednávky");
}

/**
 * Preview next issued-order (PO) number (does NOT consume sequence).
 */
export async function previewIssuedOrderNumber(
  _year?: number,
): Promise<{ number: string; next_number: string }> {
  const settings = await getSettings();
  const pattern =
    settings?.issued_order_number_pattern || DEFAULT_ISSUED_ORDER_PATTERN;
  const code = settings?.issued_order_type_code || "72";
  const year = _year || new Date().getFullYear();

  // Mirror generateIssuedOrderNumber's collision check (without consuming the
  // sequence). The number_sequences counter can lag behind real data, so the
  // raw next sequence may already be taken — advance past any number already
  // used by an issued order so the preview matches what
  // generateIssuedOrderNumber would actually assign.
  let seq = await previewNextSequence("issued_order", year);
  for (let attempt = 0; attempt < 1000; attempt++) {
    const number = applyPattern(pattern, { year, prefix: "", code, seq });
    if (!(await isIssuedOrderNumberTaken(number)))
      return { number, next_number: number };
    seq++;
  }
  const number = applyPattern(pattern, { year, prefix: "", code, seq });
  return { number, next_number: number };
}

/**
 * Deferred-assign helpers — used when a draft (number-less) document is
 * finalized. Each runs inside a transaction (the caller's `tx` if provided,
 * otherwise a fresh `prisma.$transaction`), reads the row's current number, and:
 *   - if it is already set (non-null/non-empty), returns it unchanged
 *     (idempotent — finalizing twice never re-numbers / double-consumes); else
 *   - calls the existing per-type generator (which consumes the sequence under
 *     the same row lock) and writes the number onto the row.
 * The sequence is consumed exactly once, atomically with the row write.
 */
export async function assignOfferNumber(
  id: number,
  tx?: TxClient,
): Promise<string> {
  const run = async (client: TxClient): Promise<string> => {
    const row = await client.quotations.findUnique({
      where: { id },
      select: { quotation_number: true },
    });
    if (row?.quotation_number) return row.quotation_number;
    const number = await generateOfferNumber(client);
    await client.quotations.update({
      where: { id },
      data: { quotation_number: number },
    });
    return number;
  };
  return tx ? run(tx) : prisma.$transaction(run);
}

export async function assignInvoiceNumber(
  id: number,
  tx?: TxClient,
): Promise<string> {
  const run = async (client: TxClient): Promise<string> => {
    const row = await client.invoices.findUnique({
      where: { id },
      select: { invoice_number: true },
    });
    if (row?.invoice_number) return row.invoice_number;
    const { number } = await generateInvoiceNumber(undefined, client);
    await client.invoices.update({
      where: { id },
      data: { invoice_number: number },
    });
    return number;
  };
  return tx ? run(tx) : prisma.$transaction(run);
}

export async function assignIssuedOrderNumber(
  id: number,
  tx?: TxClient,
): Promise<string> {
  const run = async (client: TxClient): Promise<string> => {
    const row = await client.issued_orders.findUnique({
      where: { id },
      select: { po_number: true },
    });
    if (row?.po_number) return row.po_number;
    const { number } = await generateIssuedOrderNumber(undefined, client);
    await client.issued_orders.update({
      where: { id },
      data: { po_number: number },
    });
    return number;
  };
  return tx ? run(tx) : prisma.$transaction(run);
}

/** Release an issued-order number back to the pool (decrement if highest). */
export async function releaseIssuedOrderNumber(
  year?: number,
  deletedNumber?: string,
) {
  await releaseSequence(
    "issued_order",
    year || new Date().getFullYear(),
    deletedNumber,
  );
}

/** Release an offer number back to the pool (decrement if highest). */
export async function releaseOfferNumber(
  year?: number,
  deletedNumber?: string,
) {
  await releaseSequence(
    "offer",
    year || new Date().getFullYear(),
    deletedNumber,
  );
}

/** Release a shared number back to the pool (decrement if highest). */
export async function releaseSharedNumber(
  year?: number,
  deletedNumber?: string,
) {
  await releaseSequence(
    "shared",
    year || new Date().getFullYear(),
    deletedNumber,
  );
}

/** Release a project number back to the pool (decrement if highest). */
export async function releaseProjectNumber(
  year?: number,
  deletedNumber?: string,
) {
  await releaseSequence(
    "project",
    year || new Date().getFullYear(),
    deletedNumber,
  );
}

/** Release an invoice number back to the pool (decrement if highest). */
export async function releaseInvoiceNumber(
  year?: number,
  deletedNumber?: string,
) {
  await releaseSequence(
    "invoice",
    year || new Date().getFullYear(),
    deletedNumber,
  );
}

/** Preview what a pattern would produce (for settings UI) */
export function previewPattern(
  pattern: string,
  prefix: string,
  code: string,
): string {
  return applyPattern(pattern, {
    year: new Date().getFullYear(),
    prefix,
    code,
    seq: 1,
  });
}
