import type { SignalResult } from './rie/types';

export type MlbDirectMarketContext = {
  marketHomeProb: number | null;
  marketSpreadHome: number | null;
  marketTotal: number | null;
  marketMoveEdge: number;
  marketMoveMagnitude: number;
  reverseLineMove: boolean;
  steamMove: boolean;
  movementRowCount: number;
  bookHomeProbStddev: number;
  bookHomeProbRange: number;
  bookHomeProbSampleCount: number;
  totalLineRange: number;
  totalLineSampleCount: number;
};

export type MlbDirectFeatureVector = {
  marketEdge: number;
  starterQualityEdge: number;
  starterLeashEdge: number;
  bullpenQualityEdge: number;
  bullpenUsageEdge: number;
  parkInteractionEdge: number;
  offenseSplitEdge: number;
  offenseContactEdge: number;
  starterOffenseInteractionEdge: number;
  travelRestEdge: number;
  decompositionEdge: number;
  impliedTotalDisagreementEdge: number;
  platoonPressureEdge: number;
  weatherEdge: number;
  marketMoveEdge: number;
  bookDisagreementEdge: number;
  featureCoverage: {
    starterData: boolean;
    bullpenData: boolean;
    parkData: boolean;
    marketData: boolean;
    movementData: boolean;
    bookDisagreementData: boolean;
    platoonData: boolean;
    weatherData: boolean;
  };
  raw: Record<string, any>;
};

export type MlbDirectRecommendedMarket = 'moneyline' | 'f5_moneyline' | 'abstain';

export type MlbDirectPolicyConfig = {
  minConfidence: number;
  minMarketEdge: number;
  minFeatureCoverageScore: number;
  minStructuralEdge: number;
  minSupportCount: number;
  maxConflictCount: number;
  f5OnlySeparation: number;
  minF5Edge: number;
};

export type MlbDirectPolicyEvaluation = {
  shouldBet: boolean;
  recommendedMarket: MlbDirectRecommendedMarket;
  featureCoverageScore: number;
  supportCount: number;
  conflictCount: number;
  structuralEdgeAbs: number;
  marketEdgeAbs: number;
  reasons: string[];
};

export type MlbDirectModelConfig = {
  starterWeight: number;
  bullpenWeight: number;
  parkWeight: number;
  marketMicroWeight: number;
  offenseWeight: number;
  contactWeight: number;
  interactionWeight: number;
  travelWeight: number;
  decompositionWeight: number;
  impliedTotalWeight: number;
  platoonWeight: number;
  weatherWeight: number;
  signalScale: number;
  maxSignalAdjustment: number;
};

export type MlbDirectDecision = {
  available: boolean;
  homeProbability: number;
  awayProbability: number;
  f5HomeProbability: number;
  f5AwayProbability: number;
  winnerPick: string;
  confidence: number;
  calibratedConfidence: number;
  confidencePenalty: number;
  featureVector: MlbDirectFeatureVector;
  explanation: string[];
  policy: MlbDirectPolicyEvaluation;
};

export type MlbF5ModelConfig = {
  starterQualityWeight: number;
  starterLeashWeight: number;
  offenseWeight: number;
  phaseWeight: number;
  contextWeight: number;
  marketWeight: number;
};

export type MlbF5Decision = {
  available: boolean;
  homeProbability: number;
  awayProbability: number;
  winnerPick: string;
  confidence: number;
  rawEdge: number;
  explanation: string[];
};

export type MlbRunLineModelConfig = {
  starterWeight: number;
  bullpenWeight: number;
  offenseWeight: number;
  phaseWeight: number;
  contextWeight: number;
  marketWeight: number;
  marginScale: number;
};

export type MlbRunLineDecision = {
  available: boolean;
  side: 'home' | 'away';
  winnerPick: string;
  line: number | null;
  coverProbability: number;
  confidence: number;
  projectedMargin: number;
  atsEdge: number;
  explanation: string[];
};


export type MlbTeamTotalModelConfig = {
  starterWeight: number;
  bullpenWeight: number;
  offenseWeight: number;
  contextWeight: number;
  shareScale: number;
  totalDeltaWeight: number;
};

export type MlbTeamTotalMarket = 'home_over' | 'home_under' | 'away_over' | 'away_under';

export type MlbTeamTotalDecision = {
  available: boolean;
  market: MlbTeamTotalMarket;
  team: 'home' | 'away';
  direction: 'over' | 'under';
  line: number | null;
  projectedTeamTotal: number;
  projectedGameTotal: number;
  inferredHomeLine: number | null;
  inferredAwayLine: number | null;
  edge: number;
  probability: number;
  confidence: number;
  explanation: string[];
};

const DEFAULT_MLB_F5_MODEL_CONFIG: MlbF5ModelConfig = {
  starterQualityWeight: 0.95,
  starterLeashWeight: 0.45,
  offenseWeight: 0.35,
  phaseWeight: 0.4,
  contextWeight: 0.15,
  marketWeight: 0.3,
};

const DEFAULT_MLB_RUN_LINE_MODEL_CONFIG: MlbRunLineModelConfig = {
  starterWeight: 1.0,
  bullpenWeight: 0.55,
  offenseWeight: 0.9,
  phaseWeight: 0.7,
  contextWeight: 0.15,
  marketWeight: 0.35,
  marginScale: 2.8,
};


const DEFAULT_MLB_TEAM_TOTAL_MODEL_CONFIG: MlbTeamTotalModelConfig = {
  starterWeight: 0.7,
  bullpenWeight: 0.55,
  offenseWeight: 1.0,
  contextWeight: 0.3,
  shareScale: 0.18,
  totalDeltaWeight: 0.55,
};

const DEFAULT_MLB_DIRECT_MODEL_CONFIG: MlbDirectModelConfig = {
  starterWeight: 0.18,
  bullpenWeight: 0.15,
  parkWeight: 0.05,
  marketMicroWeight: 0.08,
  offenseWeight: 0.15,
  contactWeight: 0.09,
  interactionWeight: 0.12,
  travelWeight: 0.06,
  decompositionWeight: 0.14,
  impliedTotalWeight: 0.08,
  platoonWeight: 0.03,
  weatherWeight: 0.04,
  signalScale: 0.45,
  maxSignalAdjustment: 0.08,
};

export const MLB_DIRECT_MODEL_LIVE_CONFIG: Partial<MlbDirectModelConfig> = {
  starterWeight: 0.135,
  bullpenWeight: 0.112,
  parkWeight: 0.025,
  marketMicroWeight: 0,
  offenseWeight: 0.112,
  contactWeight: 0.085,
  interactionWeight: 0.095,
  travelWeight: 0.045,
  decompositionWeight: 0.105,
  impliedTotalWeight: 0.04,
  platoonWeight: 0.03,
  weatherWeight: 0.03,
  signalScale: 0,
  maxSignalAdjustment: 0.03,
};

export const MLB_F5_MODEL_LIVE_CONFIG: MlbF5ModelConfig = {
  starterQualityWeight: 0.8,
  starterLeashWeight: 0.3,
  offenseWeight: 0.35,
  phaseWeight: 0.45,
  contextWeight: 0.08,
  marketWeight: 0.3,
};

