import prisma from "../config/database";
import { Prisma } from "@prisma/client";
import {
  generateSharedNumber,
  previewSharedNumber,
  releaseSharedNumber,
  isOrderNumberTaken,
  generateProjectNumber,
  releaseProjectNumber,
} from "./numbering.service";
import { NasFileManager } from "./nas-file-manager";
import { VALID_TRANSITIONS as OFFER_VALID_TRANSITIONS } from "./offers.service";

// Best-effort NAS folder creation for order-spawned projects, mirroring
// projects.service. Stateless singleton; reads NAS_PATH at construction.
const nasFileManager = new NasFileManager();

interface OrderItemInput {
  description?: string | null;
  item_description?: string | null;
  quantity?: number;
  unit?: string | null;
  unit_price?: number;
  is_included_in_total?: boolean;
  position?: number;
}
interface OrderSectionInput {
  title?: string | null;
  title_cz?: string | null;
  content?: string | null;
  position?: number;
}

// Status transition rules matching PHP
export const VALID_TRANSITIONS: Record<string, string[]> = {
  prijata: ["v_realizaci", "stornovana"],
  v_realizaci: ["dokoncena", "stornovana"],
  dokoncena: [],
  stornovana: [],
};

const ORDER_ALLOWED_SORT_FIELDS = [
  "id",
  "order_number",
  "status",
  "currency",
  "created_at",
];

// Order status -> linked-project status (matching PHP).
const ORDER_TO_PROJECT_STATUS: Record<string, string> = {
  v_realizaci: "aktivni",
  dokoncena: "dokonceny",
  stornovana: "zruseny",
};

/**
 * Propagate an order status change onto its linked project(s). No-op when the
 * new status has no project-status mapping. Accepts a Prisma client (the tx
 * client inside a transaction, or the base client otherwise) so both update
 * branches share one implementation.
 */
async function syncProjectStatus(
  client: Prisma.TransactionClient,
  orderId: number,
  newStatus: string,
): Promise<void> {
  const projectStatus = ORDER_TO_PROJECT_STATUS[newStatus];
  if (!projectStatus) return;
  await client.projects.updateMany({
    where: { order_id: orderId },
    data: { status: projectStatus },
  });
}

// ⚠ Also called by getOrderTotals with a MINIMAL select (currency, item
// quantity/unit_price/is_included_in_total). If you read a NEW order/item
// field here, add it to that select — a field missing from the select is
// silently 0 in the per-currency totals.
//
// Orders are NOT tax documents — totals are NET only (no VAT anywhere).
function enrichOrder(o: any) {
  const total = o.order_items
    .filter((i: any) => i.is_included_in_total !== false)
    .reduce(
      (s: number, i: any) =>
        s + (Number(i.quantity) || 0) * (Number(i.unit_price) || 0),
      0,
    );
  const { order_items, order_sections, ...rest } = o;
  const invoice = o.invoices?.[0] || null;
  return {
    ...rest,
    items: order_items,
    sections: order_sections,
    customer_name: o.customers?.name || null,
    quotation_number: o.quotations?.quotation_number || null,
    project_code: o.quotations?.project_code || null,
    invoice_id: invoice?.id || null,
    invoice_number: invoice?.invoice_number || null,
    total: Math.round(total * 100) / 100,
  };
}

interface OrderFilterParams {
  status?: string;
  customer_id?: number;
  search?: string;
  month?: number;
  year?: number;
}

interface ListOrdersParams extends OrderFilterParams {
  page: number;
  limit: number;
  skip: number;
  sort: string;
  order: "asc" | "desc";
}

/**
 * Shared `where` builder for the orders list and the per-currency stats
 * aggregation, so both stay in sync (any new filter applies to both). month/year
 * filters on created_at (matching the list).
 */
function buildOrderWhere(params: OrderFilterParams): Record<string, unknown> {
  const { status, customer_id, search, month, year } = params;
  const where: Record<string, unknown> = {};
  if (status) where.status = status;
  if (customer_id) where.customer_id = customer_id;
  if (search) {
    where.OR = [
      { order_number: { contains: search } },
      { customer_order_number: { contains: search } },
      { customers: { name: { contains: search } } },
    ];
  }
  if (month && year) {
    const from = new Date(year, month - 1, 1);
    const to = new Date(year, month, 1);
    where.created_at = { gte: from, lt: to };
  }
  return where;
}

