import { getPlayerPropLabelForLeague, normalizePlayerPropMarketStat } from './player-prop-market-registry';

export type PlayerPropSignalTier = 'STRONG' | 'GOOD' | 'FAIR' | 'COIN_FLIP';
export type SurfaceablePlayerPropSignalTier = Extract<PlayerPropSignalTier, 'FAIR' | 'GOOD' | 'STRONG'>;
export type PlayerPropDirection = 'OVER' | 'UNDER' | null;

export interface PlayerPropSignalInput {
  player?: string | null;
  team?: string | null;
  teamSide?: 'home' | 'away' | null;
  league?: string | null;
  prop?: string | null;
  statType?: string | null;
  normalizedStatType?: string | null;
  marketLine?: number | null;
  odds?: number | null;
  projectedProbability?: number | null;
  projectedOutcome?: number | null;
  edgePct?: number | null;
  recommendation?: string | null;
  playerRole?: string | null;
  modelContext?: Record<string, any> | null;
  marketSource?: string | null;
  marketCompletenessStatus?: 'source_complete' | 'multi_source_complete' | 'incomplete' | null;
  sourceBacked?: boolean | null;
  marketQualityScore?: number | null;
  clvFitScore?: number | null;
}

export interface PlayerPropSignalRow {
  propType: string;
  marketLine: number | null;
  odds: number | null;
  marketImpliedProbability: number | null;
  forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'>;
  projectedProbability: number | null;
  projectedOutcome: number | null;
  edgePct: number | null;
  signal: SurfaceablePlayerPropSignalTier;
}

export interface RankedPlayerPropSignal extends QualifiedPlayerPropSignal {
  rawRankScore: number;
  rankScore: number;
  rankPosition: number;
}

export interface QualifiedPlayerPropSignal extends PlayerPropSignalInput {
  forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'>;
  marketImpliedProbability: number;
  projectedProbability: number;
  projectedOutcome: number | null;
  edgePct: number;
  signalTier: SurfaceablePlayerPropSignalTier;
  signalLabel: string;
  tableRow: PlayerPropSignalRow;
  agreementScore: number;
  agreementLabel: 'LOW' | 'MEDIUM' | 'HIGH';
  agreementSources: string[];
  marketQualityScore: number;
  marketQualityLabel: 'WEAK' | 'FAIR' | 'GOOD' | 'STRONG';
}

export interface DerivedPlayerPropMetadata extends PlayerPropSignalInput {
  forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'>;
  marketImpliedProbability: number;
  projectedProbability: number;
  projectedOutcome: number | null;
  edgePct: number;
  signalTier: PlayerPropSignalTier | null;
  signalLabel: string | null;
  tableRow: PlayerPropSignalRow | null;
  agreementScore: number;
  agreementLabel: 'LOW' | 'MEDIUM' | 'HIGH';
  agreementSources: string[];
  marketQualityScore: number;
  marketQualityLabel: 'WEAK' | 'FAIR' | 'GOOD' | 'STRONG';
}

const LOW_INFORMATION_UNDER_THRESHOLDS: Record<string, Partial<Record<string, number>>> = {
  nba: {
    points: 16.5,
    rebounds: 6.5,
    assists: 5.5,
  },
  ncaab: {
    points: 11.5,
    rebounds: 5.5,
    assists: 3.5,
  },
  nhl: {
    points: 1,
    shotsongoal: 2,
    assists: 0.5,
  },
};

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

function round0(value: number): number {
  return Math.round(value);
}

function shouldAllowFlatSourceBackedSoccerEdge(input: PlayerPropSignalInput, rawEdge: number | null): boolean {
  const league = String(input.league || '').toLowerCase();
  if (!input.sourceBacked) return false;
  if (!['epl', 'bundesliga', 'la_liga', 'serie_a', 'ligue_1', 'champions_league', 'mls'].includes(league)) {
    return false;
  }
  return rawEdge != null && rawEdge >= -2;
}

function normalizeMarketQualityLabel(score: number): 'WEAK' | 'FAIR' | 'GOOD' | 'STRONG' {
  if (score >= 85) return 'STRONG';
  if (score >= 70) return 'GOOD';
  if (score >= 55) return 'FAIR';
  return 'WEAK';
}

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

