import 'dotenv/config';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { buildDigilanderBundleBody } from '../routes/forecast';

const args = process.argv.slice(2);
const DEFAULT_API_BASE = process.env.DIGILANDER_AUDIT_API_BASE || 'http://127.0.0.1:3021/api';
const DEFAULT_LEAGUES = ['mlb', 'nba', 'nhl', 'epl', 'bundesliga'];
const DEFAULT_CONCURRENCY = 4;
const DEFAULT_TIMEOUT_MS = 10000;

type BundleTimingStepName =
  | 'preview'
  | 'topPicks'
  | 'news'
  | 'market'
  | 'summarySupport'
  | 'gameData'
  | 'playerProps'
  | 'teamBreakdown'
  | 'total';

type CoverageModuleName =
  | 'preview'
  | 'bestPlays'
  | 'playerProps'
  | 'playerData'
  | 'injuries'
  | 'news'
  | 'market'
  | 'teamBreakdown';

type CoverageReadiness = 'full' | 'partial' | 'empty' | 'locked' | 'failed' | 'unknown';
type CoverageConfidence = 'high' | 'medium' | 'low' | 'unknown';

type CoverageModuleSummary = {
  available: boolean;
  status: string;
  count: number;
  readiness: CoverageReadiness;
  coverageConfidence: CoverageConfidence;
};

type BundleCoverage = Record<CoverageModuleName, CoverageModuleSummary>;

type BundleModuleReadinessDiagnostics = {
  status: string;
  available: boolean;
  count: number;
  reason: string | null;
  readiness: CoverageReadiness;
  coverageConfidence: CoverageConfidence;
  signals: Record<string, any>;
};

type BundleDiagnostics = {
  request: {
    cacheStatus: string | null;
    coalesced: boolean;
  };
  fallbackUsage: {
    news: {
      source: string;
      fallbackUsed: boolean;
      itemCount: number;
    };
    market: {
      lineMovementSource: string;
      bookSnapshotSource: string;
      fallbackUsed: boolean;
      recovered: boolean;
    };
    summarySupport: {
      source: string;
      fallbackUsed: boolean;
      playerCount: number;
      linePropsCount: number;
      marketPropsCount: number;
    };
  };
  moduleReadiness: Partial<Record<CoverageModuleName, BundleModuleReadinessDiagnostics>>;
  timeBudgetsMs: Partial<Record<Exclude<BundleTimingStepName, 'total'>, number>>;
  timingsMs: Partial<Record<BundleTimingStepName, number>>;
};

type BundleAuditEventResult = {
  league: string;
  eventId: string;
  matchup: string;
  startsAt: string | null;
  responseMs: number;
  ok: boolean;
  error: string | null;
  coverage: BundleCoverage | null;
  diagnostics: BundleDiagnostics | null;
};

type BundleTimingStepSummary = {
  avgMs: number;
  maxMs: number;
  budgetMs: number | null;
  overBudgetEvents: number;
  presentEvents: number;
};

type BundleAuditLeagueSummary = {
  league: string;
  totalEvents: number;
  auditedEvents: number;
  failedEvents: number;
  avgResponseMs: number;
  maxResponseMs: number;
  coverageRates: Record<CoverageModuleName, number>;
  statusBreakdown: Record<CoverageModuleName, Record<string, number>>;
  readinessBreakdown: Record<CoverageModuleName, Record<CoverageReadiness, number>>;
  confidenceBreakdown: Record<CoverageModuleName, Record<CoverageConfidence, number>>;
  playerPropCountSummary: {
    avgSourceCount: number;
    avgNormalizedCount: number;
    avgPublishedCount: number;
    avgFallbackCount: number;
    avgExposedCount: number;
  };
  fallbackUsageBreakdown: {
    news: {
      fallbackEvents: number;
      sourceCounts: Record<string, number>;
    };
    market: {
      fallbackEvents: number;
      mixedEvents: number;
      lineMovementSourceCounts: Record<string, number>;
      bookSnapshotSourceCounts: Record<string, number>;
    };
    summarySupport: {
      fallbackEvents: number;
      sourceCounts: Record<string, number>;
    };
  };
  cacheStats: {
    hits: number;
    misses: number;
    coalesced: number;
  };
  timingSummary: Partial<Record<BundleTimingStepName, BundleTimingStepSummary>>;
  slowestSteps: Array<{
    step: BundleTimingStepName;
    avgMs: number;
    maxMs: number;
    budgetMs: number | null;
    overBudgetEvents: number;
  }>;
  alerts: string[];
  sampleFailures: Array<{ eventId: string; matchup: string; error: string }>;
};

