/**
 * RIE Signal: KenPom (NCAAB Power Ratings) — Enhanced v2
 *
 * Multi-dimensional scoring from KenPom data:
 *   1. AdjEM MOV prediction (core power rating) — 40%
 *   2. Four-factors matchup analysis — 25%
 *   3. Fanmatch win probability (when available) — 20%
 *   4. Contextual adjustments (luck, SOS, experience, tempo) — 15%
 *
 * Games-played Bayesian weighting and tournament mode.
 */

import { Signal, SignalResult, MatchupContext } from '../types';
import { getKenPomMatchup, computeKenPomMOVScore, KenPomMatchup, KenPomTeamProfile } from '../../kenpom';

function clamp(val: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, val));
}

/** Normalize a value from [lo, hi] → [0, 1] */
function normalize(val: number, lo: number, hi: number): number {
  return clamp((val - lo) / (hi - lo), 0, 1);
}

/**
 * Compute four-factors edge score.
 * Compares offensive and defensive four factors between teams.
 * Returns 0-1 where > 0.5 = home advantage.
 */
function fourFactorsScore(home: KenPomTeamProfile, away: KenPomTeamProfile): number | null {
  if (home.efgPct == null || away.efgPct == null) return null;

  // Offensive eFG% advantage: home offense eFG% vs away defense eFG%
  // Higher offensive eFG% = better, lower defensive eFG% = better
  const homeOffEfg = home.efgPct ?? 50;
  const awayDefEfg = away.deFgPct ?? 50;
  const awayOffEfg = away.efgPct ?? 50;
  const homeDefEfg = home.deFgPct ?? 50;

  // eFG matchup: home offense vs away defense, and vice versa
  const homeEfgEdge = (homeOffEfg - awayDefEfg) - (awayOffEfg - homeDefEfg);
  const efgScore = normalize(homeEfgEdge, -6, 6);

  // Turnover rate: lower offensive TO% is better, higher defensive TO% (forced) is better
  const homeTo = home.toPct ?? 20;
  const awayTo = away.toPct ?? 20;
  const homeDTo = home.dToPct ?? 20;
  const awayDTo = away.dToPct ?? 20;
  const homeToEdge = (awayTo - homeTo) + (homeDTo - awayDTo); // positive = home forces more / commits fewer
  const toScore = normalize(homeToEdge, -6, 6);

  // Offensive rebounding: home OR% vs away DOR%
  const homeOr = home.orPct ?? 30;
  const awayDOr = away.dOrPct ?? 28;
  const awayOr = away.orPct ?? 30;
  const homeDOr = home.dOrPct ?? 28;
  const homeOrEdge = (homeOr - awayDOr) - (awayOr - homeDOr);
  const orScore = normalize(homeOrEdge, -6, 6);

  // Free throw rate: offensive FT rate advantage
  const homeFtr = home.ftRate ?? 30;
  const awayDFtr = away.dFtRate ?? 30;
  const awayFtr = away.ftRate ?? 30;
  const homeDFtr = home.dFtRate ?? 30;
  const homeFtrEdge = (homeFtr - awayDFtr) - (awayFtr - homeDFtr);
  const ftrScore = normalize(homeFtrEdge, -8, 8);

  // Dean Oliver weights: eFG% 40%, TO% 25%, OR% 20%, FTR 15%
  return efgScore * 0.40 + toScore * 0.25 + orScore * 0.20 + ftrScore * 0.15;
}

/**
 * Contextual adjustments score based on luck, SOS, experience, and tempo.
 * Returns a -0.05 to +0.05 adjustment to the base score.
 */
function contextualAdjustment(home: KenPomTeamProfile, away: KenPomTeamProfile): number {
  let adj = 0;

  // Luck regression: teams running lucky may regress
  // A team with luck > +0.04 is "lucky" and their rating may be inflated
  const homeLuckPenalty = home.luck > 0.04 ? -home.luck * 0.3 : (home.luck < -0.04 ? -home.luck * 0.2 : 0);
  const awayLuckPenalty = away.luck > 0.04 ? away.luck * 0.3 : (away.luck < -0.04 ? away.luck * 0.2 : 0);
  adj += clamp(homeLuckPenalty + awayLuckPenalty, -0.03, 0.03);

  // SOS: if one team played a much harder schedule, their rating is more reliable
  if (home.sosRank && away.sosRank) {
    const sosGap = away.sosRank - home.sosRank; // positive = home had tougher SOS
    if (Math.abs(sosGap) > 80) {
      adj += clamp(sosGap * 0.00005, -0.02, 0.02);
    }
  }

  // Experience edge: more experienced teams perform better in pressure games
  if (home.experience && away.experience) {
    const expGap = home.experience - away.experience;
    if (Math.abs(expGap) > 0.4) {
      adj += clamp(expGap * 0.02, -0.015, 0.015);
    }
  }

  // Continuity: teams with higher continuity tend to be more cohesive
  if (home.continuity && away.continuity) {
    const contGap = home.continuity - away.continuity;
    if (Math.abs(contGap) > 0.15) {
      adj += clamp(contGap * 0.03, -0.01, 0.01);
    }
  }

  return clamp(adj, -0.05, 0.05);
}