export function americanOddsToImpliedProbability(odds: number | null | undefined): number | null {
  const n = toNumber(odds);
  if (n == null || n === 0) return null;
  if (n > 0) {
    return round1((100 / (n + 100)) * 100);
  }
  const abs = Math.abs(n);
  return round1((abs / (abs + 100)) * 100);
}

export function inferMarketImpliedProbabilityFromEdge(
  projectedProbability: number | null | undefined,
  edgePct: number | null | undefined,
): number | null {
  const projected = toNumber(projectedProbability);
  const edge = toNumber(edgePct);
  if (projected == null || edge == null || edge <= 0) return null;
  const inferred = round1(projected - edge);
  if (inferred <= 0 || inferred >= 100) return null;
  return inferred;
}

export function normalizePlayerPropDirection(value: string | null | undefined): PlayerPropDirection {
  const normalized = String(value || '').trim().toUpperCase();
  if (normalized === 'OVER' || normalized === 'O') return 'OVER';
  if (normalized === 'UNDER' || normalized === 'U') return 'UNDER';
  return null;
}

function normalizeAgreementSupportScore(value: any): number | null {
  const parsed = toNumber(value);
  if (parsed == null) return null;
  if (parsed <= 1) return round0(Math.max(0, Math.min(1, parsed)) * 100);
  return round0(Math.max(0, Math.min(100, parsed)));
}

function buildAgreement(signal: {
  projectedProbability: number;
  edgePct: number;
  forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'>;
  modelContext?: Record<string, any> | null;
}): {
  agreementScore: number;
  agreementLabel: 'LOW' | 'MEDIUM' | 'HIGH';
  agreementSources: string[];
} {
  const contributions: number[] = [];
  const sources: string[] = [];

  const modelSupport = round0(Math.min(100, Math.max(0, ((Math.abs(signal.projectedProbability - 50)) / 30) * 100)));
  contributions.push(modelSupport);
  sources.push('model');

  const marketSupport = round0(Math.min(100, Math.max(0, (signal.edgePct / 25) * 100)));
  contributions.push(marketSupport);
  sources.push('market');

  const phaseSupportDirection = normalizePlayerPropDirection(
    signal.modelContext?.phase_support_direction ?? signal.modelContext?.phaseSupportDirection,
  );
  const phaseSupportScore = normalizeAgreementSupportScore(
    signal.modelContext?.phase_support_score
    ?? signal.modelContext?.phaseSupportScore
    ?? signal.modelContext?.agreement_score
    ?? signal.modelContext?.agreementScore,
  );

  if (
    phaseSupportScore != null
    && (!phaseSupportDirection || phaseSupportDirection === signal.forecastDirection)
  ) {
    contributions.push(phaseSupportScore);
    sources.push(phaseSupportDirection ? 'phase' : 'context');
  }

  const agreementScore = round0(
    contributions.reduce((sum, value) => sum + value, 0) / Math.max(1, contributions.length),
  );
  const agreementLabel = agreementScore >= 75 ? 'HIGH' : agreementScore >= 60 ? 'MEDIUM' : 'LOW';

  return { agreementScore, agreementLabel, agreementSources: sources };
}

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

function computeMarketQuality(input: PlayerPropSignalInput): {
  marketQualityScore: number;
  marketQualityLabel: 'WEAK' | 'FAIR' | 'GOOD' | 'STRONG';
} {
  const explicitScore = toNumber(input.marketQualityScore);
  let score = explicitScore ?? 65;
  const completeness = input.marketCompletenessStatus ?? null;
  const source = normalizeMarketSource(input.marketSource);

  if (explicitScore != null) {
    score = explicitScore;
  } else if (completeness === 'source_complete') score = 92;
  else if (completeness === 'multi_source_complete') score = 82;
  else if (completeness === 'incomplete') score = 45;
  else if (['fanduel', 'draftkings'].includes(source)) score = 74;
  else if (['bet365', 'betmgm', 'caesars', 'espnbet', 'fanatics'].includes(source)) score = 70;
  else if (source === 'consensus') score = 68;
  else if (input.sourceBacked === false) score = 55;

  const odds = toNumber(input.odds);
  if (odds != null && odds >= 1000) {
    score = Math.min(score, 42);
  } else if (odds != null && odds >= 600) {
    score = Math.min(score, 54);
  }

  return {
    marketQualityScore: score,
    marketQualityLabel: normalizeMarketQualityLabel(score),
  };
}