export const MLB_RUN_LINE_MODEL_LIVE_CONFIG: MlbRunLineModelConfig = {
  starterWeight: 1.0,
  bullpenWeight: 0.5,
  offenseWeight: 0.9,
  phaseWeight: 0.7,
  contextWeight: 0.15,
  marketWeight: 0.35,
  marginScale: 2.8,
};


export const MLB_TEAM_TOTAL_MODEL_LIVE_CONFIG: MlbTeamTotalModelConfig = {
  starterWeight: 0.65,
  bullpenWeight: 0.45,
  offenseWeight: 0.95,
  contextWeight: 0.25,
  shareScale: 0.16,
  totalDeltaWeight: 0.5,
};

export const MLB_DIRECT_MODEL_LIVE_POLICY: MlbDirectPolicyConfig = {
  minConfidence: 0.5,
  minMarketEdge: 0,
  minFeatureCoverageScore: 0.375,
  minStructuralEdge: 0,
  minSupportCount: 0,
  maxConflictCount: 4,
  f5OnlySeparation: 0.05,
  minF5Edge: 0.12,
};

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

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


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

function average(values: number[]): number {
  const usable = values.filter((value) => Number.isFinite(value));
  if (usable.length === 0) return 0;
  return usable.reduce((sum, value) => sum + value, 0) / usable.length;
}

function sign(value: number): number {
  if (value > 0) return 1;
  if (value < 0) return -1;
  return 0;
}

function americanToProbability(odds: number | null | undefined): number | null {
  if (odds == null || !Number.isFinite(odds) || odds === 0) return null;
  return odds > 0 ? 100 / (odds + 100) : (-odds) / ((-odds) + 100);
}

function probabilityEdge(probability: number | null | undefined): number {
  return clamp((Number(probability ?? 0.5) - 0.5), -0.42, 0.42);
}

function findSignal(signals: SignalResult[], signalId: string): SignalResult | undefined {
  return signals.find((signal) => signal.signalId === signalId);
}

function projectMarketHomeProbability(oddsData: any): { homeProb: number | null; spreadHome: number | null; total: number | null } {
  const moneyline = oddsData?.moneyline || {};
  const spread = oddsData?.spread || {};
  const total = oddsData?.total || {};
  const homeMl = americanToProbability(Number(moneyline.home ?? null));
  const awayMl = americanToProbability(Number(moneyline.away ?? null));

  let homeProb: number | null = null;
  if (homeMl != null && awayMl != null) {
    const vigTotal = homeMl + awayMl;
    homeProb = vigTotal > 0 ? homeMl / vigTotal : homeMl;
  } else if (homeMl != null) {
    homeProb = homeMl;
  } else if (awayMl != null) {
    homeProb = 1 - awayMl;
  }

  const homeSpread = spread?.home?.line != null
    ? Number(spread.home.line)
    : (spread?.away?.line != null ? -Number(spread.away.line) : null);
  const marketTotal = total?.over?.line != null
    ? Number(total.over.line)
    : (total?.under?.line != null ? Number(total.under.line) : null);

  if (homeProb == null && Number.isFinite(homeSpread)) {
    homeProb = clamp(0.5 + ((-Number(homeSpread)) * 0.055), 0.18, 0.82);
  }

  return {
    homeProb,
    spreadHome: Number.isFinite(homeSpread) ? homeSpread : null,
    total: Number.isFinite(marketTotal) ? marketTotal : null,
  };
}

function derivePlatoonPressure(matchupRaw: any, phaseRaw: any): { edge: number; available: boolean } {
  const homeStarterHand = String(
    phaseRaw?.firstFive?.homeStarter?.throws
    || matchupRaw?.starters?.home?.throws
    || matchupRaw?.starters?.home?.handedness
    || '',
  ).toLowerCase();
  const awayStarterHand = String(
    phaseRaw?.firstFive?.awayStarter?.throws
    || matchupRaw?.starters?.away?.throws
    || matchupRaw?.starters?.away?.handedness
    || '',
  ).toLowerCase();
  if (!homeStarterHand && !awayStarterHand) {
    return { edge: 0, available: false };
  }

  // If only one starter hand is known, treat same-hand uncertainty as neutral.
  const handDelta = (awayStarterHand.startsWith('l') ? 0.08 : 0) - (homeStarterHand.startsWith('l') ? 0.08 : 0);
  return {
    edge: clamp(handDelta, -0.2, 0.2),
    available: true,
  };
}

