'use strict';

/**
 * Parlay service (Session 28).
 *
 * Combines user-selected props (from the parlay builder) into a parlay:
 *   - combined American/decimal odds (multiply decimal odds)
 *   - combined grade (confidence-weighted average of leg grades)
 *   - correlation detection (interaction matrix for same-game legs)
 *   - kill-condition aggregation (any leg flagged → surface it)
 *
 * This is the lightweight BUILDER path — it operates on the simple leg
 * shape the frontend sends ({ player, team, gameId, stat, side, line,
 * odds, grade, confidence, killConditions }). It is distinct from the
 * full grading pipeline (`parlayGrader` / `correlationEngine`), which
 * consumes already-analyzed legs with full reasoning trees.
 *
 * Odds math reuses `payoutCalculator` where it can; the decimal/American
 * conversions live here because the builder needs both directions.
 */

const { calculateParlayPayout } = require('./payoutCalculator');

// ---- odds conversions -------------------------------------------------
function americanToDecimal(odds) {
  const n = Number(odds);
  if (!Number.isFinite(n) || n === 0) return 1;
  return n > 0 ? 1 + n / 100 : 1 + 100 / Math.abs(n);
}

function decimalToAmerican(decimal) {
  const d = Number(decimal);
  if (!Number.isFinite(d) || d <= 1) return 0;
  return d >= 2
    ? Math.round((d - 1) * 100)
    : Math.round(-100 / (d - 1));
}

// ---- grade <-> numeric (A+ = 12 … F = 0) ------------------------------
const GRADE_ORDER = ['F', 'D-', 'D', 'D+', 'C-', 'C', 'C+', 'B-', 'B', 'B+', 'A-', 'A', 'A+'];

function gradeToNumeric(grade) {
  const idx = GRADE_ORDER.indexOf(String(grade || 'C').toUpperCase());
  return idx === -1 ? GRADE_ORDER.indexOf('C') : idx;
}

function numericToGrade(value) {
  const idx = Math.max(0, Math.min(GRADE_ORDER.length - 1, Math.round(value)));
  return GRADE_ORDER[idx];
}

// ---- correlation interaction matrix -----------------------------------
// Keyed by the two stat types sorted + joined. `sameTeam` / `crossTeam`
// give the correlation sign. Stat names are normalized to lowercase.
const INTERACTIONS = {
  'assists_points':       { sameTeam: 'positive', crossTeam: 'neutral'  },
  'assists_rebounds':     { sameTeam: 'positive', crossTeam: 'neutral'  },
  'points_points':        { sameTeam: 'neutral',  crossTeam: 'negative' },
  'rebounds_rebounds':    { sameTeam: 'negative', crossTeam: 'negative' },
  'points_rebounds':      { sameTeam: 'positive', crossTeam: 'neutral'  },
  'assists_assists':      { sameTeam: 'negative', crossTeam: 'neutral'  },
  'pra_points':           { sameTeam: 'positive', crossTeam: 'neutral'  },
  // MLB
  'hits_strikeouts':      { sameTeam: 'neutral',  crossTeam: 'negative' },
  'hits_hits':            { sameTeam: 'positive', crossTeam: 'neutral'  },
  'strikeouts_strikeouts':{ sameTeam: 'neutral',  crossTeam: 'positive' },
  'home_runs_strikeouts': { sameTeam: 'neutral',  crossTeam: 'negative' },
};

function normStat(s) {
  return String(s || '').toLowerCase().replace(/\s+/g, '_');
}

function describeCorrelation(legA, legB, sign) {
  const a = `${legA.player} ${normStat(legA.stat).replace(/_/g, ' ')}`;
  const b = `${legB.player} ${normStat(legB.stat).replace(/_/g, ' ')}`;
  if (sign === 'negative') return `${a} and ${b} compete — these legs fight each other`;
  if (sign === 'positive') return `${a} and ${b} feed each other — correlated outcome`;
  return `${a} and ${b} are weakly related`;
}

