export interface PlayerPropSlateAuditAssetInput {
  eventId: string;
  league: string;
  forecastType: string;
  status: string | null;
  playerName: string | null;
  confidenceScore: number | null;
  payload: Record<string, any> | null;
}

export interface PlayerPropSlateAuditEventInput {
  eventId: string;
  league: string;
  matchup: string;
  startsAt: string | null;
  hasForecast: boolean;
  propHighlightsCount: number;
  sourceBackedPlayerPropsCount?: number | null;
  sourceBackedSource?: 'local' | 'direct' | 'candidates' | null;
  liveFallbackPlayerPropsCount?: number | null;
  liveFallbackSource?: 'local' | 'direct' | 'candidates' | null;
}

export interface PlayerPropDirectionAuditEntry {
  league: string;
  total: number;
  overs: number;
  unders: number;
  unknown: number;
  underPct: number;
}

export interface PlayerPropConfidenceGapAuditEntry {
  league: string;
  totalProps: number;
  missingConfidenceScore: number;
  missingConfidenceScorePct: number;
  missingProbability: number;
  missingProbabilityPct: number;
}

export interface PlayerPropPayloadGapAuditEntry {
  league: string;
  totalProps: number;
  missingSignalTier: number;
  missingSignalTierPct: number;
  missingForecastDirection: number;
  missingForecastDirectionPct: number;
  missingAgreementScore: number;
  missingAgreementScorePct: number;
  missingMarketQualityLabel: number;
  missingMarketQualityLabelPct: number;
  missingForecastDisplay: number;
  missingForecastDisplayPct: number;
  missingPropType: number;
  missingPropTypePct: number;
  missingSportsbookDisplay: number;
  missingSportsbookDisplayPct: number;
}

export interface TeamPropBundleGapAuditEntry {
  league: string;
  totalProps: number;
  missingSignalTier: number;
  missingSignalTierPct: number;
  missingForecastDirection: number;
  missingForecastDirectionPct: number;
  missingAgreementScore: number;
  missingAgreementScorePct: number;
  missingMarketQualityLabel: number;
  missingMarketQualityLabelPct: number;
}

export interface PlayerPropMarketConcentrationEntry {
  league: string;
  totalProps: number;
  topPropLabel: string | null;
  topPropCount: number;
  topPropSharePct: number;
  topPropLabels: Array<{ label: string; count: number; sharePct: number }>;
}

export interface PlayerPropProjectionClusterEntry {
  league: string;
  propKey: string;
  projectedValue: number;
  count: number;
  samplePlayers: string[];
  sampleEventIds: string[];
}

export interface PlayerPropStaleGapEntry {
  league: string;
  eventId: string;
  matchup: string;
  startsAt: string | null;
  activePlayerProps: number;
  stalePlayerProps: number;
  activeTeamProps: number;
  activeGameMarkets: number;
  sourceBackedPlayerProps: number;
  sourceBackedSource: 'local' | 'direct' | 'candidates' | null;
  liveFallbackPlayerProps: number;
  liveFallbackSource: 'local' | 'direct' | 'candidates' | null;
}

export interface PlayerPropHighlightsCoverageEntry {
  league: string;
  eventsWithForecasts: number;
  eventsWithPropHighlights: number;
  emptyPropHighlights: number;
  coveragePct: number;
}

export interface PlayerPropSourceCoverageEntry {
  league: string;
  totalEvents: number;
  eventsWithPublishedPlayerProps: number;
  eventsWithSourceBackedCoverage: number;
  eventsUsingSourceBackedRecovery: number;
  eventsWithNoPlayerPropCoverage: number;
  publishedPlayerProps: number;
  sourceBackedPlayerProps: number;
  publishedCoveragePct: number;
  sourceBackedCoveragePct: number;
  recoveryPct: number;
}