export function extractMlbDirectFeatures(params: {
  signals: SignalResult[];
  oddsData?: any;
  marketContext?: Partial<MlbDirectMarketContext> | null;
}): MlbDirectFeatureVector {
  const matchup = findSignal(params.signals, 'mlb_matchup');
  const phase = findSignal(params.signals, 'mlb_phase');
  const fangraphs = findSignal(params.signals, 'fangraphs');
  const dvp = findSignal(params.signals, 'dvp');
  const matchupRaw = matchup?.rawData || {};
  const phaseRaw = phase?.rawData || {};
  const fangraphsRaw = fangraphs?.rawData || {};
  const marketProjection = projectMarketHomeProbability(params.oddsData || {});
  const marketContext = params.marketContext || {};

  const homeStarterFip = Number(
    phaseRaw?.firstFive?.homeStarter?.fip
    ?? matchupRaw?.starters?.home?.fangraphs?.fip
    ?? fangraphsRaw?.homePit?.fip
    ?? matchupRaw?.projections?.homePit?.projFip
    ?? 4.2,
  );
  const awayStarterFip = Number(
    phaseRaw?.firstFive?.awayStarter?.fip
    ?? matchupRaw?.starters?.away?.fangraphs?.fip
    ?? fangraphsRaw?.awayPit?.fip
    ?? matchupRaw?.projections?.awayPit?.projFip
    ?? 4.2,
  );
  const homeStarterXfip = Number(
    phaseRaw?.firstFive?.homeStarter?.xfip
    ?? matchupRaw?.starters?.home?.fangraphs?.xfip
    ?? fangraphsRaw?.homePit?.xfip
    ?? matchupRaw?.projections?.homePit?.projFip
    ?? 4.2,
  );
  const awayStarterXfip = Number(
    phaseRaw?.firstFive?.awayStarter?.xfip
    ?? matchupRaw?.starters?.away?.fangraphs?.xfip
    ?? fangraphsRaw?.awayPit?.xfip
    ?? matchupRaw?.projections?.awayPit?.projFip
    ?? 4.2,
  );
  const homeStarterKbb = Number(
    phaseRaw?.firstFive?.homeStarter?.kBbPct
    ?? matchupRaw?.starters?.home?.fangraphs?.kBbPct
    ?? 0,
  );
  const awayStarterKbb = Number(
    phaseRaw?.firstFive?.awayStarter?.kBbPct
    ?? matchupRaw?.starters?.away?.fangraphs?.kBbPct
    ?? 0,
  );
  const homeStarterRecent30 = phaseRaw?.firstFive?.homeStarter?.recent30 || {};
  const awayStarterRecent30 = phaseRaw?.firstFive?.awayStarter?.recent30 || {};
  const homeStarterUsage = phaseRaw?.firstFive?.homeStarter?.recentUsage || {};
  const awayStarterUsage = phaseRaw?.firstFive?.awayStarter?.recentUsage || {};
  const homeStarterAvgInnings = Number(
    phaseRaw?.firstFive?.homeStarter?.avgInningsStart
    ?? ((phaseRaw?.firstFive?.homeStarter?.ip && phaseRaw?.firstFive?.homeStarter?.gs)
      ? (Number(phaseRaw?.firstFive?.homeStarter?.ip) / Math.max(Number(phaseRaw?.firstFive?.homeStarter?.gs), 1))
      : 5.2),
  );
  const awayStarterAvgInnings = Number(
    phaseRaw?.firstFive?.awayStarter?.avgInningsStart
    ?? ((phaseRaw?.firstFive?.awayStarter?.ip && phaseRaw?.firstFive?.awayStarter?.gs)
      ? (Number(phaseRaw?.firstFive?.awayStarter?.ip) / Math.max(Number(phaseRaw?.firstFive?.awayStarter?.gs), 1))
      : 5.2),
  );
  const homeStarterRecentAvgInnings = Number(
    homeStarterUsage?.avgInnings
    ?? homeStarterRecent30?.avgInningsStart
    ?? homeStarterAvgInnings
    ?? 5.2,
  );
  const awayStarterRecentAvgInnings = Number(
    awayStarterUsage?.avgInnings
    ?? awayStarterRecent30?.avgInningsStart
    ?? awayStarterAvgInnings
    ?? 5.2,
  );
  const homeStarterRecentPitches = Number(homeStarterUsage?.avgPitches ?? 85);
  const awayStarterRecentPitches = Number(awayStarterUsage?.avgPitches ?? 85);
  const homeStarterPitchTrend = Number(homeStarterUsage?.pitchTrend ?? 0);
  const awayStarterPitchTrend = Number(awayStarterUsage?.pitchTrend ?? 0);
  const homeStarterRecentVelo = Number(homeStarterRecent30?.fbVelo ?? phaseRaw?.firstFive?.homeStarter?.fbVelo ?? matchupRaw?.starters?.home?.fangraphs?.fbVelo ?? 93);
  const awayStarterRecentVelo = Number(awayStarterRecent30?.fbVelo ?? phaseRaw?.firstFive?.awayStarter?.fbVelo ?? matchupRaw?.starters?.away?.fangraphs?.fbVelo ?? 93);
  const homeStarterRecentFip = Number(homeStarterRecent30?.fip ?? homeStarterFip);
  const awayStarterRecentFip = Number(awayStarterRecent30?.fip ?? awayStarterFip);
  const homeStarterRecentKbb = Number(homeStarterRecent30?.kBbPct ?? homeStarterKbb);
  const awayStarterRecentKbb = Number(awayStarterRecent30?.kBbPct ?? awayStarterKbb);

  const starterQualityEdge = round3(average([
    clamp((awayStarterFip - homeStarterFip) / 4, -0.6, 0.6),
    clamp((awayStarterXfip - homeStarterXfip) / 4, -0.6, 0.6),
    clamp((homeStarterKbb - awayStarterKbb) / 0.3, -0.4, 0.4),
    clamp((awayStarterRecentFip - homeStarterRecentFip) / 4, -0.45, 0.45),
    clamp((homeStarterRecentKbb - awayStarterRecentKbb) / 0.28, -0.35, 0.35),
    clamp((homeStarterRecentVelo - awayStarterRecentVelo) / 4, -0.2, 0.2),
  ]));
  const starterLeashEdge = round3(average([
    clamp((homeStarterAvgInnings - awayStarterAvgInnings) / 2.5, -0.3, 0.3),
    clamp((homeStarterRecentAvgInnings - awayStarterRecentAvgInnings) / 2.2, -0.25, 0.25),
    clamp((homeStarterRecentPitches - awayStarterRecentPitches) / 35, -0.22, 0.22),
    clamp((homeStarterPitchTrend - awayStarterPitchTrend) / 20, -0.16, 0.16),
  ]));

  const homeBullpen = phaseRaw?.bullpen?.homeBullpen || {};
  const awayBullpen = phaseRaw?.bullpen?.awayBullpen || {};
  const homeBullpenRoles = homeBullpen.roleAvailability || homeBullpen.workload?.roleAvailability || {};
  const awayBullpenRoles = awayBullpen.roleAvailability || awayBullpen.workload?.roleAvailability || {};
  const bullpenQualityEdge = round3(average([
    clamp((Number(awayBullpen.fip ?? 4.3) - Number(homeBullpen.fip ?? 4.3)) / 4, -0.5, 0.5),
    clamp((Number(awayBullpen.xfip ?? 4.3) - Number(homeBullpen.xfip ?? 4.3)) / 4, -0.5, 0.5),
    clamp((Number(awayBullpen.whip ?? 1.4) - Number(homeBullpen.whip ?? 1.4)) / 1.5, -0.4, 0.4),
    clamp((Number(homeBullpen.kPct ?? 0.2) - Number(awayBullpen.kPct ?? 0.2)) / 0.3, -0.4, 0.4),
  ]));
  const bullpenUsageEdge = round3(average([
    clamp(((awayBullpen.workload?.fatigueScore ?? 0.5) - (homeBullpen.workload?.fatigueScore ?? 0.5)) / 0.8, -0.4, 0.4),
    clamp(((awayBullpen.workload?.last3DayPitches ?? 0) - (homeBullpen.workload?.last3DayPitches ?? 0)) / 250, -0.4, 0.4),
    clamp(((awayBullpen.workload?.highLeverageAppearancesLast3Days ?? 0) - (homeBullpen.workload?.highLeverageAppearancesLast3Days ?? 0)) / 6, -0.4, 0.4),
    clamp(((Number(homeBullpenRoles?.coreAvailabilityScore ?? 0.5) - Number(awayBullpenRoles?.coreAvailabilityScore ?? 0.5)) / 0.75), -0.35, 0.35),
    clamp((((homeBullpenRoles?.setupAvailableCount ?? 1) - (awayBullpenRoles?.setupAvailableCount ?? 1)) / 2), -0.2, 0.2),
    clamp((((homeBullpenRoles?.closerAvailable ? 1 : 0) - (awayBullpenRoles?.closerAvailable ? 1 : 0)) * 0.18), -0.18, 0.18),
  ]));

  const offenseSplitEdge = round3(average([
    clamp(((phaseRaw?.firstFive?.homeOffense?.fullWrcPlus ?? 100) - (phaseRaw?.firstFive?.awayOffense?.fullWrcPlus ?? 100)) / 80, -0.5, 0.5),
    clamp(((phaseRaw?.firstFive?.homeOffense?.recentWrcPlus ?? 100) - (phaseRaw?.firstFive?.awayOffense?.recentWrcPlus ?? 100)) / 80, -0.5, 0.5),
    clamp(((phaseRaw?.firstFive?.homeOffense?.projectedWrcPlus ?? 100) - (phaseRaw?.firstFive?.awayOffense?.projectedWrcPlus ?? 100)) / 80, -0.5, 0.5),
    clamp(((fangraphsRaw?.homeBat?.wrcPlus ?? 100) - (fangraphsRaw?.awayBat?.wrcPlus ?? 100)) / 80, -0.5, 0.5),
    clamp(((fangraphsRaw?.homeBatRecent?.wrcPlus ?? 100) - (fangraphsRaw?.awayBatRecent?.wrcPlus ?? 100)) / 80, -0.5, 0.5),
  ]));
  const homeBatOps = Number(fangraphsRaw?.homeBat?.ops ?? 0.72);
  const awayBatOps = Number(fangraphsRaw?.awayBat?.ops ?? 0.72);
  const homeBatWoba = Number(fangraphsRaw?.homeBat?.woba ?? 0.315);
  const awayBatWoba = Number(fangraphsRaw?.awayBat?.woba ?? 0.315);
  const homeBatEv = Number(fangraphsRaw?.homeBat?.ev ?? 88.5);
  const awayBatEv = Number(fangraphsRaw?.awayBat?.ev ?? 88.5);
  const homeBatBarrel = Number(fangraphsRaw?.homeBat?.barrelPct ?? 0.065);
  const awayBatBarrel = Number(fangraphsRaw?.awayBat?.barrelPct ?? 0.065);
  const homeBatKpct = Number(fangraphsRaw?.homeBat?.kPct ?? 0.22);
  const awayBatKpct = Number(fangraphsRaw?.awayBat?.kPct ?? 0.22);
  const homeRecentWoba = Number(fangraphsRaw?.homeBatRecent?.woba ?? homeBatWoba);
  const awayRecentWoba = Number(fangraphsRaw?.awayBatRecent?.woba ?? awayBatWoba);
  const homeRecentShape = round3(average([
    clamp((((fangraphsRaw?.homeBatRecent?.wrcPlus ?? fangraphsRaw?.homeBat?.wrcPlus ?? 100) - (fangraphsRaw?.homeBat?.wrcPlus ?? 100)) / 35), -0.35, 0.35),
    clamp(((homeRecentWoba - homeBatWoba) / 0.055), -0.28, 0.28),
  ]));
  const awayRecentShape = round3(average([
    clamp((((fangraphsRaw?.awayBatRecent?.wrcPlus ?? fangraphsRaw?.awayBat?.wrcPlus ?? 100) - (fangraphsRaw?.awayBat?.wrcPlus ?? 100)) / 35), -0.35, 0.35),
    clamp(((awayRecentWoba - awayBatWoba) / 0.055), -0.28, 0.28),
  ]));
  const offenseContactEdge = round3(average([
    clamp((homeBatEv - awayBatEv) / 4, -0.25, 0.25),
    clamp((homeBatBarrel - awayBatBarrel) / 0.05, -0.35, 0.35),
    clamp((homeBatOps - awayBatOps) / 0.18, -0.35, 0.35),
    clamp((homeBatWoba - awayBatWoba) / 0.07, -0.3, 0.3),
    clamp((awayBatKpct - homeBatKpct) / 0.12, -0.25, 0.25),
    clamp((homeRecentShape - awayRecentShape) / 0.45, -0.25, 0.25),
  ]));
  const homeStarterContactRisk = round3(average([
    clamp((Number(phaseRaw?.firstFive?.homeStarter?.whip ?? 1.28) - 1.28) / 0.5, -0.35, 0.35),
    clamp((Number(homeStarterRecentFip) - 4.1) / 1.8, -0.35, 0.35),
    clamp((0.16 - Number(homeStarterRecentKbb)) / 0.14, -0.3, 0.3),
    clamp((5.1 - Number(homeStarterRecentAvgInnings)) / 2.0, -0.18, 0.18),
  ]));
  const awayStarterContactRisk = round3(average([
    clamp((Number(phaseRaw?.firstFive?.awayStarter?.whip ?? 1.28) - 1.28) / 0.5, -0.35, 0.35),
    clamp((Number(awayStarterRecentFip) - 4.1) / 1.8, -0.35, 0.35),
    clamp((0.16 - Number(awayStarterRecentKbb)) / 0.14, -0.3, 0.3),
    clamp((5.1 - Number(awayStarterRecentAvgInnings)) / 2.0, -0.18, 0.18),
  ]));
  const homeOffensePressure = round3(average([
    offenseSplitEdge * 0.55,
    offenseContactEdge * 0.8,
    homeRecentShape * 0.55,
    awayStarterContactRisk,
  ]));
  const awayOffensePressure = round3(average([
    offenseSplitEdge * -0.55,
    offenseContactEdge * -0.8,
    awayRecentShape * 0.55,
    homeStarterContactRisk,
  ]));
  const starterOffenseInteractionEdge = round3(clamp(homeOffensePressure - awayOffensePressure, -0.45, 0.45));

  const parkRuns = Number(matchupRaw?.park?.runs ?? 1);
  const parkHr = Number(matchupRaw?.park?.hr ?? 1);
  const parkInteractionEdge = round3(clamp(
    ((parkRuns - 1) * offenseSplitEdge * 1.4) + ((parkHr - 1) * offenseSplitEdge * 0.8),
    -0.25,
    0.25,
  ));

  const travelRestEdge = round3(average([
    clamp((((phaseRaw?.context?.travel?.away?.travelMiles ?? 0) - (phaseRaw?.context?.travel?.home?.travelMiles ?? 0)) / 3000), -0.4, 0.4),
    clamp((((phaseRaw?.context?.travel?.home?.hoursSinceLastGame ?? 24) - (phaseRaw?.context?.travel?.away?.hoursSinceLastGame ?? 24)) / 24), -0.3, 0.3),
    clamp((((phaseRaw?.context?.travel?.away?.wasAwayLastGame ? 1 : 0) - (phaseRaw?.context?.travel?.home?.wasAwayLastGame ? 1 : 0)) * 0.08), -0.16, 0.16),
  ]));

  const firstFiveEdge = Number(phaseRaw?.firstFive?.sideScore ?? 0.5) - 0.5;
  const fullGameEdge = Number(phaseRaw?.context?.sideScore ?? 0.5) - 0.5;
  const bullpenEdge = Number(phaseRaw?.bullpen?.sideScore ?? 0.5) - 0.5;
  const decompositionEdge = round3(clamp(
    average([firstFiveEdge, bullpenEdge, fullGameEdge])
      + ((Math.sign(firstFiveEdge) === Math.sign(bullpenEdge) && Math.sign(firstFiveEdge) === Math.sign(fullGameEdge)) ? 0.06 : -0.04),
    -0.45,
    0.45,
  ));

  const modelTotalProxy = Number(matchupRaw?.runsCreated?.home ?? 0) + Number(matchupRaw?.runsCreated?.away ?? 0) + (Number(phaseRaw?.context?.weatherRunBias ?? 0) * 2.5);
  const marketTotal = marketContext.marketTotal ?? marketProjection.total ?? null;
  const impliedTotalDisagreementEdge = marketTotal != null
    ? round3(clamp(((modelTotalProxy - marketTotal) / 6) * offenseSplitEdge, -0.25, 0.25))
    : 0;

  const weather = phaseRaw?.context?.weather || {};
  const weatherRunBias = Number(phaseRaw?.context?.weatherRunBias ?? 0);
  const weatherEdge = round3(clamp(
    (weatherRunBias * offenseSplitEdge)
      + (Number(weather.windMph ?? 0) >= 10 ? weatherRunBias * 0.08 : 0),
    -0.2,
    0.2,
  ));

  const { edge: platoonPressureEdge, available: platoonAvailable } = derivePlatoonPressure(matchupRaw, phaseRaw);
  const marketEdge = round3(probabilityEdge(marketContext.marketHomeProb ?? marketProjection.homeProb));
  const signalCompositeEdge = average([
    Number(matchup?.score ?? 0.5) - 0.5,
    Number(phase?.score ?? 0.5) - 0.5,
    Number(fangraphs?.score ?? 0.5) - 0.5,
    Number(dvp?.score ?? 0.5) - 0.5,
  ]);
  const bookDisagreementEdge = round3(clamp(
    signalCompositeEdge * Number(marketContext.bookHomeProbStddev ?? 0) * 4,
    -0.25,
    0.25,
  ));

  return {
    marketEdge,
    starterQualityEdge,
    starterLeashEdge,
    bullpenQualityEdge,
    bullpenUsageEdge,
    parkInteractionEdge,
    offenseSplitEdge,
    offenseContactEdge,
    starterOffenseInteractionEdge,
    travelRestEdge,
    decompositionEdge,
    impliedTotalDisagreementEdge,
    platoonPressureEdge: round3(platoonPressureEdge),
    weatherEdge,
    marketMoveEdge: round3(Number(marketContext.marketMoveEdge ?? 0)),
    bookDisagreementEdge,
    featureCoverage: {
      starterData: Boolean(matchupRaw?.projections?.homePit || phaseRaw?.firstFive?.homeStarter || fangraphsRaw?.homePit),
      bullpenData: Boolean(phaseRaw?.bullpen?.homeBullpen && phaseRaw?.bullpen?.awayBullpen),
      parkData: Boolean(matchupRaw?.park),
      marketData: (marketContext.marketHomeProb ?? marketProjection.homeProb) != null,
      movementData: Number(marketContext.movementRowCount ?? 0) > 0,
      bookDisagreementData: Number(marketContext.bookHomeProbSampleCount ?? 0) > 0,
      platoonData: platoonAvailable,
      weatherData: Boolean(phaseRaw?.context?.weather),
    },
    raw: {
      marketHomeProb: marketContext.marketHomeProb ?? marketProjection.homeProb ?? 0.5,
      marketSpreadHome: marketContext.marketSpreadHome ?? marketProjection.spreadHome ?? null,
      marketTotal,
      modelTotalProxy: round3(modelTotalProxy),
      marketMoveMagnitude: round3(Number(marketContext.marketMoveMagnitude ?? 0)),
      movementRowCount: Number(marketContext.movementRowCount ?? 0),
      reverseLineMove: Boolean(marketContext.reverseLineMove),
      steamMove: Boolean(marketContext.steamMove),
      bookHomeProbStddev: round3(Number(marketContext.bookHomeProbStddev ?? 0)),
      bookHomeProbRange: round3(Number(marketContext.bookHomeProbRange ?? 0)),
      bookHomeProbSampleCount: Number(marketContext.bookHomeProbSampleCount ?? 0),
      totalLineRange: round3(Number(marketContext.totalLineRange ?? 0)),
      totalLineSampleCount: Number(marketContext.totalLineSampleCount ?? 0),
      parkRuns,
      parkHr,
      homeBatOps: round3(homeBatOps),
      awayBatOps: round3(awayBatOps),
      homeBatWoba: round3(homeBatWoba),
      awayBatWoba: round3(awayBatWoba),
      homeBatEv: round3(homeBatEv),
      awayBatEv: round3(awayBatEv),
      homeBatBarrel: round3(homeBatBarrel),
      awayBatBarrel: round3(awayBatBarrel),
      homeBatKpct: round3(homeBatKpct),
      awayBatKpct: round3(awayBatKpct),
      homeRecentShape,
      awayRecentShape,
      homeStarterContactRisk,
      awayStarterContactRisk,
      weatherRunBias: round3(weatherRunBias),
      firstFiveSideScore: round3(firstFiveEdge + 0.5),
      fullGameSideScore: round3(fullGameEdge + 0.5),
      bullpenSideScore: round3(bullpenEdge + 0.5),
      applicationHints: phaseRaw?.applicationHints || {},
      homeStarterAvgInnings: round3(homeStarterAvgInnings),
      awayStarterAvgInnings: round3(awayStarterAvgInnings),
      homeStarterRecentAvgInnings: round3(homeStarterRecentAvgInnings),
      awayStarterRecentAvgInnings: round3(awayStarterRecentAvgInnings),
      homeStarterRecentPitches: round3(homeStarterRecentPitches),
      awayStarterRecentPitches: round3(awayStarterRecentPitches),
      homeStarterPitchTrend: round3(homeStarterPitchTrend),
      awayStarterPitchTrend: round3(awayStarterPitchTrend),
      homeStarterRecentVelo: round3(homeStarterRecentVelo),
      awayStarterRecentVelo: round3(awayStarterRecentVelo),
      homeBullpenCoreAvailability: round3(Number(homeBullpenRoles?.coreAvailabilityScore ?? 0.5)),
      awayBullpenCoreAvailability: round3(Number(awayBullpenRoles?.coreAvailabilityScore ?? 0.5)),
      homeBullpenCloserStatus: homeBullpenRoles?.closerStatus ?? null,
      awayBullpenCloserStatus: awayBullpenRoles?.closerStatus ?? null,
    },
  };
}