/**
 * Correlation between two builder legs. Independent across different
 * games; otherwise looked up in the interaction matrix.
 */
function detectCorrelation(legA, legB) {
  if (!legA || !legB) return { correlated: false, type: 'independent' };

  // Same player, opposite directions on the same stat → direct conflict.
  if (legA.player && legB.player && legA.player.toLowerCase() === legB.player.toLowerCase()) {
    if (normStat(legA.stat) === normStat(legB.stat) && legA.side && legB.side && legA.side !== legB.side) {
      return { correlated: true, type: 'negative', description: `${legA.player}: ${legA.side} vs ${legB.side} on the same prop — direct conflict` };
    }
  }

  if (!legA.gameId || !legB.gameId || legA.gameId !== legB.gameId) {
    return { correlated: false, type: 'independent' };
  }

  const key = [normStat(legA.stat), normStat(legB.stat)].sort().join('_');
  const interaction = INTERACTIONS[key];
  if (!interaction) return { correlated: false, type: 'unknown' };

  const sameTeam = legA.team != null && legA.team === legB.team;
  const sign = sameTeam ? interaction.sameTeam : interaction.crossTeam;
  if (sign === 'neutral') return { correlated: false, type: 'neutral' };

  return { correlated: true, type: sign, description: describeCorrelation(legA, legB, sign) };
}

/**
 * Combine legs into a parlay analysis. Throws on empty/invalid input so
 * the route can 400 cleanly.
 */
function calculateParlay(legs) {
  if (!Array.isArray(legs) || legs.length === 0) {
    const err = new Error('A parlay needs at least one leg.');
    err.statusCode = 400;
    throw err;
  }

  const decimalOdds = legs.map((l) => americanToDecimal(l.odds));
  const combinedDecimal = decimalOdds.reduce((a, b) => a * b, 1);
  const combinedAmerican = decimalToAmerican(combinedDecimal);

  // Confidence-weighted grade.
  const totalConfidence = legs.reduce((sum, l) => sum + (Number(l.confidence) || 50), 0);
  const weightedGrade = legs.reduce((sum, l) => {
    const weight = (Number(l.confidence) || 50) / totalConfidence;
    return sum + gradeToNumeric(l.grade) * weight;
  }, 0);

  // All pairwise correlations.
  const correlations = [];
  for (let i = 0; i < legs.length; i += 1) {
    for (let j = i + 1; j < legs.length; j += 1) {
      const c = detectCorrelation(legs[i], legs[j]);
      if (c.correlated) correlations.push({ legA: i, legB: j, type: c.type, description: c.description });
    }
  }

  const killConditions = legs
    .map((l, i) => ({ leg: i, player: l.player, conditions: l.killConditions || [] }))
    .filter((k) => Array.isArray(k.conditions) && k.conditions.length > 0);

  return {
    legCount: legs.length,
    combinedOdds: combinedAmerican,
    combinedDecimal: Math.round(combinedDecimal * 10000) / 10000,
    combinedGrade: numericToGrade(weightedGrade),
    payoutPer10: Math.round(calculateParlayPayout(10, legs.map((l) => Number(l.odds) || 0)) * 100) / 100,
    correlations,
    hasNegativeCorrelation: correlations.some((c) => c.type === 'negative'),
    hasPositiveCorrelation: correlations.some((c) => c.type === 'positive'),
    killConditions,
    hasKillCondition: killConditions.length > 0,
  };
}

/**
 * Suggest up to `max` parlays from a pool of graded props. Greedy: take
 * the best-graded props, avoid negative correlations within a suggestion.
 * Pure — the route supplies the prop pool (no API calls here).
 */
