/**
 * RIE Signal: MLB Phase Model
 *
 * Splits baseball into the pieces that matter structurally:
 * - First five innings: probable starter vs offense
 * - Bullpen bridge: relievers vs offense after the starter exits
 * - Context: weather, travel/rest, injuries, lineup certainty
 *
 * This signal is designed so F5 sides/totals and pitcher-driven prop logic
 * can later reuse the same raw phase outputs instead of rebuilding the model.
 */

import pool from '../../../db';
import { Signal, SignalResult, MatchupContext } from '../types';
import {
  getBullpenPitching,
  getStarterProfile,
  getStarterRecentProfile,
  getTeamBatting,
  getTeamBattingProjection,
} from '../../fangraphs';
import { getMlbProbableStarters } from '../../espn-mlb';
import { fetchGameWeather } from '../../weather';
import { getVenue } from '../../venue';
import { getMlbBullpenWorkload } from '../../mlb-bullpen';


type MlbLineupPlayer = {
  name: string;
  position?: string | null;
  injuryStatus?: string | null;
};

export type MlbLineupPlayerProfile = {
  slot: number;
  name: string;
  position: string | null;
  injuryStatus: string | null;
  bats: 'L' | 'R' | 'S' | null;
  ops: number | null;
  xwoba: number | null;
  exitVelo: number | null;
  barrelPct: number | null;
  kPct: number | null;
  qualityScore: number | null;
};

export type MlbLineupTeamProfile = {
  playerCount: number;
  profiledCount: number;
  leftCount: number;
  rightCount: number;
  switchCount: number;
  topOrderQuality: number | null;
  fullLineupQuality: number | null;
  topOrderKpct: number | null;
  fullLineupKpct: number | null;
  vsLeftPressure: number | null;
  vsRightPressure: number | null;
  topOrderVsLeftPressure: number | null;
  topOrderVsRightPressure: number | null;
  players: MlbLineupPlayerProfile[];
};

export type MlbLineupSnapshot = {
  homeStatus: string | null;
  awayStatus: string | null;
  homePlayers: MlbLineupPlayer[];
  awayPlayers: MlbLineupPlayer[];
  homeProfile: MlbLineupTeamProfile | null;
  awayProfile: MlbLineupTeamProfile | null;
  source: string | null;
  updatedAt: string | null;
};

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

function normalize(val: number, lo: number, hi: number): number {
  return clamp((val - lo) / (hi - lo), 0, 1);
}

function getSeasonForDate(startsAt: string): number {
  const dt = new Date(startsAt || Date.now());
  if (dt.getMonth() < 3 || (dt.getMonth() === 2 && dt.getDate() < 25)) {
    return dt.getFullYear() - 1;
  }
  return dt.getFullYear();
}

function getProjectionSeason(startsAt: string): number {
  return new Date(startsAt || Date.now()).getFullYear();
}

function getGameDateEt(startsAt: string): string {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(new Date(startsAt || Date.now()));
}



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

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function average(values: Array<number | null | undefined>): number | null {
  const usable = values.filter(isFiniteNumber);
  if (usable.length === 0) return null;
  return usable.reduce((sum, value) => sum + value, 0) / usable.length;
}

function normalizeMlbPlayerName(name: string): string {
  return String(name || '')
    .normalize('NFD')
    .replace(/[̀-ͯ]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '');
}

function parseLineupPlayers(value: any): MlbLineupPlayer[] {
  if (!Array.isArray(value)) return [];
  return value
    .map((entry) => ({
      name: String(entry?.name || '').trim(),
      position: entry?.position ? String(entry.position) : null,
      injuryStatus: entry?.injuryStatus ? String(entry.injuryStatus) : null,
    }))
    .filter((entry) => entry.name.length > 0)
    .slice(0, 9);
}

function buildLineupQualityScore(row: {
  ops: number | null;
  xwoba: number | null;
  exitVelo: number | null;
  barrelPct: number | null;
  kPct: number | null;
}): number | null {
  const components = [
    isFiniteNumber(row.ops) ? normalize(row.ops, 0.62, 0.92) : null,
    isFiniteNumber(row.xwoba) ? normalize(row.xwoba, 0.28, 0.40) : null,
    isFiniteNumber(row.exitVelo) ? normalize(row.exitVelo, 86, 93) : null,
    isFiniteNumber(row.barrelPct) ? normalize(row.barrelPct, 2.5, 15) : null,
    isFiniteNumber(row.kPct) ? (1 - normalize(row.kPct, 14, 31)) : null,
  ].filter(isFiniteNumber);
  if (components.length === 0) return null;
  return round3(components.reduce((sum, value) => sum + value, 0) / components.length);
}