export interface PlayerPropSlateAuditReport {
  auditedAt: string;
  dateEt: string;
  summary: {
    totalEvents: number;
    eventsWithForecasts: number;
    activePlayerProps: number;
    sourceBackedPlayerProps: number;
    eventsWithSourceBackedCoverage: number;
    sourceBackedRecoverableGapCount: number;
    sourceBackedUnrecoverableGapCount: number;
    liveFallbackPlayerProps: number;
    stalePlayerProps: number;
    activeTeamProps: number;
    staleTeamProps: number;
    activeGameMarketAssets: number;
    projectionClusterCount: number;
    staleGapCount: number;
    recoverableGapCount: number;
    unrecoverableGapCount: number;
    eventsWithLiveFallbackCoverage: number;
  };
  directionsByLeague: PlayerPropDirectionAuditEntry[];
  confidenceGapsByLeague: PlayerPropConfidenceGapAuditEntry[];
  payloadGapsByLeague: PlayerPropPayloadGapAuditEntry[];
  teamBundleGapsByLeague: TeamPropBundleGapAuditEntry[];
  marketConcentrationByLeague: PlayerPropMarketConcentrationEntry[];
  projectionClusters: PlayerPropProjectionClusterEntry[];
  staleEventGaps: PlayerPropStaleGapEntry[];
  propHighlightsCoverage: PlayerPropHighlightsCoverageEntry[];
  sourceCoverageByLeague: PlayerPropSourceCoverageEntry[];
  passed: boolean;
}

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

function normalizeLeague(value: string | null | undefined): string {
  return String(value || '').trim().toLowerCase();
}

function normalizeStatus(value: string | null | undefined): string {
  return String(value || '').trim().toUpperCase();
}

function toFiniteNumber(value: any): number | null {
  if (value === null || value === undefined || value === '') return null;
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : null;
}

function normalizeDirection(value: any): 'OVER' | 'UNDER' | null {
  const normalized = String(value || '').trim().toUpperCase();
  if (normalized === 'OVER' || normalized === 'UNDER') return normalized;
  return null;
}

function getStatLabel(payload: Record<string, any> | null | undefined): string | null {
  const normalized = String(payload?.normalized_stat_type || payload?.stat_type || '').trim();
  if (normalized) return normalized.toLowerCase();

  const prop = String(payload?.prop || '').trim();
  if (!prop) return null;
  const match = prop.match(/^(.+?)(?:\s+(?:Over|Under))?\s+(-?\d+(?:\.\d+)?)$/i);
  const label = (match?.[1] || prop).trim().toLowerCase();
  return label || null;
}

function getMarketLine(payload: Record<string, any> | null | undefined): string | null {
  const direct = toFiniteNumber(payload?.market_line_value ?? payload?.line);
  if (direct != null) return String(direct);

  const prop = String(payload?.prop || '').trim();
  const match = prop.match(/(-?\d+(?:\.\d+)?)$/);
  return match?.[1] || null;
}

function getProbability(payload: Record<string, any> | null | undefined): number | null {
  return toFiniteNumber(payload?.prob ?? payload?.projected_probability);
}

function isBlankString(value: any): boolean {
  return String(value ?? '').trim() === '';
}

function getProjectedValue(payload: Record<string, any> | null | undefined): number | null {
  const projected = toFiniteNumber(payload?.projected_stat_value ?? payload?.projectedOutcome);
  return projected == null ? null : Number(projected.toFixed(3));
}

function getSourceBackedCount(event: PlayerPropSlateAuditEventInput): number {
  return Number(event.sourceBackedPlayerPropsCount ?? event.liveFallbackPlayerPropsCount ?? 0);
}

function getSourceBackedSource(event: PlayerPropSlateAuditEventInput): 'local' | 'direct' | 'candidates' | null {
  return event.sourceBackedSource ?? event.liveFallbackSource ?? null;
}

function buildProjectionClusterKey(payload: Record<string, any> | null | undefined): string | null {
  const statLabel = getStatLabel(payload);
  const marketLine = getMarketLine(payload);
  if (!statLabel || !marketLine) return null;
  return `${statLabel} @ ${marketLine}`;
}

function buildMarketConcentration(activePlayerProps: PlayerPropSlateAuditAssetInput[]): PlayerPropMarketConcentrationEntry[] {
  const byLeague = new Map<string, Map<string, number>>();

  for (const asset of activePlayerProps) {
    const league = normalizeLeague(asset.league);
    if (!league) continue;
    const label = getStatLabel(asset.payload) || 'unknown';
    const leagueCounts = byLeague.get(league) || new Map<string, number>();
    leagueCounts.set(label, (leagueCounts.get(label) || 0) + 1);
    byLeague.set(league, leagueCounts);
  }

  return [...byLeague.entries()]
    .map(([league, counts]) => {
      const topPropLabels = [...counts.entries()]
        .sort((a, b) => {
          const countDelta = b[1] - a[1];
          if (countDelta !== 0) return countDelta;
          return a[0].localeCompare(b[0]);
        })
        .map(([label, count]) => ({ label, count }));
      const totalProps = topPropLabels.reduce((sum, entry) => sum + entry.count, 0);
      const topProp = topPropLabels[0] || null;
      return {
        league,
        totalProps,
        topPropLabel: topProp?.label || null,
        topPropCount: topProp?.count || 0,
        topPropSharePct: totalProps > 0 && topProp ? roundToSingleDecimal((topProp.count / totalProps) * 100) : 0,
        topPropLabels: topPropLabels.slice(0, 5).map((entry) => ({
          ...entry,
          sharePct: totalProps > 0 ? roundToSingleDecimal((entry.count / totalProps) * 100) : 0,
        })),
      };
    })
    .sort((a, b) => a.league.localeCompare(b.league));
}