function suggestParlays(props, { legs = 3, max = 3 } = {}) {
  if (!Array.isArray(props) || props.length < legs) return [];
  const sorted = [...props].sort((a, b) => gradeToNumeric(b.grade) - gradeToNumeric(a.grade));

  const suggestions = [];
  const used = new Set();
  for (let start = 0; start < sorted.length && suggestions.length < max; start += 1) {
    if (used.has(start)) continue;
    const combo = [start];
    for (let k = 0; k < sorted.length && combo.length < legs; k += 1) {
      if (k === start || used.has(k) || combo.includes(k)) continue;
      const conflicts = combo.some((idx) => detectCorrelation(sorted[idx], sorted[k]).type === 'negative');
      if (!conflicts) combo.push(k);
    }
    if (combo.length === legs) {
      combo.forEach((idx) => used.add(idx));
      const legObjs = combo.map((idx) => sorted[idx]);
      suggestions.push({ legs: legObjs, ...calculateParlay(legObjs) });
    }
  }
  return suggestions;
}

// ───────────────────────────────────────────────────────────────────
// Session 50 — Parlay Lab correlation-score model.
//
// A numeric, GAME-AWARE correlation model (0.0 independent … 1.0 perfectly
// correlated) layered on top of the S28 categorical matrix. Legs are the light
// UI shape { player, team, game, stat, grade }.
// ───────────────────────────────────────────────────────────────────

/** Grade → 0..1 score (A+ = 1.0 … F = 0.0), reusing the 13-step order. */
function gradeScore(grade) {
  return gradeToNumeric(grade) / (GRADE_ORDER.length - 1);
}
/** 0..1 score → letter grade. */
function scoreToGrade(score) {
  const s = Math.max(0, Math.min(1, Number(score) || 0));
  return numericToGrade(s * (GRADE_ORDER.length - 1));
}

const sameVal = (a, b) => a != null && b != null && String(a).toLowerCase() === String(b).toLowerCase();

/**
 * Pairwise correlation between two legs (0.0–1.0):
 *   0.7 same player, same game (different stats move together)
 *   0.4 same team, same game (team performance drives both)
 *   0.2 same game, different teams (game pace, mostly independent)
 *   0.0 different games (fully independent — what books price)
 */
function correlationScore(leg1, leg2) {
  if (!leg1 || !leg2) return 0;
  if (!sameVal(leg1.game, leg2.game)) return 0; // different (or unknown) game
  if (sameVal(leg1.player, leg2.player)) return 0.7;
  if (sameVal(leg1.team, leg2.team)) return 0.4;
  return 0.2;
}

function pairwise(legs) {
  const out = [];
  for (let i = 0; i < legs.length; i += 1) {
    for (let j = i + 1; j < legs.length; j += 1) out.push(correlationScore(legs[i], legs[j]));
  }
  return out;
}

/**
 * Combined parlay grade — the leg-grade average PENALIZED by correlation.
 * penalty = avgCorrelation * 0.5 (each 0.1 correlation ≈ 0.05 grade-score drop).
 */
function combinedGrade(legs) {
  const list = Array.isArray(legs) ? legs : [];
  if (list.length === 0) return { grade: '—', score: 0, penalty: 0, maxCorrelation: 0, avgCorrelation: 0 };
  const rawAvg = list.reduce((s, l) => s + gradeScore(l.grade), 0) / list.length;
  const pairs = pairwise(list);
  const maxCorrelation = pairs.length ? Math.max(...pairs) : 0;
  const avgCorrelation = pairs.length ? pairs.reduce((a, b) => a + b, 0) / pairs.length : 0;
  const penalty = avgCorrelation * 0.5;
  const score = Math.max(0, Math.min(1, rawAvg - penalty));
  return {
    grade: scoreToGrade(score),
    score: Math.round(score * 1000) / 1000,
    penalty: Math.round(penalty * 1000) / 1000,
    maxCorrelation: Math.round(maxCorrelation * 100) / 100,
    avgCorrelation: Math.round(avgCorrelation * 100) / 100,
  };
}