function handedPressureScore(bats: 'L' | 'R' | 'S' | null, starterThrows: 'L' | 'R' | null): number {
  if (!bats || !starterThrows) return 0.5;
  if (bats === 'S') return 0.68;
  if (bats !== starterThrows) return 0.62;
  return 0.42;
}

function summarizeLineupProfiles(params: {
  players: MlbLineupPlayer[];
  seasonRows: Array<{
    player_name: string;
    bats: 'L' | 'R' | 'S' | null;
    ops: number | null;
    xwoba: number | null;
    exit_velo: number | null;
    barrel_pct: number | null;
    k_pct: number | null;
  }>;
}): MlbLineupTeamProfile | null {
  if (!params.players.length) return null;
  const rowByName = new Map(
    params.seasonRows.map((row) => [normalizeMlbPlayerName(row.player_name), row]),
  );
  const slotWeights = [1.35, 1.28, 1.2, 1.14, 1.08, 0.98, 0.88, 0.78, 0.7];
  const profiledPlayers: MlbLineupPlayerProfile[] = params.players.map((player, index) => {
    const seasonRow = rowByName.get(normalizeMlbPlayerName(player.name));
    const qualityScore = buildLineupQualityScore({
      ops: seasonRow?.ops ?? null,
      xwoba: seasonRow?.xwoba ?? null,
      exitVelo: seasonRow?.exit_velo ?? null,
      barrelPct: seasonRow?.barrel_pct ?? null,
      kPct: seasonRow?.k_pct ?? null,
    });
    return {
      slot: index + 1,
      name: player.name,
      position: player.position || null,
      injuryStatus: player.injuryStatus || null,
      bats: seasonRow?.bats ?? null,
      ops: seasonRow?.ops ?? null,
      xwoba: seasonRow?.xwoba ?? null,
      exitVelo: seasonRow?.exit_velo ?? null,
      barrelPct: seasonRow?.barrel_pct ?? null,
      kPct: seasonRow?.k_pct ?? null,
      qualityScore,
    };
  });
  const weightAverage = (values: Array<number | null | undefined>, topOrderOnly = false): number | null => {
    let weighted = 0;
    let total = 0;
    const maxIndex = topOrderOnly ? 5 : profiledPlayers.length;
    for (let i = 0; i < profiledPlayers.length && i < maxIndex; i += 1) {
      const value = values[i];
      if (!isFiniteNumber(value)) continue;
      const weight = slotWeights[i] ?? 0.7;
      weighted += value * weight;
      total += weight;
    }
    return total > 0 ? round3(weighted / total) : null;
  };
  const matchupPressureAverage = (starterThrows: 'L' | 'R' | null, topOrderOnly = false): number | null => {
    const values = profiledPlayers.map((player) => {
      const handScore = handedPressureScore(player.bats, starterThrows);
      const qualityScore = player.qualityScore ?? 0.5;
      return round3((qualityScore * 0.72) + (handScore * 0.28));
    });
    return weightAverage(values, topOrderOnly);
  };

  return {
    playerCount: profiledPlayers.length,
    profiledCount: profiledPlayers.filter((player) => player.qualityScore != null).length,
    leftCount: profiledPlayers.filter((player) => player.bats === 'L').length,
    rightCount: profiledPlayers.filter((player) => player.bats === 'R').length,
    switchCount: profiledPlayers.filter((player) => player.bats === 'S').length,
    topOrderQuality: weightAverage(profiledPlayers.map((player) => player.qualityScore), true),
    fullLineupQuality: weightAverage(profiledPlayers.map((player) => player.qualityScore), false),
    topOrderKpct: weightAverage(profiledPlayers.map((player) => player.kPct != null ? Number(player.kPct) / 100 : null), true),
    fullLineupKpct: weightAverage(profiledPlayers.map((player) => player.kPct != null ? Number(player.kPct) / 100 : null), false),
    vsLeftPressure: matchupPressureAverage('L', false),
    vsRightPressure: matchupPressureAverage('R', false),
    topOrderVsLeftPressure: matchupPressureAverage('L', true),
    topOrderVsRightPressure: matchupPressureAverage('R', true),
    players: profiledPlayers,
  };
}

