'use client';

import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import {
  ArrowDownRight,
  ArrowLeftRight,
  ArrowUpRight,
  TrendingDown,
  TrendingUp,
} from 'lucide-react';

import { PageHeader } from '@/components/shared/page-header';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { DateRangePicker } from '@/components/dashboard/date-range-picker';
import { ReportExportButton } from '@/components/reports/shared/report-export-button';
import { ReportTemplatesButton } from '@/components/reports/shared/report-templates-button';
import {
  FilterBar,
  type FilterDefinition,
  type FilterValues,
} from '@/components/shared/filter-bar';
import { rangeToBounds, type DateRange } from '@/lib/analytics/range';
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import {
  PIPELINE_STAGES,
  STAGE_LABELS,
  OUTCOME_LABELS,
  SOURCES,
  type PipelineStage,
} from '@/lib/constants';
import { formatMoney } from '@/lib/reports/format-currency';
import type { ReportPayload } from '@/lib/reports/types';
import { ReportEmptyState } from '@/components/reports/shared/report-empty-state';
import type { Route } from 'next';

import { SalesPipelineFunnel } from './sales-pipeline-funnel';
import { SalesStageVelocity } from './sales-stage-velocity';
import { SalesWinRateOverTime } from './sales-win-rate-over-time';
import { SalesSourceConversion } from './sales-source-conversion';
import { SalesRepLeaderboard } from './sales-rep-leaderboard';
import { SalesDealHeat } from './sales-deal-heat';
import { SalesDetailTables } from './sales-detail-tables';

interface SalesKpis {
  activeInterests: number;
  wonInWindow: number;
  lostInWindow: number;
  lossBreakdown: Array<{ outcome: string; count: number }>;
  winRate: number | null;
  pipelineValue: number;
  pipelineValueCurrency: string;
  pipelineValueExcludedCount: number;
  pipelineValueTotalActiveCount: number;
  medianTimeToCloseDays: number | null;
  timeToCloseSampleSize: number;
  newLeadsInWindow: number;
  newLeadsBySource: Array<{ source: string; count: number }>;
}

interface FunnelRow {
  stage: PipelineStage;
  count: number;
  dropoffFromPrior: number | null;
}

interface StageVelocityRow {
  stage: PipelineStage;
  medianDays: number | null;
  p90Days: number | null;
  transitions: number;
}

interface WinRatePoint {
  bucket: string;
  won: number;
  lost: number;
  winRate: number | null;
}

interface WinRateOverTime {
  granularity: 'week' | 'month' | 'quarter';
  points: WinRatePoint[];
}

type SourceOutcome = 'won' | 'lost' | 'cancelled' | 'in_flight';
interface SourceConversionRow {
  source: string;
  counts: Record<SourceOutcome, number>;
  total: number;
}

interface RepLeaderboardRow {
  userId: string | null;
  displayName: string;
  newDeals: number;
  won: number;
  lost: number;
  inFlight: number;
  pipelineValue: number;
  pipelineValueCurrency: string;
  winRate: number | null;
  medianTimeToCloseDays: number | null;
}

type HeatBucket = 'hot' | 'warm' | 'cold';
interface DealHeatSummary {
  distribution: Record<HeatBucket, number>;
  topDeals: Array<{
    id: string;
    clientName: string;
    mooringNumber: string | null;
    stage: PipelineStage;
    bucket: HeatBucket;
    daysSinceLastContact: number | null;
    pipelineValue: number;
    pipelineValueCurrency: string;
  }>;
}

interface OpenDealRow {
  id: string;
  clientName: string;
  primaryBerth: string | null;
  stage: PipelineStage;
  stageValue: number;
  stageValueCurrency: string;
  daysInStage: number | null;
  lastContact: string | null;
}

interface RepPerformanceDetailRow extends RepLeaderboardRow {
  openDeals: OpenDealRow[];
}

interface StalledDealRow {
  id: string;
  clientName: string;
  stage: PipelineStage;
  daysSinceLastContact: number | null;
  daysInStage: number | null;
  stageValue: number;
  stageValueCurrency: string;
  rep: string;
  primaryBerth: string | null;
}

interface ClosingThisMonthRow {
  id: string;
  clientName: string;
  stage: PipelineStage;
  stageValue: number;
  stageValueCurrency: string;
  daysInStage: number | null;
  rep: string;
  primaryBerth: string | null;
}

