import { View, Text } from '@react-pdf/renderer';

import { stageLabel } from '@/lib/constants';
import { formatCurrency } from '@/lib/utils/currency';
import { BrandedReportDocument } from './branded-document';
import { HorizontalBarChart, DonutChart, LineChart } from './charts';
import { makeReportStyles } from './styles';
import type { ReportBranding, DashboardReportConfig } from './types';

/**
 * Data shape consumed by the dashboard report. Caller (the route
 * handler) is responsible for fetching the dashboard service's
 * outputs and packing them into this struct. Keeps the React-PDF
 * tree pure - no DB calls inside the document tree.
 */
export interface DashboardReportData {
  kpis?: {
    totalClients: number;
    activeInterests: number;
    pipelineValue: number;
    pipelineValueCurrency: string;
    occupancyRate: number;
  };
  pipelineCounts?: Array<{ stage: string; count: number }>;
  berthStatus?: {
    available: number;
    underOffer: number;
    sold: number;
    total: number;
  };
  sourceConversion?: Array<{
    source: string;
    total: number;
    won: number;
    lost: number;
    conversionRate: number;
  }>;
  hotDeals?: Array<{
    id: string;
    clientName: string | null;
    mooringNumber: string | null;
    stage: string;
    lastContact: string | null;
  }>;
  /** Daily occupancy rate over the report window. Drives the
   *  occupancy-timeline line chart. */
  occupancyTimeline?: Array<{ date: string; rate: number }>;
  /** Lead-source mix: count of NEW interests grouped by source for
   *  the donut variant. Distinct from `sourceConversion` which is
   *  cumulative + win-rate per source. */
  leadSourceMix?: Array<{ source: string; count: number }>;
  /** Pipeline value broken down by stage with the weighted forecast
   *  (close-probability * gross) per stage. */
  pipelineValueBreakdown?: Array<{
    stage: string;
    gross: number;
    weighted: number;
    deals: number;
    currency: string;
  }>;
  /** % of interests that advance from each stage to the next, plus
   *  the absolute count moved + dropped. */
  stageConversionRates?: Array<{
    fromStage: string;
    toStage: string;
    advanced: number;
    dropped: number;
    rate: number;
  }>;
  /** Median + mean days from new-enquiry to contract-signed; null
   *  buckets mean not-enough-data. */
  avgSalesCycle?: {
    sampleSize: number;
    medianDays: number | null;
    meanDays: number | null;
  };
  /** Total weighted forecast snapshot - single dollar figure. */
  revenueForecast?: {
    grossValue: number;
    weightedValue: number;
    currency: string;
  };
  /** Inquiry-inbox triage breakdown over the report window. */
  inquiryInboxSummary?: Array<{
    kind: string;
    triageState: string;
    count: number;
  }>;
  /** Per-assignee reminder activity over the window. */
  remindersSummary?: Array<{
    assignee: string;
    open: number;
    completed: number;
  }>;
  /** Top berths ranked by active-interest count + heat tier. */
  berthDemandRanking?: Array<{
    mooringNumber: string;
    interestCount: number;
    tier: 'A' | 'B' | 'C' | 'D';
  }>;
  /** Pulse-tier histogram across active interests. */
  dealPulseDistribution?: Array<{ tier: string; count: number }>;
  /** Country-of-origin rollup for the active client book. */
  clientCountryDistribution?: Array<{ country: string; count: number }>;
  /** Compact recent-activity log for the print snapshot. */
  recentActivity?: Array<{
    when: string;
    actor: string | null;
    summary: string;
  }>;
  /** Clients added during the report window. */
  newClientsInPeriod?: Array<{
    name: string;
    createdAt: string;
    source: string | null;
  }>;
  /** Interests opened during the report window. */
  newInterestsInPeriod?: Array<{
    clientName: string;
    stage: string;
    berthLabel: string | null;
    createdAt: string;
  }>;
  /** Berths transitioned to Sold status during the report window. */
  berthsSoldInPeriod?: Array<{
    mooringNumber: string;
    soldAt: string;
  }>;
  /** Deposit payments received during the report window. */
  depositsReceivedInPeriod?: Array<{
    clientName: string;
    amount: number;
    currency: string;
    paidAt: string;
  }>;
  /** All signing documents marked completed during the report window. */
  signedDocumentsInPeriod?: Array<{
    type: string;
    title: string;
    signedAt: string;
  }>;
  /** Contracts marked completed during the report window. */
  contractsSignedInPeriod?: Array<{
    type: string;
    title: string;
    signedAt: string;
  }>;
  /** Stub markers for sections whose data resolvers ship later - the
   *  PDF renders a "data resolver pending" placeholder so the
   *  surface is discoverable even before the backend lands. */
  stubsPending?: string[];
}