function isLowInformationProp(input: PlayerPropSignalInput, forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'> | null): boolean {
  const league = String(input.league || '').toLowerCase();
  const stat = String(
    normalizePlayerPropMarketStat(league, input.normalizedStatType || input.statType || null)
    || input.normalizedStatType
    || input.statType
    || '',
  )
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '');
  const marketLine = toNumber(input.marketLine);
  if (forecastDirection !== 'UNDER' || marketLine == null) return false;

  const isMlb = league === 'mlb';
  if (isMlb && marketLine <= 0.5 && (stat === 'battingstolenbases' || stat === 'stolenbases')) {
    return true;
  }

  const threshold = LOW_INFORMATION_UNDER_THRESHOLDS[league]?.[stat];
  if (threshold != null && marketLine <= threshold) {
    return true;
  }

  return false;
}

function isSuspiciousLongshotProp(
  input: PlayerPropSignalInput,
  forecastDirection: Extract<PlayerPropDirection, 'OVER' | 'UNDER'>,
): boolean {
  const odds = toNumber(input.odds);
  const marketLine = toNumber(input.marketLine);
  const projectedOutcome = toNumber(input.projectedOutcome);
  if (odds == null || marketLine == null || projectedOutcome == null) return false;

  if (odds < 300) return false;

  const outcomeMargin = forecastDirection === 'OVER'
    ? projectedOutcome - marketLine
    : marketLine - projectedOutcome;

  const minimumRequiredMargin = odds >= 1000
    ? 0.75
    : odds >= 600
      ? 0.55
      : 0.4;

  return outcomeMargin < minimumRequiredMargin;
}

export function classifyPlayerPropSignal(edgePct: number | null | undefined): PlayerPropSignalTier {
  const edge = toNumber(edgePct);
  if (edge == null || edge < 10) return 'COIN_FLIP';
  if (edge >= 25) return 'STRONG';
  if (edge >= 15) return 'GOOD';
  return 'FAIR';
}

export function isSurfaceablePlayerPropSignal(tier: PlayerPropSignalTier): boolean {
  return tier !== 'COIN_FLIP';
}

export function getPlayerPropSignalRank(tier: PlayerPropSignalTier | null | undefined): number {
  if (tier === 'STRONG') return 3;
  if (tier === 'GOOD') return 2;
  if (tier === 'FAIR') return 1;
  return 0;
}

export function getPlayerPropTypeLabel(input: PlayerPropSignalInput): string {
  const league = String(input.league || '').toLowerCase();
  const canonicalStat = normalizePlayerPropMarketStat(
    league,
    input.normalizedStatType || input.statType || input.prop || null,
  );
  const registryLabel = getPlayerPropLabelForLeague(league, canonicalStat || null);
  if (registryLabel) return registryLabel;

  const rawLabel = String(
    input.normalizedStatType
    || input.statType
    || input.prop
    || 'Prop'
  )
    .replace(/^batting_/i, '')
    .replace(/^pitching_/i, '')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/_/g, ' ')
    .replace(/\b\w/g, (char) => char.toUpperCase());

  if (league === 'mlb' && /^points?$/i.test(rawLabel.trim())) {
    return 'Runs';
  }

  return rawLabel;
}

function applyV2DirectionCorrection(
  direction: PlayerPropDirection,
  input: PlayerPropSignalInput,
): PlayerPropDirection {
  if (!direction) return direction;
  const league = (input.league || '').toLowerCase();

  // NBA overs with low projected probability are unreliable
  if (league === 'nba' && direction === 'OVER') {
    const prob = toNumber(input.projectedProbability);
    if (prob != null && prob < 65) return null;
  }

  return direction;
}