export async function listOrders(params: ListOrdersParams) {
  const { page, limit, skip, order } = params;
  const sortField = ORDER_ALLOWED_SORT_FIELDS.includes(params.sort)
    ? params.sort
    : "id";

  const where = buildOrderWhere(params);

  const [orders, total] = await Promise.all([
    prisma.orders.findMany({
      where,
      skip,
      take: limit,
      // Tiebreak on id so same-second created_at rows sort deterministically.
      orderBy: [{ [sortField]: order }, { id: order }],
      // The PO attachment blob is served ONLY by GET /:id/attachment. Never
      // pull it into list payloads — a single 1MB PDF would serialize into
      // megabytes of JSON per row. attachment_name stays as the existence
      // signal the frontend uses.
      omit: { attachment_data: true },
      include: {
        customers: { select: { id: true, name: true } },
        order_items: { orderBy: { position: "asc" } },
        order_sections: { orderBy: { position: "asc" } },
        quotations: { select: { quotation_number: true, project_code: true } },
        invoices: {
          select: { id: true, invoice_number: true },
          take: 1,
          orderBy: { id: "desc" },
        },
      },
    }),
    prisma.orders.count({ where }),
  ]);

  const enriched = orders.map(enrichOrder);
  return { data: enriched, total, page, limit };
}

export interface CurrencyAmount {
  amount: number;
  currency: string;
}

/**
 * Sum order NET total per currency across the WHOLE filtered set (not a
 * single page). Reuses `buildOrderWhere` so the filters track the list, and
 * `enrichOrder` so the per-order math matches the list/detail exactly. Returns
 * one entry per currency, rounded to 2dp, zero/empty totals dropped.
 */
export async function getOrderTotals(
  params: OrderFilterParams,
): Promise<{ totals: CurrencyAmount[] }> {
  const where = buildOrderWhere(params);

  // Select ONLY what the math needs (currency + item numbers).
  // This runs over the WHOLE filtered set, so pulling attachment blobs,
  // sections HTML or relations here would multiply the query size for nothing.
  const orders = await prisma.orders.findMany({
    where,
    select: {
      currency: true,
      order_items: {
        select: {
          quantity: true,
          unit_price: true,
          is_included_in_total: true,
        },
      },
    },
  });

  const byCurrency: Record<string, number> = {};
  for (const o of orders) {
    const { total, currency } = enrichOrder(o);
    const cur = currency || "CZK";
    byCurrency[cur] = (byCurrency[cur] || 0) + (Number(total) || 0);
  }

  const totals: CurrencyAmount[] = Object.entries(byCurrency)
    .map(([currency, amount]) => ({
      currency,
      amount: Math.round(amount * 100) / 100,
    }))
    .filter((t) => t.amount > 0);

  return { totals };
}

export async function getOrder(id: number) {
  const order = await prisma.orders.findUnique({
    where: { id },
    // The blob belongs only to GET /:id/attachment — the detail payload
    // carries attachment_name as the existence signal.
    omit: { attachment_data: true },
    include: {
      customers: true,
      order_items: { orderBy: { position: "asc" } },
      order_sections: { orderBy: { position: "asc" } },
      quotations: {
        select: { id: true, quotation_number: true, project_code: true },
      },
      projects: {
        select: { id: true, project_number: true, name: true, status: true },
      },
      invoices: {
        select: { id: true, invoice_number: true, status: true },
        orderBy: { id: "desc" },
      },
    },
  });
  if (!order) return null;
  const { order_items, order_sections, ...rest } = order;
  const invoice = order.invoices?.[0] || null;
  return {
    ...rest,
    items: order_items,
    sections: order_sections,
    customer: order.customers,
    customer_name: order.customers?.name || null,
    quotation_number: order.quotations?.quotation_number || null,
    project_code: order.quotations?.project_code || null,
    project: order.projects?.[0] || null,
    invoice: invoice,
    invoice_id: invoice?.id || null,
    invoice_number: invoice?.invoice_number || null,
    valid_transitions: VALID_TRANSITIONS[(order.status as string) || ""] || [],
  };
}

export async function getOrderAttachment(id: number) {
  const order = await prisma.orders.findUnique({
    where: { id },
    select: { attachment_data: true, attachment_name: true },
  });
  if (!order?.attachment_data) return null;
  return {
    data: Buffer.from(order.attachment_data),
    filename: order.attachment_name || `order-${id}.pdf`,
  };
}

