import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requirePermission, requireAnyPermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import {
  CreateIssuedOrderSchema,
  UpdateIssuedOrderSchema,
} from "../../schemas/issued-orders.schema";
import {
  listIssuedOrders,
  getIssuedOrderTotals,
  getIssuedOrder,
  createIssuedOrder,
  updateIssuedOrder,
  deleteIssuedOrder,
  getNextIssuedOrderNumberPreview,
} from "../../services/issued-orders.service";
import {
  renderIssuedOrderHtml,
  buildIssuedOrderPdfFooter,
  buildIssuedOrderPdfHeader,
} from "./issued-orders-pdf";
import { htmlToPdf } from "../../utils/html-to-pdf";
import { logoDataUriFromSettings } from "../../utils/pdf-shared";
import { nasOrdersManager } from "../../services/nas-financials-manager";
import { contentDisposition } from "../../utils/content-disposition";

// 3× the client's 10s heartbeat: a single missed beat (background-tab
// throttling, transient network) must not silently hand the lock to a second
// editor. Keep in sync with quotations.ts and useDocumentLock.
const LOCK_TIMEOUT_MS = 30 * 1000;

export default async function issuedOrdersRoutes(fastify: FastifyInstance) {
  // GET /api/admin/issued-orders
  fastify.get(
    "/",
    { preHandler: requirePermission("orders.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip, sort, order, search } = parsePagination(query);
      const result = await listIssuedOrders({
        page,
        limit,
        skip,
        sort,
        order,
        search,
        status: query.status ? String(query.status) : undefined,
        supplier_id: query.supplier_id ? Number(query.supplier_id) : undefined,
        month: query.month ? Number(query.month) : undefined,
        year: query.year ? Number(query.year) : undefined,
      });
      return paginated(
        reply,
        result.data,
        buildPaginationMeta(result.total, page, limit),
      );
    },
  );

  // GET /api/admin/issued-orders/next-number
  fastify.get(
    "/next-number",
    { preHandler: requirePermission("orders.create") },
    async (_request, reply) => {
      return success(reply, await getNextIssuedOrderNumberPreview());
    },
  );

  // GET /api/admin/issued-orders/stats — per-currency NET total summed over
  // the WHOLE filtered set (not one page). Issued orders are not tax documents
  // — no VAT anywhere. Registered BEFORE "/:id" so the literal "stats" path
  // isn't captured as an order id.
  fastify.get(
    "/stats",
    { preHandler: requirePermission("orders.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const result = await getIssuedOrderTotals({
        search: query.search ? String(query.search) : undefined,
        status: query.status ? String(query.status) : undefined,
        supplier_id: query.supplier_id ? Number(query.supplier_id) : undefined,
        month: query.month ? Number(query.month) : undefined,
        year: query.year ? Number(query.year) : undefined,
      });
      return success(reply, { totals: result.totals });
    },
  );

  // GET /api/admin/issued-orders/suppliers — lightweight supplier lookup for
  // the PO supplier picker. Guarded by the ORDERS permissions (any of
  // view/create/edit), NOT warehouse.manage: the warehouse suppliers CRUD is
  // gated by warehouse.manage, which orders users typically lack — without
  // this endpoint they couldn't populate the picker. Registered BEFORE "/:id"
  // so the literal "suppliers" path isn't captured as an order id.
  fastify.get(
    "/suppliers",
    {
      preHandler: requireAnyPermission(
        "orders.view",
        "orders.create",
        "orders.edit",
      ),
    },
    async (_request, reply) => {
      const suppliers = await prisma.sklad_suppliers.findMany({
        where: { is_active: true },
        select: {
          id: true,
          name: true,
          ico: true,
          dic: true,
          street: true,
          city: true,
          postal_code: true,
          country: true,
        },
        // id tiebreak so same-name suppliers sort deterministically.
        orderBy: [{ name: "asc" }, { id: "asc" }],
      });
      return success(reply, suppliers);
    },
  );

  // GET /api/admin/issued-orders/:id
  fastify.get<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("orders.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const order = await getIssuedOrder(id);
      if (!order) return error(reply, "Objednávka nenalezena", 404);

      // `getIssuedOrder` already loaded locked_by/locked_at — enrich locked_by
      // to {user_id, username, full_name} ONLY when the lock is fresh and held
      // by ANOTHER user (same shape as offers so the frontend lock hook can be
      // shared); otherwise null.
      let lockedBy: {
        user_id: number;
        username: string;
        full_name: string;
      } | null = null;
      if (order.locked_by && order.locked_at) {
        const lockAge = Date.now() - new Date(order.locked_at).getTime();
        if (
          lockAge < LOCK_TIMEOUT_MS &&
          order.locked_by !== request.authData!.userId
        ) {
          const lockUser = await prisma.users.findUnique({
            where: { id: order.locked_by },
            select: {
              id: true,
              username: true,
              first_name: true,
              last_name: true,
            },
          });
          if (lockUser) {
            lockedBy = {
              user_id: lockUser.id,
              username: lockUser.username,
              full_name: `${lockUser.first_name} ${lockUser.last_name}`.trim(),
            };
          }
        }
      }

      return success(reply, { ...order, locked_by: lockedBy });
    },
  );

  // POST /api/admin/issued-orders/:id/lock — acquire edit lock (mirrors offers)
  fastify.post<{ Params: { id: string } }>(
    "/:id/lock",
    { preHandler: requirePermission("orders.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const order = await prisma.issued_orders.findUnique({
        where: { id },
        select: { locked_by: true, locked_at: true },
      });
      if (!order) return error(reply, "Objednávka nenalezena", 404);

      // Check if locked by someone else and lock is fresh
      if (order.locked_by && order.locked_at) {
        const lockAge = Date.now() - new Date(order.locked_at).getTime();
        if (
          lockAge < LOCK_TIMEOUT_MS &&
          order.locked_by !== request.authData!.userId
        ) {
          const lockUser = await prisma.users.findUnique({
            where: { id: order.locked_by },
            select: { first_name: true, last_name: true },
          });
          return error(
            reply,
            `Objednávku právě upravuje ${lockUser ? `${lockUser.first_name} ${lockUser.last_name}`.trim() : "jiný uživatel"}`,
            423,
          );
        }
      }

      await prisma.issued_orders.update({
        where: { id },
        data: { locked_by: request.authData!.userId, locked_at: new Date() },
      });

      return success(reply, null, 200, "Zámek nastaven");
    },
  );

  // POST /api/admin/issued-orders/:id/heartbeat — keep lock alive
  fastify.post<{ Params: { id: string } }>(
    "/:id/heartbeat",
    { preHandler: requirePermission("orders.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      await prisma.issued_orders.updateMany({
        where: { id, locked_by: request.authData!.userId },
        data: { locked_at: new Date() },
      });

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

  // POST /api/admin/issued-orders/:id/unlock — release lock
  fastify.post<{ Params: { id: string } }>(
    "/:id/unlock",
    { preHandler: requirePermission("orders.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      await prisma.issued_orders.updateMany({
        where: { id, locked_by: request.authData!.userId },
        data: { locked_by: null, locked_at: null },
      });

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

  // GET /api/admin/issued-orders/:id/file — serve the archived PDF from the
  // NAS; on an archive miss, fall back to a LIVE render from current data,
  // re-archive the fresh PDF, and serve it (archived-file model with live
  // fallback — same model as offers' /:id/file).
  fastify.get<{ Params: { id: string } }>(
    "/:id/file",
    { preHandler: requirePermission("orders.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const order = await prisma.issued_orders.findUnique({ where: { id } });
      // A draft has no po_number → nothing was ever archived; 404 like offers.
      if (!order?.po_number) return error(reply, "Objednávka nenalezena", 404);

      const fileName = `Objednavka-${order.po_number}.pdf`;

      // Archive hit: the by-number sweep finds the PDF in whichever
      // Vydané/YYYY/MM folder it was saved to (order_date can move after
      // archiving, so a fixed expected path would miss).
      const archived = nasOrdersManager.readIssuedByNumber(order.po_number);
      if (archived) {
        return reply
          .type("application/pdf")
          .header("Content-Disposition", contentDisposition("inline", fileName))
          .send(archived.data);
      }

      // Archive miss → live render (same data path as issued-orders-pdf),
      // then archive the fresh PDF so the next request is a plain hit.
      try {
        const items = await prisma.issued_order_items.findMany({
          where: { issued_order_id: id },
          orderBy: { position: "asc" },
        });
        const sections = await prisma.issued_order_sections.findMany({
          where: { issued_order_id: id },
          orderBy: [{ position: "asc" }, { id: "asc" }],
        });
        const supplier = order.supplier_id
          ? ((await prisma.sklad_suppliers.findUnique({
              where: { id: order.supplier_id },
            })) as Record<string, unknown> | null)
          : null;
        const settings = (await prisma.company_settings.findFirst()) as Record<
          string,
          unknown
        > | null;
        const lang =
          order.language === "en" ? ("en" as const) : ("cs" as const);
        const issuer = {
          name: `${request.authData?.firstName ?? ""} ${request.authData?.lastName ?? ""}`.trim(),
        };

        const html = renderIssuedOrderHtml(
          order,
          items,
          supplier,
          settings,
          lang,
          issuer,
          sections,
        );
        const pdf = await htmlToPdf(html, {
          headerTemplate: buildIssuedOrderPdfHeader(
            lang,
            logoDataUriFromSettings(settings),
            order.po_number || "",
          ),
          footerTemplate: buildIssuedOrderPdfFooter(lang, issuer.name),
        });

        if (nasOrdersManager.isConfigured()) {
          const baseDate = order.order_date
            ? new Date(order.order_date)
            : new Date();
          nasOrdersManager.cleanIssued(order.po_number);
          const saved = nasOrdersManager.saveIssuedPdf(
            order.po_number,
            baseDate.getFullYear(),
            baseDate.getMonth() + 1,
            pdf,
          );
          if (!saved) {
            request.log.error(
              `Archivace PDF objednávky ${order.po_number} na NAS při /file fallbacku selhala`,
            );
          }
        }

        return reply
          .type("application/pdf")
          .header("Content-Disposition", contentDisposition("inline", fileName))
          .send(pdf);
      } catch (err) {
        request.log.error(
          err,
          "Issued order /file live-render fallback failed",
        );
        return error(reply, "Chyba při generování PDF", 500);
      }
    },
  );

  // POST /api/admin/issued-orders
  fastify.post(
    "/",
    { preHandler: requirePermission("orders.create") },
    async (request, reply) => {
      const parsed = parseBody(CreateIssuedOrderSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const order = await createIssuedOrder(parsed.data);
      if ("error" in order) {
        if (order.error === "supplier_not_found")
          return error(reply, "Dodavatel nenalezen", 400);
        if (order.error === "po_number_taken")
          return error(reply, "Číslo objednávky je již použito", 409);
        return error(reply, "Neznámá chyba", 500);
      }
      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "issued_order",
        entityId: order.id,
        description: `Vytvořena vydaná objednávka ${order.po_number ?? "koncept"}`,
      });
      return success(
        reply,
        { id: order.id, po_number: order.po_number },
        201,
        "Objednávka byla vytvořena",
      );
    },
  );

  // PUT /api/admin/issued-orders/:id
  fastify.put<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("orders.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const parsed = parseBody(UpdateIssuedOrderSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const result = await updateIssuedOrder(id, parsed.data);
      if ("error" in result) {
        if (result.error === "not_found")
          return error(reply, "Objednávka nenalezena", 404);
        if (result.error === "not_editable")
          return error(reply, "Objednávku v tomto stavu nelze upravovat", 400);
        if (result.error === "supplier_not_found")
          return error(reply, "Dodavatel nenalezen", 400);
        if (result.error === "invalid_transition")
          return error(
            reply,
            `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`,
            400,
          );
        return error(reply, "Neznámá chyba", 500);
      }
      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "issued_order",
        entityId: id,
        description: `Upravena vydaná objednávka ${result.po_number ?? "koncept"}`,
        oldValues: result.oldValues,
        newValues: result.newValues,
      });
      // po_number in the response so a draft->sent finalize immediately shows
      // the assigned number (POST already returns it).
      return success(
        reply,
        { id, po_number: result.po_number },
        200,
        "Objednávka byla aktualizována",
      );
    },
  );

  // DELETE /api/admin/issued-orders/:id
  fastify.delete<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("orders.delete") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const existing = await deleteIssuedOrder(id);
      if (!existing) return error(reply, "Objednávka nenalezena", 404);
      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "issued_order",
        entityId: id,
        description: `Smazána vydaná objednávka ${existing.po_number ?? "koncept"}`,
        oldValues: existing as unknown as Record<string, unknown>,
      });
      return success(reply, null, 200, "Objednávka smazána");
    },
  );
}