function countFeatureCoverage(featureCoverage: MlbDirectFeatureVector['featureCoverage']): number {
  const requiredKeys: Array<keyof MlbDirectFeatureVector['featureCoverage']> = [
    'starterData',
    'bullpenData',
    'parkData',
    'marketData',
    'bookDisagreementData',
    'platoonData',
    'weatherData',
  ];
  return requiredKeys.filter((key) => Boolean(featureCoverage[key])).length;
}

function evaluateMlbDirectPolicy(params: {
  homeTeam: string;
  awayTeam: string;
  homeProbability: number;
  f5HomeProbability: number;
  confidence: number;
  calibratedConfidence: number;
  featureVector: MlbDirectFeatureVector;
  supportCount: number;
  conflictCount: number;
  structuralSignal: number;
  policy?: Partial<MlbDirectPolicyConfig>;
}): MlbDirectPolicyEvaluation {
  const policy: MlbDirectPolicyConfig = {
    ...MLB_DIRECT_MODEL_LIVE_POLICY,
    ...(params.policy || {}),
  };
  const featureCoverageCount = countFeatureCoverage(params.featureVector.featureCoverage);
  const featureCoverageScore = round3(featureCoverageCount / 7);
  const marketEdgeAbs = Math.abs(params.featureVector.marketEdge);
  const structuralEdgeAbs = Math.abs(params.structuralSignal);
  const fullGameLeanAbs = Math.abs(params.homeProbability - 0.5);
  const f5LeanAbs = Math.abs(params.f5HomeProbability - 0.5);
  const applicationHints = params.featureVector.raw.applicationHints || {};
  const firstFiveSideScore = Number(params.featureVector.raw.firstFiveSideScore ?? 0.5) - 0.5;
  const fullGameSideScore = Number(params.featureVector.raw.fullGameSideScore ?? 0.5) - 0.5;
  const phaseConflict = sign(firstFiveSideScore) !== 0
    && sign(fullGameSideScore) !== 0
    && sign(firstFiveSideScore) !== sign(fullGameSideScore);

  let recommendedMarket: MlbDirectRecommendedMarket = 'moneyline';
  const reasons: string[] = [];

  if (
    applicationHints.f5SideReady
    && f5LeanAbs >= fullGameLeanAbs + policy.f5OnlySeparation
    && Math.abs(params.featureVector.starterQualityEdge + (params.featureVector.starterLeashEdge * 0.6)) >= policy.minF5Edge
  ) {
    recommendedMarket = 'f5_moneyline';
    reasons.push('f5-only shape: starter edge is materially stronger than the full-game side');
  }

  if (params.calibratedConfidence < policy.minConfidence) reasons.push(`confidence ${round3(params.calibratedConfidence)} < ${policy.minConfidence}`);
  if (marketEdgeAbs < policy.minMarketEdge) reasons.push(`market edge ${round3(marketEdgeAbs)} < ${policy.minMarketEdge}`);
  if (featureCoverageScore < policy.minFeatureCoverageScore) reasons.push(`feature coverage ${featureCoverageScore} < ${policy.minFeatureCoverageScore}`);
  if (structuralEdgeAbs < policy.minStructuralEdge) reasons.push(`structural edge ${round3(structuralEdgeAbs)} < ${policy.minStructuralEdge}`);
  if (params.supportCount < policy.minSupportCount) reasons.push(`support count ${params.supportCount} < ${policy.minSupportCount}`);
  if (params.conflictCount > policy.maxConflictCount) reasons.push(`conflict count ${params.conflictCount} > ${policy.maxConflictCount}`);
  if (phaseConflict && fullGameLeanAbs < 0.07) reasons.push('phase conflict: first five and full-game signals disagree');
  if (recommendedMarket === 'moneyline' && !applicationHints.fullGameSideReady && !params.featureVector.featureCoverage.bullpenData) {
    reasons.push('full-game side not ready: bullpen/full-game support is thin');
  }

  const shouldBet = reasons.length === 0 && recommendedMarket === 'moneyline';
  return {
    shouldBet,
    recommendedMarket: shouldBet ? 'moneyline' : (recommendedMarket === 'f5_moneyline' ? 'f5_moneyline' : 'abstain'),
    featureCoverageScore,
    supportCount: params.supportCount,
    conflictCount: params.conflictCount,
    structuralEdgeAbs: round3(structuralEdgeAbs),
    marketEdgeAbs: round3(marketEdgeAbs),
    reasons,
  };
}