export function buildPlayerPropMetadata(input: PlayerPropSignalInput): DerivedPlayerPropMetadata | null {
  let forecastDirection = normalizePlayerPropDirection(input.recommendation);
  forecastDirection = applyV2DirectionCorrection(forecastDirection, input);
  const projectedProbability = toNumber(input.projectedProbability);
  const marketImpliedProbability = americanOddsToImpliedProbability(input.odds);
  const projectedOutcome = toNumber(input.projectedOutcome);
  const marketLine = toNumber(input.marketLine);

  if (!forecastDirection || projectedProbability == null || marketImpliedProbability == null || marketLine == null) {
    return null;
  }
  if (isLowInformationProp(input, forecastDirection)) return null;
  if (isSuspiciousLongshotProp(input, forecastDirection)) return null;

  const rawEdge = round1(projectedProbability - marketImpliedProbability);
  if (rawEdge == null) return null;
  if (rawEdge <= 0 && !shouldAllowFlatSourceBackedSoccerEdge(input, rawEdge)) return null;
  const effectiveEdge = rawEdge <= 0 ? 0 : rawEdge;

  const signalTier = classifyPlayerPropSignal(effectiveEdge);
  const surfaceableTier = isSurfaceablePlayerPropSignal(signalTier)
    ? signalTier as SurfaceablePlayerPropSignalTier
    : null;

  const propType = getPlayerPropTypeLabel(input);
  const agreement = buildAgreement({
    projectedProbability,
    edgePct: rawEdge,
    forecastDirection,
    modelContext: input.modelContext,
  });
  const marketQuality = computeMarketQuality(input);
  return {
    ...input,
    forecastDirection,
    projectedProbability,
    marketImpliedProbability,
    projectedOutcome,
    edgePct: effectiveEdge,
    signalTier,
    signalLabel: signalTier,
    agreementScore: agreement.agreementScore,
    agreementLabel: agreement.agreementLabel,
    agreementSources: agreement.agreementSources,
    marketQualityScore: marketQuality.marketQualityScore,
    marketQualityLabel: marketQuality.marketQualityLabel,
    tableRow: surfaceableTier ? {
      propType,
      marketLine,
      odds: toNumber(input.odds),
      marketImpliedProbability,
      forecastDirection,
      projectedProbability,
      projectedOutcome,
      edgePct: effectiveEdge,
      signal: surfaceableTier,
    } : null,
  };
}

export function buildPlayerPropSignal(input: PlayerPropSignalInput): QualifiedPlayerPropSignal | null {
  const metadata = buildPlayerPropMetadata(input);
  if (!metadata?.signalTier || !metadata.tableRow || !metadata.signalLabel) return null;
  if (!isSurfaceablePlayerPropSignal(metadata.signalTier)) return null;
  const surfaceableTier = metadata.signalTier as SurfaceablePlayerPropSignalTier;

  return {
    ...metadata,
    signalTier: surfaceableTier,
    signalLabel: metadata.signalLabel,
    tableRow: metadata.tableRow,
  };
}

export function explainPlayerPropMetadataRejection(input: PlayerPropSignalInput): string | null {
  let forecastDirection = normalizePlayerPropDirection(input.recommendation);
  if (!forecastDirection) return 'missing_direction';

  forecastDirection = applyV2DirectionCorrection(forecastDirection, input);
  if (!forecastDirection) return 'direction_correction_rejected';

  const projectedProbability = toNumber(input.projectedProbability);
  if (projectedProbability == null) return 'missing_projected_probability';

  const marketImpliedProbability = americanOddsToImpliedProbability(input.odds);
  if (marketImpliedProbability == null) return 'missing_market_odds';

  const projectedOutcome = toNumber(input.projectedOutcome);
  const marketLine = toNumber(input.marketLine);
  if (marketLine == null) return 'missing_market_line';

  if (isLowInformationProp(input, forecastDirection)) return 'low_information_prop';
  if (isSuspiciousLongshotProp(input, forecastDirection)) return 'suspicious_longshot';

  const rawEdge = round1(projectedProbability - marketImpliedProbability);
  if (rawEdge == null) return 'nonpositive_edge';
  if (rawEdge <= 0 && !shouldAllowFlatSourceBackedSoccerEdge(input, rawEdge)) return 'nonpositive_edge';

  return null;
}

export function computeTopPropRawScore(signal: QualifiedPlayerPropSignal, confidenceFactor?: number | null): number {
  const tierMultiplier = signal.signalTier === 'STRONG'
    ? 1.5
    : signal.signalTier === 'GOOD'
      ? 1.0
      : 0.4;
  const confidence = toNumber(confidenceFactor);
  const confidenceBonus = confidence == null
    ? 0
    : confidence > 1
      ? confidence
      : confidence * 10;
  const marketQualityBonus = ((signal.marketQualityScore ?? 65) - 50) / 10;
  const clvFitBonus = Math.max(0, ((toNumber(signal.clvFitScore) ?? 50) - 50) / 4);
  return round1(signal.edgePct + tierMultiplier + confidenceBonus + marketQualityBonus + clvFitBonus);
}