function weightedInjuryScore(status: string): number {
  switch ((status || '').toLowerCase()) {
    case 'out':
    case 'ir':
    case 'suspension':
      return 1.0;
    case 'doubtful':
    case 'gtd':
      return 0.75;
    case 'questionable':
    case 'day-to-day':
    case 'dtd':
      return 0.4;
    default:
      return 0.15;
  }
}

function haversineMiles(a: { lat: number; lon: number }, b: { lat: number; lon: number }): number {
  const toRad = (deg: number) => deg * (Math.PI / 180);
  const r = 3958.8;
  const dLat = toRad(b.lat - a.lat);
  const dLon = toRad(b.lon - a.lon);
  const lat1 = toRad(a.lat);
  const lat2 = toRad(b.lat);
  const h = Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
  return 2 * r * Math.asin(Math.sqrt(h));
}

async function fetchInjurySummary(ctx: MatchupContext): Promise<{
  homeSeverity: number;
  awaySeverity: number;
  homeCount: number;
  awayCount: number;
}> {
  const { rows } = await pool.query(
    `SELECT team, LOWER(status) AS status
     FROM (
       SELECT team, status, "playerName" AS player_name
       FROM "PlayerInjury"
       WHERE league = 'mlb'
         AND (team ILIKE $1 OR team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4)
         AND LOWER(status) IN ('out', 'ir', 'suspension', 'doubtful', 'questionable', 'day-to-day', 'gtd', 'dtd')
       UNION
       SELECT team, status, player_name
       FROM sportsclaw.injuries
       WHERE league = 'mlb'
         AND is_active = true
         AND (team ILIKE $1 OR team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4)
         AND LOWER(status) IN ('out', 'ir', 'suspension', 'doubtful', 'questionable', 'day-to-day', 'gtd', 'dtd')
     ) injury_union`,
    [`%${ctx.homeTeam}%`, `%${ctx.awayTeam}%`, ctx.homeShort, ctx.awayShort],
  ).catch(() => ({ rows: [] as Array<{ team: string; status: string }> }));

  let homeSeverity = 0;
  let awaySeverity = 0;
  let homeCount = 0;
  let awayCount = 0;

  for (const row of rows) {
    const team = (row.team || '').toUpperCase();
    const weight = weightedInjuryScore(row.status);
    if (team.includes(ctx.homeShort.toUpperCase()) || team.includes(ctx.homeTeam.toUpperCase())) {
      homeSeverity += weight;
      homeCount += 1;
    } else if (team.includes(ctx.awayShort.toUpperCase()) || team.includes(ctx.awayTeam.toUpperCase())) {
      awaySeverity += weight;
      awayCount += 1;
    }
  }

  return { homeSeverity, awaySeverity, homeCount, awayCount };
}

export async function fetchMlbLineupSnapshot(ctx: MatchupContext, season: number): Promise<MlbLineupSnapshot> {
  const gameDate = getGameDateEt(ctx.startsAt || new Date().toISOString());
  const { rows } = await pool.query(
    `SELECT "homeStatus", "awayStatus", "homePlayers", "awayPlayers", source, "updatedAt"
     FROM "GameLineup"
     WHERE league = 'mlb'
       AND "gameDate" = $1
       AND (("homeTeam" ILIKE $2 AND "awayTeam" ILIKE $3)
         OR ("homeTeam" ILIKE $4 AND "awayTeam" ILIKE $5)
         OR ("homeTeam" ILIKE $6 AND "awayTeam" ILIKE $7))
     ORDER BY CASE
       WHEN LOWER(COALESCE("homeStatus", '')) LIKE '%confirmed%' AND LOWER(COALESCE("awayStatus", '')) LIKE '%confirmed%' THEN 0
       WHEN LOWER(COALESCE("homeStatus", '')) LIKE '%confirmed%' OR LOWER(COALESCE("awayStatus", '')) LIKE '%confirmed%' THEN 1
       ELSE 2
     END,
     (jsonb_array_length(COALESCE("homePlayers", '[]'::jsonb)) + jsonb_array_length(COALESCE("awayPlayers", '[]'::jsonb))) DESC,
     "updatedAt" DESC
     LIMIT 1`,
    [
      gameDate,
      `%${ctx.homeTeam}%`,
      `%${ctx.awayTeam}%`,
      `%${ctx.homeShort}%`,
      `%${ctx.awayShort}%`,
      `%${ctx.awayTeam}%`,
      `%${ctx.homeTeam}%`,
    ],
  ).catch(() => ({ rows: [] as Array<any> }));

  const row = rows[0] || {};
  const homePlayers = parseLineupPlayers(row.homePlayers);
  const awayPlayers = parseLineupPlayers(row.awayPlayers);
  const lookupNames = [...new Set([...homePlayers, ...awayPlayers].map((player) => String(player.name || '').trim().toLowerCase()).filter(Boolean))];
  const hitterRows = lookupNames.length > 0
    ? (await pool.query(
        `SELECT DISTINCT ON (LOWER(player_name))
                player_name,
                bats,
                ops,
                xwoba,
                exit_velo,
                barrel_pct,
                k_pct,
                season
           FROM "HitterSeason"
          WHERE season <= $1
            AND LOWER(player_name) = ANY($2::text[])
          ORDER BY LOWER(player_name), season DESC, updated_at DESC NULLS LAST`,
        [season, lookupNames],
      ).catch(() => ({ rows: [] as Array<any> }))).rows
    : [];

  return {
    homeStatus: row.homeStatus || null,
    awayStatus: row.awayStatus || null,
    homePlayers,
    awayPlayers,
    homeProfile: summarizeLineupProfiles({ players: homePlayers, seasonRows: hitterRows }),
    awayProfile: summarizeLineupProfiles({ players: awayPlayers, seasonRows: hitterRows }),
    source: row.source || null,
    updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,
  };
}

