import { NextRequest, NextResponse } from 'next/server';
import { eq } from 'drizzle-orm';
import { renderToBuffer } from '@react-pdf/renderer';
import { createElement } from 'react';
import { z } from 'zod';

import { withAuth, withPermission } from '@/lib/api/helpers';
import { db } from '@/lib/db';
import { ports } from '@/lib/db/schema/ports';
import { errorResponse, NotFoundError } from '@/lib/errors';
import { parseBody } from '@/lib/api/route-helpers';
import { absolutizeBrandingUrl } from '@/lib/branding/url';
import { getPortBrandingConfig } from '@/lib/services/port-config';
import { PayloadReportDocument } from '@/lib/pdf/reports/payload-report';

/**
 * POST /api/v1/reports/export-pdf
 *
 * Generic PDF generator. Client posts a JSON `ReportPayload`; server
 * resolves branding for the active port, renders the payload through
 * the shared PayloadReportDocument, and streams back the PDF bytes.
 *
 * Used by every report's export-button dropdown ("Download PDF"
 * option) so we don't have to keep adding routes per report kind.
 */

// M15: this route renders a fully client-supplied payload synchronously
// via `renderToBuffer` on the request thread, gated only by
// `reports.view_dashboard`. Without hard bounds an authed user can POST
// a huge payload and OOM/stall the Node process. The async worker path
// caps rows at REPORT_ROW_CAP (1000); mirror that here, and additionally
// cap the section count, per-section column count, and the total cell
// budget across all sections so a fan-out of many small sections can't
// dodge the per-section row cap.
const REPORT_ROW_CAP = 1_000;
const MAX_SECTIONS = 50;
const MAX_COLUMNS = 50;
const MAX_KPIS = 100;
/** Upper bound on total rendered table cells (rows × columns, summed
 *  across every section). Sized so the worst case stays well within the
 *  per-section caps but bounds the aggregate render cost. */
const MAX_TOTAL_CELLS = 200_000;

// Minimal shape validation — full ReportPayload is structurally typed
// in TS; here we just check it has the basic envelope.
const payloadSchema = z
  .object({
    title: z.string().min(1),
    description: z.string().optional(),
    filenameSlug: z.string().min(1),
    range: z.object({
      from: z.string().datetime(),
      to: z.string().datetime(),
    }),
    kpis: z
      .array(
        z.object({
          label: z.string(),
          value: z.union([z.string(), z.number()]),
          hint: z.string().optional(),
        }),
      )
      .max(MAX_KPIS),
    sections: z
      .array(
        z.object({
          title: z.string(),
          columns: z
            .array(
              z.object({
                key: z.string(),
                label: z.string(),
                align: z.enum(['left', 'right', 'center']).optional(),
              }),
            )
            .max(MAX_COLUMNS),
          rows: z.array(z.record(z.string(), z.unknown())).max(REPORT_ROW_CAP),
        }),
      )
      .max(MAX_SECTIONS),
    /** Optional filename override (without extension) — the client
     *  passes the slug derived from the custom title. */
    filenameOverride: z.string().optional(),
  })
  // Total-cell budget: the per-section `.max()` caps bound each section,
  // but a payload could still fan out MAX_SECTIONS × REPORT_ROW_CAP ×
  // MAX_COLUMNS cells. Reject any payload whose summed cell count exceeds
  // the aggregate budget before it reaches the synchronous renderer.
  .refine(
    (p) =>
      p.sections.reduce((total, s) => total + s.rows.length * Math.max(1, s.columns.length), 0) <=
      MAX_TOTAL_CELLS,
    {
      message: `Report payload exceeds the maximum of ${MAX_TOTAL_CELLS} total cells`,
      path: ['sections'],
    },
  );

export const POST = withAuth(
  withPermission('reports', 'view_dashboard', async (req: NextRequest, ctx) => {
    try {
      const body = await parseBody(req, payloadSchema);

      // Resolve port branding (logo + primary color + name)
      const portRow = await db.query.ports.findFirst({
        where: eq(ports.id, ctx.portId),
        columns: { name: true },
      });
      if (!portRow) throw new NotFoundError('Port');
      const cfg = await getPortBrandingConfig(ctx.portId);
      const branding = {
        logoUrl: absolutizeBrandingUrl(cfg.logoUrl),
        primaryColor: cfg.primaryColor,
        portName: portRow.name,
      };

      const generatedAt = new Date().toISOString();

      // Convert ISO date strings back to Date objects for the payload
      // (client side serialised them through JSON).
      const payload = {
        ...body,
        range: {
          from: new Date(body.range.from),
          to: new Date(body.range.to),
        },
        // The format-callback isn't transferable through JSON; the
        // PDF document falls back to formatPlain when undefined,
        // which is the same default the CSV exporter falls back to.
        sections: body.sections.map((s) => ({
          ...s,
          columns: s.columns.map((c) => ({ ...c, format: undefined })),
        })),
      };

      const element = createElement(PayloadReportDocument, {
        payload,
        branding,
        generatedAt,
      });
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const buffer = await renderToBuffer(element as any);

      const filename =
        body.filenameOverride ??
        `${body.filenameSlug}-${body.range.from.slice(0, 10)}_${body.range.to.slice(0, 10)}.pdf`;

      return new NextResponse(buffer as unknown as BodyInit, {
        status: 200,
        headers: {
          'Content-Type': 'application/pdf',
          'Content-Disposition': `attachment; filename="${filename}"`,
          'Cache-Control': 'no-store',
        },
      });
    } catch (error) {
      return errorResponse(error);
    }
  }),
);