function buildProjectionClusters(activePlayerProps: PlayerPropSlateAuditAssetInput[]): PlayerPropProjectionClusterEntry[] {
  const clusters = new Map<string, {
    league: string;
    propKey: string;
    projectedValue: number;
    count: number;
    samplePlayers: Set<string>;
    sampleEventIds: Set<string>;
  }>();

  for (const asset of activePlayerProps) {
    const league = normalizeLeague(asset.league);
    const propKey = buildProjectionClusterKey(asset.payload);
    const projectedValue = getProjectedValue(asset.payload);
    if (!league || !propKey || projectedValue == null) continue;

    const key = `${league}|${propKey}|${projectedValue}`;
    const entry = clusters.get(key) || {
      league,
      propKey,
      projectedValue,
      count: 0,
      samplePlayers: new Set<string>(),
      sampleEventIds: new Set<string>(),
    };
    entry.count += 1;
    if (asset.playerName) entry.samplePlayers.add(asset.playerName);
    if (asset.eventId) entry.sampleEventIds.add(asset.eventId);
    clusters.set(key, entry);
  }

  return [...clusters.values()]
    .filter((entry) => entry.count >= 4 && entry.samplePlayers.size >= 3)
    .map((entry) => ({
      league: entry.league,
      propKey: entry.propKey,
      projectedValue: entry.projectedValue,
      count: entry.count,
      samplePlayers: [...entry.samplePlayers].sort((a, b) => a.localeCompare(b)).slice(0, 8),
      sampleEventIds: [...entry.sampleEventIds].sort((a, b) => a.localeCompare(b)).slice(0, 5),
    }))
    .sort((a, b) => {
      const countDelta = b.count - a.count;
      if (countDelta !== 0) return countDelta;
      return a.league.localeCompare(b.league);
    });
}