async function fetchTeamTravelContext(teamShort: string, startsAt: string): Promise<{
  hoursSinceLastGame: number | null;
  travelMiles: number | null;
  wasAwayLastGame: boolean | null;
}> {
  const { rows } = await pool.query(
    `SELECT home_short, away_short, starts_at
     FROM rm_events
     WHERE league = 'mlb'
       AND starts_at < $2
       AND starts_at >= $2::timestamptz - INTERVAL '7 days'
       AND (home_short = $1 OR away_short = $1)
     ORDER BY starts_at DESC
     LIMIT 1`,
    [teamShort.toUpperCase(), startsAt],
  ).catch(() => ({ rows: [] as Array<{ home_short: string; away_short: string; starts_at: string }> }));

  if (!rows[0]) {
    return { hoursSinceLastGame: null, travelMiles: null, wasAwayLastGame: null };
  }

  const lastGame = rows[0];
  const currentVenue = getVenue(teamShort, 'mlb');
  const prevVenue = getVenue(lastGame.home_short, 'mlb');
  const travelMiles = currentVenue && prevVenue
    ? Math.round(haversineMiles(prevVenue, currentVenue))
    : null;
  const hoursSinceLastGame = Math.round(
    (new Date(startsAt).getTime() - new Date(lastGame.starts_at).getTime()) / 3_600_000
  );

  return {
    hoursSinceLastGame,
    travelMiles,
    wasAwayLastGame: lastGame.away_short === teamShort.toUpperCase(),
  };
}

