import { renderToBuffer } from '@react-pdf/renderer';
import { createElement } from 'react';

import { db } from '@/lib/db';
import { ports } from '@/lib/db/schema/ports';
import { eq } from 'drizzle-orm';
import { absolutizeBrandingUrl } from '@/lib/branding/url';
import { getPortBrandingConfig } from '@/lib/services/port-config';
import { NotFoundError } from '@/lib/errors';

import { DashboardReport, type DashboardReportData } from './dashboard-report';
import { ClientListReport } from './client-list-report';
import { BerthListReport } from './berth-list-report';
import { InterestListReport } from './interest-list-report';
import type {
  ReportBranding,
  ReportConfig,
  ReportRequest,
  DashboardReportConfig,
  ClientListReportConfig,
  BerthListReportConfig,
  InterestListReportConfig,
} from './types';
import type {
  ClientReportData,
  BerthReportData,
  InterestReportData,
} from '@/lib/services/list-report-data.service';

/**
 * Pre-fetched data payloads each report kind needs at render time.
 * The route handler resolves these (one query per requested section)
 * BEFORE invoking the renderer so the React-PDF tree stays pure.
 */
export interface ReportData {
  dashboard?: DashboardReportData;
  clients?: ClientReportData;
  berths?: BerthReportData;
  interests?: InterestReportData;
}

interface RenderArgs {
  portId: string;
  request: ReportRequest;
  data: ReportData;
  /** Allows the route handler to inject the request id so the audit
   *  trail correlates the generated buffer with the originating call. */
  generatedAt?: string;
}

/**
 * Single entry point that resolves branding, dispatches to the
 * matching React-PDF document component, and returns the binary
 * buffer. Caller decides whether to stream the buffer in the
 * response, archive it to S3, or both.
 *
 * Production concerns this handles:
 *   - Branding fetch failure → ValidationError (caller can show a
 *     "configure branding before exporting" message).
 *   - Unknown report kind → falls through to discriminated-union
 *     exhaustiveness check at compile time; runtime guard in the
 *     route schema covers attacker-supplied payloads.
 *   - Render failure → bubbles the underlying React-PDF error to
 *     the caller, which logs it as a SERVICE error tier and surfaces
 *     a generic "PDF generation failed" to the rep.
 */
export async function renderReport({
  portId,
  request,
  data,
  generatedAt = new Date().toISOString(),
}: RenderArgs): Promise<Buffer> {
  const branding = await resolveBranding(portId);
  const element = pickDocument(branding, request, data, generatedAt);
  // renderToBuffer accepts a React element; cast to satisfy the
  // library's loose `JSX.Element` typing without widening callers.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return renderToBuffer(element as any) as Promise<Buffer>;
}

async function resolveBranding(portId: string): Promise<ReportBranding> {
  const portRow = await db.query.ports.findFirst({
    where: eq(ports.id, portId),
    columns: { name: true },
  });
  if (!portRow) throw new NotFoundError('Port');
  const cfg = await getPortBrandingConfig(portId);
  // `getPortBrandingConfig` returns a path-only URL (e.g. `/uploads/foo.png`)
  // when the original was uploaded against a localhost/LAN host - that's
  // the right shape for client-side surfaces that resolve against the
  // current origin. `@react-pdf/renderer` runs in the Node process and
  // needs an absolute URL to fetch the image; otherwise `<Image>` falls
  // back to the placeholder and the report ships logo-less. Absolutize
  // here against APP_URL the same way email surfaces do.
  return {
    logoUrl: absolutizeBrandingUrl(cfg.logoUrl),
    primaryColor: cfg.primaryColor,
    portName: portRow.name,
  };
}

function pickDocument(
  branding: ReportBranding,
  request: ReportRequest,
  data: ReportData,
  generatedAt: string,
) {
  const cfg: ReportConfig = request.config;
  switch (cfg.kind) {
    case 'dashboard':
      return createElement(DashboardReport, {
        title: request.title,
        subtitle: request.subtitle,
        branding,
        generatedAt,
        config: cfg satisfies DashboardReportConfig,
        data: data.dashboard ?? {},
      });
    case 'clients':
      if (!data.clients) {
        throw new Error('Client report requested without client data resolved');
      }
      return createElement(ClientListReport, {
        title: request.title,
        subtitle: request.subtitle,
        branding,
        generatedAt,
        config: cfg satisfies ClientListReportConfig,
        data: data.clients,
      });
    case 'berths':
      if (!data.berths) {
        throw new Error('Berth report requested without berth data resolved');
      }
      return createElement(BerthListReport, {
        title: request.title,
        subtitle: request.subtitle,
        branding,
        generatedAt,
        config: cfg satisfies BerthListReportConfig,
        data: data.berths,
      });
    case 'interests':
      if (!data.interests) {
        throw new Error('Interest report requested without interest data resolved');
      }
      return createElement(InterestListReport, {
        title: request.title,
        subtitle: request.subtitle,
        branding,
        generatedAt,
        config: cfg satisfies InterestListReportConfig,
        data: data.interests,
      });
    default: {
      // Exhaustiveness check - surfaces a compile error if a new
      // ReportConfig variant is added without a matching case here.
      const _exhaustive: never = cfg;
      throw new Error(`Unsupported report kind: ${(_exhaustive as { kind: string }).kind}`);
    }
  }
}