type BundleAuditReport = {
  auditedAt: string;
  apiBase: string;
  leagues: string[];
  summary: {
    totalEvents: number;
    auditedEvents: number;
    failedEvents: number;
    avgResponseMs: number;
    maxResponseMs: number;
  };
  leagueSummaries: BundleAuditLeagueSummary[];
  eventResults: BundleAuditEventResult[];
};

function getArgValue(flag: string): string | null {
  const index = args.indexOf(flag);
  if (index === -1) return null;
  return args[index + 1] || null;
}

function buildTimestampSlug(date: Date): string {
  return date.toISOString().replace(/[:.]/g, '-');
}

function toNumber(value: string | null, fallback: number): number {
  const numeric = Number(value);
  return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
}

async function fetchJson(url: string, timeoutMs: number): Promise<any> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 DigilanderCoverageAudit',
        'Accept': 'application/json',
      },
      signal: controller.signal,
    });
    const raw = await res.text();
    if (!res.ok) {
      throw new Error(raw || `Request failed (${res.status})`);
    }
    return raw ? JSON.parse(raw) : null;
  } catch (err) {
    if (err instanceof Error && err.name === 'AbortError') {
      throw new Error(`Timed out after ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }
}

async function fetchLeagueEvents(apiBase: string, league: string, timeoutMs: number): Promise<any[]> {
  const body = await fetchJson(`${apiBase}/events?league=${encodeURIComponent(league)}`, timeoutMs);
  const events = body?.events?.[league];
  return Array.isArray(events) ? events : [];
}

function normalizeCoverageModule(value: any): CoverageModuleSummary {
  const readinessValue = String(value?.readiness || 'unknown').toLowerCase();
  const confidenceValue = String(value?.coverageConfidence || 'unknown').toLowerCase();
  return {
    available: Boolean(value?.available),
    status: String(value?.status || 'unknown'),
    count: Number(value?.count || 0),
    readiness: (['full', 'partial', 'empty', 'locked', 'failed'].includes(readinessValue)
      ? readinessValue
      : 'unknown') as CoverageReadiness,
    coverageConfidence: (['high', 'medium', 'low'].includes(confidenceValue)
      ? confidenceValue
      : 'unknown') as CoverageConfidence,
  };
}

function normalizeTimingMap(value: any): Partial<Record<BundleTimingStepName, number>> {
  if (!value || typeof value !== 'object') return {};
  const result: Partial<Record<BundleTimingStepName, number>> = {};
  for (const step of ['preview', 'topPicks', 'news', 'market', 'summarySupport', 'gameData', 'playerProps', 'teamBreakdown', 'total'] as BundleTimingStepName[]) {
    const numeric = Number((value as any)[step]);
    if (Number.isFinite(numeric) && numeric >= 0) {
      result[step] = numeric;
    }
  }
  return result;
}

function normalizeDiagnostics(value: any): BundleDiagnostics | null {
  if (!value || typeof value !== 'object') return null;
  const moduleReadiness = Object.fromEntries(
    (['preview', 'bestPlays', 'playerProps', 'playerData', 'injuries', 'news', 'market', 'teamBreakdown'] as CoverageModuleName[])
      .map((module) => {
        const raw = value?.moduleReadiness?.[module];
        if (!raw || typeof raw !== 'object') return [module, null];
        const readinessValue = String(raw.readiness || 'unknown').toLowerCase();
        const confidenceValue = String(raw.coverageConfidence || 'unknown').toLowerCase();
        return [module, {
          status: String(raw.status || 'unknown'),
          available: Boolean(raw.available),
          count: Number(raw.count || 0),
          reason: raw.reason ? String(raw.reason) : null,
          readiness: (['full', 'partial', 'empty', 'locked', 'failed'].includes(readinessValue)
            ? readinessValue
            : 'unknown') as CoverageReadiness,
          coverageConfidence: (['high', 'medium', 'low'].includes(confidenceValue)
            ? confidenceValue
            : 'unknown') as CoverageConfidence,
          signals: raw.signals && typeof raw.signals === 'object' ? raw.signals : {},
        } satisfies BundleModuleReadinessDiagnostics];
      }),
  ) as Partial<Record<CoverageModuleName, BundleModuleReadinessDiagnostics>>;
  return {
    request: {
      cacheStatus: value?.request?.cacheStatus ? String(value.request.cacheStatus) : null,
      coalesced: Boolean(value?.request?.coalesced),
    },
    fallbackUsage: {
      news: {
        source: value?.fallbackUsage?.news?.source ? String(value.fallbackUsage.news.source) : 'unknown',
        fallbackUsed: Boolean(value?.fallbackUsage?.news?.fallbackUsed),
        itemCount: Number(value?.fallbackUsage?.news?.itemCount || 0),
      },
      market: {
        lineMovementSource: value?.fallbackUsage?.market?.lineMovementSource ? String(value.fallbackUsage.market.lineMovementSource) : 'unknown',
        bookSnapshotSource: value?.fallbackUsage?.market?.bookSnapshotSource ? String(value.fallbackUsage.market.bookSnapshotSource) : 'unknown',
        fallbackUsed: Boolean(value?.fallbackUsage?.market?.fallbackUsed),
        recovered: Boolean(value?.fallbackUsage?.market?.recovered),
      },
      summarySupport: {
        source: value?.fallbackUsage?.summarySupport?.source ? String(value.fallbackUsage.summarySupport.source) : 'unknown',
        fallbackUsed: Boolean(value?.fallbackUsage?.summarySupport?.fallbackUsed),
        playerCount: Number(value?.fallbackUsage?.summarySupport?.playerCount || 0),
        linePropsCount: Number(value?.fallbackUsage?.summarySupport?.linePropsCount || 0),
        marketPropsCount: Number(value?.fallbackUsage?.summarySupport?.marketPropsCount || 0),
      },
    },
    moduleReadiness,
    timeBudgetsMs: normalizeTimingMap(value?.timeBudgetsMs) as Partial<Record<Exclude<BundleTimingStepName, 'total'>, number>>,
    timingsMs: normalizeTimingMap(value?.timingsMs),
  };
}

async function auditEvent(_apiBase: string, league: string, event: any, timeoutMs: number): Promise<BundleAuditEventResult> {
  const startedAt = Date.now();
  const eventId = String(event?.id || '');
  const matchup = `${String(event?.awayShort || event?.awayTeam || 'Away')} @ ${String(event?.homeShort || event?.homeTeam || 'Home')}`;
  try {
    const body = await Promise.race([
      buildDigilanderBundleBody({
        eventId,
        signedIn: false,
        summaryOnly: true,
      }),
      new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
    ]);
    const coverageRaw = body?.coverage || {};
    const coverage: BundleCoverage = {
      preview: normalizeCoverageModule(coverageRaw.preview),
      bestPlays: normalizeCoverageModule(coverageRaw.bestPlays),
      playerProps: normalizeCoverageModule(coverageRaw.playerProps),
      playerData: normalizeCoverageModule(coverageRaw.playerData),
      injuries: normalizeCoverageModule(coverageRaw.injuries),
      news: normalizeCoverageModule(coverageRaw.news),
      market: normalizeCoverageModule(coverageRaw.market),
      teamBreakdown: normalizeCoverageModule(coverageRaw.teamBreakdown),
    };
    const diagnostics = normalizeDiagnostics(body?.diagnostics);
    return {
      league,
      eventId,
      matchup,
      startsAt: event?.startsAt || null,
      responseMs: Date.now() - startedAt,
      ok: true,
      error: null,
      coverage,
      diagnostics,
    };
  } catch (err) {
    return {
      league,
      eventId,
      matchup,
      startsAt: event?.startsAt || null,
      responseMs: Date.now() - startedAt,
      ok: false,
      error: err instanceof Error ? err.message : 'Unknown error',
      coverage: null,
      diagnostics: null,
    };
  }
}

async function runWithConcurrency<T, R>(
  items: T[],
  concurrency: number,
  worker: (item: T) => Promise<R>,
): Promise<R[]> {
  const results: R[] = [];
  let index = 0;

  async function runWorker() {
    while (index < items.length) {
      const currentIndex = index++;
      results[currentIndex] = await worker(items[currentIndex]);
    }
  }

  const workers = Array.from({ length: Math.min(concurrency, Math.max(items.length, 1)) }, () => runWorker());
  await Promise.all(workers);
  return results;
}

function round(value: number): number {
  return Math.round(value * 10) / 10;
}

function buildLeagueSummary(league: string, results: BundleAuditEventResult[]): BundleAuditLeagueSummary {
  const modules: CoverageModuleName[] = ['preview', 'bestPlays', 'playerProps', 'playerData', 'injuries', 'news', 'market', 'teamBreakdown'];
  const timingSteps: BundleTimingStepName[] = ['preview', 'topPicks', 'news', 'market', 'summarySupport', 'gameData', 'playerProps', 'teamBreakdown', 'total'];
  const auditedEvents = results.filter((result) => result.ok);
  const responseTimes = results.map((result) => result.responseMs);
  const statusBreakdown = Object.fromEntries(modules.map((module) => [module, {} as Record<string, number>])) as Record<CoverageModuleName, Record<string, number>>;
  const readinessBreakdown = Object.fromEntries(modules.map((module) => [module, {
    full: 0,
    partial: 0,
    empty: 0,
    locked: 0,
    failed: 0,
    unknown: 0,
  }])) as Record<CoverageModuleName, Record<CoverageReadiness, number>>;
  const confidenceBreakdown = Object.fromEntries(modules.map((module) => [module, {
    high: 0,
    medium: 0,
    low: 0,
    unknown: 0,
  }])) as Record<CoverageModuleName, Record<CoverageConfidence, number>>;
  const playerPropCountTotals = {
    sourceCount: 0,
    normalizedCount: 0,
    publishedCount: 0,
    fallbackCount: 0,
    exposedCount: 0,
  };
  let publishGapEvents = 0;
  const fallbackUsageBreakdown = {
    news: {
      fallbackEvents: 0,
      sourceCounts: {} as Record<string, number>,
    },
    market: {
      fallbackEvents: 0,
      mixedEvents: 0,
      lineMovementSourceCounts: {} as Record<string, number>,
      bookSnapshotSourceCounts: {} as Record<string, number>,
    },
    summarySupport: {
      fallbackEvents: 0,
      sourceCounts: {} as Record<string, number>,
    },
  };
  const availableCounts = Object.fromEntries(modules.map((module) => [module, 0])) as Record<CoverageModuleName, number>;
  const cacheStats = {
    hits: 0,
    misses: 0,
    coalesced: 0,
  };
  const timingBuckets = Object.fromEntries(timingSteps.map((step) => [step, [] as number[]])) as Record<BundleTimingStepName, number[]>;
  const timingBudgets = {} as Partial<Record<Exclude<BundleTimingStepName, 'total'>, number>>;
  const overBudgetCounts = Object.fromEntries(timingSteps.map((step) => [step, 0])) as Record<BundleTimingStepName, number>;

  for (const result of auditedEvents) {
    const diagnostics = result.diagnostics;
    if (diagnostics?.request.cacheStatus === 'hit') cacheStats.hits += 1;
    else cacheStats.misses += 1;
    if (diagnostics?.request.coalesced) cacheStats.coalesced += 1;
    if (diagnostics?.fallbackUsage?.news) {
      const source = diagnostics.fallbackUsage.news.source || 'unknown';
      fallbackUsageBreakdown.news.sourceCounts[source] = (fallbackUsageBreakdown.news.sourceCounts[source] || 0) + 1;
      if (diagnostics.fallbackUsage.news.fallbackUsed) fallbackUsageBreakdown.news.fallbackEvents += 1;
    }
    if (diagnostics?.fallbackUsage?.market) {
      const lineMovementSource = diagnostics.fallbackUsage.market.lineMovementSource || 'unknown';
      const bookSnapshotSource = diagnostics.fallbackUsage.market.bookSnapshotSource || 'unknown';
      fallbackUsageBreakdown.market.lineMovementSourceCounts[lineMovementSource] = (fallbackUsageBreakdown.market.lineMovementSourceCounts[lineMovementSource] || 0) + 1;
      fallbackUsageBreakdown.market.bookSnapshotSourceCounts[bookSnapshotSource] = (fallbackUsageBreakdown.market.bookSnapshotSourceCounts[bookSnapshotSource] || 0) + 1;
      if (diagnostics.fallbackUsage.market.fallbackUsed) fallbackUsageBreakdown.market.fallbackEvents += 1;
      if (lineMovementSource !== bookSnapshotSource) fallbackUsageBreakdown.market.mixedEvents += 1;
    }
    if (diagnostics?.fallbackUsage?.summarySupport) {
      const source = diagnostics.fallbackUsage.summarySupport.source || 'unknown';
      fallbackUsageBreakdown.summarySupport.sourceCounts[source] = (fallbackUsageBreakdown.summarySupport.sourceCounts[source] || 0) + 1;
      if (diagnostics.fallbackUsage.summarySupport.fallbackUsed) fallbackUsageBreakdown.summarySupport.fallbackEvents += 1;
    }
    const playerPropSignals = diagnostics?.moduleReadiness?.playerProps?.signals || {};
    const sourceCount = Number(playerPropSignals.sourceCount || 0);
    const normalizedCount = Number(playerPropSignals.normalizedCount || 0);
    const publishedCount = Number(playerPropSignals.publishedCount || 0);
    const fallbackCount = Number(playerPropSignals.fallbackCount || 0);
    const exposedCount = Number(playerPropSignals.exposedCount || 0);
    playerPropCountTotals.sourceCount += sourceCount;
    playerPropCountTotals.normalizedCount += normalizedCount;
    playerPropCountTotals.publishedCount += publishedCount;
    playerPropCountTotals.fallbackCount += fallbackCount;
    playerPropCountTotals.exposedCount += exposedCount;
    if (sourceCount > 0 && publishedCount === 0 && exposedCount > 0 && result.coverage?.playerProps?.available) {
      publishGapEvents += 1;
    }

    for (const step of timingSteps) {
      const stepMs = diagnostics?.timingsMs?.[step];
      if (stepMs == null) continue;
      timingBuckets[step].push(stepMs);
      if (step !== 'total') {
        const budgetMs = diagnostics?.timeBudgetsMs?.[step];
        if (budgetMs != null) {
          timingBudgets[step] = budgetMs;
          if (stepMs > budgetMs) overBudgetCounts[step] += 1;
        }
      }
    }

    for (const module of modules) {
      const summary = result.coverage?.[module];
      const status = summary?.status || 'unknown';
      statusBreakdown[module][status] = (statusBreakdown[module][status] || 0) + 1;
      if (summary?.available) availableCounts[module] += 1;
      const readiness = summary?.readiness || result.diagnostics?.moduleReadiness?.[module]?.readiness || 'unknown';
      const confidence = summary?.coverageConfidence || result.diagnostics?.moduleReadiness?.[module]?.coverageConfidence || 'unknown';
      readinessBreakdown[module][readiness] += 1;
      confidenceBreakdown[module][confidence] += 1;
    }
  }

  const timingSummary = Object.fromEntries(
    timingSteps
      .map((step) => {
        const timings = timingBuckets[step];
        if (timings.length === 0) return null;
        return [
          step,
          {
            avgMs: round(timings.reduce((sum, value) => sum + value, 0) / timings.length),
            maxMs: Math.max(...timings),
            budgetMs: step === 'total' ? null : (timingBudgets[step] ?? null),
            overBudgetEvents: overBudgetCounts[step],
            presentEvents: timings.length,
          } satisfies BundleTimingStepSummary,
        ];
      })
      .filter((entry): entry is [BundleTimingStepName, BundleTimingStepSummary] => Boolean(entry)),
  ) as Partial<Record<BundleTimingStepName, BundleTimingStepSummary>>;

  const slowestSteps = Object.entries(timingSummary)
    .map(([step, summary]) => ({
      step: step as BundleTimingStepName,
      avgMs: summary!.avgMs,
      maxMs: summary!.maxMs,
      budgetMs: summary!.budgetMs,
      overBudgetEvents: summary!.overBudgetEvents,
    }))
    .filter((entry) => entry.step !== 'total')
    .sort((left, right) => right.avgMs - left.avgMs)
    .slice(0, 4);

  const alerts: string[] = [];
  if (publishGapEvents > 0) {
    const gapRate = auditedEvents.length > 0 ? round((publishGapEvents / auditedEvents.length) * 100) : 0;
    alerts.push(
      `Publish gap: ${publishGapEvents}/${auditedEvents.length} events (${gapRate}%) had source-backed player props but zero published props.`,
    );
  }
  const lockedModuleCounts = modules
    .map((module) => ({
      module,
      count: statusBreakdown[module].locked || 0,
    }))
    .filter((entry) => entry.count > 0);
  if (lockedModuleCounts.length > 0) {
    const totalLockedEvents = lockedModuleCounts.reduce((sum, entry) => sum + entry.count, 0);
    alerts.push(
      `Public locked mismatch: ${totalLockedEvents} locked module responses appeared in the public bundle (${lockedModuleCounts.map((entry) => `${entry.module} ${entry.count}`).join(', ')}).`,
    );
  }
  const newsReadyEvents = statusBreakdown.news.ready || 0;
  const newsEmptyEvents = statusBreakdown.news.empty || 0;
  const newsFailedEvents = statusBreakdown.news.failed || 0;
  const newsLockedEvents = statusBreakdown.news.locked || 0;
  const newsNotReadyEvents = newsEmptyEvents + newsFailedEvents + newsLockedEvents;
  if (newsNotReadyEvents > 0) {
    const newsDegradeRate = auditedEvents.length > 0 ? round((newsNotReadyEvents / auditedEvents.length) * 100) : 0;
    alerts.push(
      `News degradation: ${newsNotReadyEvents}/${auditedEvents.length} events (${newsDegradeRate}%) were not ready for news (ready ${newsReadyEvents}, empty ${newsEmptyEvents}, failed ${newsFailedEvents}, locked ${newsLockedEvents}).`,
    );
  }

  return {
    league,
    totalEvents: results.length,
    auditedEvents: auditedEvents.length,
    failedEvents: results.length - auditedEvents.length,
    avgResponseMs: responseTimes.length > 0 ? round(responseTimes.reduce((sum, value) => sum + value, 0) / responseTimes.length) : 0,
    maxResponseMs: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
    coverageRates: Object.fromEntries(modules.map((module) => [
      module,
      results.length > 0 ? round((availableCounts[module] / results.length) * 100) : 0,
    ])) as Record<CoverageModuleName, number>,
    statusBreakdown,
    readinessBreakdown,
    confidenceBreakdown,
    playerPropCountSummary: {
      avgSourceCount: auditedEvents.length > 0 ? round(playerPropCountTotals.sourceCount / auditedEvents.length) : 0,
      avgNormalizedCount: auditedEvents.length > 0 ? round(playerPropCountTotals.normalizedCount / auditedEvents.length) : 0,
      avgPublishedCount: auditedEvents.length > 0 ? round(playerPropCountTotals.publishedCount / auditedEvents.length) : 0,
      avgFallbackCount: auditedEvents.length > 0 ? round(playerPropCountTotals.fallbackCount / auditedEvents.length) : 0,
      avgExposedCount: auditedEvents.length > 0 ? round(playerPropCountTotals.exposedCount / auditedEvents.length) : 0,
    },
    fallbackUsageBreakdown,
    cacheStats,
    timingSummary,
    slowestSteps,
    alerts,
    sampleFailures: results
      .filter((result) => !result.ok && result.error)
      .slice(0, 5)
      .map((result) => ({
        eventId: result.eventId,
        matchup: result.matchup,
        error: result.error || 'Unknown error',
      })),
  };
}

function renderMarkdown(report: BundleAuditReport): string {
  const modules: CoverageModuleName[] = ['preview', 'bestPlays', 'playerProps', 'playerData', 'injuries', 'news', 'market', 'teamBreakdown'];
  const lines: string[] = [];
  lines.push('# Digilander Bundle Audit');
  lines.push('');
  lines.push(`Audited at: ${report.auditedAt}`);
  lines.push(`API base: ${report.apiBase}`);
  lines.push(`Leagues: ${report.leagues.join(', ')}`);
  lines.push('');
  lines.push('## Summary');
  lines.push('');
  lines.push(`- Total events: ${report.summary.totalEvents}`);
  lines.push(`- Audited events: ${report.summary.auditedEvents}`);
  lines.push(`- Failed events: ${report.summary.failedEvents}`);
  lines.push(`- Avg response ms: ${report.summary.avgResponseMs}`);
  lines.push(`- Max response ms: ${report.summary.maxResponseMs}`);
  lines.push('');

  const overallSlowSteps = report.leagueSummaries
    .flatMap((league) => league.slowestSteps.map((step) => ({ league: league.league, ...step })))
    .sort((left, right) => right.avgMs - left.avgMs)
    .slice(0, 8);
  if (overallSlowSteps.length > 0) {
    lines.push('## Slowest Bundle Steps');
    lines.push('');
    for (const step of overallSlowSteps) {
      lines.push(`- ${step.league.toUpperCase()} ${step.step}: avg ${step.avgMs} ms, max ${step.maxMs} ms${step.budgetMs != null ? `, budget ${step.budgetMs} ms, over budget ${step.overBudgetEvents}` : ''}`);
    }
    lines.push('');
  }

  for (const league of report.leagueSummaries) {
    lines.push(`## ${league.league.toUpperCase()}`);
    lines.push('');
    lines.push(`- Events: ${league.totalEvents}`);
    lines.push(`- Failed: ${league.failedEvents}`);
    lines.push(`- Avg response ms: ${league.avgResponseMs}`);
    lines.push(`- Max response ms: ${league.maxResponseMs}`);
    lines.push(`- Cache: hits ${league.cacheStats.hits} | misses ${league.cacheStats.misses} | coalesced ${league.cacheStats.coalesced}`);
    lines.push(`- Coverage: best plays ${league.coverageRates.bestPlays}% | props ${league.coverageRates.playerProps}% | player data ${league.coverageRates.playerData}% | injuries ${league.coverageRates.injuries}% | news ${league.coverageRates.news}% | market ${league.coverageRates.market}%`);
    lines.push(`- Player-prop counts: avg source ${league.playerPropCountSummary.avgSourceCount} | normalized ${league.playerPropCountSummary.avgNormalizedCount} | published ${league.playerPropCountSummary.avgPublishedCount} | fallback ${league.playerPropCountSummary.avgFallbackCount} | exposed ${league.playerPropCountSummary.avgExposedCount}`);
    if (league.alerts.length > 0) {
      lines.push('- Alerts:');
      for (const alert of league.alerts) {
        lines.push(`  - ${alert}`);
      }
    }
    lines.push('- Readiness matrix:');
    for (const module of modules) {
      const readiness = league.readinessBreakdown[module];
      const confidence = league.confidenceBreakdown[module];
      lines.push(`  - ${module}: full ${readiness.full} | partial ${readiness.partial} | empty ${readiness.empty} | locked ${readiness.locked} | failed ${readiness.failed} | confidence high ${confidence.high} / medium ${confidence.medium} / low ${confidence.low}`);
    }
    lines.push('- Fallback matrix:');
    lines.push(`  - news: fallback ${league.fallbackUsageBreakdown.news.fallbackEvents} | sources ${Object.entries(league.fallbackUsageBreakdown.news.sourceCounts).map(([source, count]) => `${source} ${count}`).join(' | ') || 'none'}`);
    lines.push(`  - market: fallback ${league.fallbackUsageBreakdown.market.fallbackEvents} | mixed ${league.fallbackUsageBreakdown.market.mixedEvents} | line ${Object.entries(league.fallbackUsageBreakdown.market.lineMovementSourceCounts).map(([source, count]) => `${source} ${count}`).join(' | ') || 'none'} | books ${Object.entries(league.fallbackUsageBreakdown.market.bookSnapshotSourceCounts).map(([source, count]) => `${source} ${count}`).join(' | ') || 'none'}`);
    lines.push(`  - summary support: fallback ${league.fallbackUsageBreakdown.summarySupport.fallbackEvents} | sources ${Object.entries(league.fallbackUsageBreakdown.summarySupport.sourceCounts).map(([source, count]) => `${source} ${count}`).join(' | ') || 'none'}`);
    if (league.slowestSteps.length > 0) {
      lines.push('- Slowest steps:');
      for (const step of league.slowestSteps) {
        lines.push(`  - ${step.step}: avg ${step.avgMs} ms, max ${step.maxMs} ms${step.budgetMs != null ? `, budget ${step.budgetMs} ms, over budget ${step.overBudgetEvents}` : ''}`);
      }
    }
    if (league.sampleFailures.length > 0) {
      lines.push('- Sample failures:');
      for (const failure of league.sampleFailures) {
        lines.push(`  - ${failure.eventId} (${failure.matchup}): ${failure.error}`);
      }
    }
    lines.push('');
  }

  return `${lines.join('\n')}\n`;
}

async function main() {
  const auditedAt = new Date();
  const apiBase = getArgValue('--api-base') || DEFAULT_API_BASE;
  const leagues = (getArgValue('--leagues') || DEFAULT_LEAGUES.join(','))
    .split(',')
    .map((value) => value.trim().toLowerCase())
    .filter(Boolean);
  const concurrency = toNumber(getArgValue('--concurrency'), DEFAULT_CONCURRENCY);
  const timeoutMs = toNumber(getArgValue('--timeout-ms'), DEFAULT_TIMEOUT_MS);
  const outputDir = getArgValue('--output-dir') || path.join(os.tmpdir(), 'rainmaker-prop-audits');
  const maxEventsPerLeague = toNumber(getArgValue('--max-events-per-league'), 0);

  const leagueEventLists = await Promise.all(leagues.map(async (league) => ({
    league,
    events: await fetchLeagueEvents(apiBase, league, timeoutMs),
  })));

  const scopedEvents = leagueEventLists.flatMap(({ league, events }) => {
    const selectedEvents = maxEventsPerLeague > 0 ? events.slice(0, maxEventsPerLeague) : events;
    return selectedEvents.map((event) => ({ league, event }));
  });

  const eventResults = await runWithConcurrency(scopedEvents, concurrency, ({ league, event }) => auditEvent(apiBase, league, event, timeoutMs));
  const leagueSummaries = leagues.map((league) => buildLeagueSummary(
    league,
    eventResults.filter((result) => result.league === league),
  ));
  const responseTimes = eventResults.map((result) => result.responseMs);
  const report: BundleAuditReport = {
    auditedAt: auditedAt.toISOString(),
    apiBase,
    leagues,
    summary: {
      totalEvents: eventResults.length,
      auditedEvents: eventResults.filter((result) => result.ok).length,
      failedEvents: eventResults.filter((result) => !result.ok).length,
      avgResponseMs: responseTimes.length > 0 ? round(responseTimes.reduce((sum, value) => sum + value, 0) / responseTimes.length) : 0,
      maxResponseMs: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
    },
    leagueSummaries,
    eventResults,
  };

  fs.mkdirSync(outputDir, { recursive: true });
  const slug = buildTimestampSlug(auditedAt);
  const jsonPath = path.join(outputDir, `digilander-bundle-audit-${slug}.json`);
  const markdownPath = path.join(outputDir, `digilander-bundle-audit-${slug}.md`);
  fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
  fs.writeFileSync(markdownPath, renderMarkdown(report));

  console.log(JSON.stringify({
    auditedAt: report.auditedAt,
    summary: report.summary,
    leagueSummaries: report.leagueSummaries,
    jsonPath,
    markdownPath,
  }, null, 2));
}

if (require.main === module) {
  main().catch((err) => {
    console.error('[digilander-bundle-audit] Fatal:', err);
    process.exit(1);
  });
}