async function fetchStarterRecentUsage(name: string | null, teamShort: string, startsAt: string): Promise<{
  startsSample: number;
  avgInnings: number | null;
  avgPitches: number | null;
  avgStrikes: number | null;
  lastStartInnings: number | null;
  lastStartPitches: number | null;
  lastStartStrikes: number | null;
  inningsTrend: number | null;
  pitchTrend: number | null;
  strikePct: number | null;
} | null> {
  if (!name || !teamShort) return null;

  const { rows } = await pool.query(
    `WITH game_stats AS (
       SELECT
         COALESCE("canonicalGameId", 0) AS game_id,
         "gameDate"::date AS game_date,
         MAX(CASE WHEN "statKey" = 'mlb_innings_pitched' THEN value END) AS innings_pitched,
         MAX(CASE WHEN "statKey" = 'mlb_pitches' THEN value END) AS pitches,
         MAX(CASE WHEN "statKey" = 'mlb_strikes' THEN value END) AS strikes
       FROM "PlayerGameMetric"
       WHERE league = 'mlb'
         AND position = 'P'
         AND "gameDate" < $3::timestamp
         AND (LOWER("playerName") = LOWER($1) OR "playerName" ILIKE '%' || $1 || '%')
         AND (team = $2 OR team ILIKE $4 OR team ILIKE '%' || $2 || '%')
         AND "statKey" IN ('mlb_innings_pitched', 'mlb_pitches', 'mlb_strikes')
       GROUP BY COALESCE("canonicalGameId", 0), "gameDate"::date
     )
     SELECT *
     FROM game_stats
     WHERE COALESCE(pitches, 0) >= 50 OR COALESCE(innings_pitched, 0) >= 3
     ORDER BY game_date DESC
     LIMIT 3`,
    [name, teamShort.toUpperCase(), startsAt, `%${teamShort.toUpperCase()}%`],
  ).catch(() => ({ rows: [] as Array<{ game_date: string; innings_pitched: number | null; pitches: number | null; strikes: number | null }> }));

  if (!rows.length) return null;

  const innings = rows.map((row: any) => Number(row.innings_pitched || 0)).filter((value: number) => value > 0);
  const pitches = rows.map((row: any) => Number(row.pitches || 0)).filter((value: number) => value > 0);
  const strikes = rows.map((row: any) => Number(row.strikes || 0)).filter((value: number) => value > 0);
  const avg = (values: number[]) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
  const priorInnings = innings.slice(1);
  const priorPitches = pitches.slice(1);
  const avgPriorInnings = avg(priorInnings);
  const avgPriorPitches = avg(priorPitches);
  const avgPitches = avg(pitches);
  const avgStrikes = avg(strikes);

  return {
    startsSample: rows.length,
    avgInnings: avg(innings),
    avgPitches,
    avgStrikes,
    lastStartInnings: innings[0] ?? null,
    lastStartPitches: pitches[0] ?? null,
    lastStartStrikes: strikes[0] ?? null,
    inningsTrend: avgPriorInnings != null && innings[0] != null ? Number((innings[0] - avgPriorInnings).toFixed(2)) : null,
    pitchTrend: avgPriorPitches != null && pitches[0] != null ? Number((pitches[0] - avgPriorPitches).toFixed(2)) : null,
    strikePct: avgPitches && avgStrikes ? Number((avgStrikes / avgPitches).toFixed(3)) : null,
  };
}

