import { FastifyInstance } from "fastify";
import multipart from "@fastify/multipart";
import prisma from "../../config/database";
import { config } from "../../config/env";
import { requireAuth, requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response";
import { contentDisposition } from "../../utils/content-disposition";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import {
  encodeCustomFields,
  decodeCustomFields,
} from "../../utils/custom-fields";
import { parseBody } from "../../schemas/common";
import { nasInvoicesManager } from "../../services/nas-financials-manager";
import {
  getItemAvailableQty,
  getItemTotalStock,
  getItemStockValue,
  confirmReceipt,
  cancelReceipt,
  confirmIssue,
  cancelIssue,
  selectFifoBatches,
  createReservation,
  cancelReservation,
  confirmInventorySession,
  getBelowMinimumItems,
  validateIssueReservationRules,
} from "../../services/warehouse.service";
import {
  CreateCategorySchema,
  UpdateCategorySchema,
  CreateSupplierSchema,
  UpdateSupplierSchema,
  CreateLocationSchema,
  UpdateLocationSchema,
  CreateItemSchema,
  UpdateItemSchema,
  CreateReceiptSchema,
  UpdateReceiptSchema,
  CreateIssueSchema,
  UpdateIssueSchema,
  CreateReservationSchema,
  CreateInventorySessionSchema,
  UpdateInventorySessionSchema,
} from "../../schemas/warehouse.schema";

// Hard cap on the number of source documents (issues/receipts) a report will
// scan before aggregating/expanding. Bounds worst-case work + response size on
// a wide date range; the per-report pagination then pages the derived rows.
const REPORT_SOURCE_CAP = 5000;

/**
 * Coerce a querystring `status` filter to a plain string, or undefined if it
 * isn't a usable scalar. Querystrings can be arrays/objects (`?status[]=x`);
 * passing one straight into a Prisma string `where` is a type mismatch that
 * 500s. Returning undefined for non-strings makes a bad value a no-op filter
 * instead of a crash.
 */
function statusFilter(raw: unknown): string | undefined {
  return typeof raw === "string" && raw.length > 0 ? raw : undefined;
}

/**
 * Coerce a querystring numeric filter (FK id etc.) to a finite number, or
 * undefined if it isn't parseable. A junk value becomes a no-op filter rather
 * than a NaN reaching Prisma (which 500s).
 */
function numFilter(raw: unknown): number | undefined {
  if (raw === undefined || raw === null || raw === "") return undefined;
  const n = Number(raw);
  return Number.isFinite(n) ? n : undefined;
}

// NOTE: this file bundles 8 warehouse sub-entities (~2400 lines). Splitting it
// per sub-entity (items/receipts/issues/reservations/inventory/reports) is a
// tracked refactor, deferred — see REVIEW_FINDINGS.md.
export default async function warehouseRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  // =============================================================
  // CATEGORIES
  // =============================================================

  // GET /categories — list all, ordered by sort_order asc
  fastify.get(
    "/categories",
    { preHandler: requirePermission("warehouse.manage") },
    async (_request, reply) => {
      const categories = await prisma.sklad_categories.findMany({
        orderBy: { sort_order: "asc" },
      });
      return success(reply, categories);
    },
  );

  // POST /categories — create
  fastify.post(
    "/categories",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const parsed = parseBody(CreateCategorySchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const category = await prisma.sklad_categories.create({
        data: {
          name: body.name,
          description: body.description ?? null,
          sort_order: body.sort_order ?? 0,
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_category",
        entityId: category.id,
        description: `Vytvořena kategorie ${category.name}`,
        newValues: {
          name: category.name,
          description: category.description,
          sort_order: category.sort_order,
        },
      });

      return success(reply, category, 201, "Kategorie byla vytvořena");
    },
  );

  // PUT /categories/:id — update
  fastify.put<{ Params: { id: string } }>(
    "/categories/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateCategorySchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_categories.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Kategorie nenalezena", 404);

      const updateData: Record<string, unknown> = {};
      if (body.name !== undefined) updateData.name = body.name;
      if (body.description !== undefined)
        updateData.description = body.description;
      if (body.sort_order !== undefined)
        updateData.sort_order = body.sort_order;

      const updated = await prisma.sklad_categories.update({
        where: { id },
        data: updateData,
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_category",
        entityId: id,
        description: `Upravena kategorie ${updated.name}`,
        oldValues: {
          name: existing.name,
          description: existing.description,
          sort_order: existing.sort_order,
        },
        newValues: {
          name: updated.name,
          description: updated.description,
          sort_order: updated.sort_order,
        },
      });

      return success(reply, updated, 200, "Kategorie byla uložena");
    },
  );

  // DELETE /categories/:id — delete only if no items reference it
  fastify.delete<{ Params: { id: string } }>(
    "/categories/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const existing = await prisma.sklad_categories.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Kategorie nenalezena", 404);

      const itemCount = await prisma.sklad_items.count({
        where: { category_id: id },
      });
      if (itemCount > 0) {
        return error(
          reply,
          "Kategorii nelze smazat — obsahuje přiřazené položky",
          409,
        );
      }

      await prisma.sklad_categories.delete({ where: { id } });

      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "warehouse_category",
        entityId: id,
        description: `Smazána kategorie ${existing.name}`,
        oldValues: {
          name: existing.name,
          description: existing.description,
          sort_order: existing.sort_order,
        },
      });

      return success(reply, null, 200, "Kategorie smazána");
    },
  );

  // =============================================================
  // SUPPLIERS
  // =============================================================

  // GET /suppliers — paginated list, search by name/ico/city
  fastify.get(
    "/suppliers",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const { page, limit, skip, search } = parsePagination(
        request.query as Record<string, unknown>,
      );

      const where: Record<string, unknown> = {};
      if (search) {
        where.OR = [
          { name: { contains: search } },
          { ico: { contains: search } },
          { city: { contains: search } },
        ];
      }

      const [suppliers, total] = await Promise.all([
        prisma.sklad_suppliers.findMany({
          where,
          skip,
          take: limit,
          orderBy: { name: "asc" },
        }),
        prisma.sklad_suppliers.count({ where }),
      ]);

      // Decode the custom_fields blob into the array shape the modal edits
      // (same model as customers).
      const enriched = suppliers.map((s) => ({
        ...s,
        custom_fields: decodeCustomFields(s.custom_fields).custom_fields,
      }));

      return paginated(
        reply,
        enriched,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /suppliers/:id — detail
  fastify.get<{ Params: { id: string } }>(
    "/suppliers/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const supplier = await prisma.sklad_suppliers.findUnique({
        where: { id },
      });
      if (!supplier) return error(reply, "Dodavatel nenalezen", 404);

      return success(reply, {
        ...supplier,
        custom_fields: decodeCustomFields(supplier.custom_fields).custom_fields,
      });
    },
  );

  // POST /suppliers — create
  fastify.post(
    "/suppliers",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const parsed = parseBody(CreateSupplierSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const supplier = await prisma.sklad_suppliers.create({
        data: {
          name: body.name,
          ico: body.ico ?? null,
          dic: body.dic ?? null,
          street: body.street ?? null,
          city: body.city ?? null,
          postal_code: body.postal_code ?? null,
          country: body.country ?? null,
          // Suppliers have no PDF field-order picker — order is always [].
          custom_fields: encodeCustomFields(body.custom_fields, []),
          is_active: body.is_active ?? true,
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_supplier",
        entityId: supplier.id,
        description: `Vytvořen dodavatel ${supplier.name}`,
        newValues: {
          name: supplier.name,
          ico: supplier.ico,
          city: supplier.city,
          is_active: supplier.is_active,
        },
      });

      return success(reply, supplier, 201, "Dodavatel byl vytvořen");
    },
  );

  // PUT /suppliers/:id — update
  fastify.put<{ Params: { id: string } }>(
    "/suppliers/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateSupplierSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_suppliers.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Dodavatel nenalezen", 404);

      const updateData: Record<string, unknown> = {};
      if (body.name !== undefined) updateData.name = body.name;
      if (body.ico !== undefined) updateData.ico = body.ico;
      if (body.dic !== undefined) updateData.dic = body.dic;
      if (body.street !== undefined) updateData.street = body.street;
      if (body.city !== undefined) updateData.city = body.city;
      if (body.postal_code !== undefined)
        updateData.postal_code = body.postal_code;
      if (body.country !== undefined) updateData.country = body.country;
      if (body.custom_fields !== undefined)
        updateData.custom_fields = encodeCustomFields(body.custom_fields, []);
      if (body.is_active !== undefined) updateData.is_active = body.is_active;

      const updated = await prisma.sklad_suppliers.update({
        where: { id },
        data: updateData,
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_supplier",
        entityId: id,
        description: `Upraven dodavatel ${updated.name}`,
        oldValues: {
          name: existing.name,
          ico: existing.ico,
          city: existing.city,
          is_active: existing.is_active,
        },
        newValues: {
          name: updated.name,
          ico: updated.ico,
          city: updated.city,
          is_active: updated.is_active,
        },
      });

      return success(reply, updated, 200, "Dodavatel byl uložen");
    },
  );

  // DELETE /suppliers/:id — soft delete (set is_active=false)
  fastify.delete<{ Params: { id: string } }>(
    "/suppliers/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const existing = await prisma.sklad_suppliers.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Dodavatel nenalezen", 404);

      await prisma.sklad_suppliers.update({
        where: { id },
        data: { is_active: false },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "deactivate",
        entityType: "warehouse_supplier",
        entityId: id,
        description: `Deaktivován dodavatel ${existing.name}`,
        oldValues: { is_active: existing.is_active },
        newValues: { is_active: false },
      });

      return success(reply, null, 200, "Dodavatel byl deaktivován");
    },
  );

  // =============================================================
  // LOCATIONS
  // =============================================================

  // GET /locations — list locations, ordered by code asc
  // Query param `active`: "true" (default — only active, used by dropdowns),
  // "false" (only inactive), or "all" (everything, used by the management page
  // so deactivated rows remain visible — soft-delete, not a hard hide).
  fastify.get<{ Querystring: { active?: string } }>(
    "/locations",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const where: Record<string, unknown> = {};
      const active = request.query.active ?? "true";
      if (active === "true") where.is_active = true;
      else if (active === "false") where.is_active = false;
      // "all" or anything else: no filter

      const locations = await prisma.sklad_locations.findMany({
        where,
        orderBy: { code: "asc" },
      });
      return success(reply, locations);
    },
  );

  // POST /locations — create
  fastify.post(
    "/locations",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const parsed = parseBody(CreateLocationSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      // Check for duplicate code
      const duplicate = await prisma.sklad_locations.findUnique({
        where: { code: body.code },
      });
      if (duplicate) {
        return error(reply, "Kód umístění již existuje", 409);
      }

      const location = await prisma.sklad_locations.create({
        data: {
          code: body.code,
          name: body.name,
          description: body.description ?? null,
          is_active: body.is_active ?? true,
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_location",
        entityId: location.id,
        description: `Vytvořeno umístění ${location.code} - ${location.name}`,
        newValues: {
          code: location.code,
          name: location.name,
          is_active: location.is_active,
        },
      });

      return success(reply, location, 201, "Umístění bylo vytvořeno");
    },
  );

  // PUT /locations/:id — update
  fastify.put<{ Params: { id: string } }>(
    "/locations/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateLocationSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_locations.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Umístění nenalezeno", 404);

      // Check for duplicate code if code is being changed
      if (body.code !== undefined && body.code !== existing.code) {
        const duplicate = await prisma.sklad_locations.findUnique({
          where: { code: body.code },
        });
        if (duplicate) {
          return error(reply, "Kód umístění již existuje", 409);
        }
      }

      const updateData: Record<string, unknown> = {};
      if (body.code !== undefined) updateData.code = body.code;
      if (body.name !== undefined) updateData.name = body.name;
      if (body.description !== undefined)
        updateData.description = body.description;
      if (body.is_active !== undefined) updateData.is_active = body.is_active;

      const updated = await prisma.sklad_locations.update({
        where: { id },
        data: updateData,
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_location",
        entityId: id,
        description: `Upraveno umístění ${updated.code} - ${updated.name}`,
        oldValues: {
          code: existing.code,
          name: existing.name,
          description: existing.description,
          is_active: existing.is_active,
        },
        newValues: {
          code: updated.code,
          name: updated.name,
          description: updated.description,
          is_active: updated.is_active,
        },
      });

      return success(reply, updated, 200, "Umístění bylo uloženo");
    },
  );

  // DELETE /locations/:id — delete only if no items at location with non-zero quantity
  fastify.delete<{ Params: { id: string } }>(
    "/locations/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const existing = await prisma.sklad_locations.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Umístění nenalezeno", 404);

      // Check for items at this location with non-zero quantity
      const itemsAtLocation = await prisma.sklad_item_locations.findFirst({
        where: {
          location_id: id,
          quantity: { not: 0 },
        },
      });
      if (itemsAtLocation) {
        return error(
          reply,
          "Umístění nelze smazat — obsahuje položky s nenulovým množstvím",
          409,
        );
      }

      // Clean up zero-quantity item_location rows before deleting the location
      await prisma.sklad_item_locations.deleteMany({
        where: { location_id: id },
      });

      await prisma.sklad_locations.delete({ where: { id } });

      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "warehouse_location",
        entityId: id,
        description: `Smazáno umístění ${existing.code} - ${existing.name}`,
        oldValues: {
          code: existing.code,
          name: existing.name,
          description: existing.description,
        },
      });

      return success(reply, null, 200, "Umístění smazáno");
    },
  );

  // =============================================================
  // ITEMS
  // =============================================================

  // GET /items — paginated list, search by name/item_number, filter by category_id
  fastify.get(
    "/items",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const { page, limit, skip, search } = parsePagination(
        request.query as Record<string, unknown>,
      );
      const query = request.query as Record<string, unknown>;
      const categoryFilter = query.category_id
        ? Number(query.category_id)
        : undefined;

      const where: Record<string, unknown> = {};
      if (search || categoryFilter !== undefined) {
        const conditions: Record<string, unknown>[] = [];
        if (search) {
          conditions.push({
            OR: [
              { name: { contains: search } },
              { item_number: { contains: search } },
            ],
          });
        }
        if (categoryFilter !== undefined && !isNaN(categoryFilter)) {
          conditions.push({ category_id: categoryFilter });
        }
        if (conditions.length === 1) {
          Object.assign(where, conditions[0]);
        } else if (conditions.length > 1) {
          where.AND = conditions;
        }
      }

      const [items, total] = await Promise.all([
        prisma.sklad_items.findMany({
          where,
          skip,
          take: limit,
          orderBy: { name: "asc" },
          include: {
            category: { select: { id: true, name: true } },
            item_locations: {
              include: {
                location: { select: { id: true, code: true, name: true } },
              },
            },
          },
        }),
        prisma.sklad_items.count({ where }),
      ]);

      // Add computed fields for each item
      const enriched = await Promise.all(
        items.map(async (item) => {
          const total_quantity = await getItemTotalStock(item.id);
          const available_quantity = await getItemAvailableQty(item.id);
          const stock_value = await getItemStockValue(item.id);
          const minQty = item.min_quantity ? Number(item.min_quantity) : null;
          const below_minimum =
            minQty !== null ? total_quantity < minQty : false;

          return {
            ...item,
            total_quantity,
            available_quantity,
            stock_value,
            below_minimum,
          };
        }),
      );

      return paginated(
        reply,
        enriched,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /items/:id — detail with category, batches, item_locations, plus computed fields
  fastify.get<{ Params: { id: string } }>(
    "/items/:id",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const item = await prisma.sklad_items.findUnique({
        where: { id },
        include: {
          category: { select: { id: true, name: true } },
          batches: {
            where: { is_consumed: false },
            orderBy: { received_at: "asc" },
          },
          item_locations: {
            include: {
              location: { select: { id: true, code: true, name: true } },
            },
          },
        },
      });
      if (!item) return error(reply, "Položka nenalezena", 404);

      const total_quantity = await getItemTotalStock(id);
      const available_quantity = await getItemAvailableQty(id);
      const stock_value = await getItemStockValue(id);

      return success(reply, {
        ...item,
        total_quantity,
        available_quantity,
        stock_value,
      });
    },
  );

  // POST /items — create
  fastify.post(
    "/items",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const parsed = parseBody(CreateItemSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      // Check for duplicate item_number if provided
      if (body.item_number) {
        const duplicate = await prisma.sklad_items.findUnique({
          where: { item_number: body.item_number },
        });
        if (duplicate) {
          return error(reply, "Číslo položky již existuje", 409);
        }
      }

      const item = await prisma.sklad_items.create({
        data: {
          item_number: body.item_number ?? null,
          name: body.name,
          description: body.description ?? null,
          category_id: body.category_id ?? null,
          unit: body.unit,
          min_quantity: body.min_quantity ?? null,
          notes: body.notes ?? null,
          is_active: body.is_active ?? true,
        },
        include: {
          category: { select: { id: true, name: true } },
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_item",
        entityId: item.id,
        description: `Vytvořena položka ${item.name}`,
        newValues: {
          item_number: item.item_number,
          name: item.name,
          unit: item.unit,
          min_quantity: item.min_quantity,
          is_active: item.is_active,
        },
      });

      return success(reply, item, 201, "Položka byla vytvořena");
    },
  );

  // PUT /items/:id — update
  fastify.put<{ Params: { id: string } }>(
    "/items/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateItemSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_items.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Položka nenalezena", 404);

      // Extra validation: if unit is being changed and batches exist, return 409
      if (body.unit !== undefined && body.unit !== existing.unit) {
        const batchCount = await prisma.sklad_batches.count({
          where: { item_id: id },
        });
        if (batchCount > 0) {
          return error(reply, "Jednotku nelze změnit - položka má pohyby", 409);
        }
      }

      // Check for duplicate item_number if changing
      if (
        body.item_number !== undefined &&
        body.item_number !== existing.item_number
      ) {
        if (body.item_number) {
          const duplicate = await prisma.sklad_items.findUnique({
            where: { item_number: body.item_number },
          });
          if (duplicate) {
            return error(reply, "Číslo položky již existuje", 409);
          }
        }
      }

      const updateData: Record<string, unknown> = {};
      if (body.item_number !== undefined)
        updateData.item_number = body.item_number;
      if (body.name !== undefined) updateData.name = body.name;
      if (body.description !== undefined)
        updateData.description = body.description;
      if (body.category_id !== undefined)
        updateData.category_id = body.category_id;
      if (body.unit !== undefined) updateData.unit = body.unit;
      if (body.min_quantity !== undefined)
        updateData.min_quantity = body.min_quantity;
      if (body.notes !== undefined) updateData.notes = body.notes;
      if (body.is_active !== undefined) updateData.is_active = body.is_active;

      const updated = await prisma.sklad_items.update({
        where: { id },
        data: updateData,
        include: {
          category: { select: { id: true, name: true } },
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_item",
        entityId: id,
        description: `Upravena položka ${updated.name}`,
        oldValues: {
          item_number: existing.item_number,
          name: existing.name,
          unit: existing.unit,
          min_quantity: existing.min_quantity,
          is_active: existing.is_active,
          category_id: existing.category_id,
        },
        newValues: {
          item_number: updated.item_number,
          name: updated.name,
          unit: updated.unit,
          min_quantity: updated.min_quantity,
          is_active: updated.is_active,
          category_id: updated.category_id,
        },
      });

      return success(reply, updated, 200, "Položka byla uložena");
    },
  );

  // DELETE /items/:id — hard delete, blocked if item has stock activity
  fastify.delete<{ Params: { id: string } }>(
    "/items/:id",
    { preHandler: requirePermission("warehouse.manage") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const existing = await prisma.sklad_items.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Položka nenalezena", 404);

      // Check if item is referenced by any stock activity
      const [receiptLines, issueLines, activeReservations] = await Promise.all([
        prisma.sklad_receipt_lines.count({ where: { item_id: id } }),
        prisma.sklad_issue_lines.count({ where: { item_id: id } }),
        prisma.sklad_reservations.count({
          where: { item_id: id, status: "ACTIVE" },
        }),
      ]);

      if (receiptLines > 0 || issueLines > 0 || activeReservations > 0) {
        return error(
          reply,
          "Položku nelze smazat — má vazby na příjmy, výdaje nebo aktivní rezervace",
          409,
        );
      }

      // Safe to delete: clean up locations and consumed batches first
      await prisma.sklad_item_locations.deleteMany({ where: { item_id: id } });
      await prisma.sklad_batches.deleteMany({ where: { item_id: id } });
      await prisma.sklad_reservations.deleteMany({
        where: { item_id: id, status: "CANCELLED" },
      });
      await prisma.sklad_inventory_lines.deleteMany({ where: { item_id: id } });
      await prisma.sklad_items.delete({ where: { id } });

      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "warehouse_item",
        entityId: id,
        description: `Smazána položka ${existing.name}`,
        oldValues: { name: existing.name, item_number: existing.item_number },
      });

      return success(reply, null, 200, "Položka byla smazána");
    },
  );

  // GET /items/:id/available-qty — return available quantity
  fastify.get<{ Params: { id: string } }>(
    "/items/:id/available-qty",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const item = await prisma.sklad_items.findUnique({
        where: { id },
        select: { id: true },
      });
      if (!item) return error(reply, "Položka nenalezena", 404);

      const available_quantity = await getItemAvailableQty(id);

      return success(reply, {
        item_id: id,
        available_quantity,
      });
    },
  );

  // GET /items/:id/batches — return unconsumed batches with quantity > 0
  fastify.get<{ Params: { id: string } }>(
    "/items/:id/batches",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const item = await prisma.sklad_items.findUnique({
        where: { id },
        select: { id: true },
      });
      if (!item) return error(reply, "Položka nenalezena", 404);

      const batches = await prisma.sklad_batches.findMany({
        where: {
          item_id: id,
          is_consumed: false,
          quantity: { gt: 0 },
        },
        orderBy: { received_at: "asc" },
      });

      // Include reservation-aware available quantity
      const totalStock = await getItemTotalStock(id);
      const reservedResult = await prisma.sklad_reservations.aggregate({
        _sum: { remaining_qty: true },
        where: { item_id: id, status: "ACTIVE" },
      });
      const reservedQty = Number(reservedResult._sum.remaining_qty ?? 0);
      const availableQuantity = totalStock - reservedQty;

      return success(reply, {
        batches,
        total_stock: totalStock,
        reserved_quantity: reservedQty,
        available_quantity: availableQuantity,
      });
    },
  );

  // =============================================================
  // RECEIPTS
  // =============================================================

  await fastify.register(multipart, {
    limits: { fileSize: config.nas.maxUploadSize },
  });

  // GET /receipts — paginated list, filter by status/supplier_id/date_from/date_to
  fastify.get(
    "/receipts",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const { page, limit, skip, search } = parsePagination(
        request.query as Record<string, unknown>,
      );
      const query = request.query as Record<string, unknown>;

      const where: Record<string, unknown>[] = [];

      const status = statusFilter(query.status);
      if (status) {
        where.push({ status });
      }
      const supplierId = numFilter(query.supplier_id);
      if (supplierId !== undefined) {
        where.push({ supplier_id: supplierId });
      }
      if (query.date_from) {
        where.push({ created_at: { gte: new Date(String(query.date_from)) } });
      }
      if (query.date_to) {
        where.push({ created_at: { lte: new Date(String(query.date_to)) } });
      }
      if (search) {
        where.push({
          OR: [
            { receipt_number: { contains: search } },
            { delivery_note_number: { contains: search } },
            { notes: { contains: search } },
          ],
        });
      }

      const filter = where.length > 0 ? { AND: where } : {};

      const [receipts, total] = await Promise.all([
        prisma.sklad_receipts.findMany({
          where: filter,
          skip,
          take: limit,
          orderBy: { id: "desc" },
          include: {
            supplier: { select: { id: true, name: true } },
            received_by_user: {
              select: { id: true, first_name: true, last_name: true },
            },
            _count: { select: { items: true } },
          },
        }),
        prisma.sklad_receipts.count({ where: filter }),
      ]);

      return paginated(
        reply,
        receipts,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /receipts/:id — detail with supplier, received_by_user, items, attachments
  fastify.get<{ Params: { id: string } }>(
    "/receipts/:id",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const receipt = await prisma.sklad_receipts.findUnique({
        where: { id },
        include: {
          supplier: { select: { id: true, name: true } },
          received_by_user: {
            select: { id: true, first_name: true, last_name: true },
          },
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
              location: { select: { id: true, code: true, name: true } },
            },
          },
          attachments: true,
        },
      });
      if (!receipt) return error(reply, "Příjem nenalezen", 404);

      return success(reply, receipt);
    },
  );

  // POST /receipts — create DRAFT with items
  fastify.post(
    "/receipts",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const parsed = parseBody(CreateReceiptSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const authUserId = request.authData!.userId;

      const receipt = await prisma.sklad_receipts.create({
        data: {
          supplier_id: body.supplier_id ?? null,
          delivery_note_number: body.delivery_note_number ?? null,
          delivery_note_date: body.delivery_note_date
            ? new Date(body.delivery_note_date)
            : null,
          notes: body.notes ?? null,
          status: "DRAFT",
          received_by: authUserId,
          items: {
            create: body.items.map((item) => ({
              item_id: item.item_id,
              quantity: item.quantity,
              unit_price: item.unit_price,
              location_id: item.location_id ?? null,
              notes: item.notes ?? null,
            })),
          },
        },
        include: {
          supplier: { select: { id: true, name: true } },
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
            },
          },
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_receipt",
        entityId: receipt.id,
        description: `Vytvořen příjem #${receipt.id}`,
        newValues: {
          supplier_id: receipt.supplier_id,
          status: receipt.status,
          item_count: body.items.length,
        },
      });

      return success(reply, receipt, 201, "Příjem byl vytvořen");
    },
  );

  // PUT /receipts/:id — update header + replace items (DRAFT only)
  fastify.put<{ Params: { id: string } }>(
    "/receipts/:id",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateReceiptSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_receipts.findUnique({
        where: { id },
        include: { items: true },
      });
      if (!existing) return error(reply, "Příjem nenalezen", 404);
      if (existing.status !== "DRAFT") {
        return error(reply, "Příjem lze upravit pouze ve stavu DRAFT", 400);
      }

      const updateData: Record<string, unknown> = {};
      if (body.supplier_id !== undefined)
        updateData.supplier_id = body.supplier_id;
      if (body.delivery_note_number !== undefined)
        updateData.delivery_note_number = body.delivery_note_number;
      if (body.delivery_note_date !== undefined)
        updateData.delivery_note_date = body.delivery_note_date
          ? new Date(body.delivery_note_date)
          : null;
      if (body.notes !== undefined) updateData.notes = body.notes;
      updateData.modified_at = new Date();

      // Replace items + persist header atomically: the line deleteMany +
      // createMany and the header update run in ONE transaction, so a failure
      // mid-replacement can't leave the draft with zero (or partial) lines.
      const updated = await prisma.$transaction(async (tx) => {
        if (body.items) {
          await tx.sklad_receipt_lines.deleteMany({
            where: { receipt_id: id },
          });
          await tx.sklad_receipt_lines.createMany({
            data: body.items.map((item) => ({
              receipt_id: id,
              item_id: item.item_id,
              quantity: item.quantity,
              unit_price: item.unit_price,
              location_id: item.location_id ?? null,
              notes: item.notes ?? null,
            })),
          });
        }

        return tx.sklad_receipts.update({
          where: { id },
          data: updateData,
          include: {
            supplier: { select: { id: true, name: true } },
            items: {
              include: {
                item: { select: { id: true, name: true, unit: true } },
                location: { select: { id: true, code: true, name: true } },
              },
            },
          },
        });
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_receipt",
        entityId: id,
        description: `Upraven příjem #${id}`,
        oldValues: {
          supplier_id: existing.supplier_id,
          delivery_note_number: existing.delivery_note_number,
          notes: existing.notes,
          item_count: existing.items.length,
        },
        newValues: {
          supplier_id: updated.supplier_id,
          delivery_note_number: updated.delivery_note_number,
          notes: updated.notes,
          item_count: updated.items.length,
        },
      });

      return success(reply, updated, 200, "Příjem byl uložen");
    },
  );

  // POST /receipts/:id/confirm — confirm receipt
  fastify.post<{ Params: { id: string } }>(
    "/receipts/:id/confirm",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const authUserId = request.authData!.userId;
      const result = await confirmReceipt(id, authUserId);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "confirm",
        entityType: "warehouse_receipt",
        entityId: id,
        description: `Potvrzen příjem ${result.data.receipt_number}`,
        newValues: {
          status: "CONFIRMED",
          receipt_number: result.data.receipt_number,
        },
      });

      return success(reply, result.data, 200, "Příjem byl potvrzen");
    },
  );

  // POST /receipts/:id/cancel — cancel receipt
  fastify.post<{ Params: { id: string } }>(
    "/receipts/:id/cancel",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await cancelReceipt(id);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "cancel",
        entityType: "warehouse_receipt",
        entityId: id,
        description: `Zrušen příjem #${id}`,
        newValues: { status: "CANCELLED" },
      });

      return success(reply, result.data, 200, "Příjem byl zrušen");
    },
  );

  // GET /receipts/:id/attachments/:attachmentId — download attachment from NAS
  fastify.get<{ Params: { id: string; attachmentId: string } }>(
    "/receipts/:id/attachments/:attachmentId",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const attachmentId = parseId(request.params.attachmentId, reply);
      if (attachmentId === null) return;

      const attachment = await prisma.sklad_receipt_attachments.findUnique({
        where: { id: attachmentId },
      });
      // 404 (not 400) when the attachment belongs to a different receipt:
      // a read endpoint should not confirm that the id exists elsewhere.
      if (!attachment || attachment.receipt_id !== id) {
        return error(reply, "Příloha nenalezena", 404);
      }

      const nasFile = nasInvoicesManager.readReceived(attachment.file_path);
      if (!nasFile) return error(reply, "Soubor na NAS nenalezen", 404);

      const mime = attachment.file_mime || "application/octet-stream";
      // "attachment": the UI always downloads via blob + a.download, never
      // renders inline — and attachment is safer with client-controlled mime.
      return reply
        .type(mime)
        .header(
          "Content-Disposition",
          contentDisposition("attachment", attachment.file_name),
        )
        .send(nasFile.data);
    },
  );

  // POST /receipts/:id/attachments — upload attachment
  fastify.post<{ Params: { id: string } }>(
    "/receipts/:id/attachments",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const receipt = await prisma.sklad_receipts.findUnique({
        where: { id },
      });
      if (!receipt) return error(reply, "Příjem nenalezen", 404);

      const part = await request.file();
      if (!part) return error(reply, "Soubor nebyl nahrán", 400);

      const buffer = await part.toBuffer();
      const now = new Date();
      const year = now.getFullYear();
      const month = now.getMonth() + 1;

      if (!nasInvoicesManager.isConfigured()) {
        return error(reply, "Úložiště souborů není dostupné", 503);
      }

      const result = nasInvoicesManager.saveReceived(
        part.filename,
        year,
        month,
        buffer,
      );
      if ("error" in result) return error(reply, result.error, 500);

      // Compensating delete: the NAS write already succeeded above. If the DB
      // insert now fails we must remove the just-written file, otherwise it is
      // orphaned on the NAS with no DB row pointing at it.
      let attachment: Awaited<
        ReturnType<typeof prisma.sklad_receipt_attachments.create>
      >;
      try {
        attachment = await prisma.sklad_receipt_attachments.create({
          data: {
            receipt_id: id,
            file_name: part.filename,
            file_mime: part.mimetype,
            file_size: buffer.length,
            file_path: result.filePath,
          },
        });
      } catch (e) {
        const removed = nasInvoicesManager.deleteReceived(result.filePath);
        request.log.error(
          { err: e, filePath: result.filePath, orphanRemoved: removed },
          "warehouse attachment DB insert failed after NAS write; rolled back NAS file",
        );
        return error(reply, "Přílohu se nepodařilo uložit", 500);
      }

      await logAudit({
        request,
        authData: request.authData,
        action: "upload",
        entityType: "warehouse_receipt_attachment",
        entityId: attachment.id,
        description: `Nahrána příloha k příjmu #${id}: ${part.filename}`,
        newValues: {
          file_name: attachment.file_name,
          file_size: attachment.file_size,
        },
      });

      return success(reply, attachment, 201, "Příloha byla nahrána");
    },
  );

  // DELETE /receipts/:id/attachments/:attachmentId — delete attachment
  fastify.delete<{ Params: { id: string; attachmentId: string } }>(
    "/receipts/:id/attachments/:attachmentId",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const attachmentId = parseId(request.params.attachmentId, reply);
      if (attachmentId === null) return;

      const attachment = await prisma.sklad_receipt_attachments.findUnique({
        where: { id: attachmentId },
      });
      if (!attachment) return error(reply, "Příloha nenalezena", 404);
      if (attachment.receipt_id !== id) {
        return error(reply, "Příloha nepatří k tomuto příjmu", 400);
      }

      // Delete file from NAS. A failure here is non-fatal (we still remove the
      // DB row) but must not be silent — log it so an undeleted NAS file is
      // visible rather than leaking quietly.
      const nasDeleted = nasInvoicesManager.deleteReceived(
        attachment.file_path,
      );
      if (!nasDeleted) {
        request.log.error(
          { filePath: attachment.file_path, attachmentId },
          "warehouse attachment NAS delete failed; DB row will still be removed",
        );
      }

      await prisma.sklad_receipt_attachments.delete({
        where: { id: attachmentId },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "warehouse_receipt_attachment",
        entityId: attachmentId,
        description: `Smazána příloha příjmu #${id}: ${attachment.file_name}`,
        oldValues: {
          file_name: attachment.file_name,
          file_path: attachment.file_path,
        },
      });

      return success(reply, null, 200, "Příloha smazána");
    },
  );

  // =============================================================
  // ISSUES
  // =============================================================

  // GET /issues — paginated list, filter by status/project_id/date_from/date_to
  fastify.get(
    "/issues",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const { page, limit, skip, search } = parsePagination(
        request.query as Record<string, unknown>,
      );
      const query = request.query as Record<string, unknown>;

      const where: Record<string, unknown>[] = [];

      const status = statusFilter(query.status);
      if (status) {
        where.push({ status });
      }
      const projectId = numFilter(query.project_id);
      if (projectId !== undefined) {
        where.push({ project_id: projectId });
      }
      if (query.date_from) {
        where.push({ created_at: { gte: new Date(String(query.date_from)) } });
      }
      if (query.date_to) {
        where.push({ created_at: { lte: new Date(String(query.date_to)) } });
      }
      if (search) {
        where.push({
          OR: [
            { issue_number: { contains: search } },
            { notes: { contains: search } },
          ],
        });
      }

      const filter = where.length > 0 ? { AND: where } : {};

      const [issues, total] = await Promise.all([
        prisma.sklad_issues.findMany({
          where: filter,
          skip,
          take: limit,
          orderBy: { id: "desc" },
          include: {
            project: { select: { id: true, name: true, project_number: true } },
            issued_by_user: {
              select: { id: true, first_name: true, last_name: true },
            },
            _count: { select: { items: true } },
          },
        }),
        prisma.sklad_issues.count({ where: filter }),
      ]);

      return paginated(reply, issues, buildPaginationMeta(total, page, limit));
    },
  );

  // GET /issues/:id — detail with project, issued_by_user, items
  fastify.get<{ Params: { id: string } }>(
    "/issues/:id",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const issue = await prisma.sklad_issues.findUnique({
        where: { id },
        include: {
          project: { select: { id: true, name: true, project_number: true } },
          issued_by_user: {
            select: { id: true, first_name: true, last_name: true },
          },
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
              batch: {
                select: { id: true, received_at: true, unit_price: true },
              },
              location: { select: { id: true, code: true, name: true } },
              reservation: {
                select: {
                  id: true,
                  quantity: true,
                  remaining_qty: true,
                  status: true,
                },
              },
            },
          },
        },
      });
      if (!issue) return error(reply, "Výdej nenalezen", 404);

      return success(reply, issue);
    },
  );

  // POST /issues — create DRAFT with items, auto-FIFO for items without batch_id
  fastify.post(
    "/issues",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const parsed = parseBody(CreateIssueSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const authUserId = request.authData!.userId;

      // Resolve FIFO batches for items without batch_id
      const resolvedItems: Array<{
        item_id: number;
        batch_id: number;
        quantity: number;
        location_id: number | null;
        reservation_id: number | null;
        notes: string | null;
      }> = [];

      for (const item of body.items) {
        if (item.batch_id != null) {
          resolvedItems.push({
            item_id: item.item_id,
            batch_id: item.batch_id,
            quantity: item.quantity,
            location_id: item.location_id ?? null,
            reservation_id: item.reservation_id ?? null,
            notes: item.notes ?? null,
          });
        } else {
          // Auto-select FIFO batches
          try {
            const fifoBatches = await selectFifoBatches(
              item.item_id,
              item.quantity,
            );
            for (const fb of fifoBatches) {
              resolvedItems.push({
                item_id: item.item_id,
                batch_id: fb.batchId,
                quantity: fb.qty,
                location_id: item.location_id ?? null,
                reservation_id: item.reservation_id ?? null,
                notes: item.notes ?? null,
              });
            }
          } catch (err) {
            const message =
              err instanceof Error
                ? err.message
                : "Nedostatečné množství na skladě";
            return error(reply, message, 400);
          }
        }
      }

      // Validate reservation rules
      const reservationValidation = await validateIssueReservationRules(
        resolvedItems,
        body.project_id,
      );
      if ("error" in reservationValidation) {
        return error(
          reply,
          reservationValidation.error,
          reservationValidation.status,
        );
      }

      const issue = await prisma.sklad_issues.create({
        data: {
          project_id: body.project_id,
          notes: body.notes ?? null,
          status: "DRAFT",
          issued_by: authUserId,
          items: {
            create: resolvedItems,
          },
        },
        include: {
          project: { select: { id: true, name: true, project_number: true } },
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
            },
          },
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_issue",
        entityId: issue.id,
        description: `Vytvořen výdej #${issue.id}`,
        newValues: {
          project_id: issue.project_id,
          status: issue.status,
          item_count: resolvedItems.length,
        },
      });

      return success(reply, issue, 201, "Výdej byl vytvořen");
    },
  );

  // PUT /issues/:id — update (DRAFT only), auto-FIFO for items without batch_id
  fastify.put<{ Params: { id: string } }>(
    "/issues/:id",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateIssueSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_issues.findUnique({
        where: { id },
        include: { items: true },
      });
      if (!existing) return error(reply, "Výdej nenalezen", 404);
      if (existing.status !== "DRAFT") {
        return error(reply, "Výdej lze upravit pouze ve stavu DRAFT", 400);
      }

      const updateData: Record<string, unknown> = {};
      if (body.project_id !== undefined)
        updateData.project_id = body.project_id;
      if (body.notes !== undefined) updateData.notes = body.notes;
      updateData.modified_at = new Date();

      // Resolved replacement lines (null = caller did not send `items`, so the
      // lines are left untouched). Populated/validated below, then persisted
      // atomically with the header update.
      let resolvedItems: Array<{
        item_id: number;
        batch_id: number;
        quantity: number;
        location_id: number | null;
        reservation_id: number | null;
        notes: string | null;
      }> | null = null;

      // Replace items if provided
      if (body.items) {
        resolvedItems = [];

        for (const item of body.items) {
          if (item.batch_id != null) {
            resolvedItems.push({
              item_id: item.item_id,
              batch_id: item.batch_id,
              quantity: item.quantity,
              location_id: item.location_id ?? null,
              reservation_id: item.reservation_id ?? null,
              notes: item.notes ?? null,
            });
          } else {
            try {
              const fifoBatches = await selectFifoBatches(
                item.item_id,
                item.quantity,
              );
              for (const fb of fifoBatches) {
                resolvedItems.push({
                  item_id: item.item_id,
                  batch_id: fb.batchId,
                  quantity: fb.qty,
                  location_id: item.location_id ?? null,
                  reservation_id: item.reservation_id ?? null,
                  notes: item.notes ?? null,
                });
              }
            } catch (err) {
              const message =
                err instanceof Error
                  ? err.message
                  : "Nedostatečné množství na skladě";
              return error(reply, message, 400);
            }
          }
        }

        // Validate reservation rules
        const issueProjectId = body.project_id ?? existing.project_id;
        const reservationValidation = await validateIssueReservationRules(
          resolvedItems,
          issueProjectId,
        );
        if ("error" in reservationValidation) {
          return error(
            reply,
            reservationValidation.error,
            reservationValidation.status,
          );
        }
      }

      // Replace lines + persist header atomically: the line deleteMany +
      // createMany and the header update run in ONE transaction, so a failure
      // mid-replacement can't leave the draft with zero (or partial) lines.
      const linesToWrite = resolvedItems;
      const updated = await prisma.$transaction(async (tx) => {
        if (linesToWrite) {
          await tx.sklad_issue_lines.deleteMany({
            where: { issue_id: id },
          });
          await tx.sklad_issue_lines.createMany({
            data: linesToWrite.map((item) => ({
              issue_id: id,
              item_id: item.item_id,
              batch_id: item.batch_id,
              quantity: item.quantity,
              location_id: item.location_id,
              reservation_id: item.reservation_id,
              notes: item.notes,
            })),
          });
        }

        return tx.sklad_issues.update({
          where: { id },
          data: updateData,
          include: {
            project: {
              select: { id: true, name: true, project_number: true },
            },
            items: {
              include: {
                item: { select: { id: true, name: true, unit: true } },
              },
            },
          },
        });
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_issue",
        entityId: id,
        description: `Upraven výdej #${id}`,
        oldValues: {
          project_id: existing.project_id,
          notes: existing.notes,
          item_count: existing.items.length,
        },
        newValues: {
          project_id: updated.project_id,
          notes: updated.notes,
          item_count: updated.items.length,
        },
      });

      return success(reply, updated, 200, "Výdej byl uložen");
    },
  );

  // POST /issues/:id/confirm — confirm issue
  fastify.post<{ Params: { id: string } }>(
    "/issues/:id/confirm",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const authUserId = request.authData!.userId;
      const result = await confirmIssue(id, authUserId);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "confirm",
        entityType: "warehouse_issue",
        entityId: id,
        description: `Potvrzen výdej ${result.data.issue_number}`,
        newValues: {
          status: "CONFIRMED",
          issue_number: result.data.issue_number,
        },
      });

      return success(reply, result.data, 200, "Výdej byl potvrzen");
    },
  );

  // POST /issues/:id/cancel — cancel issue
  fastify.post<{ Params: { id: string } }>(
    "/issues/:id/cancel",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await cancelIssue(id);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "cancel",
        entityType: "warehouse_issue",
        entityId: id,
        description: `Zrušen výdej #${id}`,
        newValues: { status: "CANCELLED" },
      });

      return success(reply, result.data, 200, "Výdej byl zrušen");
    },
  );

  // =============================================================
  // RESERVATIONS
  // =============================================================

  // GET /reservations — paginated list, filter by item_id/project_id/status
  fastify.get(
    "/reservations",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const { page, limit, skip } = parsePagination(
        request.query as Record<string, unknown>,
      );
      const query = request.query as Record<string, unknown>;

      const where: Record<string, unknown>[] = [];

      const itemId = numFilter(query.item_id);
      if (itemId !== undefined) {
        where.push({ item_id: itemId });
      }
      const projectId = numFilter(query.project_id);
      if (projectId !== undefined) {
        where.push({ project_id: projectId });
      }
      const status = statusFilter(query.status);
      if (status) {
        where.push({ status });
      }

      const filter = where.length > 0 ? { AND: where } : {};

      const [reservations, total] = await Promise.all([
        prisma.sklad_reservations.findMany({
          where: filter,
          skip,
          take: limit,
          orderBy: { id: "desc" },
          include: {
            item: { select: { id: true, name: true, unit: true } },
            project: { select: { id: true, name: true, project_number: true } },
            reserved_by_user: {
              select: { id: true, first_name: true, last_name: true },
            },
          },
        }),
        prisma.sklad_reservations.count({ where: filter }),
      ]);

      return paginated(
        reply,
        reservations,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // POST /reservations — create reservation
  fastify.post(
    "/reservations",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const parsed = parseBody(CreateReservationSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const authUserId = request.authData!.userId;

      const result = await createReservation({
        item_id: body.item_id,
        project_id: body.project_id,
        quantity: body.quantity,
        reserved_by: authUserId,
        notes: body.notes ?? null,
      });
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_reservation",
        entityId: (result.data as Record<string, unknown>).id as number,
        description: `Vytvořena rezervace položky ID ${body.item_id} pro projekt ID ${body.project_id}`,
        newValues: {
          item_id: body.item_id,
          project_id: body.project_id,
          quantity: body.quantity,
        },
      });

      return success(reply, result.data, 201, "Rezervace byla vytvořena");
    },
  );

  // POST /reservations/:id/cancel — cancel reservation
  fastify.post<{ Params: { id: string } }>(
    "/reservations/:id/cancel",
    { preHandler: requirePermission("warehouse.operate") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await cancelReservation(id);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "cancel",
        entityType: "warehouse_reservation",
        entityId: id,
        description: `Zrušena rezervace #${id}`,
        newValues: { status: "CANCELLED" },
      });

      return success(reply, result.data, 200, "Rezervace byla zrušena");
    },
  );

  // =============================================================
  // INVENTORY
  // =============================================================

  // GET /inventory-sessions — paginated list, filter by status
  fastify.get(
    "/inventory-sessions",
    { preHandler: requirePermission("warehouse.inventory") },
    async (request, reply) => {
      const { page, limit, skip } = parsePagination(
        request.query as Record<string, unknown>,
      );
      const query = request.query as Record<string, unknown>;

      const where: Record<string, unknown> = {};
      const status = statusFilter(query.status);
      if (status) {
        where.status = status;
      }

      const [sessions, total] = await Promise.all([
        prisma.sklad_inventory_sessions.findMany({
          where,
          skip,
          take: limit,
          orderBy: { id: "desc" },
          include: {
            _count: { select: { items: true } },
          },
        }),
        prisma.sklad_inventory_sessions.count({ where }),
      ]);

      return paginated(
        reply,
        sessions,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /inventory-sessions/:id — detail with items
  fastify.get<{ Params: { id: string } }>(
    "/inventory-sessions/:id",
    { preHandler: requirePermission("warehouse.inventory") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const session = await prisma.sklad_inventory_sessions.findUnique({
        where: { id },
        include: {
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
              location: { select: { id: true, code: true, name: true } },
            },
          },
        },
      });
      if (!session) return error(reply, "Inventarizace nenalezena", 404);

      return success(reply, session);
    },
  );

  // POST /inventory-sessions — create DRAFT, auto-fill system_qty
  fastify.post(
    "/inventory-sessions",
    { preHandler: requirePermission("warehouse.inventory") },
    async (request, reply) => {
      const parsed = parseBody(CreateInventorySessionSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      // Build items with auto-filled system_qty and calculated difference
      const itemsData = await Promise.all(
        body.items.map(async (item) => {
          let systemQty = 0;

          if (item.location_id != null) {
            // Get stock at specific location
            const loc = await prisma.sklad_item_locations.findUnique({
              where: {
                item_id_location_id: {
                  item_id: item.item_id,
                  location_id: item.location_id,
                },
              },
            });
            systemQty = loc ? Number(loc.quantity) : 0;
          } else {
            // Get total stock across all locations
            systemQty = await getItemTotalStock(item.item_id);
          }

          const difference = Number(item.actual_qty) - systemQty;

          return {
            item_id: item.item_id,
            location_id: item.location_id ?? null,
            system_qty: systemQty,
            actual_qty: Number(item.actual_qty),
            difference,
            notes: item.notes ?? null,
          };
        }),
      );

      const session = await prisma.sklad_inventory_sessions.create({
        data: {
          notes: body.notes ?? null,
          status: "DRAFT",
          items: {
            create: itemsData,
          },
        },
        include: {
          items: {
            include: {
              item: { select: { id: true, name: true, unit: true } },
              location: { select: { id: true, code: true, name: true } },
            },
          },
        },
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "warehouse_inventory",
        entityId: session.id,
        description: `Vytvořena inventarizace #${session.id}`,
        newValues: {
          status: session.status,
          item_count: itemsData.length,
        },
      });

      return success(reply, session, 201, "Inventarizace byla vytvořena");
    },
  );

  // PUT /inventory-sessions/:id — update items (DRAFT only), recalculate system_qty and difference
  fastify.put<{ Params: { id: string } }>(
    "/inventory-sessions/:id",
    { preHandler: requirePermission("warehouse.inventory") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const parsed = parseBody(UpdateInventorySessionSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;

      const existing = await prisma.sklad_inventory_sessions.findUnique({
        where: { id },
      });
      if (!existing) return error(reply, "Inventarizace nenalezena", 404);
      if (existing.status !== "DRAFT") {
        return error(
          reply,
          "Inventarizaci lze upravit pouze ve stavu DRAFT",
          400,
        );
      }

      const updateData: Record<string, unknown> = {
        modified_at: new Date(),
      };
      if (body.notes !== undefined) updateData.notes = body.notes;

      // Replace items if provided, recalculating system_qty and difference.
      // The system_qty/difference computation is read-only and runs first;
      // the resulting lines are then persisted atomically with the header.
      let itemsData: Array<{
        item_id: number;
        location_id: number | null;
        system_qty: number;
        actual_qty: number;
        difference: number;
        notes: string | null;
      }> | null = null;
      if (body.items) {
        itemsData = await Promise.all(
          body.items.map(async (item) => {
            let systemQty = 0;

            if (item.location_id != null) {
              const loc = await prisma.sklad_item_locations.findUnique({
                where: {
                  item_id_location_id: {
                    item_id: item.item_id,
                    location_id: item.location_id,
                  },
                },
              });
              systemQty = loc ? Number(loc.quantity) : 0;
            } else {
              systemQty = await getItemTotalStock(item.item_id);
            }

            const difference = Number(item.actual_qty) - systemQty;

            return {
              item_id: item.item_id,
              location_id: item.location_id ?? null,
              system_qty: systemQty,
              actual_qty: Number(item.actual_qty),
              difference,
              notes: item.notes ?? null,
            };
          }),
        );
      }

      // Replace lines + persist header atomically: the line deleteMany +
      // createMany and the header update run in ONE transaction, so a failure
      // mid-replacement can't leave the session with zero (or partial) lines.
      const linesToWrite = itemsData;
      const updated = await prisma.$transaction(async (tx) => {
        if (linesToWrite) {
          await tx.sklad_inventory_lines.deleteMany({
            where: { session_id: id },
          });
          await tx.sklad_inventory_lines.createMany({
            data: linesToWrite.map((item) => ({
              session_id: id,
              ...item,
            })),
          });
        }

        return tx.sklad_inventory_sessions.update({
          where: { id },
          data: updateData,
          include: {
            items: {
              include: {
                item: { select: { id: true, name: true, unit: true } },
                location: { select: { id: true, code: true, name: true } },
              },
            },
          },
        });
      });

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "warehouse_inventory",
        entityId: id,
        description: `Upravena inventarizace #${id}`,
        oldValues: { notes: existing.notes },
        newValues: { notes: updated.notes },
      });

      return success(reply, updated, 200, "Inventarizace byla uložena");
    },
  );

  // POST /inventory-sessions/:id/confirm — confirm inventory session
  fastify.post<{ Params: { id: string } }>(
    "/inventory-sessions/:id/confirm",
    { preHandler: requirePermission("warehouse.inventory") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await confirmInventorySession(id);
      if ("error" in result)
        return error(reply, result.error, result.status ?? 400);

      await logAudit({
        request,
        authData: request.authData,
        action: "confirm",
        entityType: "warehouse_inventory",
        entityId: id,
        description: `Potvrzena inventarizace ${result.data.session_number}`,
        newValues: {
          status: "CONFIRMED",
          session_number: result.data.session_number,
        },
      });

      return success(reply, result.data, 200, "Inventarizace byla potvrzena");
    },
  );

  // =============================================================
  // REPORTS
  // =============================================================

  // GET /reports/stock-status — items with stock info (paginated)
  // Pagination + a hard `take` cap (parsePagination clamps limit to ≤100) keep
  // this from returning an unbounded result set. Response stays `data`-as-array
  // (paginated() adds a top-level `pagination` meta the existing clients ignore).
  fastify.get(
    "/reports/stock-status",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip } = parsePagination(query, {
        defaultLimit: 2000,
        maxLimit: 5000,
      });

      const where: Record<string, unknown> = { is_active: true };
      // NaN-guard the category filter: a junk value yields no filter rather
      // than a NaN reaching Prisma (which 500s) or silently matching nothing.
      if (query.category_id !== undefined) {
        const categoryId = Number(query.category_id);
        if (!Number.isNaN(categoryId)) where.category_id = categoryId;
      }

      const [items, total] = await Promise.all([
        prisma.sklad_items.findMany({
          where,
          orderBy: { name: "asc" },
          skip,
          take: limit,
          include: {
            category: { select: { id: true, name: true } },
          },
        }),
        prisma.sklad_items.count({ where }),
      ]);

      const enriched = await Promise.all(
        items.map(async (item) => {
          const total_quantity = await getItemTotalStock(item.id);
          const available_quantity = await getItemAvailableQty(item.id);
          const stock_value = await getItemStockValue(item.id);
          const minQty = item.min_quantity ? Number(item.min_quantity) : null;
          const below_minimum =
            minQty !== null ? total_quantity < minQty : false;

          return {
            id: item.id,
            item_number: item.item_number,
            name: item.name,
            unit: item.unit,
            category: item.category,
            total_quantity,
            available_quantity,
            stock_value,
            below_minimum,
          };
        }),
      );

      return paginated(
        reply,
        enriched,
        buildPaginationMeta(total, page, limit),
      );
    },
  );

  // GET /reports/project-consumption — aggregated material consumed per project
  // Bounded two ways: (1) at most REPORT_SOURCE_CAP source issues are scanned
  // for the aggregate, and (2) the aggregated rows are paginated. This prevents
  // an unbounded scan/response on a wide date range. Response stays
  // `data`-as-array via paginated().
  fastify.get(
    "/reports/project-consumption",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip } = parsePagination(query, {
        defaultLimit: 2000,
        maxLimit: 5000,
      });

      const issueWhere: Record<string, unknown>[] = [{ status: "CONFIRMED" }];
      // NaN-guard the project filter (junk value -> no filter, not a 500).
      if (query.project_id !== undefined) {
        const projectId = Number(query.project_id);
        if (!Number.isNaN(projectId))
          issueWhere.push({ project_id: projectId });
      }
      if (query.date_from) {
        issueWhere.push({
          created_at: { gte: new Date(String(query.date_from)) },
        });
      }
      if (query.date_to) {
        issueWhere.push({
          created_at: { lte: new Date(String(query.date_to)) },
        });
      }

      const issues = await prisma.sklad_issues.findMany({
        where: { AND: issueWhere },
        take: REPORT_SOURCE_CAP,
        orderBy: { id: "desc" },
        include: {
          project: { select: { id: true, name: true, project_number: true } },
          items: {
            include: {
              item: { select: { id: true, name: true } },
              batch: { select: { unit_price: true } },
            },
          },
        },
      });

      // Aggregate by item + project
      const consumptionMap = new Map<
        string,
        {
          item_id: number;
          item_name: string;
          project_id: number;
          project_name: string | null;
          total_qty: number;
          total_value: number;
        }
      >();

      for (const issue of issues) {
        for (const item of issue.items) {
          const key = `${item.item_id}-${issue.project_id}`;
          const qty = Number(item.quantity);
          const price = Number(item.batch?.unit_price ?? 0);
          const value = qty * price;

          const existing = consumptionMap.get(key);
          if (existing) {
            existing.total_qty += qty;
            existing.total_value += value;
          } else {
            consumptionMap.set(key, {
              item_id: item.item_id,
              item_name: item.item.name,
              project_id: issue.project_id,
              project_name: issue.project.name,
              total_qty: qty,
              total_value: value,
            });
          }
        }
      }

      const allRows = Array.from(consumptionMap.values());
      const pageRows = allRows.slice(skip, skip + limit);

      return paginated(
        reply,
        pageRows,
        buildPaginationMeta(allRows.length, page, limit),
      );
    },
  );

  // GET /reports/movement-log — combined list of confirmed receipts + issues
  // Bounded: each source query is capped at REPORT_SOURCE_CAP rows and the
  // merged, date-sorted movement list is paginated. Without this a wide date
  // range expands to an unbounded number of movement lines. Response stays
  // `data`-as-array via paginated().
  fastify.get(
    "/reports/movement-log",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip } = parsePagination(query, {
        defaultLimit: 2000,
        maxLimit: 5000,
      });

      const receiptWhere: Record<string, unknown>[] = [{ status: "CONFIRMED" }];
      const issueWhere: Record<string, unknown>[] = [{ status: "CONFIRMED" }];

      if (query.date_from) {
        const dateFrom = new Date(String(query.date_from));
        receiptWhere.push({ created_at: { gte: dateFrom } });
        issueWhere.push({ created_at: { gte: dateFrom } });
      }
      if (query.date_to) {
        const dateTo = new Date(String(query.date_to));
        receiptWhere.push({ created_at: { lte: dateTo } });
        issueWhere.push({ created_at: { lte: dateTo } });
      }

      const [receipts, issues] = await Promise.all([
        prisma.sklad_receipts.findMany({
          where: { AND: receiptWhere },
          take: REPORT_SOURCE_CAP,
          include: {
            supplier: { select: { id: true, name: true } },
            items: {
              include: {
                item: { select: { id: true, name: true } },
              },
            },
          },
          orderBy: { id: "desc" },
        }),
        prisma.sklad_issues.findMany({
          where: { AND: issueWhere },
          take: REPORT_SOURCE_CAP,
          include: {
            project: { select: { id: true, name: true, project_number: true } },
            items: {
              include: {
                item: { select: { id: true, name: true } },
                batch: { select: { unit_price: true } },
              },
            },
          },
          orderBy: { id: "desc" },
        }),
      ]);

      const movements: Array<{
        type: "receipt" | "issue";
        document_id: number;
        document_number: string | null;
        date: Date | null;
        item_id: number;
        item_name: string;
        quantity: number;
        unit_price: number;
        supplier?: { id: number; name: string | null } | null;
        project?: { id: number; name: string | null } | null;
      }> = [];

      for (const receipt of receipts) {
        for (const item of receipt.items) {
          movements.push({
            type: "receipt",
            document_id: receipt.id,
            document_number: receipt.receipt_number,
            date: receipt.created_at,
            item_id: item.item_id,
            item_name: item.item.name,
            quantity: Number(item.quantity),
            unit_price: Number(item.unit_price),
            supplier: receipt.supplier,
          });
        }
      }

      for (const issue of issues) {
        for (const item of issue.items) {
          movements.push({
            type: "issue",
            document_id: issue.id,
            document_number: issue.issue_number,
            date: issue.created_at,
            item_id: item.item_id,
            item_name: item.item.name,
            quantity: Number(item.quantity),
            unit_price: Number(item.batch?.unit_price ?? 0),
            project: issue.project,
          });
        }
      }

      // Sort by date descending
      movements.sort((a, b) => {
        const dateA = a.date ? a.date.getTime() : 0;
        const dateB = b.date ? b.date.getTime() : 0;
        return dateB - dateA;
      });

      const pageMovements = movements.slice(skip, skip + limit);

      return paginated(
        reply,
        pageMovements,
        buildPaginationMeta(movements.length, page, limit),
      );
    },
  );

  // GET /reports/below-minimum — items below minimum stock level (paginated)
  // getBelowMinimumItems() now does ONE grouped aggregate (no per-item N+1),
  // and the result is paginated here so the response can't grow unbounded.
  fastify.get(
    "/reports/below-minimum",
    { preHandler: requirePermission("warehouse.view") },
    async (request, reply) => {
      const { page, limit, skip } = parsePagination(
        request.query as Record<string, unknown>,
        { defaultLimit: 2000, maxLimit: 5000 },
      );
      const all = await getBelowMinimumItems();
      const pageItems = all.slice(skip, skip + limit);

      return paginated(
        reply,
        pageItems,
        buildPaginationMeta(all.length, page, limit),
      );
    },
  );
}