export function scoreMlbDirectModel(params: {
  homeTeam: string;
  awayTeam: string;
  featureVector: MlbDirectFeatureVector;
  config?: Partial<MlbDirectModelConfig>;
  policy?: Partial<MlbDirectPolicyConfig>;
}): MlbDirectDecision {
  const config: MlbDirectModelConfig = {
    ...DEFAULT_MLB_DIRECT_MODEL_CONFIG,
    ...(params.config || {}),
  };
  const f = params.featureVector;
  const starterCompositeEdge = f.starterQualityEdge + (f.starterLeashEdge * 0.6);
  const majorComponents: number[] = [
    starterCompositeEdge * config.starterWeight,
    (f.bullpenQualityEdge + (f.bullpenUsageEdge * 0.75)) * config.bullpenWeight,
    f.offenseSplitEdge * config.offenseWeight,
    f.offenseContactEdge * config.contactWeight,
    f.starterOffenseInteractionEdge * config.interactionWeight,
    f.decompositionEdge * config.decompositionWeight,
    f.impliedTotalDisagreementEdge * config.impliedTotalWeight,
    f.travelRestEdge * config.travelWeight,
    f.parkInteractionEdge * config.parkWeight,
    f.platoonPressureEdge * config.platoonWeight,
    f.weatherEdge * config.weatherWeight,
  ];
  const structuralSignal = majorComponents.reduce((sum, value) => sum + value, 0);
  const structuralDirection = Math.sign(structuralSignal);
  const supportCount = majorComponents.filter((value) => Math.abs(value) >= 0.01 && Math.sign(value) === structuralDirection).length;
  const conflictCount = majorComponents.filter((value) => Math.abs(value) >= 0.01 && Math.sign(value) === -structuralDirection).length;
  const bookConsensusPenalty = clamp(1 - (Number(f.raw.bookHomeProbStddev ?? 0) * 6), 0.55, 1);
  const marketResistance = clamp(1 - (Math.abs(f.marketEdge) * 2.4), 0.4, 1);
  let structuralScale = config.signalScale * bookConsensusPenalty * marketResistance;
  if (supportCount >= 4) structuralScale *= 1.15;
  if (supportCount <= 2) structuralScale *= 0.7;
  if (conflictCount >= 2) structuralScale *= 0.55;

  let marketMicroSignal = f.marketMoveEdge * config.marketMicroWeight;
  if (f.marketMoveEdge !== 0 && structuralDirection !== 0 && Math.sign(f.marketMoveEdge) !== structuralDirection) {
    marketMicroSignal *= 0.35;
  }

  const boundedStructuralAdjustment = clamp(structuralSignal * structuralScale, -config.maxSignalAdjustment, config.maxSignalAdjustment);
  const boundedMarketMicroAdjustment = clamp(marketMicroSignal, -0.025, 0.025);
  const signalAdjustment = boundedStructuralAdjustment + boundedMarketMicroAdjustment;
  const marketBase = f.featureCoverage.marketData
    ? (0.5 + f.marketEdge)
    : 0.5;
  const fallbackStructuralCap = f.featureCoverage.marketData ? config.maxSignalAdjustment : 0.18;
  const effectiveAdjustment = clamp(signalAdjustment, -fallbackStructuralCap, fallbackStructuralCap);
  const homeProbability = round3(clamp(marketBase + effectiveAdjustment, 0.08, 0.92));
  const awayProbability = round3(1 - homeProbability);
  const f5StructuralSignal = (
    starterCompositeEdge * config.starterWeight +
    (f.offenseSplitEdge * config.offenseWeight * 0.45) +
    (f.offenseContactEdge * config.contactWeight * 0.4) +
    (f.starterOffenseInteractionEdge * config.interactionWeight * 0.55) +
    (f.parkInteractionEdge * config.parkWeight * 0.35) +
    (f.weatherEdge * config.weatherWeight * 0.35)
  );
  const f5Adjustment = clamp(f5StructuralSignal * Math.max(config.signalScale, 0.2), -0.12, 0.12);
  const f5HomeProbability = round3(clamp(marketBase + f5Adjustment, 0.08, 0.92));
  const f5AwayProbability = round3(1 - f5HomeProbability);
  const winnerPick = homeProbability >= 0.5 ? params.homeTeam : params.awayTeam;
  const confidence = round3(0.5 + Math.abs(homeProbability - 0.5) * 0.9);
  const marketEdgeAbs = Math.abs(f.marketEdge);
  const structuralEdgeAbs = Math.abs(structuralSignal);
  const confidencePenalty = round3(clamp(
    (confidence >= 0.66 ? Math.max(0, marketEdgeAbs - 0.16) * 0.75 : 0)
    + Math.max(0, 0.07 - structuralEdgeAbs) * 1.15
    + Math.max(0, 4 - supportCount) * 0.025
    + Math.max(0, conflictCount - 1) * 0.02,
    0,
    0.18,
  ));
  const calibratedConfidence = round3(clamp(confidence - confidencePenalty, 0.5, 0.92));

  const contributionPairs: Array<[string, number]> = [
    ['market', f.marketEdge],
    ['starter', starterCompositeEdge * config.starterWeight],
    ['bullpen', (f.bullpenQualityEdge + (f.bullpenUsageEdge * 0.75)) * config.bullpenWeight],
    ['offense', f.offenseSplitEdge * config.offenseWeight],
    ['contact', f.offenseContactEdge * config.contactWeight],
    ['interaction', f.starterOffenseInteractionEdge * config.interactionWeight],
    ['decomposition', f.decompositionEdge * config.decompositionWeight],
    ['market_micro', (f.marketMoveEdge + f.bookDisagreementEdge) * config.marketMicroWeight],
  ];
  const explanation = contributionPairs
    .sort((left, right) => Math.abs(right[1]) - Math.abs(left[1]))
    .slice(0, 5)
    .map(([label, value]) => `${label} ${value >= 0 ? 'leans home' : 'leans away'} (${round3(Number(value))})`);
  const policy = evaluateMlbDirectPolicy({
    homeTeam: params.homeTeam,
    awayTeam: params.awayTeam,
    homeProbability,
    f5HomeProbability,
    confidence,
    calibratedConfidence,
    featureVector: f,
    supportCount,
    conflictCount,
    structuralSignal,
    policy: params.policy,
  });
  if (policy.recommendedMarket === 'f5_moneyline') {
    explanation.unshift(`f5 stronger than full game (${round3(f5HomeProbability - 0.5)} vs ${round3(homeProbability - 0.5)})`);
  }
  if (policy.reasons.length > 0) {
    explanation.push(...policy.reasons.map((reason) => `policy: ${reason}`));
  }

  return {
    available: f.featureCoverage.marketData,
    homeProbability,
    awayProbability,
    f5HomeProbability,
    f5AwayProbability,
    winnerPick,
    confidence,
    calibratedConfidence,
    confidencePenalty,
    featureVector: f,
    explanation,
    policy,
  };
}