export const mlbPhaseSignal: Signal = {
  id: 'mlb_phase',
  name: 'MLB Phase Model',
  supportedLeagues: ['mlb'],

  async collect(ctx: MatchupContext): Promise<SignalResult> {
    const start = Date.now();
    try {
      const statsSeason = getSeasonForDate(ctx.startsAt);
      const projectionSeason = getProjectionSeason(ctx.startsAt);
      const homeShort = ctx.homeShort?.toUpperCase() || '';
      const awayShort = ctx.awayShort?.toUpperCase() || '';

      if (!homeShort || !awayShort) {
        return {
          signalId: 'mlb_phase',
          score: 0.5,
          weight: 0,
          available: false,
          rawData: { error: 'No team abbreviations' },
          metadata: { latencyMs: Date.now() - start, source: 'db', freshness: '' },
        };
      }

      const venue = getVenue(homeShort, 'mlb');
      const weatherPromise = venue && !venue.indoor
        ? fetchGameWeather(venue.lat, venue.lon, ctx.startsAt).catch(() => null)
        : Promise.resolve(null);

      const probables = await getMlbProbableStarters({
        homeShort,
        awayShort,
        startsAt: ctx.startsAt,
      });

      const homeStarterName = probables?.homeStarter?.name || null;
      const awayStarterName = probables?.awayStarter?.name || null;

      const [
        homeStarter,
        awayStarter,
        homeStarterRecent,
        awayStarterRecent,
        homeStarterUsage,
        awayStarterUsage,
        homeBat,
        awayBat,
        homeBatRecent,
        awayBatRecent,
        homeBatProj,
        awayBatProj,
        homeBullpen,
        awayBullpen,
        homeBullpenWorkload,
        awayBullpenWorkload,
        homeTravel,
        awayTravel,
        injuries,
        lineupStatus,
        weather,
      ] = await Promise.all([
        homeStarterName ? getStarterProfile(homeStarterName, homeShort, statsSeason) : Promise.resolve(null),
        awayStarterName ? getStarterProfile(awayStarterName, awayShort, statsSeason) : Promise.resolve(null),
        homeStarterName ? getStarterRecentProfile(homeStarterName, homeShort, statsSeason) : Promise.resolve(null),
        awayStarterName ? getStarterRecentProfile(awayStarterName, awayShort, statsSeason) : Promise.resolve(null),
        fetchStarterRecentUsage(homeStarterName, homeShort, ctx.startsAt),
        fetchStarterRecentUsage(awayStarterName, awayShort, ctx.startsAt),
        getTeamBatting(homeShort, statsSeason, 'full'),
        getTeamBatting(awayShort, statsSeason, 'full'),
        getTeamBatting(homeShort, statsSeason, 'last14'),
        getTeamBatting(awayShort, statsSeason, 'last14'),
        getTeamBattingProjection(homeShort, projectionSeason),
        getTeamBattingProjection(awayShort, projectionSeason),
        getBullpenPitching(homeShort, statsSeason, 'full'),
        getBullpenPitching(awayShort, statsSeason, 'full'),
        getMlbBullpenWorkload(homeShort, ctx.startsAt),
        getMlbBullpenWorkload(awayShort, ctx.startsAt),
        fetchTeamTravelContext(homeShort, ctx.startsAt),
        fetchTeamTravelContext(awayShort, ctx.startsAt),
        fetchInjurySummary(ctx),
        fetchMlbLineupSnapshot(ctx, statsSeason),
        weatherPromise,
      ]);

      let firstFiveScore = 0.5;
      if (homeStarter && awayStarter) {
        const starterEdge =
          ((awayStarter.fip - homeStarter.fip) * 0.35) +
          ((awayStarter.xfip - homeStarter.xfip) * 0.18) +
          ((homeStarter.kBbPct - awayStarter.kBbPct) * 2.0 * 0.17) +
          ((awayStarter.whip - homeStarter.whip) * 0.10);
        const recentStarterEdge =
          (((awayStarterRecent?.fip ?? awayStarter.fip) - (homeStarterRecent?.fip ?? homeStarter.fip)) * 0.30) +
          (((awayStarterRecent?.xfip ?? awayStarter.xfip) - (homeStarterRecent?.xfip ?? homeStarter.xfip)) * 0.12) +
          ((((homeStarterRecent?.kBbPct ?? homeStarter.kBbPct) - (awayStarterRecent?.kBbPct ?? awayStarter.kBbPct)) * 2.0) * 0.12) +
          (((homeStarterRecent?.fbVelo ?? homeStarter.fbVelo) - (awayStarterRecent?.fbVelo ?? awayStarter.fbVelo)) * 0.03);
        const starterLeashEdge =
          (((homeStarterUsage?.avgInnings ?? (homeStarter.gs > 0 ? homeStarter.ip / homeStarter.gs : 5.2)) - (awayStarterUsage?.avgInnings ?? (awayStarter.gs > 0 ? awayStarter.ip / awayStarter.gs : 5.2))) * 0.55) +
          (((homeStarterUsage?.avgPitches ?? 85) - (awayStarterUsage?.avgPitches ?? 85)) / 18 * 0.25) +
          (((homeStarterUsage?.pitchTrend ?? 0) - (awayStarterUsage?.pitchTrend ?? 0)) / 15 * 0.20);
        const offenseEdge =
          (((homeBatRecent?.wrcPlus ?? homeBat?.wrcPlus ?? 100) - (awayBatRecent?.wrcPlus ?? awayBat?.wrcPlus ?? 100)) * 0.65) +
          (((homeBatProj?.projWrcPlus ?? homeBat?.wrcPlus ?? 100) - (awayBatProj?.projWrcPlus ?? awayBat?.wrcPlus ?? 100)) * 0.35);
        firstFiveScore = clamp(
          normalize(starterEdge, -1.8, 1.8) * 0.40 +
          normalize(recentStarterEdge, -1.6, 1.6) * 0.22 +
          normalize(starterLeashEdge, -1.2, 1.2) * 0.13 +
          normalize(offenseEdge, -25, 25) * 0.25,
          0,
          1
        );
      }

      let bullpenScore = 0.5;
      if (homeBullpen && awayBullpen) {
        const bullpenPitchingEdge =
          ((awayBullpen.fip - homeBullpen.fip) * 0.50) +
          ((awayBullpen.xfip - homeBullpen.xfip) * 0.18) +
          ((homeBullpen.kPct - awayBullpen.kPct) * 2.0 * 0.14) +
          ((awayBullpen.whip - homeBullpen.whip) * 0.10);
        const bullpenOffenseEdge =
          (((homeBat?.wrcPlus ?? 100) - (awayBat?.wrcPlus ?? 100)) * 0.5) +
          (((homeBatProj?.projWrcPlus ?? 100) - (awayBatProj?.projWrcPlus ?? 100)) * 0.5);
        const bullpenFatigueEdge = (awayBullpenWorkload?.fatigueScore ?? 0.5) - (homeBullpenWorkload?.fatigueScore ?? 0.5);
        const bullpenRoleEdge = (homeBullpenWorkload?.roleAvailability?.coreAvailabilityScore ?? 0.5) - (awayBullpenWorkload?.roleAvailability?.coreAvailabilityScore ?? 0.5);
        bullpenScore = clamp(
          normalize(bullpenPitchingEdge, -1.6, 1.6) * 0.55 +
          normalize(bullpenOffenseEdge, -20, 20) * 0.22 +
          normalize(bullpenFatigueEdge, -0.5, 0.5) * 0.10 +
          normalize(bullpenRoleEdge, -0.6, 0.6) * 0.13,
          0,
          1
        );
      }

      const travelEdge =
        normalize((awayTravel.travelMiles ?? 0) - (homeTravel.travelMiles ?? 0), -1500, 1500) * 0.45 +
        normalize((homeTravel.hoursSinceLastGame ?? 36) - (awayTravel.hoursSinceLastGame ?? 36), -24, 24) * 0.25 +
        normalize((awayTravel.wasAwayLastGame ? 1 : 0) - (homeTravel.wasAwayLastGame ? 1 : 0), -1, 1) * 0.30;

      const injuryEdge = normalize(injuries.awaySeverity - injuries.homeSeverity, -4, 4);
      const lineupEdge = normalize(
        ((lineupStatus.homeStatus || '').toLowerCase().includes('confirmed') ? 1 : 0) -
        ((lineupStatus.awayStatus || '').toLowerCase().includes('confirmed') ? 1 : 0),
        -1,
        1
      );
      const contextScore = clamp(
        travelEdge * 0.45 +
        injuryEdge * 0.45 +
        lineupEdge * 0.10,
        0,
        1
      );

      const wx = weather?.gameTime || weather?.current || null;
      const weatherRunBias = wx
        ? clamp(
            ((wx.wind_mph >= 15 ? 0.08 : 0) +
            (wx.temperature_f >= 82 ? 0.04 : 0) -
            (wx.temperature_f <= 48 ? 0.05 : 0) -
            (wx.precipitation_chance >= 50 ? 0.04 : 0)),
            -0.15,
            0.15
          )
        : 0;

      const score = clamp(
        firstFiveScore * 0.50 +
        bullpenScore * 0.30 +
        contextScore * 0.20,
        0,
        1
      );

      const hasData = !!(
        homeStarter || awayStarter ||
        homeBullpen || awayBullpen ||
        homeBat || awayBat ||
        weather || injuries.homeCount || injuries.awayCount
      );

      return {
        signalId: 'mlb_phase',
        score,
        weight: 0,
        available: hasData,
        rawData: {
          firstFive: {
            sideScore: firstFiveScore,
            homeStarter: homeStarter ? {
              name: homeStarter.name,
              throws: homeStarter.throws,
              fip: homeStarter.fip,
              xfip: homeStarter.xfip,
              whip: homeStarter.whip,
              kBbPct: homeStarter.kBbPct,
              ev: homeStarter.ev,
              hardHitPct: homeStarter.hardHitPct,
              barrelPct: homeStarter.barrelPct,
              gbPct: homeStarter.gbPct,
              fbPct: homeStarter.fbPct,
              hrFb: homeStarter.hrFb,
              fbVelo: homeStarter.fbVelo,
              ip: homeStarter.ip,
              gs: homeStarter.gs,
              avgInningsStart: homeStarter.gs > 0 ? Number((homeStarter.ip / homeStarter.gs).toFixed(2)) : null,
              recent30: homeStarterRecent ? {
                throws: homeStarterRecent.throws,
                fip: homeStarterRecent.fip,
                xfip: homeStarterRecent.xfip,
                whip: homeStarterRecent.whip,
                kBbPct: homeStarterRecent.kBbPct,
                ev: homeStarterRecent.ev,
                hardHitPct: homeStarterRecent.hardHitPct,
                barrelPct: homeStarterRecent.barrelPct,
                gbPct: homeStarterRecent.gbPct,
                fbPct: homeStarterRecent.fbPct,
                hrFb: homeStarterRecent.hrFb,
                fbVelo: homeStarterRecent.fbVelo,
                avgInningsStart: homeStarterRecent.gs > 0 ? Number((homeStarterRecent.ip / homeStarterRecent.gs).toFixed(2)) : null,
              } : null,
              recentUsage: homeStarterUsage,
            } : homeStarterName ? { name: homeStarterName, recentUsage: homeStarterUsage } : null,
            awayStarter: awayStarter ? {
              name: awayStarter.name,
              throws: awayStarter.throws,
              fip: awayStarter.fip,
              xfip: awayStarter.xfip,
              whip: awayStarter.whip,
              kBbPct: awayStarter.kBbPct,
              ev: awayStarter.ev,
              hardHitPct: awayStarter.hardHitPct,
              barrelPct: awayStarter.barrelPct,
              gbPct: awayStarter.gbPct,
              fbPct: awayStarter.fbPct,
              hrFb: awayStarter.hrFb,
              fbVelo: awayStarter.fbVelo,
              ip: awayStarter.ip,
              gs: awayStarter.gs,
              avgInningsStart: awayStarter.gs > 0 ? Number((awayStarter.ip / awayStarter.gs).toFixed(2)) : null,
              recent30: awayStarterRecent ? {
                throws: awayStarterRecent.throws,
                fip: awayStarterRecent.fip,
                xfip: awayStarterRecent.xfip,
                whip: awayStarterRecent.whip,
                kBbPct: awayStarterRecent.kBbPct,
                ev: awayStarterRecent.ev,
                hardHitPct: awayStarterRecent.hardHitPct,
                barrelPct: awayStarterRecent.barrelPct,
                gbPct: awayStarterRecent.gbPct,
                fbPct: awayStarterRecent.fbPct,
                hrFb: awayStarterRecent.hrFb,
                fbVelo: awayStarterRecent.fbVelo,
                avgInningsStart: awayStarterRecent.gs > 0 ? Number((awayStarterRecent.ip / awayStarterRecent.gs).toFixed(2)) : null,
              } : null,
              recentUsage: awayStarterUsage,
            } : awayStarterName ? { name: awayStarterName, recentUsage: awayStarterUsage } : null,
            homeOffense: {
              fullWrcPlus: homeBat?.wrcPlus ?? null,
              recentWrcPlus: homeBatRecent?.wrcPlus ?? null,
              projectedWrcPlus: homeBatProj?.projWrcPlus ?? null,
            },
            awayOffense: {
              fullWrcPlus: awayBat?.wrcPlus ?? null,
              recentWrcPlus: awayBatRecent?.wrcPlus ?? null,
              projectedWrcPlus: awayBatProj?.projWrcPlus ?? null,
            },
          },
          bullpen: {
            sideScore: bullpenScore,
            homeBullpen: homeBullpen ? {
              fip: homeBullpen.fip,
              xfip: homeBullpen.xfip,
              whip: homeBullpen.whip,
              kPct: homeBullpen.kPct,
              relieverCount: homeBullpen.relieverCount,
              workload: homeBullpenWorkload,
              roleAvailability: homeBullpenWorkload?.roleAvailability || null,
            } : null,
            awayBullpen: awayBullpen ? {
              fip: awayBullpen.fip,
              xfip: awayBullpen.xfip,
              whip: awayBullpen.whip,
              kPct: awayBullpen.kPct,
              relieverCount: awayBullpen.relieverCount,
              workload: awayBullpenWorkload,
              roleAvailability: awayBullpenWorkload?.roleAvailability || null,
            } : null,
          },
          context: {
            sideScore: contextScore,
            weather: wx ? {
              temperatureF: wx.temperature_f,
              windMph: wx.wind_mph,
              windDirection: wx.wind_direction,
              precipitationChance: wx.precipitation_chance,
              conditions: wx.conditions,
              alerts: weather?.alerts || [],
            } : null,
            weatherRunBias,
            travel: {
              home: homeTravel,
              away: awayTravel,
            },
            injuries,
            lineups: lineupStatus,
            venue: venue ? {
              name: venue.name,
              city: venue.city,
              indoor: venue.indoor,
            } : null,
          },
          applicationHints: {
            f5SideReady: !!(homeStarterName && awayStarterName),
            f5TotalReady: !!(homeStarterName && awayStarterName && weather),
            fullGameSideReady: !!(homeBullpen && awayBullpen),
            propMatrixReady: !!(homeStarterName && awayStarterName),
          },
        },
        metadata: {
          latencyMs: Date.now() - start,
          source: 'db',
          freshness: new Date().toISOString().slice(0, 10),
        },
      };
    } catch (err) {
      return {
        signalId: 'mlb_phase',
        score: 0.5,
        weight: 0,
        available: false,
        rawData: { error: String(err) },
        metadata: { latencyMs: Date.now() - start, source: 'db', freshness: '' },
      };
    }
  },
};

export const __mlbPhaseSignalInternals = { getGameDateEt };