export const kenpomSignal: Signal = {
  id: 'kenpom',
  name: 'KenPom Power Ratings',
  supportedLeagues: ['ncaab'],

  async collect(ctx: MatchupContext): Promise<SignalResult> {
    const start = Date.now();
    try {
      const matchup = await getKenPomMatchup(ctx.homeTeam, ctx.awayTeam, ctx.league, ctx.startsAt);
      if (!matchup || (!matchup.home && !matchup.away)) {
        return {
          signalId: 'kenpom', score: 0.5, weight: 0, available: false,
          rawData: { reason: `no KenPom data for ${ctx.homeTeam} vs ${ctx.awayTeam}` },
          metadata: { latencyMs: Date.now() - start, source: 'db', freshness: '' },
        };
      }

      // === Dimension 1: AdjEM MOV (40% of signal) ===
      const mov = computeKenPomMOVScore(matchup as KenPomMatchup);
      const movScore = mov.confidence;

      // === Dimension 2: Four-factors matchup (25%) ===
      let ffScore = 0.5;
      let hasFF = false;
      if (matchup.home && matchup.away) {
        const ff = fourFactorsScore(matchup.home, matchup.away);
        if (ff !== null) {
          ffScore = ff;
          hasFF = true;
        }
      }

      // === Dimension 3: Fanmatch win probability (20%) ===
      let fmScore = 0.5;
      let hasFM = false;
      if (matchup.fanmatch?.homeWP != null) {
        // KenPom homeWP is stored as percentage (69 = 69%), normalize to 0-1
        const wpRaw = matchup.fanmatch.homeWP;
        const wp = wpRaw > 1 ? wpRaw / 100 : wpRaw; // handle both 0.69 and 69 formats
        fmScore = clamp(wp, 0.02, 0.98);
        hasFM = true;
      }

      // === Dimension 4: Contextual adjustments (15%) ===
      let ctxAdj = 0;
      if (matchup.home && matchup.away) {
        ctxAdj = contextualAdjustment(matchup.home, matchup.away);
      }

      // === Weighted composite ===
      let totalWeight = 0;
      let weightedSum = 0;

      // MOV always available if we have matchup
      weightedSum += movScore * 0.40;
      totalWeight += 0.40;

      if (hasFF) {
        weightedSum += ffScore * 0.25;
        totalWeight += 0.25;
      }

      if (hasFM) {
        weightedSum += fmScore * 0.20;
        totalWeight += 0.20;
      }

      // Contextual always contributes (as an adjustment)
      const baseScore = totalWeight > 0 ? weightedSum / totalWeight : 0.5;
      let score = baseScore + ctxAdj;

      // === Games-played Bayesian weighting ===
      if (mov.gamesPlayed < 10) {
        score = 0.5 + (score - 0.5) * 0.55; // Very early season: heavy regression
      } else if (mov.gamesPlayed < 15) {
        score = 0.5 + (score - 0.5) * 0.75; // Early season
      } else if (mov.gamesPlayed < 20) {
        score = 0.5 + (score - 0.5) * 0.90; // Mid season
      } else if (mov.gamesPlayed >= 28) {
        // Late season: ratings well-calibrated, slight boost to extreme values
        score = 0.5 + (score - 0.5) * 1.05;
      }

      score = clamp(score, 0.02, 0.98);

      // Build edge signals summary
      const edgeSignals: string[] = [];
      if (matchup.edgeSignals) {
        const es = matchup.edgeSignals as Record<string, any>;
        for (const key of Object.keys(es)) {
          if (es[key]) edgeSignals.push(`${key}: ${es[key]}`);
        }
      }

      return {
        signalId: 'kenpom',
        score,
        weight: 0,
        available: true,
        rawData: {
          predictedMOV: Math.round(mov.predictedMOV * 10) / 10,
          confidence: Math.round(mov.confidence * 1000) / 1000,
          gamesPlayed: mov.gamesPlayed,
          homeAdjEM: matchup.home?.adjEM,
          awayAdjEM: matchup.away?.adjEM,
          dimensions: {
            movScore: Math.round(movScore * 1000) / 1000,
            ffScore: hasFF ? Math.round(ffScore * 1000) / 1000 : null,
            fmScore: hasFM ? Math.round(fmScore * 1000) / 1000 : null,
            ctxAdj: Math.round(ctxAdj * 1000) / 1000,
          },
          fanmatch: matchup.fanmatch || null,
          edgeSignals,
        },
        metadata: {
          latencyMs: Date.now() - start,
          source: 'db',
          freshness: new Date().toISOString().slice(0, 10),
        },
      };
    } catch (err) {
      return {
        signalId: 'kenpom', score: 0.5, weight: 0, available: false,
        rawData: { error: String(err) },
        metadata: { latencyMs: Date.now() - start, source: 'db', freshness: '' },
      };
    }
  },
};