function buildMlbF5Contributions(featureVector: MlbDirectFeatureVector, config: MlbF5ModelConfig): Array<[string, number]> {
  const firstFiveLean = (Number(featureVector.raw.firstFiveSideScore ?? 0.5) - 0.5) * config.phaseWeight;
  const contextLean = (
    (featureVector.parkInteractionEdge * 0.6)
    + (featureVector.weatherEdge * 0.25)
    + (featureVector.platoonPressureEdge * 0.15)
  ) * config.contextWeight;
  return [
    ['starter_quality', featureVector.starterQualityEdge * config.starterQualityWeight],
    ['starter_leash', featureVector.starterLeashEdge * config.starterLeashWeight],
    ['offense', featureVector.offenseSplitEdge * config.offenseWeight],
    ['phase', firstFiveLean],
    ['context', contextLean],
    ['market', featureVector.marketEdge * config.marketWeight],
  ];
}

export function scoreMlbF5Model(params: {
  homeTeam: string;
  awayTeam: string;
  featureVector: MlbDirectFeatureVector;
  config?: Partial<MlbF5ModelConfig>;
}): MlbF5Decision {
  const config: MlbF5ModelConfig = {
    ...DEFAULT_MLB_F5_MODEL_CONFIG,
    ...(params.config || {}),
  };
  const contributions = buildMlbF5Contributions(params.featureVector, config);
  const rawEdge = round3(contributions.reduce((sum, [, value]) => sum + value, 0));
  const marketBase = params.featureVector.featureCoverage.marketData
    ? (0.5 + params.featureVector.marketEdge)
    : 0.5;
  const homeProbability = round3(clamp(marketBase + rawEdge, 0.08, 0.92));
  const awayProbability = round3(1 - homeProbability);
  const winnerPick = homeProbability >= 0.5 ? params.homeTeam : params.awayTeam;
  const confidence = round3(0.5 + Math.abs(homeProbability - 0.5) * 0.9);
  const explanation = contributions
    .sort((left, right) => Math.abs(right[1]) - Math.abs(left[1]))
    .slice(0, 5)
    .map(([label, value]) => `${label} ${value >= 0 ? 'leans home' : 'leans away'} (${round3(Number(value))})`);
  return {
    available: params.featureVector.featureCoverage.marketData && params.featureVector.featureCoverage.starterData,
    homeProbability,
    awayProbability,
    winnerPick,
    confidence,
    rawEdge,
    explanation,
  };
}

