import { FastifyInstance } from "fastify";
import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId, paginated } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { contentDisposition } from "../../utils/content-disposition";
import { parseBody } from "../../schemas/common";
import {
  CreateQuotationSchema,
  UpdateQuotationSchema,
} from "../../schemas/offers.schema";
import prisma from "../../config/database";
import {
  listOffers,
  getOfferTotals,
  getOffer,
  createOffer,
  updateOffer,
  deleteOffer,
  duplicateOffer,
  invalidateOffer,
  getNextOfferNumber,
} from "../../services/offers.service";
import { nasOffersManager } from "../../services/nas-offers-manager";
import { renderOfferHtml } from "./offers-pdf";
import { htmlToPdf } from "../../utils/html-to-pdf";

// 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 issued-orders.ts and useDocumentLock.
const LOCK_TIMEOUT_MS = 30 * 1000;

export default async function quotationsRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  fastify.get(
    "/",
    { preHandler: requirePermission("offers.view") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip, sort, order, search } = parsePagination(query);

      const result = await listOffers({
        page,
        limit,
        skip,
        sort,
        order,
        search,
        status: query.status ? String(query.status) : undefined,
        customer_id: query.customer_id ? Number(query.customer_id) : undefined,
      });

      return paginated(
        reply,
        result.data,
        buildPaginationMeta(result.total, page, limit),
      );
    },
  );

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

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

  // POST /api/admin/offers/:id/duplicate
  fastify.post<{ Params: { id: string } }>(
    "/:id/duplicate",
    { preHandler: requirePermission("offers.create") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await duplicateOffer(id);
      if (!result) return error(reply, "Nabídka nenalezena", 404);

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "quotation",
        entityId: result.copy.id,
        description: `Duplikována nabídka ${result.original.quotation_number} → ${result.copy.quotation_number}`,
      });
      return success(
        reply,
        { id: result.copy.id, quotation_number: result.copy.quotation_number },
        201,
        "Nabídka byla duplikována",
      );
    },
  );

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

      const result = await invalidateOffer(id);
      if ("error" in result) {
        if (result.error === "not_found")
          return error(reply, "Nabídka nenalezena", 404);
        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);
      }
      const existing = result.data;

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "quotation",
        entityId: id,
        description: `Zneplatněna nabídka ${existing.quotation_number ?? "koncept"}`,
        oldValues: { status: existing.status },
        newValues: { status: "invalidated" },
      });
      return success(reply, null, 200, "Nabídka zneplatněna");
    },
  );

  fastify.get<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("offers.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const data = await getOffer(id);
      if (!data) return error(reply, "Nabídka nenalezena", 404);

      // `getOffer` already loaded the quotation row, including locked_by /
      // locked_at — reuse them instead of a second findUnique.
      let lockedBy: {
        user_id: number;
        username: string;
        full_name: string;
      } | null = null;
      if (data.locked_by && data.locked_at) {
        const lockAge = Date.now() - new Date(data.locked_at).getTime();
        if (
          lockAge < LOCK_TIMEOUT_MS &&
          data.locked_by !== request.authData!.userId
        ) {
          const lockUser = await prisma.users.findUnique({
            where: { id: data.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, { ...data, locked_by: lockedBy });
    },
  );

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

      const quotation = await prisma.quotations.findUnique({
        where: { id },
        select: { locked_by: true, locked_at: true },
      });
      if (!quotation) return error(reply, "Nabídka nenalezena", 404);

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

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

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

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

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

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

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

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

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

  fastify.post(
    "/",
    { preHandler: requirePermission("offers.create") },
    async (request, reply) => {
      const parsed = parseBody(CreateQuotationSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);

      const quotation = await createOffer(parsed.data);
      if ("error" in quotation) {
        if (quotation.error === "customer_not_found")
          return error(reply, "Zákazník nenalezen", 400);
        if (quotation.error === "number_taken")
          return error(reply, "Číslo nabídky je již použito", 409);
        return error(reply, "Neznámá chyba", 500);
      }

      await logAudit({
        request,
        authData: request.authData,
        action: "create",
        entityType: "quotation",
        entityId: quotation.id,
        description: `Vytvořena nabídka ${quotation.quotation_number ?? "koncept"}`,
      });
      return success(
        reply,
        { id: quotation.id },
        201,
        "Nabídka byla vytvořena",
      );
    },
  );

  fastify.put<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("offers.edit") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const parsed = parseBody(UpdateQuotationSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);

      const result = await updateOffer(id, parsed.data);
      if ("error" in result) {
        if (result.error === "not_found")
          return error(reply, "Nabídka nenalezena", 404);
        if (result.error === "not_editable")
          return error(reply, "Nabídku v tomto stavu nelze upravovat", 400);
        if (result.error === "invalid_transition")
          return error(
            reply,
            `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`,
            400,
          );
        if (result.error === "customer_not_found")
          return error(reply, "Zákazník nenalezen", 400);
        return error(reply, "Neznámá chyba", 500);
      }

      // Keep lock — user stays on the page after save

      await logAudit({
        request,
        authData: request.authData,
        action: "update",
        entityType: "quotation",
        entityId: id,
        description: `Upravena nabídka ${result.quotation_number ?? "koncept"}`,
        oldValues: result.oldValues,
        newValues: result.newValues,
      });
      return success(reply, { id }, 200, "Nabídka byla uložena");
    },
  );

  fastify.delete<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requirePermission("offers.delete") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;

      const result = await deleteOffer(id);
      if ("error" in result) {
        if (result.error === "not_found")
          return error(reply, "Nabídka nenalezena", 404);
        if (result.error === "linked")
          return error(
            reply,
            "Nabídku nelze smazat, je navázána na objednávku nebo projekt",
            409,
          );
        return error(reply, "Neznámá chyba", 500);
      }
      const existing = result.data;

      // NAS PDF cleanup is owned by deleteOffer (single owner) — the route
      // used to repeat it here, which always failed the second time and logged
      // a spurious error.

      await logAudit({
        request,
        authData: request.authData,
        action: "delete",
        entityType: "quotation",
        entityId: id,
        description: `Smazána nabídka ${existing.quotation_number ?? "koncept"}`,
        oldValues: existing as unknown as Record<string, unknown>,
      });
      return success(reply, null, 200, "Nabídka smazána");
    },
  );

  // GET /api/admin/offers/:id/file — serve PDF from NAS
  fastify.get<{ Params: { id: string } }>(
    "/:id/file",
    { preHandler: requirePermission("offers.view") },
    async (request, reply) => {
      const id = parseId(request.params.id, reply);
      if (id === null) return;
      const offer = await prisma.quotations.findUnique({
        where: { id },
        select: { quotation_number: true, created_at: true },
      });
      if (!offer?.quotation_number)
        return error(reply, "Nabídka nenalezena", 404);

      const year = offer.created_at
        ? new Date(offer.created_at).getFullYear()
        : new Date().getFullYear();
      const relPath = nasOffersManager.buildRelativePath(
        offer.quotation_number,
        year,
      );
      const file = nasOffersManager.readOfferPdf(relPath);
      if (file) {
        return reply
          .type("application/pdf")
          .header(
            "Content-Disposition",
            // Quotation numbers contain "/" segments — not header- or
            // filename-safe raw; the helper handles non-ASCII per RFC 5987.
            contentDisposition(
              "inline",
              `Nabidka-${(offer.quotation_number || "").replace(/\//g, "-")}.pdf`,
            ),
          )
          .send(file.data);
      }

      // Archive miss → live-render fallback (same model as issued orders'
      // /:id/file): rebuild the PDF from current data with the exact PDF-route
      // template, re-archive it so the next request is a plain hit, and serve
      // the fresh bytes with identical response semantics.
      try {
        const quotation = await prisma.quotations.findUnique({
          where: { id },
          include: {
            customers: true,
            quotation_items: { orderBy: { position: "asc" } },
            scope_sections: { orderBy: [{ position: "asc" }, { id: "asc" }] },
          },
        });
        if (!quotation) return error(reply, "Nabídka nenalezena", 404);

        const settings = await prisma.company_settings.findFirst();
        const html = renderOfferHtml(quotation, settings);
        const pdfBuffer = await htmlToPdf(html);

        if (nasOffersManager.isConfigured()) {
          // saveOfferPdf logs internally and returns null on failure —
          // archiving is best-effort here, serving the PDF must not fail.
          const saved = nasOffersManager.saveOfferPdf(
            offer.quotation_number,
            year,
            pdfBuffer,
          );
          if (!saved) {
            request.log.error(
              `Archivace PDF nabídky ${offer.quotation_number} na NAS při /file fallbacku selhala`,
            );
          }
        }

        return reply
          .type("application/pdf")
          .header(
            "Content-Disposition",
            // Quotation numbers contain "/" segments — not header- or
            // filename-safe raw; the helper handles non-ASCII per RFC 5987.
            contentDisposition(
              "inline",
              `Nabidka-${(offer.quotation_number || "").replace(/\//g, "-")}.pdf`,
            ),
          )
          .send(pdfBuffer);
      } catch (err) {
        request.log.error(err, "Offer /file live-render fallback failed");
        return error(reply, "Chyba při generování PDF", 500);
      }
    },
  );
}