interface CreateOrderFromQuotationData {
  quotationId: number;
  customerOrderNumber?: string;
  attachmentBuffer?: Buffer | null;
  attachmentName?: string | null;
}

export async function createOrderFromQuotation(
  data: CreateOrderFromQuotationData,
) {
  const { quotationId, customerOrderNumber, attachmentBuffer, attachmentName } =
    data;

  const quotation = await prisma.quotations.findUnique({
    where: { id: quotationId },
    include: {
      quotation_items: { orderBy: { position: "asc" } },
      scope_sections: { orderBy: [{ position: "asc" }, { id: "asc" }] },
    },
  });

  if (!quotation) return { error: "Nabídka nenalezena", status: 404 } as const;
  if (quotation.order_id)
    return {
      error: "Z této nabídky již byla vytvořena objednávka",
      status: 400,
    } as const;

  // The flow sets the offer's status to `ordered`, so it must respect the
  // offers transition table: only an offer whose current status allows the
  // `ordered` transition qualifies. A draft must be finalized first (it has no
  // number yet — an ordered offer without a number must never exist) and an
  // invalidated offer is terminal.
  const allowedFromStatus = OFFER_VALID_TRANSITIONS[quotation.status] || [];
  if (!allowedFromStatus.includes("ordered")) {
    return {
      error: `Objednávku nelze vytvořit z nabídky ve stavu "${quotation.status}"`,
      status: 400,
    } as const;
  }

  const result = await prisma.$transaction(async (tx) => {
    const orderNumber = await generateSharedNumber(tx);
    // The order keeps the shared (order) sequence; the auto-created project
    // gets its OWN number from the dedicated project sequence. Both are
    // assigned inside this transaction so they're allocated atomically.
    const projectNumber = await generateProjectNumber(tx);

    const order = await tx.orders.create({
      data: {
        order_number: orderNumber,
        customer_order_number: customerOrderNumber || null,
        quotation_id: quotationId,
        customer_id: quotation.customer_id,
        status: "prijata",
        currency: quotation.currency || "CZK",
        language: quotation.language || "cs",
        scope_title: quotation.scope_title,
        scope_description: quotation.scope_description,
        attachment_data: attachmentBuffer
          ? new Uint8Array(attachmentBuffer)
          : null,
        attachment_name: attachmentName || null,
      },
    });

    if (quotation.quotation_items.length > 0) {
      await tx.order_items.createMany({
        data: quotation.quotation_items.map((item) => ({
          order_id: order.id,
          description: item.description,
          item_description: item.item_description,
          quantity: item.quantity,
          unit: item.unit,
          unit_price: item.unit_price,
          is_included_in_total: item.is_included_in_total,
          position: item.position,
        })),
      });
    }

    if (quotation.scope_sections.length > 0) {
      await tx.order_sections.createMany({
        data: quotation.scope_sections.map((s) => ({
          order_id: order.id,
          title: s.title,
          title_cz: s.title_cz,
          content: s.content,
          position: s.position,
        })),
      });
    }

    await tx.quotations.update({
      where: { id: quotationId },
      data: { order_id: order.id, status: "ordered", modified_at: new Date() },
    });

    const project = await tx.projects.create({
      data: {
        project_number: projectNumber,
        name:
          quotation.project_code || quotation.quotation_number || projectNumber,
        customer_id: quotation.customer_id,
        quotation_id: quotationId,
        order_id: order.id,
        status: "aktivni",
      },
    });

    return { order, project, orderNumber };
  });

  // Best-effort NAS project folder, outside the transaction (mirrors
  // createProject). A project created from an accepted offer must get the same
  // 02_PROJEKTY/<number>_<name> folder as a manually-created one. Non-fatal: a
  // NAS outage must never roll back the order.
  if (result.project.project_number && nasFileManager.isConfigured()) {
    const created = nasFileManager.createProjectFolder(
      result.project.project_number,
      result.project.name || "",
    );
    if (!created) {
      console.error(
        "[orders.service] NAS folder not created for project",
        result.project.project_number,
      );
    }
  }

  return {
    data: {
      order_id: result.order.id,
      id: result.order.id,
      order_number: result.orderNumber,
      quotationId,
    },
  };
}