function buildMlbRunLineContributions(featureVector: MlbDirectFeatureVector, config: MlbRunLineModelConfig): Array<[string, number]> {
  const bullpenComposite = featureVector.bullpenQualityEdge + (featureVector.bullpenUsageEdge * 0.75);
  const phaseLean = ((Number(featureVector.raw.fullGameSideScore ?? 0.5) - 0.5) * 0.55)
    + ((Number(featureVector.raw.bullpenSideScore ?? 0.5) - 0.5) * 0.25)
    + ((Number(featureVector.raw.firstFiveSideScore ?? 0.5) - 0.5) * 0.2);
  const contextLean = (featureVector.parkInteractionEdge * 0.5) + (featureVector.weatherEdge * 0.2) + (featureVector.travelRestEdge * 0.3);
  return [
    ['starter', featureVector.starterQualityEdge * config.starterWeight],
    ['bullpen', bullpenComposite * config.bullpenWeight],
    ['offense', featureVector.offenseSplitEdge * config.offenseWeight],
    ['phase', phaseLean * config.phaseWeight],
    ['context', contextLean * config.contextWeight],
    ['market', featureVector.marketEdge * config.marketWeight],
  ];
}

export function scoreMlbRunLineModel(params: {
  homeTeam: string;
  awayTeam: string;
  featureVector: MlbDirectFeatureVector;
  config?: Partial<MlbRunLineModelConfig>;
}): MlbRunLineDecision {
  const config: MlbRunLineModelConfig = {
    ...DEFAULT_MLB_RUN_LINE_MODEL_CONFIG,
    ...(params.config || {}),
  };
  const homeLine = Number(params.featureVector.raw.marketSpreadHome ?? null);
  const awayLine = Number.isFinite(homeLine) ? -homeLine : null;
  const contributions = buildMlbRunLineContributions(params.featureVector, config);
  const rawEdge = contributions.reduce((sum, [, value]) => sum + value, 0);
  const projectedMargin = round3(clamp(rawEdge * config.marginScale, -8.5, 8.5));
  const atsEdgeHome = Number.isFinite(homeLine) ? round3(projectedMargin + homeLine) : 0;
  const side: 'home' | 'away' = atsEdgeHome >= 0 ? 'home' : 'away';
  const line = side === 'home' ? (Number.isFinite(homeLine) ? homeLine : null) : awayLine;
  const winnerPick = side === 'home' ? params.homeTeam : params.awayTeam;
  const atsEdge = side === 'home' ? atsEdgeHome : round3(-atsEdgeHome);
  const coverProbability = round3(clamp(0.5 + (atsEdge / 3.5), 0.08, 0.92));
  const confidence = round3(0.5 + Math.abs(coverProbability - 0.5) * 0.9);
  const explanation = contributions
    .sort((left, right) => Math.abs(right[1]) - Math.abs(left[1]))
    .slice(0, 5)
    .map(([label, value]) => `${label} ${value >= 0 ? 'leans home' : 'leans away'} (${round3(Number(value))})`);
  return {
    available: Number.isFinite(homeLine) && params.featureVector.featureCoverage.marketData,
    side,
    winnerPick,
    line,
    coverProbability,
    confidence,
    projectedMargin,
    atsEdge,
    explanation,
  };
}