// Fair decimal odds the model assigns each grade (lower grade = longer odds).
const FAIR_ODDS = {
  'A+': 1.15, A: 1.25, 'A-': 1.35, 'B+': 1.45, B: 1.70, 'B-': 1.90,
  'C+': 2.0, C: 2.10, 'C-': 2.5, 'D+': 2.8, D: 3.0, 'D-': 4.0, F: 5.0,
};
function fairOdds(grade) {
  return FAIR_ODDS[String(grade || 'C').toUpperCase()] ?? 2.1;
}

/**
 * Estimated payout. Books price parlays as independent (product of fair odds);
 * VYNDR discounts that by the slip's average correlation.
 */
function estimatedPayout(legs, betAmount = 10) {
  const list = Array.isArray(legs) ? legs : [];
  const bet = Number(betAmount) || 10;
  const fairMultiplier = list.reduce((m, l) => m * fairOdds(l.grade), 1);
  const pairs = pairwise(list);
  const avgCorrelation = pairs.length ? pairs.reduce((a, b) => a + b, 0) / pairs.length : 0;
  const correlationDiscount = Math.max(0.5, Math.min(1, 1 - avgCorrelation));
  const multiplier = fairMultiplier * correlationDiscount;
  return {
    payout: Math.round(bet * multiplier * 100) / 100,
    multiplier: Math.round(multiplier * 100) / 100,
    fairMultiplier: Math.round(fairMultiplier * 100) / 100,
    correlationDiscount: Math.round(correlationDiscount * 100) / 100,
  };
}

/** Human warning for the most-correlated cluster, or null when independent. */
function correlationWarning(legs) {
  const list = Array.isArray(legs) ? legs : [];
  if (list.length < 2) return null;
  const byTeam = {};
  for (const l of list) {
    if (!l.team || !l.game) continue;
    const k = `${String(l.team).toUpperCase()}|${l.game}`;
    (byTeam[k] = byTeam[k] || []).push(l);
  }
  let worst = null;
  for (const [k, group] of Object.entries(byTeam)) {
    if (group.length >= 2 && (!worst || group.length > worst.count)) worst = { team: k.split('|')[0], count: group.length };
  }
  if (worst) return `⚠ ${worst.count} legs from ${worst.team} — high correlation`;
  for (let i = 0; i < list.length; i += 1) {
    for (let j = i + 1; j < list.length; j += 1) {
      if (sameVal(list[i].game, list[j].game)) {
        const g = list[i].game;
        return g ? `⚠ 2 legs from the same game (${g}) — correlated` : '⚠ 2 legs from the same game — correlated';
      }
    }
  }
  return null;
}

/** The full Parlay-Lab analysis the /grade route returns. */
function gradeParlay(legs, betAmount = 10) {
  const combined = combinedGrade(legs);
  const payout = estimatedPayout(legs, betAmount);
  return {
    combined: { grade: combined.grade, score: combined.score, penalty: combined.penalty },
    correlation: { max: combined.maxCorrelation, avg: combined.avgCorrelation, warning: correlationWarning(legs) },
    payout: { amount: payout.payout, multiplier: payout.multiplier, fairMultiplier: payout.fairMultiplier, discount: payout.correlationDiscount },
    legs: (Array.isArray(legs) ? legs : []).map((l) => ({ ...l, score: gradeScore(l.grade) })),
  };
}

module.exports = {
  calculateParlay,
  detectCorrelation,
  suggestParlays,
  // Session 50 — Parlay Lab model
  correlationScore,
  combinedGrade,
  estimatedPayout,
  correlationWarning,
  gradeParlay,
  gradeToNumeric,
  numericToGrade,
  gradeScore,
  scoreToGrade,
  __internals: { americanToDecimal, decimalToAmerican, gradeToNumeric, numericToGrade, INTERACTIONS, FAIR_ODDS },
};