interface CreateOrderData {
  order_number?: string | null;
  customer_order_number?: string | null;
  quotation_id?: number | null;
  customer_id?: number | null;
  status: string;
  currency: string;
  language: string;
  exchange_rate?: number;
  scope_title?: string | null;
  scope_description?: string | null;
  notes?: string | null;
  items?: OrderItemInput[];
  sections?: OrderSectionInput[];
  create_project?: boolean;
}

export async function createOrder(
  body: CreateOrderData,
  attachmentBuffer?: Buffer | null,
  attachmentName?: string | null,
) {
  try {
    const result = await prisma.$transaction(async (tx) => {
      const orderNumber =
        body.order_number !== undefined && body.order_number !== null
          ? String(body.order_number)
          : await generateSharedNumber(tx);

      if (body.order_number !== undefined && body.order_number !== null) {
        const taken = await isOrderNumberTaken(String(body.order_number));
        if (taken) {
          throw Object.assign(new Error("Číslo objednávky je již použito"), {
            status: 400,
          });
        }
      }

      const order = await tx.orders.create({
        data: {
          order_number: orderNumber,
          customer_order_number: body.customer_order_number ?? null,
          quotation_id: body.quotation_id ?? null,
          customer_id: body.customer_id ?? null,
          status: body.status,
          currency: body.currency,
          language: body.language,
          exchange_rate: body.exchange_rate,
          scope_title: body.scope_title ?? null,
          scope_description: body.scope_description ?? null,
          notes: body.notes ?? null,
          attachment_data: attachmentBuffer
            ? new Uint8Array(attachmentBuffer)
            : null,
          attachment_name: attachmentName || null,
        },
      });

      if (Array.isArray(body.items)) {
        await tx.order_items.createMany({
          data: body.items.map((item, i) => ({
            order_id: order.id,
            description: item.description ?? null,
            item_description: item.item_description ?? null,
            quantity: item.quantity ?? 1,
            unit: item.unit ?? null,
            unit_price: item.unit_price ?? 0,
            is_included_in_total: item.is_included_in_total !== false,
            position: item.position ?? i,
          })),
        });
      }

      if (Array.isArray(body.sections)) {
        await tx.order_sections.createMany({
          data: body.sections.map((s, i) => ({
            order_id: order.id,
            title: s.title ?? null,
            title_cz: s.title_cz ?? null,
            content: s.content ?? null,
            position: s.position ?? i,
          })),
        });
      }

      let projectId: number | undefined;
      let createdProject: { project_number: string; name: string } | null =
        null;
      // Only auto-create a project for genuine offer-less orders. The project
      // gets its OWN number from the dedicated project sequence (not the
      // order's shared number), exactly like the from-quotation flow.
      if (body.create_project !== false && body.quotation_id == null) {
        const projectNumber = await generateProjectNumber(tx);
        const project = await tx.projects.create({
          data: {
            project_number: projectNumber,
            name:
              body.scope_title || body.customer_order_number || projectNumber,
            customer_id: order.customer_id,
            order_id: order.id,
            status: "aktivni",
          },
        });
        projectId = project.id;
        if (project.project_number) {
          createdProject = {
            project_number: project.project_number,
            name: project.name || "",
          };
        }
      }

      return {
        id: order.id,
        order_number: order.order_number,
        project_id: projectId,
        createdProject,
      };
    });

    // Best-effort NAS project folder, outside the transaction (mirrors
    // createProject). Non-fatal so a NAS outage can't roll back the order.
    if (result.createdProject && nasFileManager.isConfigured()) {
      const created = nasFileManager.createProjectFolder(
        result.createdProject.project_number,
        result.createdProject.name,
      );
      if (!created) {
        console.error(
          "[orders.service] NAS folder not created for project",
          result.createdProject.project_number,
        );
      }
    }

    return {
      id: result.id,
      order_number: result.order_number,
      project_id: result.project_id,
    };
  } catch (err) {
    if (err instanceof Error && "status" in err) {
      return {
        error: err.message,
        status: (err as Error & { status: number }).status,
      };
    }
    throw err;
  }
}

interface UpdateOrderData {
  [key: string]: unknown;
  customer_id?: number | string | null;
  items?: OrderItemInput[];
  sections?: OrderSectionInput[];
}