interface RecentWinRow {
  id: string;
  clientName: string;
  primaryBerth: string | null;
  finalValue: number;
  currency: string;
  daysToClose: number | null;
  rep: string;
  outcomeAt: string;
}

interface LostReasonRow {
  outcome: string;
  count: number;
  totalValueLost: number;
  currency: string;
  avgDaysFromFirstContactToLoss: number | null;
}

// Per-KPI deltas for the prior-period comparison toggle. Only the
// window-derived tiles get a delta; the point-in-time tiles (Active
// interests, Pipeline value) have no prior-window analogue.
interface SalesKpiDeltas {
  wonInWindow: number;
  lostInWindow: number;
  newLeadsInWindow: number;
  winRate: number | null;
  medianTimeToCloseDays: number | null;
}

interface SalesComparison {
  deltas: SalesKpiDeltas;
  priorRange: { from: string; to: string };
}

interface SalesReportPayload {
  data: {
    kpis: SalesKpis;
    comparison: SalesComparison | null;
    funnel: FunnelRow[];
    stageVelocity: StageVelocityRow[];
    winRateOverTime: WinRateOverTime;
    sourceConversion: SourceConversionRow[];
    repLeaderboard: RepLeaderboardRow[];
    repOptions: Array<{ userId: string; displayName: string }>;
    dealHeat: DealHeatSummary;
    repPerformanceDetail: RepPerformanceDetailRow[];
    stalledDeals: StalledDealRow[];
    closingThisMonth: ClosingThisMonthRow[];
    recentWins: RecentWinRow[];
    lostReasonBreakdown: LostReasonRow[];
    range: { from: string; to: string };
    hasData: boolean;
  };
}

const LOSS_LABELS: Record<string, string> = {
  lost_other_marina: 'to competitor',
  lost_unqualified: 'unqualified',
  lost_no_response: 'no response',
  cancelled: 'cancelled',
};

const SOURCE_LABELS: Record<string, string> = {
  website: 'website',
  referral: 'referral',
  broker: 'broker',
  manual: 'manual',
  unknown: 'unknown',
};

// Static filters — the rep ("Assigned to") filter is appended at render
// time from the report payload's `repOptions` (dynamic per port).
const STATIC_FILTER_DEFS: FilterDefinition[] = [
  {
    key: 'stage',
    label: 'Stage',
    type: 'multi-select',
    options: PIPELINE_STAGES.map((s) => ({ value: s, label: STAGE_LABELS[s] })),
  },
  {
    key: 'leadCategory',
    label: 'Lead category',
    type: 'multi-select',
    options: [
      { value: 'hot_lead', label: 'Hot lead' },
      { value: 'specific_qualified', label: 'Specific qualified' },
      { value: 'general_interest', label: 'General interest' },
    ],
  },
  {
    key: 'outcome',
    label: 'Outcome',
    type: 'multi-select',
    options: Object.entries(OUTCOME_LABELS).map(([value, label]) => ({ value, label })),
  },
  {
    key: 'source',
    label: 'Source',
    type: 'multi-select',
    options: SOURCES.map((s) => ({ value: s.value, label: s.label })),
  },
];

// All filter keys we serialise to the query string. Kept stable here so
// the query builder is decoupled from the dynamic rep options (which
// arrive in the report payload).
const FILTER_KEYS = ['stage', 'leadCategory', 'outcome', 'source', 'assignedTo'] as const;

interface SalesTemplateConfig extends Record<string, unknown> {
  kind: 'sales';
  range: DateRange;
  filters: FilterValues;
  compare?: boolean;
}