export function buildPlayerPropSlateAuditReport(params: {
  auditedAt: string;
  dateEt: string;
  events: PlayerPropSlateAuditEventInput[];
  assets: PlayerPropSlateAuditAssetInput[];
}): PlayerPropSlateAuditReport {
  const activePlayerProps = params.assets.filter((asset) => (
    normalizeStatus(asset.status) === 'ACTIVE'
    && String(asset.forecastType || '').toUpperCase() === 'PLAYER_PROP'
  ));
  const stalePlayerProps = params.assets.filter((asset) => (
    normalizeStatus(asset.status) === 'STALE'
    && String(asset.forecastType || '').toUpperCase() === 'PLAYER_PROP'
  ));
  const activeTeamProps = params.assets.filter((asset) => (
    normalizeStatus(asset.status) === 'ACTIVE'
    && String(asset.forecastType || '').toUpperCase() === 'TEAM_PROPS'
  ));
  const staleTeamProps = params.assets.filter((asset) => (
    normalizeStatus(asset.status) === 'STALE'
    && String(asset.forecastType || '').toUpperCase() === 'TEAM_PROPS'
  ));
  const activeGameMarketAssets = params.assets.filter((asset) => (
    normalizeStatus(asset.status) === 'ACTIVE'
    && !['PLAYER_PROP', 'TEAM_PROPS'].includes(String(asset.forecastType || '').toUpperCase())
  ));

  const directionRollups = new Map<string, PlayerPropDirectionAuditEntry>();
  const confidenceRollups = new Map<string, PlayerPropConfidenceGapAuditEntry>();
  const payloadGapRollups = new Map<string, PlayerPropPayloadGapAuditEntry>();
  const teamBundleGapRollups = new Map<string, TeamPropBundleGapAuditEntry>();
  const eventAssetCounts = new Map<string, {
    activePlayerProps: number;
    stalePlayerProps: number;
    activeTeamProps: number;
    activeGameMarkets: number;
  }>();

  for (const asset of params.assets) {
    const eventCounts = eventAssetCounts.get(asset.eventId) || {
      activePlayerProps: 0,
      stalePlayerProps: 0,
      activeTeamProps: 0,
      activeGameMarkets: 0,
    };
    const type = String(asset.forecastType || '').toUpperCase();
    const status = normalizeStatus(asset.status);

    if (type === 'PLAYER_PROP' && status === 'ACTIVE') eventCounts.activePlayerProps += 1;
    if (type === 'PLAYER_PROP' && status === 'STALE') eventCounts.stalePlayerProps += 1;
    if (type === 'TEAM_PROPS' && status === 'ACTIVE') eventCounts.activeTeamProps += 1;
    if (!['PLAYER_PROP', 'TEAM_PROPS'].includes(type) && status === 'ACTIVE') eventCounts.activeGameMarkets += 1;
    eventAssetCounts.set(asset.eventId, eventCounts);

    if (type === 'TEAM_PROPS' && status === 'ACTIVE') {
      const league = normalizeLeague(asset.league);
      if (!league) continue;
      const props = Array.isArray(asset.payload?.props) ? asset.payload!.props : [];
      const teamBundleEntry = teamBundleGapRollups.get(league) || {
        league,
        totalProps: 0,
        missingSignalTier: 0,
        missingSignalTierPct: 0,
        missingForecastDirection: 0,
        missingForecastDirectionPct: 0,
        missingAgreementScore: 0,
        missingAgreementScorePct: 0,
        missingMarketQualityLabel: 0,
        missingMarketQualityLabelPct: 0,
      };
      for (const prop of props) {
        teamBundleEntry.totalProps += 1;
        if (isBlankString(prop?.signal_tier)) teamBundleEntry.missingSignalTier += 1;
        if (!normalizeDirection(prop?.forecast_direction)) teamBundleEntry.missingForecastDirection += 1;
        if (toFiniteNumber(prop?.agreement_score) == null) teamBundleEntry.missingAgreementScore += 1;
        if (isBlankString(prop?.market_quality_label)) teamBundleEntry.missingMarketQualityLabel += 1;
      }
      teamBundleGapRollups.set(league, teamBundleEntry);
    }

    if (type !== 'PLAYER_PROP' || status !== 'ACTIVE') continue;

    const league = normalizeLeague(asset.league);
    if (!league) continue;

    const directionEntry = directionRollups.get(league) || {
      league,
      total: 0,
      overs: 0,
      unders: 0,
      unknown: 0,
      underPct: 0,
    };
    const direction = normalizeDirection(asset.payload?.recommendation);
    directionEntry.total += 1;
    if (direction === 'OVER') directionEntry.overs += 1;
    else if (direction === 'UNDER') directionEntry.unders += 1;
    else directionEntry.unknown += 1;
    directionRollups.set(league, directionEntry);

    const confidenceEntry = confidenceRollups.get(league) || {
      league,
      totalProps: 0,
      missingConfidenceScore: 0,
      missingConfidenceScorePct: 0,
      missingProbability: 0,
      missingProbabilityPct: 0,
    };
    confidenceEntry.totalProps += 1;
    if (toFiniteNumber(asset.confidenceScore) == null) confidenceEntry.missingConfidenceScore += 1;
    if (getProbability(asset.payload) == null) confidenceEntry.missingProbability += 1;
    confidenceRollups.set(league, confidenceEntry);

    const payloadGapEntry = payloadGapRollups.get(league) || {
      league,
      totalProps: 0,
      missingSignalTier: 0,
      missingSignalTierPct: 0,
      missingForecastDirection: 0,
      missingForecastDirectionPct: 0,
      missingAgreementScore: 0,
      missingAgreementScorePct: 0,
      missingMarketQualityLabel: 0,
      missingMarketQualityLabelPct: 0,
      missingForecastDisplay: 0,
      missingForecastDisplayPct: 0,
      missingPropType: 0,
      missingPropTypePct: 0,
      missingSportsbookDisplay: 0,
      missingSportsbookDisplayPct: 0,
    };
    payloadGapEntry.totalProps += 1;
    if (isBlankString(asset.payload?.signal_tier)) payloadGapEntry.missingSignalTier += 1;
    if (!normalizeDirection(asset.payload?.forecast_direction)) payloadGapEntry.missingForecastDirection += 1;
    if (toFiniteNumber(asset.payload?.agreement_score) == null) payloadGapEntry.missingAgreementScore += 1;
    if (isBlankString(asset.payload?.market_quality_label)) payloadGapEntry.missingMarketQualityLabel += 1;
    if (isBlankString(asset.payload?.forecast)) payloadGapEntry.missingForecastDisplay += 1;
    if (isBlankString(asset.payload?.prop_type)) payloadGapEntry.missingPropType += 1;
    if (isBlankString(asset.payload?.sportsbook_display)) payloadGapEntry.missingSportsbookDisplay += 1;
    payloadGapRollups.set(league, payloadGapEntry);
  }

  const directionsByLeague = [...directionRollups.values()]
    .map((entry) => ({
      ...entry,
      underPct: entry.total > 0 ? roundToSingleDecimal((entry.unders / entry.total) * 100) : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const confidenceGapsByLeague = [...confidenceRollups.values()]
    .map((entry) => ({
      ...entry,
      missingConfidenceScorePct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingConfidenceScore / entry.totalProps) * 100) : 0,
      missingProbabilityPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingProbability / entry.totalProps) * 100) : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const payloadGapsByLeague = [...payloadGapRollups.values()]
    .map((entry) => ({
      ...entry,
      missingSignalTierPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingSignalTier / entry.totalProps) * 100) : 0,
      missingForecastDirectionPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingForecastDirection / entry.totalProps) * 100) : 0,
      missingAgreementScorePct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingAgreementScore / entry.totalProps) * 100) : 0,
      missingMarketQualityLabelPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingMarketQualityLabel / entry.totalProps) * 100) : 0,
      missingForecastDisplayPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingForecastDisplay / entry.totalProps) * 100) : 0,
      missingPropTypePct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingPropType / entry.totalProps) * 100) : 0,
      missingSportsbookDisplayPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingSportsbookDisplay / entry.totalProps) * 100) : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const teamBundleGapsByLeague = [...teamBundleGapRollups.values()]
    .map((entry) => ({
      ...entry,
      missingSignalTierPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingSignalTier / entry.totalProps) * 100) : 0,
      missingForecastDirectionPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingForecastDirection / entry.totalProps) * 100) : 0,
      missingAgreementScorePct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingAgreementScore / entry.totalProps) * 100) : 0,
      missingMarketQualityLabelPct: entry.totalProps > 0 ? roundToSingleDecimal((entry.missingMarketQualityLabel / entry.totalProps) * 100) : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const marketConcentrationByLeague = buildMarketConcentration(activePlayerProps);
  const projectionClusters = buildProjectionClusters(activePlayerProps);

  const staleEventGaps = params.events
    .map((event) => {
      const counts = eventAssetCounts.get(event.eventId) || {
        activePlayerProps: 0,
        stalePlayerProps: 0,
        activeTeamProps: 0,
        activeGameMarkets: 0,
      };
      const sourceBackedPlayerProps = getSourceBackedCount(event);
      const sourceBackedSource = getSourceBackedSource(event);
      return {
        league: normalizeLeague(event.league),
        eventId: event.eventId,
        matchup: event.matchup,
        startsAt: event.startsAt,
        sourceBackedPlayerProps,
        sourceBackedSource,
        liveFallbackPlayerProps: sourceBackedPlayerProps,
        liveFallbackSource: sourceBackedSource,
        ...counts,
      };
    })
    .filter((event) => (
      event.activePlayerProps === 0
      && event.sourceBackedPlayerProps === 0
      && (event.stalePlayerProps > 0 || event.activeTeamProps > 0 || event.activeGameMarkets > 0)
    ))
    .sort((a, b) => {
      const staleDelta = b.stalePlayerProps - a.stalePlayerProps;
      if (staleDelta !== 0) return staleDelta;
      return a.eventId.localeCompare(b.eventId);
    });

  const coverageRollups = new Map<string, PlayerPropHighlightsCoverageEntry>();
  for (const event of params.events) {
    if (!event.hasForecast) continue;
    const league = normalizeLeague(event.league);
    const coverage = coverageRollups.get(league) || {
      league,
      eventsWithForecasts: 0,
      eventsWithPropHighlights: 0,
      emptyPropHighlights: 0,
      coveragePct: 0,
    };
    coverage.eventsWithForecasts += 1;
    if (event.propHighlightsCount > 0) coverage.eventsWithPropHighlights += 1;
    else coverage.emptyPropHighlights += 1;
    coverageRollups.set(league, coverage);
  }

  const propHighlightsCoverage = [...coverageRollups.values()]
    .map((entry) => ({
      ...entry,
      coveragePct: entry.eventsWithForecasts > 0
        ? roundToSingleDecimal((entry.eventsWithPropHighlights / entry.eventsWithForecasts) * 100)
        : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const sourceCoverageRollups = new Map<string, PlayerPropSourceCoverageEntry>();
  for (const event of params.events) {
    const league = normalizeLeague(event.league);
    if (!league) continue;
    const counts = eventAssetCounts.get(event.eventId) || {
      activePlayerProps: 0,
      stalePlayerProps: 0,
      activeTeamProps: 0,
      activeGameMarkets: 0,
    };
    const sourceBackedCount = getSourceBackedCount(event);
    const coverage = sourceCoverageRollups.get(league) || {
      league,
      totalEvents: 0,
      eventsWithPublishedPlayerProps: 0,
      eventsWithSourceBackedCoverage: 0,
      eventsUsingSourceBackedRecovery: 0,
      eventsWithNoPlayerPropCoverage: 0,
      publishedPlayerProps: 0,
      sourceBackedPlayerProps: 0,
      publishedCoveragePct: 0,
      sourceBackedCoveragePct: 0,
      recoveryPct: 0,
    };

    coverage.totalEvents += 1;
    coverage.publishedPlayerProps += counts.activePlayerProps;
    coverage.sourceBackedPlayerProps += sourceBackedCount;

    if (counts.activePlayerProps > 0) coverage.eventsWithPublishedPlayerProps += 1;
    if (sourceBackedCount > 0) coverage.eventsWithSourceBackedCoverage += 1;
    if (counts.activePlayerProps === 0 && sourceBackedCount > 0) coverage.eventsUsingSourceBackedRecovery += 1;
    if (counts.activePlayerProps === 0 && sourceBackedCount === 0) coverage.eventsWithNoPlayerPropCoverage += 1;

    sourceCoverageRollups.set(league, coverage);
  }

  const sourceCoverageByLeague = [...sourceCoverageRollups.values()]
    .map((entry) => ({
      ...entry,
      publishedCoveragePct: entry.totalEvents > 0
        ? roundToSingleDecimal((entry.eventsWithPublishedPlayerProps / entry.totalEvents) * 100)
        : 0,
      sourceBackedCoveragePct: entry.totalEvents > 0
        ? roundToSingleDecimal((entry.eventsWithSourceBackedCoverage / entry.totalEvents) * 100)
        : 0,
      recoveryPct: entry.totalEvents > 0
        ? roundToSingleDecimal((entry.eventsUsingSourceBackedRecovery / entry.totalEvents) * 100)
        : 0,
    }))
    .sort((a, b) => a.league.localeCompare(b.league));

  const severeUnderSkew = directionsByLeague.some((entry) => entry.total >= 10 && entry.underPct >= 85);
  const severeConfidenceGap = confidenceGapsByLeague.some((entry) => entry.totalProps >= 10 && entry.missingConfidenceScorePct >= 50);
  const severePayloadGap = payloadGapsByLeague.some((entry) => (
    entry.missingSignalTier > 0
    || entry.missingForecastDirection > 0
    || entry.missingAgreementScore > 0
    || entry.missingMarketQualityLabel > 0
    || entry.missingForecastDisplay > 0
    || entry.missingPropType > 0
    || entry.missingSportsbookDisplay > 0
  ));
  const severeTeamBundleGap = teamBundleGapsByLeague.some((entry) => (
    entry.missingSignalTier > 0
    || entry.missingForecastDirection > 0
    || entry.missingAgreementScore > 0
    || entry.missingMarketQualityLabel > 0
  ));
  const recoverableGapCount = params.events.filter((event) => {
    const counts = eventAssetCounts.get(event.eventId) || {
      activePlayerProps: 0,
      stalePlayerProps: 0,
      activeTeamProps: 0,
      activeGameMarkets: 0,
    };
    return counts.activePlayerProps === 0
      && getSourceBackedCount(event) > 0
      && (counts.stalePlayerProps > 0 || counts.activeTeamProps > 0 || counts.activeGameMarkets > 0 || event.hasForecast);
  }).length;
  const eventsWithSourceBackedCoverage = params.events.filter((event) => getSourceBackedCount(event) > 0).length;
  const totalSourceBackedPlayerProps = params.events.reduce((sum, event) => sum + getSourceBackedCount(event), 0);

  return {
    auditedAt: params.auditedAt,
    dateEt: params.dateEt,
    summary: {
      totalEvents: params.events.length,
      eventsWithForecasts: params.events.filter((event) => event.hasForecast).length,
      activePlayerProps: activePlayerProps.length,
      sourceBackedPlayerProps: totalSourceBackedPlayerProps,
      eventsWithSourceBackedCoverage,
      sourceBackedRecoverableGapCount: recoverableGapCount,
      sourceBackedUnrecoverableGapCount: staleEventGaps.length,
      liveFallbackPlayerProps: totalSourceBackedPlayerProps,
      stalePlayerProps: stalePlayerProps.length,
      activeTeamProps: activeTeamProps.length,
      staleTeamProps: staleTeamProps.length,
      activeGameMarketAssets: activeGameMarketAssets.length,
      projectionClusterCount: projectionClusters.length,
      staleGapCount: staleEventGaps.length,
      recoverableGapCount,
      unrecoverableGapCount: staleEventGaps.length,
      eventsWithLiveFallbackCoverage: eventsWithSourceBackedCoverage,
    },
    directionsByLeague,
    confidenceGapsByLeague,
    payloadGapsByLeague,
    teamBundleGapsByLeague,
    marketConcentrationByLeague,
    projectionClusters,
    staleEventGaps,
    propHighlightsCoverage,
    sourceCoverageByLeague,
    passed: !severeUnderSkew && !severeConfidenceGap && !severePayloadGap && !severeTeamBundleGap && projectionClusters.length === 0 && staleEventGaps.length === 0,
  };
}

export function renderPlayerPropSlateAuditMarkdown(report: PlayerPropSlateAuditReport): string {
  const lines: string[] = [];
  lines.push('# Player Prop Slate Audit');
  lines.push('');
  lines.push(`Audited At: ${report.auditedAt}`);
  lines.push(`Date ET: ${report.dateEt}`);
  lines.push('');
  lines.push('## Summary');
  lines.push('');
  lines.push(`- Events: ${report.summary.totalEvents}`);
  lines.push(`- Events with forecasts: ${report.summary.eventsWithForecasts}`);
  lines.push(`- Active player props: ${report.summary.activePlayerProps}`);
  lines.push(`- Source-backed player props: ${report.summary.sourceBackedPlayerProps}`);
  lines.push(`- Stale player props: ${report.summary.stalePlayerProps}`);
  lines.push(`- Active team props: ${report.summary.activeTeamProps}`);
  lines.push(`- Stale team props: ${report.summary.staleTeamProps}`);
  lines.push(`- Active game market assets: ${report.summary.activeGameMarketAssets}`);
  lines.push(`- Projection clusters: ${report.summary.projectionClusterCount}`);
  lines.push(`- Stale event gaps: ${report.summary.staleGapCount}`);
  lines.push(`- Recoverable gaps via source-backed coverage: ${report.summary.sourceBackedRecoverableGapCount}`);
  lines.push(`- Unrecoverable gaps after source-backed coverage: ${report.summary.sourceBackedUnrecoverableGapCount}`);
  lines.push(`- Events with source-backed coverage: ${report.summary.eventsWithSourceBackedCoverage}`);
  lines.push(`- Passed: ${report.passed ? 'yes' : 'no'}`);
  lines.push('');

  if (report.directionsByLeague.length > 0) {
    lines.push('## Direction Skew');
    lines.push('');
    lines.push('| League | Total | Overs | Unders | Unknown | Under % |');
    lines.push('| --- | --- | --- | --- | --- | --- |');
    for (const entry of report.directionsByLeague) {
      lines.push(`| ${entry.league} | ${entry.total} | ${entry.overs} | ${entry.unders} | ${entry.unknown} | ${entry.underPct}% |`);
    }
    lines.push('');
  }

  if (report.confidenceGapsByLeague.length > 0) {
    lines.push('## Confidence Gaps');
    lines.push('');
    lines.push('| League | Total Props | Missing confidence_score | Missing prob/projected_probability |');
    lines.push('| --- | --- | --- | --- |');
    for (const entry of report.confidenceGapsByLeague) {
      lines.push(`| ${entry.league} | ${entry.totalProps} | ${entry.missingConfidenceScore} (${entry.missingConfidenceScorePct}%) | ${entry.missingProbability} (${entry.missingProbabilityPct}%) |`);
    }
    lines.push('');
  }

  if (report.payloadGapsByLeague.length > 0) {
    lines.push('## Payload Gaps');
    lines.push('');
    lines.push('| League | Total Props | Missing signal tier | Missing direction | Missing agreement | Missing market quality | Missing forecast display | Missing prop_type | Missing sportsbook_display |');
    lines.push('| --- | --- | --- | --- | --- | --- | --- | --- | --- |');
    for (const entry of report.payloadGapsByLeague) {
      lines.push(`| ${entry.league} | ${entry.totalProps} | ${entry.missingSignalTier} (${entry.missingSignalTierPct}%) | ${entry.missingForecastDirection} (${entry.missingForecastDirectionPct}%) | ${entry.missingAgreementScore} (${entry.missingAgreementScorePct}%) | ${entry.missingMarketQualityLabel} (${entry.missingMarketQualityLabelPct}%) | ${entry.missingForecastDisplay} (${entry.missingForecastDisplayPct}%) | ${entry.missingPropType} (${entry.missingPropTypePct}%) | ${entry.missingSportsbookDisplay} (${entry.missingSportsbookDisplayPct}%) |`);
    }
    lines.push('');
  }

  if (report.teamBundleGapsByLeague.length > 0) {
    lines.push('## Team Bundle Gaps');
    lines.push('');
    lines.push('| League | Total Bundle Props | Missing signal tier | Missing direction | Missing agreement | Missing market quality |');
    lines.push('| --- | --- | --- | --- | --- | --- |');
    for (const entry of report.teamBundleGapsByLeague) {
      lines.push(`| ${entry.league} | ${entry.totalProps} | ${entry.missingSignalTier} (${entry.missingSignalTierPct}%) | ${entry.missingForecastDirection} (${entry.missingForecastDirectionPct}%) | ${entry.missingAgreementScore} (${entry.missingAgreementScorePct}%) | ${entry.missingMarketQualityLabel} (${entry.missingMarketQualityLabelPct}%) |`);
    }
    lines.push('');
  }

  if (report.marketConcentrationByLeague.length > 0) {
    lines.push('## Market Concentration');
    lines.push('');
    for (const entry of report.marketConcentrationByLeague) {
      const topLabels = entry.topPropLabels
        .map((label) => `${label.label}: ${label.count} (${label.sharePct}%)`)
        .join('; ');
      lines.push(`- ${entry.league}: ${entry.topPropLabel || 'n/a'} leads with ${entry.topPropCount}/${entry.totalProps} (${entry.topPropSharePct}%). Top labels: ${topLabels || 'none'}`);
    }
    lines.push('');
  }

  if (report.projectionClusters.length > 0) {
    lines.push('## Suspicious Projection Clusters');
    lines.push('');
    for (const cluster of report.projectionClusters) {
      lines.push(`- ${cluster.league}: ${cluster.propKey} projected to ${cluster.projectedValue} for ${cluster.count} props. Players: ${cluster.samplePlayers.join(', ')}`);
    }
    lines.push('');
  }

  if (report.staleEventGaps.length > 0) {
    lines.push('## Events With Zero Active Player Props');
    lines.push('');
    for (const event of report.staleEventGaps) {
      lines.push(`- ${event.league}: ${event.matchup} (${event.eventId}) active=${event.activePlayerProps}, sourceBacked=${event.sourceBackedPlayerProps}, source=${event.sourceBackedSource || 'n/a'}, stale=${event.stalePlayerProps}, team=${event.activeTeamProps}, game=${event.activeGameMarkets}`);
    }
    lines.push('');
  }

  if (report.propHighlightsCoverage.length > 0) {
    lines.push('## Prop Highlights Coverage');
    lines.push('');
    for (const entry of report.propHighlightsCoverage) {
      lines.push(`- ${entry.league}: ${entry.eventsWithPropHighlights}/${entry.eventsWithForecasts} forecasts populated (${entry.coveragePct}%), empty=${entry.emptyPropHighlights}`);
    }
    lines.push('');
  }

  if (report.sourceCoverageByLeague.length > 0) {
    lines.push('## Source Coverage By League');
    lines.push('');
    lines.push('| League | Events | Published events | Source-backed events | Recovery events | No prop coverage | Published props | Source-backed props |');
    lines.push('| --- | --- | --- | --- | --- | --- | --- | --- |');
    for (const entry of report.sourceCoverageByLeague) {
      lines.push(`| ${entry.league} | ${entry.totalEvents} | ${entry.eventsWithPublishedPlayerProps} (${entry.publishedCoveragePct}%) | ${entry.eventsWithSourceBackedCoverage} (${entry.sourceBackedCoveragePct}%) | ${entry.eventsUsingSourceBackedRecovery} (${entry.recoveryPct}%) | ${entry.eventsWithNoPlayerPropCoverage} | ${entry.publishedPlayerProps} | ${entry.sourceBackedPlayerProps} |`);
    }
    lines.push('');
  }

  return lines.join('\n').trim() + '\n';
}