function buildMlbTeamTotalContributions(featureVector: MlbDirectFeatureVector, config: MlbTeamTotalModelConfig): Array<[string, number]> {
  const starterComposite = featureVector.starterQualityEdge + (featureVector.starterLeashEdge * 0.6);
  const bullpenComposite = featureVector.bullpenQualityEdge + (featureVector.bullpenUsageEdge * 0.75);
  const contextComposite = (
    (featureVector.travelRestEdge * 0.35)
    + (featureVector.parkInteractionEdge * 0.2)
    + (featureVector.weatherEdge * 0.2)
    + (featureVector.platoonPressureEdge * 0.1)
    + (featureVector.decompositionEdge * 0.15)
  );
  return [
    ['starter', starterComposite * config.starterWeight],
    ['bullpen', bullpenComposite * config.bullpenWeight],
    ['offense', featureVector.offenseSplitEdge * config.offenseWeight],
    ['context', contextComposite * config.contextWeight],
  ];
}

export function scoreMlbTeamTotalModel(params: {
  homeTeam: string;
  awayTeam: string;
  featureVector: MlbDirectFeatureVector;
  config?: Partial<MlbTeamTotalModelConfig>;
}): MlbTeamTotalDecision {
  const config: MlbTeamTotalModelConfig = {
    ...DEFAULT_MLB_TEAM_TOTAL_MODEL_CONFIG,
    ...(params.config || {}),
  };
  const marketTotal = Number(params.featureVector.raw.marketTotal ?? null);
  const marketHomeProb = Number(params.featureVector.raw.marketHomeProb ?? 0.5);
  const marketSpreadHome = Number(params.featureVector.raw.marketSpreadHome ?? null);
  if (!Number.isFinite(marketTotal)) {
    return {
      available: false,
      market: 'home_over',
      team: 'home',
      direction: 'over',
      line: null,
      projectedTeamTotal: 0,
      projectedGameTotal: 0,
      inferredHomeLine: null,
      inferredAwayLine: null,
      edge: 0,
      probability: 0.5,
      confidence: 0.5,
      explanation: ['market total unavailable'],
    };
  }

  const probHomeShare = clamp(0.5 + ((marketHomeProb - 0.5) * 0.55), 0.36, 0.64);
  const spreadHomeShare = Number.isFinite(marketSpreadHome)
    ? clamp(0.5 + ((-marketSpreadHome) * 0.04), 0.36, 0.64)
    : null;
  const marketHomeShare = clamp(
    average([probHomeShare, ...(spreadHomeShare != null ? [spreadHomeShare] : [])]),
    0.36,
    0.64,
  );

  const contributions = buildMlbTeamTotalContributions(params.featureVector, config);
  const structuralShareDelta = clamp(
    contributions.reduce((sum, [, value]) => sum + value, 0) * config.shareScale,
    -0.18,
    0.18,
  );
  const projectedHomeShare = clamp(marketHomeShare + structuralShareDelta, 0.28, 0.72);
  const projectedAwayShare = 1 - projectedHomeShare;

  const modelTotalProxy = Number(params.featureVector.raw.modelTotalProxy ?? marketTotal);
  const parkRuns = Number(params.featureVector.raw.parkRuns ?? 1);
  const weatherRunBias = Number(params.featureVector.raw.weatherRunBias ?? 0);
  const totalEnvironmentAdj = ((parkRuns - 1) * 1.25) + (weatherRunBias * 0.75);
  const projectedGameTotal = roundToQuarter(clamp(
    marketTotal + ((modelTotalProxy - marketTotal) * config.totalDeltaWeight) + totalEnvironmentAdj,
    4,
    14,
  ));

  const inferredHomeLine = roundToQuarter(marketTotal * marketHomeShare);
  const inferredAwayLine = roundToQuarter(marketTotal - inferredHomeLine);
  const projectedHomeTotal = roundToQuarter(projectedGameTotal * projectedHomeShare);
  const projectedAwayTotal = roundToQuarter(projectedGameTotal * projectedAwayShare);

  const edges: Array<{
    market: MlbTeamTotalMarket;
    team: 'home' | 'away';
    direction: 'over' | 'under';
    line: number;
    projected: number;
    edge: number;
  }> = [
    {
      market: 'home_over',
      team: 'home',
      direction: 'over',
      line: inferredHomeLine,
      projected: projectedHomeTotal,
      edge: round3(projectedHomeTotal - inferredHomeLine),
    },
    {
      market: 'home_under',
      team: 'home',
      direction: 'under',
      line: inferredHomeLine,
      projected: projectedHomeTotal,
      edge: round3(inferredHomeLine - projectedHomeTotal),
    },
    {
      market: 'away_over',
      team: 'away',
      direction: 'over',
      line: inferredAwayLine,
      projected: projectedAwayTotal,
      edge: round3(projectedAwayTotal - inferredAwayLine),
    },
    {
      market: 'away_under',
      team: 'away',
      direction: 'under',
      line: inferredAwayLine,
      projected: projectedAwayTotal,
      edge: round3(inferredAwayLine - projectedAwayTotal),
    },
  ];
  const best = edges.sort((left, right) => Math.abs(right.edge) - Math.abs(left.edge))[0];
  const probability = round3(clamp(0.5 + (best.edge / 2.2), 0.08, 0.92));
  const confidence = round3(0.5 + Math.abs(probability - 0.5) * 0.9);
  const explanation = [
    `projected ${best.team} team total ${best.projected} vs inferred line ${best.line}`,
    ...contributions
      .sort((left, right) => Math.abs(right[1]) - Math.abs(left[1]))
      .slice(0, 4)
      .map(([label, value]) => `${label} ${value >= 0 ? 'leans home scoring' : 'leans away scoring'} (${round3(Number(value))})`),
    `projected game total ${projectedGameTotal} vs market ${roundToQuarter(marketTotal)}`,
  ];

  return {
    available: params.featureVector.featureCoverage.marketData,
    market: best.market,
    team: best.team,
    direction: best.direction,
    line: best.line,
    projectedTeamTotal: best.projected,
    projectedGameTotal,
    inferredHomeLine,
    inferredAwayLine,
    edge: best.edge,
    probability,
    confidence,
    explanation,
  };
}

export const MLB_RUN_LINE_MODEL_DEFAULTS = DEFAULT_MLB_RUN_LINE_MODEL_CONFIG;
export const MLB_F5_MODEL_DEFAULTS = DEFAULT_MLB_F5_MODEL_CONFIG;
export const MLB_DIRECT_MODEL_DEFAULTS = DEFAULT_MLB_DIRECT_MODEL_CONFIG;
export const MLB_TEAM_TOTAL_MODEL_DEFAULTS = DEFAULT_MLB_TEAM_TOTAL_MODEL_CONFIG;