interface DashboardReportProps {
  title: string;
  subtitle?: string;
  branding: ReportBranding;
  generatedAt: string;
  config: DashboardReportConfig;
  data: DashboardReportData;
}

/**
 * Dashboard summary report. The rep picks which widgets to include in
 * `config.widgetIds`; this template renders sections only for widgets
 * present in both the config AND the supplied `data` payload (route
 * handler is responsible for skipping fetches for unselected widgets).
 *
 * Chart-style widgets render as tables here (counts, percentages,
 * cohort breakdowns). The deliberate choice trades a chart's at-a-
 * glance shape for the actual numbers - a printed report is for
 * later reference / sharing, not in-the-moment dashboard scanning,
 * and the table format is fully accessible to screen readers and
 * holds up if the PDF is OCR-scanned downstream.
 */
export function DashboardReport({
  title,
  subtitle,
  branding,
  generatedAt,
  config,
  data,
}: DashboardReportProps) {
  const styles = makeReportStyles(branding);
  const include = (id: string) => config.widgetIds.includes(id);
  const dateRangeLine =
    config.dateFrom || config.dateTo
      ? `${config.dateFrom ?? 'open'} → ${config.dateTo ?? 'today'}`
      : null;

  return (
    <BrandedReportDocument
      branding={branding}
      title={title}
      subtitle={subtitle ?? `Dashboard summary${dateRangeLine ? ` · ${dateRangeLine}` : ''}`}
      generatedAt={generatedAt}
    >
      {/* Every section uses `wrap={false}` so the title + description
          + table are guaranteed to land on the same page. If a section
          doesn't fit in the remaining space on the current page, the
          whole block moves to the next one - avoids the "section
          heading orphaned at the bottom of a page with the data on
          the next" layout the rep hit on 2026-05-22. */}
      {include('kpi_overview') && data.kpis ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Key metrics</Text>
          <View style={styles.kpiGrid}>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Total clients</Text>
              <Text style={styles.kpiValue}>{data.kpis.totalClients.toLocaleString()}</Text>
            </View>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Active interests</Text>
              <Text style={styles.kpiValue}>{data.kpis.activeInterests.toLocaleString()}</Text>
            </View>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Pipeline value</Text>
              <Text style={styles.kpiValue}>
                {formatCurrency(String(data.kpis.pipelineValue), data.kpis.pipelineValueCurrency, {
                  maxFractionDigits: 0,
                })}
              </Text>
              <Text style={styles.kpiSubvalue}>Sum of primary-berth prices, active deals only</Text>
            </View>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Occupancy</Text>
              <Text style={styles.kpiValue}>{data.kpis.occupancyRate.toFixed(1)}%</Text>
              <Text style={styles.kpiSubvalue}>Sold berths / total</Text>
            </View>
          </View>
        </View>
      ) : null}

      {include('pipeline_funnel') && data.pipelineCounts ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Pipeline funnel</Text>
          <Text style={styles.sectionSubtitle}>Active interests grouped by pipeline stage.</Text>
          <SimpleTable
            styles={styles}
            headers={['Stage', 'Count']}
            widths={[70, 30]}
            rows={data.pipelineCounts.map((row) => [stageLabel(row.stage), String(row.count)])}
          />
        </View>
      ) : null}

      {include('berth_status') && data.berthStatus ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Berth status</Text>
          <Text style={styles.sectionSubtitle}>Current distribution across the marina.</Text>
          <SimpleTable
            styles={styles}
            headers={['Status', 'Count', '% of total']}
            widths={[50, 25, 25]}
            rows={[
              [
                'Available',
                String(data.berthStatus.available),
                pct(data.berthStatus.available, data.berthStatus.total),
              ],
              [
                'Under offer',
                String(data.berthStatus.underOffer),
                pct(data.berthStatus.underOffer, data.berthStatus.total),
              ],
              [
                'Sold',
                String(data.berthStatus.sold),
                pct(data.berthStatus.sold, data.berthStatus.total),
              ],
            ]}
          />
        </View>
      ) : null}

      {include('source_conversion') && data.sourceConversion ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Source conversion</Text>
          <Text style={styles.sectionSubtitle}>
            Interest counts grouped by lead source, with win rate per source.
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Source', 'Total', 'Won', 'Lost', 'Win rate']}
            widths={[40, 15, 15, 15, 15]}
            rows={data.sourceConversion.map((row) => [
              row.source,
              String(row.total),
              String(row.won),
              String(row.lost),
              `${(row.conversionRate * 100).toFixed(1)}%`,
            ])}
          />
        </View>
      ) : null}

      {include('pipeline_funnel_chart') && data.pipelineCounts ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Pipeline funnel</Text>
          <Text style={styles.sectionSubtitle}>
            Active interests per pipeline stage. Bar length is proportional to count.
          </Text>
          <HorizontalBarChart
            width={500}
            data={data.pipelineCounts.map((row) => ({
              label: stageLabel(row.stage),
              value: row.count,
            }))}
            primaryColor={branding.primaryColor}
          />
        </View>
      ) : null}

      {include('berth_status_donut') && data.berthStatus ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Berth status</Text>
          <Text style={styles.sectionSubtitle}>
            Current distribution across the marina, donut variant.
          </Text>
          <DonutChart
            width={420}
            centerLabel={`${data.berthStatus.total}`}
            data={[
              { label: 'Available', value: data.berthStatus.available, color: '#0d9488' },
              { label: 'Under offer', value: data.berthStatus.underOffer, color: '#f59e0b' },
              { label: 'Sold', value: data.berthStatus.sold, color: '#0284c7' },
            ]}
          />
        </View>
      ) : null}

      {include('source_conversion_chart') && data.sourceConversion ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Source conversion</Text>
          <Text style={styles.sectionSubtitle}>
            Win rate per lead source. Faint bar shows total volume, primary bar shows won deals.
          </Text>
          <HorizontalBarChart
            width={500}
            data={data.sourceConversion.map((row) => ({
              label: row.source,
              value: row.won,
              secondaryValue: row.total,
            }))}
            primaryColor={branding.primaryColor}
            formatValue={(n) => `${n}`}
          />
        </View>
      ) : null}

      {include('lead_source_donut') && data.leadSourceMix && data.leadSourceMix.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Lead source mix</Text>
          <Text style={styles.sectionSubtitle}>
            Share of new interests by lead source over the report window.
          </Text>
          <DonutChart
            width={420}
            centerLabel={`${data.leadSourceMix.reduce((s, r) => s + r.count, 0)}`}
            data={data.leadSourceMix.map((r) => ({ label: r.source, value: r.count }))}
          />
        </View>
      ) : null}

      {include('occupancy_timeline_chart') &&
      data.occupancyTimeline &&
      data.occupancyTimeline.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Occupancy timeline</Text>
          <Text style={styles.sectionSubtitle}>
            Daily berth occupancy rate over the report window.
          </Text>
          <LineChart
            width={500}
            data={data.occupancyTimeline.map((p) => ({
              label: new Date(p.date).toLocaleDateString('en-GB', {
                day: '2-digit',
                month: 'short',
              }),
              value: p.rate,
            }))}
            color={branding.primaryColor}
          />
        </View>
      ) : null}

      {include('pipeline_value_breakdown') &&
      data.pipelineValueBreakdown &&
      data.pipelineValueBreakdown.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Pipeline value breakdown</Text>
          <Text style={styles.sectionSubtitle}>
            Gross + weighted pipeline value per stage, weighted by close probability.
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Stage', 'Deals', 'Gross', 'Weighted']}
            widths={[40, 15, 22, 23]}
            rows={data.pipelineValueBreakdown.map((row) => [
              stageLabel(row.stage),
              String(row.deals),
              formatCurrency(String(row.gross), row.currency, { maxFractionDigits: 0 }),
              formatCurrency(String(row.weighted), row.currency, { maxFractionDigits: 0 }),
            ])}
          />
        </View>
      ) : null}

      {include('stage_conversion_rates') &&
      data.stageConversionRates &&
      data.stageConversionRates.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Stage conversion rates</Text>
          <Text style={styles.sectionSubtitle}>
            % of interests that advance from each pipeline stage to the next.
          </Text>
          <SimpleTable
            styles={styles}
            headers={['From → To', 'Advanced', 'Dropped', 'Rate']}
            widths={[42, 18, 18, 22]}
            rows={data.stageConversionRates.map((row) => [
              `${stageLabel(row.fromStage)} → ${stageLabel(row.toStage)}`,
              String(row.advanced),
              String(row.dropped),
              `${(row.rate * 100).toFixed(1)}%`,
            ])}
          />
        </View>
      ) : null}

      {include('reminders_summary') && data.remindersSummary && data.remindersSummary.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Reminders summary</Text>
          <Text style={styles.sectionSubtitle}>
            Open + completed reminders per assignee over the report window.
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Assignee', 'Open', 'Completed']}
            widths={[60, 20, 20]}
            rows={data.remindersSummary.map((r) => [
              r.assignee,
              String(r.open),
              String(r.completed),
            ])}
          />
        </View>
      ) : null}

      {include('inquiry_inbox_summary') &&
      data.inquiryInboxSummary &&
      data.inquiryInboxSummary.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Inbound inquiry summary</Text>
          <Text style={styles.sectionSubtitle}>
            Public-site submissions received during the report window, grouped by triage state.
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Kind', 'Triage state', 'Count']}
            widths={[40, 40, 20]}
            rows={data.inquiryInboxSummary.map((r) => [r.kind, r.triageState, String(r.count)])}
          />
        </View>
      ) : null}

      {include('revenue_forecast') && data.revenueForecast ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Revenue forecast snapshot</Text>
          <Text style={styles.sectionSubtitle}>
            Total pipeline value, weighted by close probability per stage.
          </Text>
          <View style={styles.kpiGrid}>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Gross</Text>
              <Text style={styles.kpiValue}>
                {formatCurrency(
                  String(data.revenueForecast.grossValue),
                  data.revenueForecast.currency,
                  { maxFractionDigits: 0 },
                )}
              </Text>
              <Text style={styles.kpiSubvalue}>Sum of primary-berth prices, active deals</Text>
            </View>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Weighted forecast</Text>
              <Text style={styles.kpiValue}>
                {formatCurrency(
                  String(data.revenueForecast.weightedValue),
                  data.revenueForecast.currency,
                  { maxFractionDigits: 0 },
                )}
              </Text>
              <Text style={styles.kpiSubvalue}>Gross x close-probability per stage</Text>
            </View>
          </View>
        </View>
      ) : null}

      {include('avg_sales_cycle') && data.avgSalesCycle ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Average sales cycle</Text>
          <Text style={styles.sectionSubtitle}>
            Days from new-enquiry to contract-signed across {data.avgSalesCycle.sampleSize} closed
            deals.
          </Text>
          <View style={styles.kpiGrid}>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Median</Text>
              <Text style={styles.kpiValue}>
                {data.avgSalesCycle.medianDays !== null
                  ? `${data.avgSalesCycle.medianDays} d`
                  : 'n/a'}
              </Text>
            </View>
            <View style={styles.kpiCard}>
              <Text style={styles.kpiLabel}>Mean</Text>
              <Text style={styles.kpiValue}>
                {data.avgSalesCycle.meanDays !== null ? `${data.avgSalesCycle.meanDays} d` : 'n/a'}
              </Text>
            </View>
          </View>
        </View>
      ) : null}

      {include('berth_demand_ranking') &&
      data.berthDemandRanking &&
      data.berthDemandRanking.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Berth demand ranking</Text>
          <Text style={styles.sectionSubtitle}>Top berths by active-interest count.</Text>
          <SimpleTable
            styles={styles}
            headers={['Mooring', 'Active interests']}
            widths={[50, 50]}
            rows={data.berthDemandRanking.map((row) => [
              row.mooringNumber,
              String(row.interestCount),
            ])}
          />
        </View>
      ) : null}

      {include('deal_pulse_distribution') &&
      data.dealPulseDistribution &&
      data.dealPulseDistribution.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Deal pulse distribution</Text>
          <Text style={styles.sectionSubtitle}>
            Counts of active interests in each pulse tier (Hot / Warm / Cool / Cold).
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Tier', 'Count']}
            widths={[70, 30]}
            rows={data.dealPulseDistribution.map((row) => [
              row.tier ? row.tier.charAt(0).toUpperCase() + row.tier.slice(1) : row.tier,
              String(row.count),
            ])}
          />
        </View>
      ) : null}

      {include('client_country_distribution') &&
      data.clientCountryDistribution &&
      data.clientCountryDistribution.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Client country distribution</Text>
          <Text style={styles.sectionSubtitle}>
            Active-client counts grouped by nationality (ISO 3166-1 alpha-2).
          </Text>
          <SimpleTable
            styles={styles}
            headers={['Country', 'Clients']}
            widths={[70, 30]}
            rows={data.clientCountryDistribution.map((row) => [row.country, String(row.count)])}
          />
        </View>
      ) : null}

      {include('recent_activity') && data.recentActivity && data.recentActivity.length > 0 ? (
        <TableSection
          styles={styles}
          title="Recent activity"
          subtitle="Last entries from the audit log, compact snapshot."
          headers={['When', 'Who', 'Summary']}
          widths={[18, 22, 60]}
          rows={data.recentActivity.map((row) => [
            new Date(row.when).toLocaleString('en-GB'),
            row.actor ?? 'system',
            row.summary,
          ])}
        />
      ) : null}

      {include('new_clients_period') &&
      data.newClientsInPeriod &&
      data.newClientsInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="New clients (in period)"
          subtitle="Clients added during the report window with their lead source. Capped at 50 rows; full list lives in the client export."
          headers={['Client', 'Source', 'Added']}
          widths={[55, 25, 20]}
          rows={data.newClientsInPeriod.map((r) => [
            r.name,
            r.source ?? '-',
            new Date(r.createdAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('new_interests_period') &&
      data.newInterestsInPeriod &&
      data.newInterestsInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="New interests (in period)"
          subtitle="Interests opened during the report window, with the stage they currently sit at and the berth(s) attached."
          headers={['Client', 'Stage', 'Berth', 'Opened']}
          widths={[35, 22, 23, 20]}
          rows={data.newInterestsInPeriod.map((r) => [
            r.clientName,
            stageLabel(r.stage),
            r.berthLabel ?? '-',
            new Date(r.createdAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('berths_sold_period') &&
      data.berthsSoldInPeriod &&
      data.berthsSoldInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="Berths sold (in period)"
          subtitle="Berths transitioned to Sold status during the report window, resolved from the audit log."
          headers={['Mooring', 'Sold on']}
          widths={[50, 50]}
          rows={data.berthsSoldInPeriod.map((r) => [
            r.mooringNumber,
            new Date(r.soldAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('signed_documents_period') &&
      data.signedDocumentsInPeriod &&
      data.signedDocumentsInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="Documents signed (in period)"
          subtitle="EOIs, reservations, and contracts marked completed during the report window."
          headers={['Type', 'Title', 'Signed on']}
          widths={[20, 55, 25]}
          rows={data.signedDocumentsInPeriod.map((r) => [
            r.type,
            r.title,
            new Date(r.signedAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('contracts_signed_period') &&
      data.contractsSignedInPeriod &&
      data.contractsSignedInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="Contracts signed (in period)"
          subtitle="Contract documents that completed signing during the report window."
          headers={['Title', 'Signed on']}
          widths={[75, 25]}
          rows={data.contractsSignedInPeriod.map((r) => [
            r.title,
            new Date(r.signedAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('deposits_received_period') &&
      data.depositsReceivedInPeriod &&
      data.depositsReceivedInPeriod.length > 0 ? (
        <TableSection
          styles={styles}
          title="Deposits received (in period)"
          subtitle="Deposit payments received during the report window, with client + $ amount."
          headers={['Client', 'Amount', 'Date']}
          widths={[55, 25, 20]}
          rows={data.depositsReceivedInPeriod.map((r) => [
            r.clientName,
            formatCurrency(String(r.amount), r.currency, { maxFractionDigits: 0 }),
            new Date(r.paidAt).toLocaleDateString('en-GB'),
          ])}
        />
      ) : null}

      {include('hot_deals') && data.hotDeals && data.hotDeals.length > 0 ? (
        <TableSection
          styles={styles}
          title="Hot deals"
          subtitle="Top active interests, ranked by pipeline stage with most-recent activity as tiebreaker."
          headers={['Client', 'Mooring', 'Stage', 'Last contact']}
          widths={[40, 20, 20, 20]}
          rows={data.hotDeals.map((d) => [
            d.clientName ?? '-',
            d.mooringNumber ?? '-',
            stageLabel(d.stage),
            d.lastContact ? new Date(d.lastContact).toLocaleDateString('en-GB') : '-',
          ])}
        />
      ) : null}

      {/* Pending-resolver placeholder. Lets the user see that a
          selected widget IS recognised by the renderer even when its
          data resolver hasn't shipped yet - keeps the surface
          discoverable instead of silently dropping the request. */}
      {data.stubsPending && data.stubsPending.length > 0 ? (
        <View wrap={false}>
          <Text style={styles.sectionTitle}>Coming soon</Text>
          <Text style={styles.sectionSubtitle}>
            These sections are wired into the export dialog but their data resolvers ship in the
            next iteration. The choice persists, so the next report run will include them
            automatically:
          </Text>
          {data.stubsPending.map((id) => (
            <Text key={id} style={styles.kpiSubvalue}>
              · {id}
            </Text>
          ))}
        </View>
      ) : null}
    </BrandedReportDocument>
  );
}

function pct(n: number, total: number): string {
  return total > 0 ? `${((n / total) * 100).toFixed(1)}%` : '-';
}

interface SimpleTableProps {
  styles: ReturnType<typeof makeReportStyles>;
  headers: string[];
  widths: number[];
  rows: string[][];
}

/**
 * Plain table primitive used by every chart-style widget section.
 * Widths are percentages of the container (sum to 100). Header row is
 * gray; data rows alternate fafafa/white for scannability without the
 * "spreadsheet" feel.
 */
function SimpleTable({ styles, headers, widths, rows }: SimpleTableProps) {
  return (
    <View style={styles.table}>
      {/* `minPresenceAhead` keeps the header attached to at least ~48pt
          of body rows: if the header would otherwise land at the very
          bottom of a page it moves to the next page WITH its rows rather
          than orphaning. */}
      <View style={styles.tableHeader} minPresenceAhead={48}>
        {headers.map((header, i) => (
          <Text key={header + i} style={{ ...styles.tableHeaderCell, width: `${widths[i]}%` }}>
            {header}
          </Text>
        ))}
      </View>
      {rows.map((row, rowIdx) => (
        // `wrap={false}` per row so a cell that wraps to two lines (long
        // document filenames) never splits across a page boundary — the
        // whole row moves to the next page intact rather than rendering
        // half on each, which reads as overlapping text.
        <View
          key={rowIdx}
          style={rowIdx % 2 === 1 ? styles.tableRowZebra : styles.tableRow}
          wrap={false}
        >
          {row.map((cell, i) => (
            <Text key={`${rowIdx}-${i}`} style={{ ...styles.tableCell, width: `${widths[i]}%` }}>
              {cell}
            </Text>
          ))}
        </View>
      ))}
    </View>
  );
}

/**
 * Section wrapper for list-style widgets whose row count is unbounded
 * (the per-period lists, recent activity, hot deals). Unlike the
 * fixed-size KPI/chart sections, these MUST be allowed to paginate: a
 * `wrap={false}` around an oversized table forces React-PDF to render
 * it crushed / overlapping when it can't fit a single page. Here we let
 * the table flow across pages and use `minPresenceAhead` on the heading
 * so the title isn't orphaned at a page bottom away from its rows.
 */
function TableSection({
  styles,
  title,
  subtitle,
  headers,
  widths,
  rows,
}: {
  styles: ReturnType<typeof makeReportStyles>;
  title: string;
  subtitle: string;
  headers: string[];
  widths: number[];
  rows: string[][];
}) {
  return (
    <View>
      <Text style={styles.sectionTitle} minPresenceAhead={72}>
        {title}
      </Text>
      <Text style={styles.sectionSubtitle}>{subtitle}</Text>
      <SimpleTable styles={styles} headers={headers} widths={widths} rows={rows} />
    </View>
  );
}