export async function updateOrder(id: number, body: UpdateOrderData) {
  // Pre-read only the fields the guards below check — never the PDF blob.
  const existing = await prisma.orders.findUnique({
    where: { id },
    select: { status: true, order_number: true },
  });
  if (!existing)
    return { error: "Objednávka nenalezena", status: 404 } as const;

  const currentStatus = existing.status as string;

  if (
    body.order_number !== undefined &&
    String(body.order_number) !== existing.order_number
  ) {
    return {
      error: "Číslo objednávky nelze změnit",
      status: 400,
    } as const;
  }

  if (body.status !== undefined && String(body.status) !== currentStatus) {
    const newStatus = String(body.status);
    const allowed = VALID_TRANSITIONS[currentStatus] || [];
    if (!allowed.includes(newStatus)) {
      return {
        error: `Neplatný přechod stavu z "${currentStatus}" na "${newStatus}"`,
        status: 400,
      } as const;
    }
  }

  const data: Record<string, unknown> = { modified_at: new Date() };
  const strFields = [
    "customer_order_number",
    "status",
    "currency",
    "language",
    "scope_title",
    "scope_description",
    "notes",
  ];
  for (const f of strFields) {
    if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
  }
  if (body.customer_id !== undefined)
    data.customer_id = body.customer_id ? Number(body.customer_id) : null;

  if (Array.isArray(body.items) || Array.isArray(body.sections)) {
    if (currentStatus !== "prijata" && currentStatus !== "v_realizaci") {
      return {
        error: "Nelze upravit položky dokončené/stornované objednávky",
        status: 400,
      } as const;
    }
    if (
      body.status !== undefined &&
      (String(body.status) === "dokoncena" ||
        String(body.status) === "stornovana")
    ) {
      return {
        error: "Nelze upravit položky při změně stavu na dokončeno/storno",
        status: 400,
      } as const;
    }
    await prisma.$transaction(async (tx) => {
      await tx.orders.update({ where: { id }, data });

      // Sync project status when order status changes (matching PHP)
      if (body.status !== undefined && String(body.status) !== currentStatus) {
        await syncProjectStatus(tx, id, String(body.status));
      }

      if (Array.isArray(body.items)) {
        await tx.order_items.deleteMany({ where: { order_id: id } });
        await tx.order_items.createMany({
          data: (body.items as OrderItemInput[]).map((item, i) => ({
            order_id: id,
            description: item.description ?? null,
            item_description: item.item_description ?? null,
            quantity: item.quantity ?? 1,
            unit: item.unit ?? null,
            unit_price: item.unit_price ?? 0,
            is_included_in_total: item.is_included_in_total !== false,
            position: item.position ?? i,
          })),
        });
      }
      if (Array.isArray(body.sections)) {
        await tx.order_sections.deleteMany({ where: { order_id: id } });
        await tx.order_sections.createMany({
          data: (body.sections as OrderSectionInput[]).map((s, i) => ({
            order_id: id,
            title: s.title ?? null,
            title_cz: s.title_cz ?? null,
            content: s.content ?? null,
            position: s.position ?? i,
          })),
        });
      }
    });
  } else {
    await prisma.orders.update({ where: { id }, data });

    // Sync project status when order status changes (matching PHP)
    if (body.status !== undefined && String(body.status) !== currentStatus) {
      await syncProjectStatus(prisma, id, String(body.status));
    }
  }

  return { data: { id, order_number: existing.order_number } };
}

