/**
 * Pure-math helpers extracted from report-generators.ts so the
 * revenue/forecast/occupancy/funnel computations can be unit-tested
 * deterministically without spinning up a Postgres fixture.
 *
 * The corresponding DB-bound `fetch*Data` functions in report-generators
 * call into these helpers after gathering rows. Tests for the SQL itself
 * remain integration-tier; this module covers the arithmetic so a future
 * weight-tuning change can't silently shift the forecast number.
 */
import { STAGE_WEIGHTS, canonicalizeStage } from '@/lib/constants';

export interface StageRevenueRow {
  stage: string;
  revenue: string | number | null;
}

export interface StageCountRow {
  stage: string;
  count: number;
}

export interface BerthStatusRow {
  status: string;
  count: number;
}

/**
 * Collapse a per-pipeline-stage revenue list into a canonicalized
 * Record<canonicalStage, totalRevenueString>. Handles the legacy 9-stage
 * keys via canonicalizeStage so historical rows fold into the modern
 * 7-stage bucket they belong to.
 */
export function rollupStageRevenue(rows: StageRevenueRow[]): Record<string, string> {
  const out: Record<string, string> = {};
  for (const row of rows) {
    const key = canonicalizeStage(row.stage);
    const prior = parseFloat(out[key] ?? '0');
    const next = row.revenue ? parseFloat(String(row.revenue)) : 0;
    out[key] = String(prior + next);
  }
  return out;
}

/**
 * Same as rollupStageRevenue but for counts (funnel breakdown).
 */
export function rollupStageCounts(rows: StageCountRow[]): Record<string, number> {
  const out: Record<string, number> = {};
  for (const row of rows) {
    const key = canonicalizeStage(row.stage);
    out[key] = (out[key] ?? 0) + row.count;
  }
  return out;
}

/**
 * Pipeline-weighted forecast: sum(berth_price × stage_weight) for every
 * active interest. The weight per stage resolves from per-port admin
 * overrides (`system_settings.pipeline_weights`) and falls back to the
 * STAGE_WEIGHTS defaults. Legacy stage keys canonicalize before lookup
 * so the forecast doesn't silently undershoot due to a key miss.
 *
 * Returns the forecast as a 2-decimal-fixed string for stable
 * comparison + downstream PDF rendering.
 */
export function computeTotalForecast(
  rows: StageRevenueRow[],
  weights: Record<string, number> = STAGE_WEIGHTS,
): string {
  let total = 0;
  for (const row of rows) {
    if (!row.revenue) continue;
    const weight = weights[canonicalizeStage(row.stage)] ?? 0;
    total += parseFloat(String(row.revenue)) * weight;
  }
  return total.toFixed(2);
}

/**
 * Occupancy rate as a percentage. "Occupied" = sold only - per the
 * 2026-05-14 product decision, under_offer is a hold (blocks sale to
 * other clients) but doesn't count as the berth being occupied yet.
 * Returns the rate to 1 decimal place; returns 0 when totalBerths=0
 * to avoid NaN propagation through the PDF.
 */
export function computeOccupancyRate(statusCounts: Record<string, number>): {
  occupancyRate: number;
  totalBerths: number;
} {
  let totalBerths = 0;
  for (const k of Object.keys(statusCounts)) {
    totalBerths += statusCounts[k] ?? 0;
  }
  const occupiedCount = statusCounts['sold'] ?? 0;
  const occupancyRate =
    totalBerths > 0 ? Math.round((occupiedCount / totalBerths) * 100 * 10) / 10 : 0;
  return { occupancyRate, totalBerths };
}

/**
 * Build the per-status count map from a status-grouped query result.
 * Returns the map AND the total count so callers don't have to sum
 * again themselves.
 */
export function rollupBerthStatusCounts(rows: BerthStatusRow[]): {
  statusCounts: Record<string, number>;
  totalBerths: number;
} {
  const statusCounts: Record<string, number> = {};
  let totalBerths = 0;
  for (const row of rows) {
    statusCounts[row.status] = row.count;
    totalBerths += row.count;
  }
  return { statusCounts, totalBerths };
}