export function rankQualifiedSignals(
  signals: Array<QualifiedPlayerPropSignal & { confidenceFactor?: number | null }>,
  limits: {
    maxPropsPerPlayer?: number;
    maxPerPropTypePerPlayer?: number;
    limit?: number;
  } = {},
): RankedPlayerPropSignal[] {
  const maxPropsPerPlayer = limits.maxPropsPerPlayer ?? 2;
  const maxPerPropTypePerPlayer = limits.maxPerPropTypePerPlayer ?? 1;
  const limit = limits.limit ?? 10;

  const scored = signals
    .map((signal) => ({
      ...signal,
      rawRankScore: computeTopPropRawScore(signal, signal.confidenceFactor ?? null),
    }))
    .sort((a, b) => {
      if (b.rawRankScore !== a.rawRankScore) return b.rawRankScore - a.rawRankScore;
      if (b.edgePct !== a.edgePct) return b.edgePct - a.edgePct;
      if (b.projectedProbability !== a.projectedProbability) return b.projectedProbability - a.projectedProbability;
      return (a.player || '').localeCompare(b.player || '');
    });

  const byPlayerCount = new Map<string, number>();
  const byPlayerPropTypeCount = new Map<string, number>();
  const retained = scored.filter((signal) => {
    const playerKey = `${signal.player || ''}|${signal.team || ''}`;
    const propKey = `${playerKey}|${signal.tableRow.propType}`;
    const playerCount = byPlayerCount.get(playerKey) || 0;
    const propCount = byPlayerPropTypeCount.get(propKey) || 0;
    if (playerCount >= maxPropsPerPlayer || propCount >= maxPerPropTypePerPlayer) {
      return false;
    }
    byPlayerCount.set(playerKey, playerCount + 1);
    byPlayerPropTypeCount.set(propKey, propCount + 1);
    return true;
  }).slice(0, limit);

  const rawScores = retained.map((signal) => signal.rawRankScore);
  const minScore = rawScores.length > 0 ? Math.min(...rawScores) : 0;
  const maxScore = rawScores.length > 0 ? Math.max(...rawScores) : 0;

  return retained.map((signal, index) => {
    const rankScore = maxScore === minScore
      ? 100
      : round1(((signal.rawRankScore - minScore) / (maxScore - minScore)) * 100);

    return {
      ...signal,
      rankScore,
      rankPosition: index + 1,
    };
  });
}

export function groupQualifiedSignalsByPlayer(
  signals: QualifiedPlayerPropSignal[],
): Array<{
  player: string;
  team: string | null;
  teamSide: 'home' | 'away' | null;
  playerRole: string | null;
  strongestSignal: SurfaceablePlayerPropSignalTier;
  maxEdgePct: number;
  props: PlayerPropSignalRow[];
}> {
  const grouped = new Map<string, {
    player: string;
    team: string | null;
    teamSide: 'home' | 'away' | null;
    playerRole: string | null;
    strongestSignal: SurfaceablePlayerPropSignalTier;
    maxEdgePct: number;
    props: PlayerPropSignalRow[];
  }>();

  for (const signal of signals) {
    const key = `${signal.player || ''}|${signal.team || ''}|${signal.teamSide || ''}`;
    const existing = grouped.get(key);
    if (!existing) {
      grouped.set(key, {
        player: signal.player || 'Unknown',
        team: signal.team || null,
        teamSide: signal.teamSide || null,
        playerRole: signal.playerRole || null,
        strongestSignal: signal.signalTier,
        maxEdgePct: signal.edgePct,
        props: [signal.tableRow],
      });
      continue;
    }

    existing.props.push(signal.tableRow);
    if (getPlayerPropSignalRank(signal.signalTier) > getPlayerPropSignalRank(existing.strongestSignal)) {
      existing.strongestSignal = signal.signalTier;
    }
    if (signal.edgePct > existing.maxEdgePct) {
      existing.maxEdgePct = signal.edgePct;
    }
  }

  return Array.from(grouped.values())
    .map((group) => ({
      ...group,
      props: group.props.sort((a, b) => (b.edgePct || 0) - (a.edgePct || 0)),
    }))
    .sort((a, b) => b.maxEdgePct - a.maxEdgePct);
}