export async function deleteOrder(id: number, deleteFiles = false) {
  // Pre-read only what the delete needs (number release + year) — not the blob.
  const existing = await prisma.orders.findUnique({
    where: { id },
    select: { order_number: true, created_at: true },
  });
  if (!existing)
    return { error: "Objednávka nenalezena", status: 404 } as const;

  // Fetch linked projects before the transaction for number release and
  // (optional) NAS folder cleanup later. project_number is needed to locate
  // the folder on the share.
  const linkedProjects = await prisma.projects.findMany({
    where: { order_id: id },
    select: { id: true, created_at: true, project_number: true },
  });

  try {
    await prisma.$transaction(async (tx) => {
      // Guard: projects may have non-cascaded warehouse refs (sklad_issues,
      // sklad_reservations, attendance_project_logs all use project_id as a
      // non-nullable FK with no onDelete). Surface as 409 (resource conflict)
      // rather than letting Prisma throw P2003 -> 500. The count check runs
      // INSIDE the transaction so a row can't be re-introduced between the
      // guard and the project delete (closes the narrow re-intro window).
      //
      // Only ACTIVE records count: a CANCELLED issue/restored batch or
      // CANCELLED reservation is audit-trail-only (cancelIssue() has already
      // restored the batch qty; cancelReservation() zeroes remaining_qty).
      // Attendance project logs are time-records — they always block, since
      // deleting the project would orphan the time record. The user must
      // re-assign the attendance log to a different project first.
      if (linkedProjects.length > 0) {
        const projectIds = linkedProjects.map((p) => p.id);
        const [activeIssuesCount, activeReservationsCount, attendanceLogCount] =
          await Promise.all([
            tx.sklad_issues.count({
              where: {
                project_id: { in: projectIds },
                status: { not: "CANCELLED" },
              },
            }),
            tx.sklad_reservations.count({
              where: {
                project_id: { in: projectIds },
                status: { not: "CANCELLED" },
              },
            }),
            tx.attendance_project_logs.count({
              where: { project_id: { in: projectIds } },
            }),
          ]);
        if (
          activeIssuesCount + activeReservationsCount + attendanceLogCount >
          0
        ) {
          throw Object.assign(
            new Error(
              "Nelze smazat objednávku, protože navázaný projekt má aktivní skladové výdeje, rezervace nebo docházkové záznamy. Zrušte je nebo přeřaďte docházku na jiný projekt.",
            ),
            { status: 409 },
          );
        }
      }

      // Clear the quotation back-reference AND revert its status: an
      // `ordered` offer with no order is a dead end (the transition table
      // only allows ordered -> invalidated), so it goes back to `active`,
      // making it orderable again.
      await tx.quotations.updateMany({
        where: { order_id: id, status: "ordered" },
        data: { order_id: null, status: "active", modified_at: new Date() },
      });
      // Backstop for rows in any other status (shouldn't exist — only an
      // ordered offer can hold an order_id).
      await tx.quotations.updateMany({
        where: { order_id: id },
        data: { order_id: null },
      });

      // Delete linked project and its notes (matching PHP)
      if (linkedProjects.length > 0) {
        const projectIds = linkedProjects.map((p) => p.id);
        await tx.project_notes.deleteMany({
          where: { project_id: { in: projectIds } },
        });
        await tx.projects.deleteMany({ where: { order_id: id } });
      }

      // Explicitly clean up child rows
      await tx.order_items.deleteMany({ where: { order_id: id } });
      await tx.order_sections.deleteMany({ where: { order_id: id } });

      await tx.orders.delete({ where: { id } });
    });
  } catch (err) {
    if (err instanceof Error && "status" in err) {
      return {
        error: err.message,
        status: (err as Error & { status: number }).status,
      } as const;
    }
    throw err;
  }

  // Best-effort NAS folder cleanup for the order's project(s), outside the
  // transaction and only when the user ticked the "delete folder" checkbox in
  // the order delete modal. Non-fatal: a NAS error must not undo the
  // already-committed DB delete. deleteProjectFolder is idempotent — it returns
  // true (no-op) when the folder is already absent.
  if (deleteFiles && nasFileManager.isConfigured()) {
    for (const p of linkedProjects) {
      if (!p.project_number) continue;
      const ok = await nasFileManager.deleteProjectFolder(p.project_number);
      if (!ok) {
        console.error(
          "[orders.service] NAS folder not deleted for project",
          p.project_number,
        );
      }
    }
  }

  const year = existing.created_at
    ? new Date(existing.created_at).getFullYear()
    : new Date().getFullYear();
  await releaseSharedNumber(year, existing.order_number ?? undefined);

  // Release each deleted project's OWN number from the dedicated `project`
  // sequence. Auto-created projects now draw a distinct number from the
  // `project` pool (not the order's `shared` number), so deleting the order
  // must release the project number too — otherwise the highest project number
  // leaves a permanent gap. Use each project's OWN created_at year: project
  // numbers embed {YY} and the sequence is (type, year)-keyed, so the order's
  // year could decrement the wrong row. Best-effort, post-commit, mirroring the
  // shared release above — releaseSequence row-locks and only decrements when
  // the deleted number is the current highest, so this is safe/idempotent.
  for (const p of linkedProjects) {
    if (!p.project_number) continue;
    const projYear = p.created_at ? new Date(p.created_at).getFullYear() : year;
    await releaseProjectNumber(projYear, p.project_number);
  }

  return { data: { id, order_number: existing.order_number } };
}

export async function getNextOrderNumber() {
  return previewSharedNumber();
}