export function SalesReportClient({ portSlug }: { portSlug: string }) {
  const searchParams = useSearchParams();
  const initialTemplateId = searchParams?.get('templateId') ?? null;

  const [range, setRange] = useState<DateRange>('30d');
  const [filterValues, setFilterValues] = useState<FilterValues>({});
  const [compareEnabled, setCompareEnabled] = useState(false);
  const [activeTemplateId, setActiveTemplateId] = useState<string | null>(initialTemplateId);

  // Wrap the user-driven setters so any view-state change clears the
  // "Using template X" badge. Template-apply goes through the raw
  // setters via handleApplyTemplate, so loading a template doesn't
  // immediately clear its own badge.
  const handleRangeChange = useCallback((next: DateRange) => {
    setRange(next);
    setActiveTemplateId(null);
  }, []);

  const handleFilterChange = useCallback((key: string, value: unknown) => {
    setFilterValues((prev) => ({ ...prev, [key]: value }));
    setActiveTemplateId(null);
  }, []);

  const handleFiltersClear = useCallback(() => {
    setFilterValues({});
    setActiveTemplateId(null);
  }, []);

  const currentConfig: SalesTemplateConfig = useMemo(
    () => ({ kind: 'sales', range, filters: filterValues, compare: compareEnabled }),
    [range, filterValues, compareEnabled],
  );

  const handleApplyTemplate = useCallback((config: SalesTemplateConfig) => {
    // Raw setters here: applying a template MUST NOT clear the
    // active-template badge, which the user-facing setters above do.
    if (config.range) setRange(config.range);
    setFilterValues(config.filters ?? {});
    setCompareEnabled(config.compare ?? false);
  }, []);

  const bounds = useMemo(() => rangeToBounds(range), [range]);

  const filterQs = useMemo(() => {
    const parts: string[] = [];
    for (const key of FILTER_KEYS) {
      const v = filterValues[key];
      if (Array.isArray(v) && v.length > 0) {
        parts.push(`${key}=${encodeURIComponent(v.join(','))}`);
      }
    }
    return parts.length > 0 ? `&${parts.join('&')}` : '';
  }, [filterValues]);

  const query = useQuery<SalesReportPayload>({
    queryKey: [
      'reports',
      'sales',
      bounds.from.toISOString(),
      bounds.to.toISOString(),
      filterQs,
      compareEnabled,
    ],
    queryFn: () =>
      apiFetch<SalesReportPayload>(
        `/api/v1/reports/sales?from=${encodeURIComponent(bounds.from.toISOString())}&to=${encodeURIComponent(bounds.to.toISOString())}${filterQs}${compareEnabled ? '&compare=1' : ''}`,
      ),
    staleTime: 30_000,
  });

  const kpis = query.data?.data.kpis;
  const data = query.data?.data;
  const deltas = query.data?.data.comparison?.deltas ?? null;
  const funnel = query.data?.data.funnel ?? [];
  const stageVelocity = query.data?.data.stageVelocity ?? [];
  const winRateOverTime = query.data?.data.winRateOverTime ?? {
    granularity: 'week' as const,
    points: [],
  };
  const sourceConversion = query.data?.data.sourceConversion ?? [];
  const repLeaderboard = query.data?.data.repLeaderboard ?? [];
  const repOptions = query.data?.data.repOptions;
  // Locked decision: when only ONE rep has activity in window, the
  // leaderboard table is awkward (1-row scoreboard). Hide it; the Rep
  // performance detail (Task #32) will pick up the slack.
  const showLeaderboard = repLeaderboard.length > 1;

  // Append the dynamic rep ("Assigned to") filter once the payload's
  // repOptions land. The FilterBar hides any multi-select with no
  // options, so the rep filter simply doesn't render until then — and
  // not at all for a port whose interests are all unassigned.
  const filterDefs = useMemo<FilterDefinition[]>(() => {
    if (!repOptions || repOptions.length === 0) return STATIC_FILTER_DEFS;
    return [
      ...STATIC_FILTER_DEFS,
      {
        key: 'assignedTo',
        label: 'Assigned to',
        type: 'multi-select',
        options: repOptions.map((r) => ({ value: r.userId, label: r.displayName })),
      },
    ];
  }, [repOptions]);
  const dealHeat = query.data?.data.dealHeat;
  const repPerformanceDetail = query.data?.data.repPerformanceDetail ?? [];
  const stalledDeals = query.data?.data.stalledDeals ?? [];
  const closingThisMonth = query.data?.data.closingThisMonth ?? [];
  const recentWins = query.data?.data.recentWins ?? [];
  const lostReasonBreakdown = query.data?.data.lostReasonBreakdown ?? [];

  /**
   * Build the export payload at click time. Closed over the current
   * `kpis` / `funnel` / `bounds` so the user gets the report they're
   * looking at, not whatever the page state was at first render.
   */
  function buildExportPayload(): ReportPayload {
    if (!kpis) {
      throw new Error('Report still loading');
    }
    // Every money figure in the payload is already in the port's
    // reporting currency (service converts on read). Money rows below
    // are pre-formatted into strings so the export-pdf route (which
    // strips column.format callbacks at the JSON boundary) and the
    // CSV / XLSX exporters (which keep them) all render the same
    // currency-formatted text.
    return {
      title: 'Sales performance',
      description: 'Rep performance, win rates, pipeline value, stalled deals, deal heat.',
      filenameSlug: 'sales-performance',
      range: bounds,
      kpis: [
        { label: 'Active interests', value: kpis.activeInterests },
        { label: 'Won in period', value: kpis.wonInWindow },
        {
          label: 'Lost in period',
          value: kpis.lostInWindow,
          hint: kpis.lossBreakdown
            .map((b) => `${b.count} ${b.outcome.replace(/^lost_/, '')}`)
            .join(', '),
        },
        {
          label: 'Win rate',
          value: kpis.winRate === null ? '—' : `${(kpis.winRate * 100).toFixed(1)}%`,
        },
        {
          label: 'Pipeline value',
          value: formatMoney(kpis.pipelineValue, kpis.pipelineValueCurrency),
          hint: `${kpis.pipelineValueTotalActiveCount} active interests`,
        },
        {
          label: 'Avg time to close',
          value:
            kpis.medianTimeToCloseDays === null
              ? '—'
              : `${kpis.medianTimeToCloseDays.toFixed(1)} days`,
          hint:
            kpis.medianTimeToCloseDays !== null
              ? `based on ${kpis.timeToCloseSampleSize} won deals`
              : 'need ≥3 won deals',
        },
        {
          label: 'New leads',
          value: kpis.newLeadsInWindow,
          hint: kpis.newLeadsBySource.map((s) => `${s.count} ${s.source}`).join(', '),
        },
      ],
      sections: [
        {
          title: 'Pipeline funnel',
          columns: [
            { key: 'stage', label: 'Stage' },
            { key: 'count', label: 'Active deals', align: 'right' },
            {
              key: 'dropoffFromPrior',
              label: 'Drop-off vs prior',
              align: 'right',
              format: (v) =>
                v === null || v === undefined ? '' : `${((v as number) * 100).toFixed(0)}%`,
            },
          ],
          rows: funnel.map((r) => ({
            stage: STAGE_LABELS[r.stage],
            count: r.count,
            dropoffFromPrior: r.dropoffFromPrior,
          })),
        },
        {
          title: 'Stage velocity',
          columns: [
            { key: 'stage', label: 'Stage' },
            {
              key: 'medianDays',
              label: 'Median days in stage',
              align: 'right',
              format: (v) => (v === null || v === undefined ? '—' : (v as number).toFixed(1)),
            },
            {
              key: 'p90Days',
              label: 'p90 days',
              align: 'right',
              format: (v) => (v === null || v === undefined ? '—' : (v as number).toFixed(1)),
            },
            { key: 'transitions', label: 'Sample size', align: 'right' },
          ],
          rows: stageVelocity.map((r) => ({
            stage: STAGE_LABELS[r.stage],
            medianDays: r.medianDays,
            p90Days: r.p90Days,
            transitions: r.transitions,
          })),
        },
        {
          title: `Win rate over time (${winRateOverTime.granularity})`,
          columns: [
            { key: 'bucket', label: 'Period' },
            { key: 'won', label: 'Won', align: 'right' },
            { key: 'lost', label: 'Lost', align: 'right' },
            {
              key: 'winRate',
              label: 'Win rate',
              align: 'right',
              format: (v) =>
                v === null || v === undefined ? '—' : `${((v as number) * 100).toFixed(1)}%`,
            },
          ],
          rows: winRateOverTime.points.map((p) => ({ ...p })),
        },
        {
          title: 'Source → win conversion',
          columns: [
            { key: 'source', label: 'Source' },
            { key: 'won', label: 'Won', align: 'right' },
            { key: 'lost', label: 'Lost', align: 'right' },
            { key: 'cancelled', label: 'Cancelled', align: 'right' },
            { key: 'in_flight', label: 'In flight', align: 'right' },
            { key: 'total', label: 'Total', align: 'right' },
          ],
          rows: sourceConversion.map((r) => ({
            source: r.source,
            won: r.counts.won,
            lost: r.counts.lost,
            cancelled: r.counts.cancelled,
            in_flight: r.counts.in_flight,
            total: r.total,
          })),
        },
        {
          title: 'Rep leaderboard',
          columns: [
            { key: 'displayName', label: 'Rep' },
            { key: 'newDeals', label: 'New', align: 'right' },
            { key: 'won', label: 'Won', align: 'right' },
            { key: 'lost', label: 'Lost', align: 'right' },
            { key: 'inFlight', label: 'In flight', align: 'right' },
            {
              key: 'pipelineValue',
              label: 'Pipeline value',
              align: 'right',
            },
            {
              key: 'winRate',
              label: 'Win rate',
              align: 'right',
              format: (v) =>
                v === null || v === undefined ? '' : `${((v as number) * 100).toFixed(0)}%`,
            },
            {
              key: 'medianTimeToCloseDays',
              label: 'Median close (days)',
              align: 'right',
              format: (v) => (v === null || v === undefined ? '' : (v as number).toFixed(1)),
            },
          ],
          // Pre-format `pipelineValue` per row so PDF (which strips the
          // column.format callback at the server boundary) and CSV / XLSX
          // (which keep it) all render the same currency-formatted
          // string.
          rows: repLeaderboard.map((r) => ({
            ...r,
            pipelineValue: formatMoney(r.pipelineValue, r.pipelineValueCurrency),
          })),
        },
        ...(dealHeat
          ? [
              {
                title: 'Deal heat — hottest deals',
                columns: [
                  { key: 'clientName', label: 'Client' },
                  { key: 'mooringNumber', label: 'Berth' },
                  {
                    key: 'stage',
                    label: 'Stage',
                    format: (v: unknown) => STAGE_LABELS[v as PipelineStage] ?? '',
                  },
                  { key: 'bucket', label: 'Heat' },
                  {
                    key: 'daysSinceLastContact',
                    label: 'Days since contact',
                    align: 'right' as const,
                    format: (v: unknown) => (v === null || v === undefined ? 'never' : String(v)),
                  },
                  {
                    key: 'pipelineValue',
                    label: 'Value',
                    align: 'right' as const,
                  },
                ],
                // Same pre-format treatment as the leaderboard above —
                // closure-format here so the PDF render path sees a
                // ready-to-print string.
                rows: dealHeat.topDeals.map((d) => ({
                  ...d,
                  pipelineValue: formatMoney(d.pipelineValue, d.pipelineValueCurrency),
                })),
              },
            ]
          : []),
      ],
    };
  }

  if (!query.isLoading && data && !data.hasData) {
    return (
      <div className="space-y-6">
        <PageHeader
          eyebrow="Reports"
          title="Sales performance"
          description="Rep performance, win rates, pipeline value, stalled deals, and deal heat."
        />
        <ReportEmptyState
          icon={TrendingUp}
          title="No sales activity yet"
          body="Once you add clients and log interests, this report fills with win rates, pipeline value, and deal heat."
          actionLabel="Add an interest"
          actionHref={`/${portSlug}/interests` as Route}
        />
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <PageHeader
        eyebrow="Reports"
        title="Sales performance"
        description="Rep performance, win rates, pipeline value, stalled deals, and deal heat."
        actions={
          <div className="flex items-center gap-2">
            <DateRangePicker value={range} onChange={handleRangeChange} />
            <Button
              type="button"
              variant={compareEnabled ? 'default' : 'outline'}
              size="sm"
              onClick={() => {
                setCompareEnabled((v) => !v);
                setActiveTemplateId(null);
              }}
              aria-pressed={compareEnabled}
              title="Show change vs the equal-length window immediately before the selected period"
            >
              <ArrowLeftRight className="h-4 w-4" aria-hidden />
              Compare to prior period
            </Button>
            <ReportTemplatesButton<SalesTemplateConfig>
              kind="sales"
              currentConfig={currentConfig}
              onApply={handleApplyTemplate}
              activeTemplateId={activeTemplateId}
              onActiveTemplateChange={setActiveTemplateId}
              initialTemplateId={initialTemplateId}
            />
            <ReportExportButton buildPayload={buildExportPayload} disabled={!kpis} />
          </div>
        }
      />

      {/* KPI STRIP - 7 tiles. Grid scales from 2-up on mobile to 4-up
          on lg; the 7th tile wraps naturally to a second row. */}
      <section
        aria-label="Sales KPIs"
        className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4"
      >
        {query.isLoading || !kpis ? (
          Array.from({ length: 7 }).map((_, i) => <KpiSkeleton key={i} />)
        ) : (
          <>
            <KpiCard
              label="Active interests"
              value={formatInt(kpis.activeInterests)}
              hint="Not archived, no outcome set"
            />
            <KpiCard
              label="Won in period"
              value={formatInt(kpis.wonInWindow)}
              valueTrend={kpis.wonInWindow > 0 ? 'positive' : 'neutral'}
              delta={deltas ? buildKpiDelta(deltas.wonInWindow, 'up', formatInt) : null}
            />
            <KpiCard
              label="Lost in period"
              value={formatInt(kpis.lostInWindow)}
              valueTrend={kpis.lostInWindow > 0 ? 'negative' : 'neutral'}
              delta={deltas ? buildKpiDelta(deltas.lostInWindow, 'down', formatInt) : null}
              hint={
                kpis.lossBreakdown.length > 0
                  ? kpis.lossBreakdown
                      .map((b) => `${b.count} ${LOSS_LABELS[b.outcome] ?? b.outcome}`)
                      .join(' · ')
                  : undefined
              }
            />
            <KpiCard
              label="Win rate"
              value={kpis.winRate === null ? '—' : formatPercent(kpis.winRate)}
              delta={
                deltas
                  ? buildKpiDelta(deltas.winRate, 'up', (abs) => `${(abs * 100).toFixed(1)}pp`)
                  : null
              }
              hint={kpis.winRate === null ? 'No closed deals in period' : 'Excludes cancellations'}
            />
            <KpiCard
              label="Pipeline value"
              value={formatMoney(kpis.pipelineValue, kpis.pipelineValueCurrency)}
              hint={
                kpis.pipelineValueExcludedCount > 0
                  ? `${kpis.pipelineValueExcludedCount} of ${kpis.pipelineValueTotalActiveCount} interests have no value`
                  : `${kpis.pipelineValueTotalActiveCount} active interests · weighted by stage`
              }
            />
            <KpiCard
              label="Avg time to close"
              value={
                kpis.medianTimeToCloseDays === null
                  ? '—'
                  : formatDurationFromDays(kpis.medianTimeToCloseDays)
              }
              delta={
                deltas
                  ? buildKpiDelta(
                      deltas.medianTimeToCloseDays,
                      'down',
                      (abs) => `${Math.round(abs)}d`,
                    )
                  : null
              }
              hint={
                kpis.medianTimeToCloseDays === null
                  ? 'Need ≥3 won deals for a meaningful median'
                  : `Based on ${kpis.timeToCloseSampleSize} won deals`
              }
            />
            <KpiCard
              label="New leads"
              value={formatInt(kpis.newLeadsInWindow)}
              delta={deltas ? buildKpiDelta(deltas.newLeadsInWindow, 'up', formatInt) : null}
              hint={
                kpis.newLeadsBySource.length > 0
                  ? kpis.newLeadsBySource
                      .map((s) => `${s.count} ${SOURCE_LABELS[s.source] ?? s.source}`)
                      .join(' · ')
                  : undefined
              }
            />
          </>
        )}
      </section>

      {/* CHART 1 - Pipeline funnel */}
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Pipeline funnel</CardTitle>
          <p className="text-xs text-muted-foreground">
            Active interests grouped by stage. Drop-off rate shown between consecutive stages.
          </p>
        </CardHeader>
        <CardContent>
          {query.isLoading ? (
            <Skeleton className="h-[360px] w-full" />
          ) : funnel.every((r) => r.count === 0) ? (
            <EmptyState>
              No active interests yet. New deals appear here as they enter the pipeline.
            </EmptyState>
          ) : (
            <SalesPipelineFunnel rows={funnel} />
          )}
        </CardContent>
      </Card>

      {/* CHART 2 - Stage velocity */}
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Stage velocity</CardTitle>
          <p className="text-xs text-muted-foreground">
            Median days deals spend in each stage before moving on, with the p90 marker on each bar.
            Derived from the stage-change audit log.
          </p>
        </CardHeader>
        <CardContent>
          {query.isLoading ? (
            <Skeleton className="h-[280px] w-full" />
          ) : (
            <SalesStageVelocity rows={stageVelocity} />
          )}
        </CardContent>
      </Card>

      {/* CHART 3 - Win rate over time */}
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Win rate over time</CardTitle>
          <p className="text-xs text-muted-foreground">
            Win rate per {winRateOverTime.granularity}. Faint area underlay is the total deals
            closed in each bucket so 100% on 1 deal doesn&apos;t read as 100% on 50.
          </p>
        </CardHeader>
        <CardContent>
          {query.isLoading ? (
            <Skeleton className="h-[280px] w-full" />
          ) : (
            <SalesWinRateOverTime
              granularity={winRateOverTime.granularity}
              points={winRateOverTime.points}
            />
          )}
        </CardContent>
      </Card>

      {/* CHART 4 - Source → win conversion */}
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Source → win conversion</CardTitle>
          <p className="text-xs text-muted-foreground">
            For each lead source, the share of deals that ended up won, lost, cancelled, or are
            still in flight. PDF-friendly stacked bars (not sankey).
          </p>
        </CardHeader>
        <CardContent>
          {query.isLoading ? (
            <Skeleton className="h-[200px] w-full" />
          ) : (
            <SalesSourceConversion rows={sourceConversion} />
          )}
        </CardContent>
      </Card>

      {/* CHART 5 - Rep leaderboard (auto-hidden when only one rep
          has activity; the Rep performance detail block ships as
          Task #32 and will fill that slot). */}
      {showLeaderboard ? (
        <Card>
          <CardHeader>
            <CardTitle className="text-base">Rep leaderboard</CardTitle>
            <p className="text-xs text-muted-foreground">
              Per-rep activity in the period. Pipeline value is the rep&apos;s slice of the
              port-wide stage-weighted forecast, normalised to port currency.
            </p>
          </CardHeader>
          <CardContent>
            {query.isLoading ? (
              <Skeleton className="h-[200px] w-full" />
            ) : (
              <SalesRepLeaderboard rows={repLeaderboard} />
            )}
          </CardContent>
        </Card>
      ) : null}

      {/* DEAL HEAT SECTION - sits between leaderboard + detail tables
          per the locked spec. Hot deals count + heat distribution +
          hottest 5 deals (linkable). */}
      {query.isLoading || !dealHeat ? (
        <Skeleton className="h-[180px] w-full" />
      ) : (
        <SalesDealHeat data={dealHeat} />
      )}

      {/* DETAIL-TABLE FILTERS — narrow the next 5 tables by stage / lead
          category / outcome. KPIs + charts above intentionally stay
          unfiltered (macro view). */}
      <div className="flex items-center justify-between gap-2 pt-2">
        <h2 className="text-sm font-semibold text-foreground">Deal detail</h2>
        <FilterBar
          filters={filterDefs}
          values={filterValues}
          onChange={handleFilterChange}
          onClear={handleFiltersClear}
        />
      </div>

      {/* 5 DETAIL TABLES - Rep performance detail (single-rep only) /
          Stalled deals / Closing soon / Recent wins / Lost-reason
          breakdown. */}
      {query.isLoading ? (
        <Skeleton className="h-[400px] w-full" />
      ) : (
        <SalesDetailTables
          repPerformanceDetail={repPerformanceDetail}
          stalledDeals={stalledDeals}
          closingThisMonth={closingThisMonth}
          recentWins={recentWins}
          lostReasonBreakdown={lostReasonBreakdown}
          showRepPerformanceDetail={!showLeaderboard}
        />
      )}
    </div>
  );
}

// ─── KPI tile primitives ─────────────────────────────────────────────────────

interface KpiDeltaDisplay {
  /** Pre-formatted magnitude with sign, e.g. "+3", "−2.0pp", "−1.5d". */
  formatted: string;
  /** Colour semantics: positive = good (green), negative = bad (rose),
   *  neutral = no change (muted). Computed by the caller because "good"
   *  depends on the metric (more wins = good; longer time-to-close = bad). */
  tone: 'positive' | 'negative' | 'neutral';
}

interface KpiCardProps {
  label: string;
  value: string;
  hint?: string;
  valueTrend?: 'positive' | 'negative' | 'neutral';
  /** Prior-period comparison delta. Rendered as a small "vs prior" line
   *  under the value when the comparison toggle is on. */
  delta?: KpiDeltaDisplay | null;
}

function KpiCard({ label, value, hint, valueTrend = 'neutral', delta }: KpiCardProps) {
  // Padding goes directly on the bare Card (skipping CardContent)
  // because CardContent ships with `p-4 pt-0 sm:p-6 sm:pt-0` for
  // use-with-CardHeader contexts. KPI tiles have no header, so any
  // `pt-*` override gets stripped or stomped by tailwind-merge +
  // breakpoint specificity. Cleaner to skip CardContent entirely.
  return (
    <Card className="h-full p-4 space-y-1.5">
      <p className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
        {label}
      </p>
      <div className="flex items-baseline gap-1.5">
        <p className="text-2xl font-semibold tracking-tight text-foreground tabular-nums">
          {value}
        </p>
        {valueTrend === 'positive' ? (
          <TrendingUp className="h-3.5 w-3.5 text-emerald-600" aria-hidden />
        ) : valueTrend === 'negative' ? (
          <TrendingDown className="h-3.5 w-3.5 text-rose-600" aria-hidden />
        ) : null}
      </div>
      {delta ? (
        <p
          className={cn(
            'flex items-center gap-0.5 text-[11px] font-medium tabular-nums',
            delta.tone === 'positive'
              ? 'text-emerald-600'
              : delta.tone === 'negative'
                ? 'text-rose-600'
                : 'text-muted-foreground',
          )}
        >
          {delta.tone === 'positive' ? (
            <ArrowUpRight className="h-3 w-3" aria-hidden />
          ) : delta.tone === 'negative' ? (
            <ArrowDownRight className="h-3 w-3" aria-hidden />
          ) : null}
          {delta.formatted}
          <span className="font-normal text-muted-foreground">vs prior</span>
        </p>
      ) : null}
      {hint ? (
        <p className="text-[11px] text-muted-foreground leading-snug line-clamp-2">{hint}</p>
      ) : null}
    </Card>
  );
}

function KpiSkeleton() {
  return (
    <Card className="h-full p-4 space-y-2">
      <Skeleton className="h-3 w-20" />
      <Skeleton className="h-7 w-16" />
      <Skeleton className="h-3 w-32" />
    </Card>
  );
}

function EmptyState({ children }: { children: React.ReactNode }) {
  return (
    <div className="py-16 flex flex-col items-center justify-center text-center space-y-2">
      <Badge variant="outline" className="text-muted-foreground">
        No data
      </Badge>
      <p className="text-sm text-muted-foreground max-w-xs">{children}</p>
    </div>
  );
}

// ─── Formatting helpers ──────────────────────────────────────────────────────

function formatInt(n: number): string {
  return new Intl.NumberFormat(undefined).format(n);
}

function formatPercent(fraction: number): string {
  return `${Math.round(fraction * 1000) / 10}%`;
}

/**
 * Build the prior-period delta display for a KPI tile. `goodDirection`
 * encodes which way is good for THIS metric (more wins = up-is-good;
 * shorter time-to-close = down-is-good) so the colour matches intuition
 * regardless of the raw sign. Returns null when the metric had no value
 * in one of the two periods (e.g. win rate with no closed deals).
 */
function buildKpiDelta(
  value: number | null,
  goodDirection: 'up' | 'down',
  format: (abs: number) => string,
): KpiDeltaDisplay | null {
  if (value === null) return null;
  if (value === 0) return { formatted: 'no change', tone: 'neutral' };
  const sign = value > 0 ? '+' : '−';
  const formatted = `${sign}${format(Math.abs(value))}`;
  const isGood = goodDirection === 'up' ? value > 0 : value < 0;
  return { formatted, tone: isGood ? 'positive' : 'negative' };
}

// Money helpers come from the shared module — `formatMoney` for KPI
// tile readability, `formatMoneyCompact` for tight dense tables.

/**
 * Adaptive duration string per locked decision: days under 60, weeks
 * under 24 weeks, otherwise months. Single-decimal rounding keeps the
 * tile compact.
 */
function formatDurationFromDays(days: number): string {
  if (days < 60) return `${Math.round(days)}d`;
  const weeks = days / 7;
  if (weeks < 24) return `${Math.round(weeks)}w`;
  const months = days / 30.44;
  return `${months.toFixed(1)}mo`;
}

// Reference the stage labels import so it stays load-bearing across
// later phases (used by the funnel + leaderboard component imports).
void STAGE_LABELS;
