// Week 10, 2025 QC snapshot
// Mock-backed fantasy data scoped through NFL Week 10, 2025 (actuals wk 1–10; forward from wk 11).
// Not production ingestion — numbers are best-effort for review.

const SEASON_YEAR = 2025;

const TEAMS = {
  KC:  { name: "Kansas City", color: "#E31837" },
  BUF: { name: "Buffalo", color: "#00338D" },
  PHI: { name: "Philadelphia", color: "#004C54" },
  SF:  { name: "San Francisco", color: "#AA0000" },
  DAL: { name: "Dallas", color: "#003594" },
  MIA: { name: "Miami", color: "#008E97" },
  CIN: { name: "Cincinnati", color: "#FB4F14" },
  BAL: { name: "Baltimore", color: "#241773" },
  DET: { name: "Detroit", color: "#0076B6" },
  GB:  { name: "Green Bay", color: "#203731" },
  MIN: { name: "Minnesota", color: "#4F2683" },
  LAR: { name: "L.A. Rams", color: "#003594" },
  HOU: { name: "Houston", color: "#03202F" },
  NYJ: { name: "N.Y. Jets", color: "#125740" },
  ATL: { name: "Atlanta", color: "#A71930" },
  CHI: { name: "Chicago", color: "#0B162A" },
  TB:  { name: "Tampa Bay", color: "#D50A0A" },
  IND: { name: "Indianapolis", color: "#002C5F" },
  NO:  { name: "New Orleans", color: "#D3BC8D" },
  SEA: { name: "Seattle", color: "#002244" },
  WAS: { name: "Washington", color: "#5A1414" },
  NE:  { name: "New England", color: "#002244" },
  PIT: { name: "Pittsburgh", color: "#FFB612" },
  CLE: { name: "Cleveland", color: "#311D00" },
  TEN: { name: "Tennessee", color: "#4B92DB" },
  CAR: { name: "Carolina", color: "#0085CA" },
  JAX: { name: "Jacksonville", color: "#006778" },
  DEN: { name: "Denver", color: "#FB4F14" },
  LV:  { name: "Las Vegas", color: "#000000" },
  LAC: { name: "L.A. Chargers", color: "#0080C6" },
  ARI: { name: "Arizona", color: "#972235" },
  NYG: { name: "N.Y. Giants", color: "#0B2265" },
};

const isLocalPrototype = typeof window !== "undefined"
  && ["localhost", "127.0.0.1"].includes(window.location.hostname)
  && window.location.port === "3456";

function nflApiUrl(path) {
  const normalized = path.startsWith("/") ? path : `/${path}`;
  const override = typeof window !== "undefined" && window.NFL_API_BASE_URL
    ? String(window.NFL_API_BASE_URL).replace(/\/$/, "")
    : "";
  if (override) return `${override}${normalized}`;
  if (isLocalPrototype) return `http://127.0.0.1:3000${normalized}`;
  return normalized;
}

function nflApiBases() {
  const override = typeof window !== "undefined" && window.NFL_API_BASE_URL
    ? String(window.NFL_API_BASE_URL).replace(/\/$/, "")
    : "";
  const bases = [];
  if (override) bases.push(override);
  if (isLocalPrototype) {
    bases.push("http://127.0.0.1:3000", "http://127.0.0.1:3001", "https://949fantasy.vercel.app");
  } else {
    bases.push("");
  }
  return [...new Set(bases)];
}

async function fetchNflJson(path, timeoutMs = 12000) {
  const normalized = path.startsWith("/") ? path : `/${path}`;
  let lastError = new Error(`Failed ${normalized}`);
  for (const base of nflApiBases()) {
    const controller = timeoutMs > 0 ? new AbortController() : null;
    const timer = timeoutMs > 0 ? window.setTimeout(() => controller.abort(), timeoutMs) : null;
    try {
      const response = await fetch(`${base}${normalized}`, {
        signal: controller ? controller.signal : undefined,
        cache: "no-store",
      });
      if (timer) window.clearTimeout(timer);
      if (!response.ok) {
        lastError = new Error(`${response.status} ${normalized}`);
        continue;
      }
      const payload = await response.json();
      if (payload && payload.ok === false) {
        lastError = new Error(payload.error || `Bad payload ${normalized}`);
        continue;
      }
      return payload;
    } catch (error) {
      if (timer) window.clearTimeout(timer);
      if (error && error.name === "AbortError") {
        lastError = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s (${normalized})`);
      } else {
        lastError = error instanceof Error ? error : new Error(String(error));
      }
    }
  }
  throw lastError;
}

const NFL_DATA_ENDPOINT = nflApiUrl("/api/nfl/teams");
const PROJECTIONS_ENDPOINTS = {
  ppr: nflApiUrl("/api/nfl/projections?season=2026&scoring=ppr"),
  half: nflApiUrl("/api/nfl/projections?season=2026&scoring=half"),
  std: nflApiUrl("/api/nfl/projections?season=2026&scoring=standard"),
};
const ROSTER_STATS_ENDPOINT = nflApiUrl(`/api/nfl/roster-stats?season=${SEASON_YEAR}&throughWeek=10&scoring=ppr`);
const SOS_ENDPOINT = nflApiUrl("/api/nfl/sos?season=2026");
const SCHEDULE_ENDPOINT = nflApiUrl("/api/nfl/schedule?season=2026");
const WEEKLY_LOGS_ENDPOINT = nflApiUrl("/api/nfl/weekly-logs?season=2025");
window.WEEKLY_LOGS_BY_ID = window.WEEKLY_LOGS_BY_ID || {};
window.WEEKLY_LOGS_BY_NAME = window.WEEKLY_LOGS_BY_NAME || {};
window.WEEKLY_PROJ_BY_ID = window.WEEKLY_PROJ_BY_ID || {};
window.WEEKLY_PROJ_BY_NAME = window.WEEKLY_PROJ_BY_NAME || {};
window.TAPE_SEASON = window.TAPE_SEASON || 2026;
window.LIVE_CURRENT_SEASON = window.LIVE_CURRENT_SEASON || 2026;
window.NFL_SOS = window.NFL_SOS || [];
window.NFL_SCHEDULE = window.NFL_SCHEDULE || [];
window.LIVE_LEAGUE = window.LIVE_LEAGUE || null;
const ROSTER_PERSONA_IDS = ["jallen", "cmcaff", "saquon", "jchase", "puka", "amon", "kelce", "henry", "smith"];

window.REQUIRE_RAPIDAPI_DATA = true;
window.ALLOW_SEEDED_PROTOTYPE_DATA = false;
window.PROJECTIONS_OK = false;
window.PLAYER_PROFILES = {};
window.DRAFT_PLAYERS = [];
window.PLAYER_PROFILES_BY_SCORING = { ppr: {}, half: {}, std: {} };
window.DRAFT_PLAYERS_BY_SCORING = { ppr: [], half: [], std: [] };
window.ACTIVE_DRAFT_SCORING = "ppr";
window.DRAFT_BOARD_PAUSED = false;
window.NFL_DATA_STATUS = window.NFL_DATA_STATUS || {
  status: window.REQUIRE_RAPIDAPI_DATA ? "loading" : "mock",
  source: window.REQUIRE_RAPIDAPI_DATA ? "rapidapi_nfl_api_data" : "prototype_mock",
  projections: window.REQUIRE_RAPIDAPI_DATA ? "loading" : "mock",
  projectionsLoaded: 0,
  teamsLoaded: 0,
  updatedAt: null,
  message: window.REQUIRE_RAPIDAPI_DATA ? "Loading live NFL data" : "Using prototype data",
};

const PROJECTION_CACHE_KEY = "949.projections.compact.v2";
const PROJECTION_CACHE_TTL_MS = 12 * 60 * 60 * 1000;

function compactCachedPlayer(player) {
  const seasons = (player.historicalSeasons || player.profile?.historical?.seasons || []).map((row) => ({
    year: row.year || row.season,
    gamesPlayed: row.gamesPlayed,
    fantasyPoints: row.fantasyPoints,
    pointsPerGame: row.pointsPerGame,
    receptions: row.receptions,
    passingYards: row.passingYards,
    passingTds: row.passingTds,
    interceptions: row.interceptions,
    rushingYards: row.rushingYards,
    rushingTds: row.rushingTds,
    receivingYards: row.receivingYards,
    receivingTds: row.receivingTds,
  }));
  const model = player.nine49Model || {};
  return {
    id: player.id,
    name: player.name,
    pos: player.pos,
    team: player.team,
    proj: player.proj,
    adjusted: player.adjusted,
    floor: player.floor,
    ceiling: player.ceiling,
    ros: player.ros,
    adp: player.adp,
    boom: player.boom,
    bust: player.bust,
    traj: Array.isArray(player.traj) ? player.traj.slice(-8) : [],
    matchup: player.matchup,
    matchupRating: player.matchupRating,
    byeWeek: player.byeWeek || player.bye || null,
    historicalSeasons: seasons,
    weeklyConsistency: player.weeklyConsistency || null,
    nine49Model: {
      history: model.history || seasons.map((row) => row.fantasyPoints),
      historyGames: model.historyGames || seasons.map((row) => row.gamesPlayed),
      historyPpg: model.historyPpg || seasons.map((row) => row.pointsPerGame),
    },
  };
}

function restoreCachedProjections() {
  try {
    const parsed = JSON.parse(window.localStorage.getItem(PROJECTION_CACHE_KEY) || "null");
    if (!parsed?.byScoring?.ppr?.length) return false;
    if (Date.now() - Number(parsed.savedAt || 0) > PROJECTION_CACHE_TTL_MS) return false;
    window.DRAFT_PLAYERS_BY_SCORING = {
      ppr: parsed.byScoring.ppr || [],
      half: parsed.byScoring.half || [],
      std: parsed.byScoring.std || [],
    };
    syncDraftScoringContext(window.ACTIVE_DRAFT_SCORING || "ppr");
    window.PROJECTIONS_OK = true;
    window.NFL_DATA_STATUS = {
      ...(window.NFL_DATA_STATUS || {}),
      projections: "cached",
      projectionsLoaded: window.DRAFT_PLAYERS.length,
      message: `Cached projections ready (${window.DRAFT_PLAYERS.length} players). Refreshing live files…`,
    };
    window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
    return true;
  } catch {
    return false;
  }
}

function saveCachedProjections() {
  try {
    const byScoring = window.DRAFT_PLAYERS_BY_SCORING || {};
    if (!(byScoring.ppr || []).length) return;
    window.localStorage.setItem(PROJECTION_CACHE_KEY, JSON.stringify({
      savedAt: Date.now(),
      byScoring: {
        ppr: (byScoring.ppr || []).map(compactCachedPlayer),
        half: (byScoring.half || []).map(compactCachedPlayer),
        std: (byScoring.std || []).map(compactCachedPlayer),
      },
    }));
  } catch (error) {
    console.warn("Projection cache skipped", error);
  }
}

function syncDraftScoringContext(scoring = "ppr") {
  const key = scoring === "std" ? "std" : scoring === "half" ? "half" : "ppr";
  window.ACTIVE_DRAFT_SCORING = key;
  window.PLAYER_PROFILES = window.PLAYER_PROFILES_BY_SCORING?.[key] || {};
  window.DRAFT_PLAYERS = window.DRAFT_PLAYERS_BY_SCORING?.[key] || [];
}

function normalizePrototypeTeam(team) {
  const abbreviation = team.abbreviation || team.id;
  if (!abbreviation) return null;

  return {
    abbreviation,
    name: team.city || team.name || team.displayName || abbreviation,
    displayName: team.displayName || [team.city, team.name].filter(Boolean).join(" ") || abbreviation,
    conference: team.conference,
    division: team.division,
  };
}

async function hydrateNflTeamsFromApi() {
  window.NFL_DATA_STATUS = {
    status: "loading",
    source: "rapidapi_nfl_api_data",
    message: "Loading NFL team data",
    teamsLoaded: 0,
    updatedAt: null,
  };
  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));

  try {
    const response = await fetch(NFL_DATA_ENDPOINT);
    const payload = await response.json();

    if (!payload.ok) {
      throw new Error(payload.error || "NFL data request failed");
    }

    const liveTeams = {};
    (payload.teams || []).forEach(team => {
      const normalized = normalizePrototypeTeam(team);
      if (!normalized) return;

      liveTeams[normalized.abbreviation] = {
        ...(TEAMS[normalized.abbreviation] || {}),
        name: normalized.name,
        displayName: normalized.displayName,
        conference: normalized.conference,
        division: normalized.division,
      };
    });

    Object.assign(TEAMS, liveTeams);

    window.NFL_DATA_STATUS = {
      status: "live",
      source: payload.provider || "rapidapi_nfl_api_data",
      message: "Live NFL teams loaded",
      teamsLoaded: Object.keys(liveTeams).length,
      updatedAt: new Date().toISOString(),
    };
  } catch (error) {
    window.NFL_DATA_STATUS = {
      status: "mock",
      source: "prototype_mock",
      message: error instanceof Error ? error.message : "Using prototype mock data",
      teamsLoaded: 0,
      updatedAt: new Date().toISOString(),
    };
  }

  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
}

async function hydrateRosterStatsFromApi() {
  const status = window.NFL_DATA_STATUS || { status: "mock" };

  try {
    const response = await fetch(ROSTER_STATS_ENDPOINT);
    const payload = await response.json();

    if (!payload.ok) {
      throw new Error(payload.error || "Roster stats request failed");
    }

    let merged = 0;

    ROSTER_PERSONA_IDS.forEach((id) => {
      const stats = payload.players?.[id];
      const player = PLAYERS.find((p) => p.id === id);
      if (!stats || !player) return;

      if (Array.isArray(stats.traj) && stats.traj.length) {
        player.traj = stats.traj;
        player.liveStats = true;
      }
      if (stats.actual != null) {
        player.actual = stats.actual;
      }
      merged += 1;
    });

    (payload.teamWeeks || []).forEach((tw) => {
      const row = TEAM_POSITION_WEEKS.find((r) => r.wk === tw.wk);
      if (!row) return;

      ["QB", "RB1", "RB2", "WR1", "WR2", "FLEX", "TE"].forEach((slot) => {
        if (tw[slot] != null) row[slot] = tw[slot];
      });

      if (tw.total != null) {
        row.total = +(tw.total + (row.DST || 0) + (row.K || 0)).toFixed(1);
      }
    });

    clearGameLogCache();

    window.NFL_DATA_STATUS = {
      ...status,
      rosterStats: "live",
      rosterMerged: merged,
      statsSeason: payload.season,
      statsThroughWeek: payload.throughWeek,
      message: `Live ${payload.season} stats on demo roster (${merged} players)`,
    };
  } catch (error) {
    window.NFL_DATA_STATUS = {
      ...status,
      rosterStats: "mock",
      rosterMerged: 0,
      message: error instanceof Error ? error.message : "Roster stats unavailable — mock points",
    };
  }

  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
}

async function hydrateProjectionsFromApi() {
  restoreCachedProjections();
  window.NFL_DATA_STATUS = {
    ...(window.NFL_DATA_STATUS || {}),
    projections: "loading",
    message: "Loading 949 projection profiles",
  };
  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));

  try {
    const payload = await fetchNflJson("/api/nfl/projections?season=2026&scoring=ppr", 120000);
    if (!payload.ok || !Array.isArray(payload.profiles) || !payload.profiles.length) {
      throw new Error(payload.error || "Projection pipeline returned no PPR profiles");
    }
    const pprPlayers = payload.profiles.map((profile) => profileToLegacyPlayer(profile, "ppr"));
    window.PLAYER_PROFILES_BY_SCORING.ppr = Object.fromEntries(payload.profiles.map((profile) => [profile.id, profile]));
    window.PLAYER_PROFILES_BY_SCORING.half = window.PLAYER_PROFILES_BY_SCORING.ppr;
    window.PLAYER_PROFILES_BY_SCORING.std = window.PLAYER_PROFILES_BY_SCORING.ppr;
    window.DRAFT_PLAYERS_BY_SCORING.ppr = pprPlayers;
    window.DRAFT_PLAYERS_BY_SCORING.half = pprPlayers;
    window.DRAFT_PLAYERS_BY_SCORING.std = pprPlayers;
    applyByeWeeksToPlayers(pprPlayers, window.NFL_SOS);
    syncDraftScoringContext(window.ACTIVE_DRAFT_SCORING || "ppr");
    window.PROJECTIONS_OK = true;

    payload.profiles.forEach((profile) => {
      const legacy = profileToLegacyPlayer(profile, "ppr");
      const idx = PLAYERS.findIndex((p) => p.id === legacy.id || p.name === legacy.name);
      if (idx >= 0) {
        PLAYERS[idx] = { ...PLAYERS[idx], ...legacy };
      }
    });
    applyByeWeeksToPlayers(PLAYERS, window.NFL_SOS);
    clearGameLogCache();

    window.NFL_DATA_STATUS = {
      ...(window.NFL_DATA_STATUS || {}),
      projections: "live",
      projectionsLoaded: payload.profiles.length,
      projectionsWarnings: payload.warnings || [],
      message: `Live PPR projections loaded (${payload.profiles.length} players). Half and Standard score that same spine without catch points.`,
    };
    saveCachedProjections();
  } catch (error) {
    if (window.PROJECTIONS_OK && (window.DRAFT_PLAYERS_BY_SCORING?.ppr || []).length) {
      window.NFL_DATA_STATUS = {
        ...(window.NFL_DATA_STATUS || {}),
        projections: "live",
        message: error instanceof Error ? error.message : "Using cached PPR projections",
      };
    } else {
      window.PROJECTIONS_OK = false;
      window.DRAFT_PLAYERS = [];
      window.PLAYER_PROFILES = {};
      window.DRAFT_PLAYERS_BY_SCORING = { ppr: [], half: [], std: [] };
      window.PLAYER_PROFILES_BY_SCORING = { ppr: {}, half: {}, std: {} };
      window.NFL_DATA_STATUS = {
        ...(window.NFL_DATA_STATUS || {}),
        projections: "unavailable",
        projectionsLoaded: 0,
        message: error instanceof Error ? error.message : "Projection data unavailable",
      };
    }
  }

  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
}

function profileToLegacyPlayer(profile, scoring = "ppr") {
  const scoreBlock = scoring === "std"
    ? profile.scoring?.standard
    : scoring === "half"
      ? profile.scoring?.halfPpr
      : profile.scoring?.ppr;
  const weekly = scoreBlock?.weekly ?? profile.projection?.weeklyAvg ?? 0;
  const season = scoreBlock?.season ?? profile.projection?.seasonPoints ?? weekly * 17;
  const historicalLogs = (profile.historical?.gameLogs || [])
    .slice()
    .sort((a, b) => (a.season - b.season) || (a.week - b.week));
  const trajSeasons = [...new Set(historicalLogs.map((log) => Number(log.season)).filter(Boolean))].slice(-3);
  const traj = historicalLogs
    .filter((log) => trajSeasons.includes(Number(log.season)))
    .map((log) => log.fantasyPoints);
  const outcomes = profile.historical?.weeklyOutcomes;
  const outcomeGames = outcomes
    ? (outcomes.boomWeeks || 0) + (outcomes.bustWeeks || 0) + (outcomes.expectedWeeks || 0)
    : 0;

  return {
    id: profile.id,
    name: profile.name,
    pos: profile.pos,
    team: profile.team,
    byeWeek: profile.byeWeek || null,
    bye: profile.byeWeek || null,
    age: profile.age || 0,
    rookie: !!profile.rookie,
    rosterStatus: profile.rosterStatus || (profile.team === "FA" ? "free_agent" : "active"),
    draftEligible: profile.draftEligible !== false,
    depthChartRole: profile.depthChartRole || profile.workload?.depthChartRole || "unknown",
    nflDraftRound: profile.nflDraftRound || profile.bio?.nflDraftRound || null,
    nflDraftPick: profile.nflDraftPick || profile.bio?.nflDraftPick || null,
    proj: +weekly.toFixed(1),
    actual: traj[traj.length - 1] ?? +weekly.toFixed(1),
    rank: profile.unranked ? 900 + (profile.rank || 0) : profile.projection?.impactRank || profile.rank || 999,
    unranked: !!profile.unranked || profile.pos === "DST",
    prev: profile.projection?.impactRank || profile.rank || 999,
    ros: +season.toFixed(1),
    own: 0,
    trend: profile.projection?.yoyPpg?.length >= 4 && profile.projection.yoyPpg[3] >= profile.projection.yoyPpg[2] ? "up" : "flat",
    risk: profile.risk || "med",
    adp: profile.adp > 0 ? profile.adp : profile.projection?.impactRank || 999,
    liveDraftAdp: profile.adp > 0 ? profile.adp : null,
    adpTrendPct: profile.adpTrendPct ?? null,
    adpSource: profile.adpSource || null,
    round: Math.ceil((profile.adp || profile.projection?.impactRank || 1) / 12),
    matchup: "TBD",
    matchupRating: 3,
    traj,
    historicalLogs,
    historicalSeasons: profile.historical?.seasons || [],
    profile,
    notes: profile.drivers?.[0]?.text || "949 projection profile",
    boom: profile.projection?.boomPct ?? 0,
    bust: profile.projection?.bustPct ?? 0,
    weeklyConsistency: {
      games: outcomeGames,
      boomPct: profile.projection?.boomPct ?? 0,
      bustPct: profile.projection?.bustPct ?? 0,
      boomWeeks: outcomes?.boomWeeks || 0,
      bustWeeks: outcomes?.bustWeeks || 0,
      hitWeeks: outcomes?.expectedWeeks || 0,
      source: "weekly projection vs game log",
    },
    snap: typeof profile.workload?.snapPct === "number" ? profile.workload.snapPct : 0,
    tgt: typeof profile.workload?.targetsPerGame === "number" ? profile.workload.targetsPerGame : 0,
    rec: typeof profile.workload?.receptionsPerGame === "number"
      ? profile.workload.receptionsPerGame
      : typeof profile.receiving?.receptionsPerGame === "number" ? profile.receiving.receptionsPerGame : 0,
    rush: typeof profile.workload?.carriesPerGame === "number" ? profile.workload.carriesPerGame : 0,
    catchRate: (() => {
      const fromProfile = profile.efficiency?.catchRate ?? profile.receiving?.catchRate ?? profile.workload?.catchRate;
      if (typeof fromProfile === "number" && Number.isFinite(fromProfile)) {
        return fromProfile > 1 ? fromProfile / 100 : fromProfile;
      }
      const targets = typeof profile.workload?.targetsPerGame === "number" ? profile.workload.targetsPerGame : 0;
      const receptions = typeof profile.workload?.receptionsPerGame === "number" ? profile.workload.receptionsPerGame : 0;
      return targets > 0 && receptions > 0 ? Math.min(1, receptions / targets) : null;
    })(),
    news: "",
    ceiling: +((profile.projection?.ceiling || season) / 17).toFixed(1),
    floor: +((profile.projection?.floor || 0) / 17).toFixed(1),
    profile,
    nine49Model: {
      r: profile.projection?.impactRank,
      s: profile.projection?.seasonPoints,
      p: profile.projection?.seasonPoints,
      c: profile.projection?.ceiling,
      f: profile.projection?.floor,
      k: profile.projection?.consistency,
      v: profile.projection?.valueScore,
      pv: profile.projection?.valueScore,
      history: (profile.historical?.seasons || []).map((s) => s.fantasyPoints),
      historyGames: (profile.historical?.seasons || []).map((s) => s.gamesPlayed),
      historyPpg: (profile.historical?.seasons || []).map((s) => +s.pointsPerGame.toFixed(1)),
    },
    nine49Rank: profile.projection?.impactRank,
    nine49Score: profile.projection?.seasonPoints,
    nine49Consistency: profile.projection?.consistency,
    valueIndex: profile.projection?.valueScore,
    projectedValueIndex: profile.projection?.valueScore,
    adjusted: +weekly.toFixed(2),
    liveStats: true,
  };
}

window.setDraftScoringContext = function setDraftScoringContext(scoring = "ppr") {
  syncDraftScoringContext(scoring);
  window.dispatchEvent(new CustomEvent("draft-scoring-context", { detail: { scoring: window.ACTIVE_DRAFT_SCORING } }));
};

window.getDraftPlayersForScoring = function getDraftPlayersForScoring(scoring = "ppr") {
  return window.DRAFT_PLAYERS_BY_SCORING?.ppr || [];
};

window.getPlayerProfilesForScoring = function getPlayerProfilesForScoring(scoring = "ppr") {
  return window.PLAYER_PROFILES_BY_SCORING?.ppr || {};
};

function applyByeWeeksToPlayers(players, sosRows = window.NFL_SOS || []) {
  const byeByTeam = Object.fromEntries((sosRows || []).filter(row => row.team && row.byeWeek).map(row => [row.team, row.byeWeek]));
  (players || []).forEach(player => {
    const bye = byeByTeam[player.team] || player.byeWeek || player.bye;
    if (!bye) return;
    player.byeWeek = bye;
    player.bye = bye;
  });
}

function uniquifyLiveSos(rows) {
  return (rows || []).slice()
    .sort((a, b) => (Number(a.sosRank) || 99) - (Number(b.sosRank) || 99) || String(a.team).localeCompare(String(b.team)))
    .map((row, index) => ({ ...row, sosRank: index + 1 }));
}

async function hydrateNflSosFromApi() {
  try {
    const response = await fetch(SOS_ENDPOINT);
    const payload = await response.json();
    if (!payload.ok) throw new Error(payload.error || "SOS request failed");
    window.NFL_SOS = uniquifyLiveSos(payload.teams || []);
    ["ppr", "half", "std"].forEach((key) => applyByeWeeksToPlayers(window.DRAFT_PLAYERS_BY_SCORING?.[key], window.NFL_SOS));
    applyByeWeeksToPlayers(window.DRAFT_PLAYERS, window.NFL_SOS);
    applyByeWeeksToPlayers(window.PLAYERS, window.NFL_SOS);
    window.dispatchEvent(new CustomEvent("nfl-sos", { detail: window.NFL_SOS }));
  } catch (error) {
    window.NFL_SOS = window.NFL_SOS || [];
    console.warn("SOS hydrate failed", error);
  }
}

async function hydrateNflScheduleFromApi() {
  try {
    const payload = await fetchNflJson("/api/nfl/schedule?season=2026", 30000);
    if (!payload.ok) throw new Error(payload.error || "Schedule request failed");
    window.NFL_SCHEDULE = (payload.games || []).map((game) => ({
      season: game.season,
      week: game.week,
      homeTeam: game.homeTeam,
      awayTeam: game.awayTeam,
      gameId: game.gameId,
    }));
    window.dispatchEvent(new CustomEvent("nfl-schedule", { detail: window.NFL_SCHEDULE }));
  } catch (error) {
    window.NFL_SCHEDULE = window.NFL_SCHEDULE || [];
    console.warn("Schedule hydrate failed", error);
  }
}

function weeklyLogKey(name, pos) {
  return `${normalizePlayerName(name)}:${pos || ""}`;
}

function displayNameFromPlayerId(id) {
  const slug = String(id || "").replace(/^csv-/i, "").replace(/-/g, " ").trim();
  if (!slug.includes(" ")) return slug;
  return slug.replace(/\b[a-z]/g, (ch) => ch.toUpperCase());
}

function readablePlayerName(name, id) {
  const raw = String(name || "").trim();
  if (raw && !/^csv-/i.test(raw) && !/^[a-z0-9]+(-[a-z0-9]+)+$/.test(raw)) return raw;
  return displayNameFromPlayerId(id || raw) || raw;
}

function playerIdAliases(id) {
  const raw = String(id || "").trim();
  if (!raw) return [];
  const stripped = raw.replace(/^csv-/i, "");
  const compact = stripped.replace(/-/g, "");
  return [...new Set([raw, stripped, compact, stripped ? `csv-${stripped}` : ""].filter(Boolean))];
}

function lookupByAliases(map, id) {
  for (const alias of playerIdAliases(id)) {
    if (map[alias]?.length) return map[alias];
  }
  return null;
}

function findDraftPoolPlayer(pool, pick) {
  const list = pool || [];
  const aliases = new Set(playerIdAliases(pick?.playerId));
  const byId = list.find((p) => aliases.has(p.id) || playerIdAliases(p.id).some((a) => aliases.has(a)));
  if (byId) return byId;
  const rawName = String(pick?.playerName || "").trim().toLowerCase();
  if (!rawName) return null;
  const aliasesMap = { ...(window.OBSERVED_DRAFT_ADP?.aliases || {}), "jerimyah love": "jeremiyah love", "jeremiah love": "jeremiyah love" };
  const name = aliasesMap[rawName] || rawName;
  const pos = pick?.playerPosition;
  return list.find((p) => String(p.name || "").trim().toLowerCase() === name && (!pos || p.pos === pos))
    || list.find((p) => String(p.name || "").trim().toLowerCase() === name)
    || list.find((p) => String(p.name || "").trim().toLowerCase().includes(name) && (!pos || p.pos === pos))
    || null;
}

function weeklyModelSlug(name) {
  return String(name || "")
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/\s+[QO]$/i, "")
    .replace(/^csv-/i, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/-(jr|sr|ii|iii|iv)$/g, "")
    .replace(/^-+|-+$/g, "");
}

function weeklyModelRecord(player) {
  const table = window.WEEKLY_PROJECTION_MODEL?.players || {};
  const guesses = [
    weeklyModelSlug(player?.name),
    weeklyModelSlug(player?.id),
    weeklyModelSlug(String(player?.id || "").replace(/-/g, " ")),
  ].filter(Boolean);
  for (const key of guesses) {
    if (table[key]) return table[key];
  }
  return null;
}

function stretchFactor(value, gain, lo, hi) {
  const stretched = 1 + (Number(value || 1) - 1) * gain;
  return Math.max(lo, Math.min(hi, stretched));
}

function scheduleGameForWeek(team, week) {
  const wk = Number(week) || 1;
  return (window.NFL_SCHEDULE || []).find((game) => (
    Number(game.week) === wk && (game.homeTeam === team || game.awayTeam === team)
  )) || null;
}

function opponentForModeledWeek(player, week, rec) {
  const wk = Number(week) || 1;
  const team = player?.team || rec?.team;
  const game = scheduleGameForWeek(team, wk);
  if (game) return game.homeTeam === team ? game.awayTeam : game.homeTeam;
  return rec?.csv2026?.[String(wk)]?.opp || null;
}

function weeklyOpponentFactor(pos, opp, rec) {
  const passCatcher = pos === "WR" || pos === "TE";
  const rawPos = Number(window.WEEKLY_PROJECTION_MODEL?.oppFactor?.[pos]?.[opp]) || 1;
  const posFactor = stretchFactor(rawPos, passCatcher ? 0.45 : 1.65, passCatcher ? 0.92 : 0.86, passCatcher ? 1.08 : 1.16);
  const hit = rec?.vsOpp?.[opp];
  if (!hit || !opp) return posFactor;
  const rel = stretchFactor(Number(hit.rel) || posFactor, passCatcher ? 0.4 : 1.25, passCatcher ? 0.9 : 0.8, passCatcher ? 1.1 : 1.24);
  const n = Number(hit.n) || 0;
  if (n >= 2) return Math.max(passCatcher ? 0.9 : 0.82, Math.min(passCatcher ? 1.1 : 1.22, rel * 0.8 + posFactor * 0.2));
  return Math.max(passCatcher ? 0.92 : 0.86, Math.min(passCatcher ? 1.08 : 1.16, rel * 0.45 + posFactor * 0.55));
}

function receivingPriorFromSeasons(seasons) {
  let tgt = 0;
  let recs = 0;
  let yds = 0;
  let td = 0;
  let gp = 0;
  (seasons || []).forEach((row) => {
    const year = Number(row.year || row.season) || 0;
    const games = Number(row.gamesPlayed || row.gp) || 0;
    const targets = Number(row.targets) || 0;
    const receptions = Number(row.receptions) || 0;
    if (year < 2022 || year > 2025 || games <= 0 || targets <= 0) return;
    tgt += targets;
    recs += receptions;
    yds += Number(row.receivingYards) || 0;
    const recTd = Number(row.receivingTds);
    if (Number.isFinite(recTd) && recTd > 0) {
      td += recTd;
    } else {
      const ppr = Number(row.fantasyPoints || row.pointsPerGame * games) || 0;
      td += Math.max(0, (ppr - receptions - (Number(row.receivingYards) || 0) / 10) / 6);
    }
    gp += games;
  });
  if (gp < 8 || tgt < 20) return null;
  return {
    gp,
    tgtPg: tgt / gp,
    recPg: recs / gp,
    catchRate: recs / tgt,
    ypr: recs ? yds / recs : 0,
    tdPerTgt: td / tgt,
    pprPg: recs / gp + yds / gp / 10 + td / gp * 6,
  };
}

function liveProjectionSeason() {
  return Number(window.LIVE_LEAGUE?.season || window.LIVE_CURRENT_SEASON || 2026) || 2026;
}

function statPresent(value) {
  return value != null && value !== "" && Number.isFinite(Number(value));
}

function liveCurrentWeek() {
  return Number(window.LIVE_CURRENT_WEEK || 4) || 4;
}

function inferLiveCurrentWeekFromLogs() {
  let max = 0;
  const visit = (rows) => {
    (rows || []).forEach((row) => {
      if (Number(row.season) !== 2026) return;
      max = Math.max(max, Number(row.week) || 0);
    });
  };
  Object.values(window.WEEKLY_LOGS_BY_ID || {}).forEach(visit);
  Object.values(window.WEEKLY_LOGS_BY_NAME || {}).forEach(visit);
  (window.DRAFT_PLAYERS || []).forEach((player) => {
    visit(player.historicalLogs);
    visit(player.weeklyLogRaw);
  });
  if (!max) return Number(window.LIVE_CURRENT_WEEK || 1) || 1;
  return Math.min(18, max + 1);
}

function currentSeasonGameLogs(player, beforeWeek) {
  const season = liveProjectionSeason();
  // Only completed weeks. In-progress week stays frozen until LIVE_CURRENT_WEEK advances.
  const cutoff = Math.min(Number(beforeWeek) || 99, liveCurrentWeek());
  const byeWeek = Number(player?.byeWeek || player?.bye) || null;
  const named = player ? (window.WEEKLY_LOGS_BY_NAME?.[weeklyLogKey(player.name, player.pos)]
    || window.WEEKLY_LOGS_BY_PLAIN?.[normalizePlayerName(player.name)]) : null;
  const bags = [
    player?.historicalLogs,
    player?.weeklyLogRaw,
    player?.profile?.historical?.gameLogs,
    player?.id ? window.WEEKLY_LOGS_BY_ID?.[player.id] : null,
    named,
  ];
  const byWeek = new Map();
  bags.forEach((rows) => {
    (rows || []).forEach((row) => {
      const yr = Number(row.season);
      const wk = Number(row.week);
      if (yr !== season || !wk || wk >= cutoff) return;
      if (byeWeek && wk === byeWeek) return;
      const prev = byWeek.get(wk) || { season: yr, week: wk, stats: {}, ppr: null };
      const stats = { ...(prev.stats || {}), ...(row.stats || {}) };
      const ppr = statPresent(row.ppr) ? Number(row.ppr)
        : statPresent(row.fantasyPoints) ? Number(row.fantasyPoints)
        : prev.ppr;
      byWeek.set(wk, { season: yr, week: wk, stats, ppr });
    });
  });
  return Array.from(byWeek.values()).sort((a, b) => a.week - b.week);
}

function observedWorkloadFromLogs(logs) {
  let tgt = 0;
  let tgtN = 0;
  let rec = 0;
  let recTgt = 0;
  let recYds = 0;
  let recCatch = 0;
  let recTd = 0;
  let recTdTgt = 0;
  let rush = 0;
  let rushN = 0;
  let rushYds = 0;
  let rushAtt = 0;
  let rushTd = 0;
  let rushTdAtt = 0;
  let att = 0;
  let attN = 0;
  let comp = 0;
  let compAtt = 0;
  let passYds = 0;
  let passComp = 0;
  let passTd = 0;
  let passTdAtt = 0;
  let ints = 0;
  let intAtt = 0;
  (logs || []).forEach((log) => {
    const s = log.stats || {};
    if (statPresent(s.targets)) { tgt += Number(s.targets); tgtN += 1; }
    if (statPresent(s.receptions) && statPresent(s.targets)) {
      rec += Number(s.receptions);
      recTgt += Number(s.targets);
    }
    if (statPresent(s.receivingYards) && statPresent(s.receptions)) {
      recYds += Number(s.receivingYards);
      recCatch += Number(s.receptions);
    }
    if (statPresent(s.receivingTds) && statPresent(s.targets)) {
      recTd += Number(s.receivingTds);
      recTdTgt += Number(s.targets);
    }
    if (statPresent(s.carries) || statPresent(s.rushingAttempts)) {
      rush += Number(statPresent(s.carries) ? s.carries : s.rushingAttempts);
      rushN += 1;
    }
    if (statPresent(s.rushingYards) && (statPresent(s.carries) || statPresent(s.rushingAttempts))) {
      rushYds += Number(s.rushingYards);
      rushAtt += Number(statPresent(s.carries) ? s.carries : s.rushingAttempts);
    }
    if (statPresent(s.rushingTds) && (statPresent(s.carries) || statPresent(s.rushingAttempts))) {
      rushTd += Number(s.rushingTds);
      rushTdAtt += Number(statPresent(s.carries) ? s.carries : s.rushingAttempts);
    }
    if (statPresent(s.attempts) || statPresent(s.passingAttempts)) {
      att += Number(statPresent(s.attempts) ? s.attempts : s.passingAttempts);
      attN += 1;
    }
    if (statPresent(s.completions) && (statPresent(s.attempts) || statPresent(s.passingAttempts))) {
      comp += Number(s.completions);
      compAtt += Number(statPresent(s.attempts) ? s.attempts : s.passingAttempts);
    }
    if (statPresent(s.passingYards) && statPresent(s.completions)) {
      passYds += Number(s.passingYards);
      passComp += Number(s.completions);
    }
    if (statPresent(s.passingTds) && (statPresent(s.attempts) || statPresent(s.passingAttempts))) {
      passTd += Number(s.passingTds);
      passTdAtt += Number(statPresent(s.attempts) ? s.attempts : s.passingAttempts);
    }
    if (statPresent(s.interceptions) && (statPresent(s.attempts) || statPresent(s.passingAttempts))) {
      ints += Number(s.interceptions);
      intAtt += Number(statPresent(s.attempts) ? s.attempts : s.passingAttempts);
    }
  });
  return {
    games: (logs || []).length,
    tgtPg: tgtN ? tgt / tgtN : null,
    tgtN,
    catchRate: recTgt > 0 ? rec / recTgt : null,
    ypr: recCatch > 0 ? recYds / recCatch : null,
    tdPerTgt: recTdTgt > 0 ? recTd / recTdTgt : null,
    carPg: rushN ? rush / rushN : null,
    rushN,
    ypc: rushAtt > 0 ? rushYds / rushAtt : null,
    tdPerCarry: rushTdAtt > 0 ? rushTd / rushTdAtt : null,
    attPg: attN ? att / attN : null,
    attN,
    catch: compAtt > 0 ? comp / compAtt : null,
    ypcatch: passComp > 0 ? passYds / passComp : null,
    passTd: passTdAtt > 0 ? passTd / passTdAtt : null,
    intRt: intAtt > 0 ? ints / intAtt : null,
    rushPg: rushN ? rush / rushN : null,
  };
}

function blendStat(prior, observed, n, k = 5) {
  if (!n || observed == null || !Number.isFinite(Number(observed))) return prior;
  const w = n / (n + k);
  return (1 - w) * (Number(prior) || 0) + w * Number(observed);
}

function applyLiveSeasonActuals(prior, player, week, team) {
  const logs = currentSeasonGameLogs(player, week);
  const live = observedWorkloadFromLogs(logs);
  if (!prior && !live.games) return prior;
  const base = prior || {
    tgtPg: 0, catchRate: 0, ypr: 0, tdPerTgt: 0, tgtShare: 0, rushShare: 0,
    ypc: 0, ypcatch: 0, tdPerCarry: 0, attPg: 0, comp: 0, ypa: 0, passTd: 0,
    intRt: 0, rushPg: 0, carPg: 0,
  };
  if (!live.games) return { ...base, liveGames: 0 };
  const tgtShareObs = (live.tgtPg != null && team?.tgtPg)
    ? live.tgtPg / team.tgtPg
    : null;
  const rushShareObs = (live.carPg != null && team?.rushAtt)
    ? live.carPg / team.rushAtt
    : null;
  return {
    ...base,
    tgtPg: blendStat(base.tgtPg, live.tgtPg, live.tgtN),
    tgtShare: blendStat(base.tgtShare, tgtShareObs, live.tgtN),
    catchRate: blendStat(base.catchRate, live.catchRate, live.tgtN),
    ypr: blendStat(base.ypr, live.ypr, live.tgtN),
    tdPerTgt: blendStat(base.tdPerTgt, live.tdPerTgt, live.tgtN),
    rushShare: blendStat(base.rushShare, rushShareObs, live.rushN),
    carPg: blendStat(base.carPg, live.carPg, live.rushN),
    ypc: blendStat(base.ypc, live.ypc, live.rushN),
    tdPerCarry: blendStat(base.tdPerCarry, live.tdPerCarry, live.rushN),
    attPg: blendStat(base.attPg, live.attPg, live.attN),
    comp: blendStat(base.comp, live.catch, live.attN),
    ypcatch: blendStat(base.ypcatch, live.ypcatch, live.attN),
    passTd: blendStat(base.passTd, live.passTd, live.attN),
    intRt: blendStat(base.intRt, live.intRt, live.attN),
    rushPg: blendStat(base.rushPg, live.rushPg, live.rushN),
    liveGames: live.games,
  };
}

function receivingPrior(player, rec, week, team) {
  const baked = rec?.work || rec?.recv;
  let prior = null;
  if (baked && (Number(baked.tgtShare) > 0 || Number(baked.tgtPg) > 0 || Number(baked.tgtPg25) > 0 || Number(baked.attPg25) > 0 || Number(baked.attPg) > 0)) {
    prior = {
      gp: Number(baked.gp) || 0,
      tgtPg: Number(baked.tgtPg25 || baked.tgtPg) || 0,
      recPg: Number(baked.recPg) || 0,
      catchRate: Number(baked.catch) || 0,
      ypr: Number(baked.ypr) || 0,
      tdPerTgt: Number(baked.tdPerTgt) || 0,
      pprPg: Number(baked.pprPg) || 0,
      tgtShare: Number(baked.tgtShare) || 0,
      rushShare: Number(baked.rushShare) || 0,
      attShare: Number(baked.attShare) || 0,
      ypc: Number(baked.ypc) || 0,
      ypcatch: Number(baked.ypcatch) || 0,
      tdPerCarry: Number(baked.tdPerCarry) || 0,
      attPg: Number(baked.attPg25 || baked.attPg) || 0,
      comp: Number(baked.comp) || 0,
      ypa: Number(baked.ypa) || 0,
      passTd: Number(baked.passTd) || 0,
      intRt: Number(baked.intRt) || 0,
      rushPg: Number(baked.rushPg) || Number(baked.carPg25) || 0,
      carPg: Number(baked.carPg25) || 0,
      liveGames: 0,
    };
  } else {
    prior = receivingPriorFromSeasons(
      player?.profile?.historical?.seasons || player?.historicalSeasons || [],
    );
  }
  return applyLiveSeasonActuals(prior, player, week, team);
}

function volumeScale(base, oppFactor, weekFactor, venueFactor) {
  return Math.max(0, Number(base) || 0)
    * (1 + (Number(oppFactor || 1) - 1) * 0.45)
    * (1 + (Number(weekFactor || 1) - 1) * 0.35)
    * (1 + (Number(venueFactor || 1) - 1) * 0.5);
}

function teamOffenseFor(player, rec) {
  const teams = window.WEEKLY_PROJECTION_MODEL?.teams || {};
  return teams[player?.team] || teams[rec?.team] || {};
}

function workloadPpr(player, rec, pos, oppFactor, weekFactor, venueFactor, week) {
  const team = teamOffenseFor(player, rec);
  const work = receivingPrior(player, rec, week, team);
  if (!work) return null;

  if (pos === "WR" || pos === "TE") {
    const tgtBase = (team.tgtPg && work.tgtShare)
      ? team.tgtPg * work.tgtShare
      : (work.tgtPg || 0);
    if (tgtBase <= 0 || work.catchRate <= 0) return null;
    const tgt = volumeScale(tgtBase, oppFactor, weekFactor, venueFactor);
    const recs = tgt * work.catchRate;
    const yds = recs * (work.ypr || 0);
    const tds = tgt * (work.tdPerTgt || 0);
    return { ppr: recs + yds / 10 + tds * 6, recs };
  }

  if (pos === "RB") {
    const carryBase = (team.rushAtt && work.rushShare)
      ? team.rushAtt * work.rushShare
      : (work.carPg || work.rushPg || 0);
    const tgtBase = (team.tgtPg && work.tgtShare)
      ? team.tgtPg * work.tgtShare
      : 0;
    const carries = volumeScale(carryBase, oppFactor, weekFactor, venueFactor);
    const tgt = volumeScale(tgtBase, oppFactor, weekFactor, venueFactor);
    const rushYds = carries * (work.ypc || team.ypc || 4.2);
    const rushTd = carries * (work.tdPerCarry || 0.03);
    const recs = tgt * (work.catchRate || 0.7);
    const recYds = recs * (work.ypr || 7);
    const recTd = tgt * (work.tdPerTgt || 0.03);
    return { ppr: rushYds / 10 + rushTd * 6 + recs + recYds / 10 + recTd * 6, recs };
  }

  if (pos === "QB") {
    const attBase = work.attPg || team.passAtt || 0;
    const rushBase = work.rushPg || 0;
    if (attBase <= 0) return null;
    const att = volumeScale(attBase, oppFactor, weekFactor, venueFactor);
    const rush = volumeScale(rushBase, oppFactor, weekFactor, venueFactor);
    const catchRate = (work.liveGames && work.comp)
      ? work.comp
      : (team.catch || team.comp || work.comp || 0.62);
    const yardsPerCatch = (work.liveGames && work.ypcatch)
      ? work.ypcatch
      : (team.ypcatch || work.ypcatch
        || ((work.ypa || team.ypa || 7.2) / Math.max(0.45, catchRate)));
    const completions = att * catchRate;
    const passYds = completions * yardsPerCatch;
    const passTd = att * (work.passTd || team.passTd || 0.045);
    const ints = att * (work.intRt || team.intRt || 0.025);
    const rushYds = rush * (work.ypc || 5.5);
    const rushTd = rush * (work.tdPerCarry || 0.03);
    return { ppr: passYds / 25 + passTd * 4 - ints * 2 + rushYds / 10 + rushTd * 6, recs: 0 };
  }

  return null;
}

function homeAwayFactor(player, week, rec) {
  const team = player?.team || rec?.team;
  const game = scheduleGameForWeek(team, week);
  if (!game || !team) return 1;
  return game.homeTeam === team ? 1.03 : 0.97;
}

function matchupRatingFromFactor(oppFactor) {
  if (oppFactor >= 1.1) return 5;
  if (oppFactor >= 1.04) return 4;
  if (oppFactor >= 0.97) return 3;
  if (oppFactor >= 0.91) return 2;
  return 1;
}

function applyWeeklyProjectionModel(player, week, scoring) {
  if (!player) return player;
  const model = window.WEEKLY_PROJECTION_MODEL;
  const wk = Number(week) || 1;
  const rec = weeklyModelRecord(player);
  const bye = Number(player.byeWeek || player.bye || rec?.bye) || null;
  if (bye && bye === wk) {
    return {
      ...player,
      seasonAvg: Number(player.seasonAvg || player.proj) || rec?.anchor || 0,
      proj: 0,
      adjusted: 0,
      floor: 0,
      ceiling: 0,
      weeklyModel: { week: wk, opp: null, points: 0, bye: true, anchor: rec?.anchor || Number(player.proj) || 0 },
    };
  }
  if (!model && !rec) return player;
  const pos = player.pos || rec?.pos;
  const opp = opponentForModeledWeek(player, wk, rec);
  const weekFactor = stretchFactor(Number(model?.weekFactor?.[pos]?.[String(wk)]) || 1, 1.8, 0.9, 1.12);
  const oppFactor = weeklyOpponentFactor(pos, opp, rec);
  const venueFactor = homeAwayFactor(player, wk, rec);
  const csvRow = rec?.csv2026?.[String(wk)];
  const prior = receivingPrior(player, rec, wk, teamOffenseFor(player, rec));
  const workload = workloadPpr(player, rec, pos, oppFactor, weekFactor, venueFactor, wk);
  const workloadPprPts = Number(workload?.ppr);
  const workloadRecs = Number(workload?.recs) || 0;
  const anchor = (Number.isFinite(workloadPprPts) && workloadPprPts > 0 ? workloadPprPts : 0)
    || Number(rec?.anchor)
    || Number(player.seasonAvg)
    || Number(player.weeklyModel?.anchor)
    || Number(player.proj)
    || 0;
  let ppr;
  if (Number.isFinite(workloadPprPts) && workloadPprPts > 0) {
    ppr = workloadPprPts;
  } else if (csvRow && rec && rec.flat2026 === false && Number(csvRow.ppr) > 0) {
    ppr = Number(csvRow.ppr);
  } else if (anchor > 0) {
    ppr = anchor * oppFactor * weekFactor * venueFactor;
  } else {
    ppr = Number(player.proj) || 0;
  }
  const format = scoring === "standard" ? "std" : (scoring || window.ACTIVE_DRAFT_SCORING || "ppr");
  const recPts = format === "std" ? 0 : format === "half" ? 0.5 : 1;
  const recs = ["RB", "WR", "TE"].includes(pos)
    ? (workloadRecs || Number(player.rec) || 0)
    : 0;
  let pts = recPts >= 1 ? ppr : Math.max(0, ppr - recs * (1 - recPts));
  pts = Math.max(0, +Number(pts).toFixed(1));
  return {
    ...player,
    seasonAvg: Number(player.seasonAvg) || anchor || Number(player.proj) || pts,
    proj: pts,
    adjusted: pts,
    floor: +(pts * 0.82).toFixed(1),
    ceiling: +(pts * 1.22).toFixed(1),
    matchupRating: matchupRatingFromFactor(oppFactor),
    weeklyModel: {
      week: wk,
      opp,
      points: pts,
      oppFactor: +oppFactor.toFixed(4),
      weekFactor: +weekFactor.toFixed(4),
      venueFactor,
      anchor,
      tgtPg: prior?.tgtPg || null,
      catchRate: prior?.catchRate || null,
      liveGames: prior?.liveGames || 0,
    },
  };
}

function modeledWeekProjection(player, week, scoring) {
  if (!player) return 0;
  if (typeof applyWeeklyProjectionModel !== "function") {
    return Number(player.adjusted || player.proj || 0) || 0;
  }
  const seed = {
    ...player,
    proj: player.seasonAvg || player.weeklyModel?.anchor || player.proj,
    seasonAvg: player.seasonAvg || player.weeklyModel?.anchor || player.proj,
  };
  return Number(applyWeeklyProjectionModel(seed, week, scoring)?.proj || 0) || 0;
}

function materializeWeeklyLogs(rows, scoring = window.ACTIVE_DRAFT_SCORING || "ppr") {
  return (rows || []).map((row) => {
    const scored = scoreFromStatLine(row.stats);
    const pts = scored
      ? (scoring === "std" ? scored.std : scoring === "half" ? scored.half : scored.ppr)
      : (scoring === "std" ? Number(row.std) : scoring === "half" ? Number(row.half) : Number(row.ppr));
    return {
      season: Number(row.season) || Number(window.TAPE_SEASON || 2025),
      week: Number(row.week) || 0,
      opponent: row.opponent || undefined,
      stats: row.stats || undefined,
      std: scored ? scored.std : Number(row.std),
      half: scored ? scored.half : Number(row.half),
      ppr: scored ? scored.ppr : Number(row.ppr),
      fantasyPoints: pts,
    };
  });
}

async function hydrateWeeklyLogsFromApi() {
  window.NFL_DATA_STATUS = { ...(window.NFL_DATA_STATUS || {}), weeklyLogs: "loading" };
  window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
  try {
    const [logs2025, logs2024, logs2023, logs2026, weeklyProj] = await Promise.all([
      fetchNflJson("/api/nfl/weekly-logs?season=2025", 120000),
      fetchNflJson("/api/nfl/weekly-logs?season=2024", 120000).catch(() => ({ players: [] })),
      fetchNflJson("/api/nfl/weekly-logs?season=2023", 120000).catch(() => ({ players: [] })),
      fetchNflJson("/api/nfl/weekly-logs?season=2026", 120000).catch(() => ({ ok: true, players: [] })),
      fetchNflJson("/api/nfl/weekly-projections?season=2024,2025,2026", 120000).catch(() => ({ ok: false, players: [] })),
    ]);
    const byId = {};
    const byName = {};
    const mergeLogs = (players) => {
      (players || []).forEach((row) => {
        const logs = row.logs || [];
        const display = readablePlayerName(row.name, row.id);
        playerIdAliases(row.id).forEach((alias) => {
          byId[alias] = (byId[alias] || []).concat(logs);
        });
        const key = weeklyLogKey(display, row.pos);
        byName[key] = (byName[key] || []).concat(logs);
        const plain = normalizePlayerName(display);
        if (plain) byPlain[plain] = (byPlain[plain] || []).concat(logs);
      });
    };
    const byPlain = {};
    mergeLogs(logs2025.players);
    mergeLogs(logs2024.players);
    mergeLogs(logs2023?.players);
    mergeLogs(logs2026?.players);
    window.WEEKLY_LOGS_BY_ID = byId;
    window.WEEKLY_LOGS_BY_NAME = byName;
    window.WEEKLY_LOGS_BY_PLAIN = byPlain;

    const projById = {};
    const projByName = {};
    const projByPlain = {};
    (weeklyProj.players || []).forEach((row) => {
      const weeks = row.weeks || [];
      const display = readablePlayerName(row.name, row.id);
      playerIdAliases(row.id).forEach((alias) => { projById[alias] = weeks; });
      projByName[weeklyLogKey(display, row.pos)] = weeks;
      const plain = normalizePlayerName(display);
      if (plain) projByPlain[plain] = weeks;
    });
    window.WEEKLY_PROJ_BY_ID = projById;
    window.WEEKLY_PROJ_BY_NAME = projByName;
    window.WEEKLY_PROJ_BY_PLAIN = projByPlain;

    const attach = (player) => {
      const logs = lookupByAliases(byId, player.id) || byName[weeklyLogKey(player.name, player.pos)] || byPlain[normalizePlayerName(player.name)];
      const weeks = lookupByAliases(projById, player.id) || projByName[weeklyLogKey(player.name, player.pos)] || projByPlain[normalizePlayerName(player.name)];
      if (weeks?.length) player.weeklyProjRaw = weeks;
      if (logs?.length) {
        player.weeklyLogRaw = logs;
        const projKey = {};
        (player.weeklyProjRaw || []).forEach((row) => {
          projKey[`${Number(row.season)}:${Number(row.week)}`] = row;
        });
        player.historicalLogs = materializeWeeklyLogs(logs).map((log) => {
          const issued = projKey[`${log.season}:${log.week}`];
          if (!issued) return log;
          return {
            ...log,
            expectedPoints: issuedWeeklyPts(issued, window.ACTIVE_DRAFT_SCORING || "ppr"),
            expectedStd: Number(issued.std ?? issued.fantasyPointsStd),
            expectedHalf: Number(issued.half ?? issued.fantasyPointsHalfPpr),
            expectedPpr: Number(issued.ppr ?? issued.fantasyPointsPpr ?? issued.fantasyPoints),
          };
        });
      }
    };
    (window.DRAFT_PLAYERS || []).forEach(attach);
    Object.values(window.DRAFT_PLAYERS_BY_SCORING || {}).forEach((pool) => (pool || []).forEach(attach));
    PLAYERS.forEach(attach);
    clearGameLogCache();
    window.LIVE_CURRENT_WEEK = inferLiveCurrentWeekFromLogs();
    window.TAPE_SEASON = 2026;
    window.NFL_DATA_STATUS = {
      ...(window.NFL_DATA_STATUS || {}),
      weeklyLogs: "live",
      weeklyLogsLoaded: Object.keys(byId).length,
      weeklyProjectionsLoaded: (weeklyProj.players || []).length,
      liveCurrentWeek: window.LIVE_CURRENT_WEEK,
    };
    window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
  } catch (error) {
    window.NFL_DATA_STATUS = {
      ...(window.NFL_DATA_STATUS || {}),
      weeklyLogs: "unavailable",
      weeklyLogsLoaded: 0,
      weeklyLogsError: error instanceof Error ? error.message : "Weekly logs unavailable",
    };
    window.dispatchEvent(new CustomEvent("nfl-data-status", { detail: window.NFL_DATA_STATUS }));
  }
}

async function hydrateDemoDataFromApi() {
  restoreCachedProjections();
  await hydrateNflTeamsFromApi();
  await Promise.all([hydrateNflSosFromApi(), hydrateNflScheduleFromApi(), hydrateRosterStatsFromApi()]);
  await hydrateProjectionsFromApi();
  await hydrateWeeklyLogsFromApi();
}

function traj(seed, base, vol, trend = 0) {
  const out = [];
  let v = base;
  let rng = seed;
  for (let i = 0; i < 10; i++) {
    rng = (rng * 9301 + 49297) % 233280;
    const r = rng / 233280;
    v = v + (r - 0.5) * vol + trend;
    out.push(Math.max(0, +v.toFixed(1)));
  }
  return out;
}

let PLAYERS = [
  // QBs — 2025 mid-season; actual = Week 10, proj/matchup = Week 11 lookahead
  { id: "jallen",  name: "Josh Allen",       pos: "QB", team: "BUF", num: 17, age: 29, proj: 25.4, actual: 18.4, rank: 1,  prev: 1,  ros: 248.2, own: 99.8, trend: "flat", risk: "low",   adp: 14,  round: 2,  matchup: "vs MIA", matchupRating: 4, traj: [24.2, 28.1, 17.4, 26.5, 31.8, 19.6, 25.6, 22.0, 29.4, 18.4], notes: "Rushing floor stable; home vs Miami in Week 11.", boom: 61, bust: 13, snap: 100, tgt: 0, rush: 8.8, news: "Full participant all week.", ceiling: 35, floor: 17 },
  { id: "lamar",   name: "Lamar Jackson",    pos: "QB", team: "BAL", num: 8,  age: 28, proj: 23.8, actual: 22.6, rank: 2,  prev: 3,  ros: 241.5, own: 99.6, trend: "up",   risk: "low",   adp: 18,  round: 2,  matchup: "@CLE", matchupRating: 4, traj: [19.2, 21.4, 24.8, 18.6, 27.2, 20.4, 23.0, 19.8, 26.4, 22.6], notes: "Dual-threat ceiling; Cleveland road spot next.", boom: 57, bust: 14, snap: 100, tgt: 0, rush: 10.2, news: "No injury designation.", ceiling: 34, floor: 16 },
  { id: "burrow",  name: "Joe Burrow",       pos: "QB", team: "CIN", num: 9,  age: 28, proj: 22.4, actual: 21.8, rank: 3,  prev: 4,  ros: 235.8, own: 98.8, trend: "up",   risk: "med",   adp: 28,  round: 3,  matchup: "@PIT", matchupRating: 3, traj: [20.8, 22.2, 19.4, 25.1, 23.6, 18.2, 24.4, 21.0, 27.8, 21.8], notes: "Volume steady through ten weeks.", boom: 53, bust: 17, snap: 100, tgt: 0, rush: 1.2, news: "Wrist monitored; expected to play.", ceiling: 32, floor: 15 },
  { id: "hurts",   name: "Jalen Hurts",      pos: "QB", team: "PHI", num: 1,  age: 27, proj: 23.2, actual: 19.2, rank: 4,  prev: 2,  ros: 228.4, own: 99.5, trend: "down", risk: "low",   adp: 22,  round: 2,  matchup: "vs GB",  matchupRating: 4, traj: [22.4, 24.0, 21.6, 23.8, 26.2, 20.8, 24.6, 22.2, 25.8, 19.2], notes: "Rush TDs buoy floor; Green Bay at home Week 11.", boom: 50, bust: 12, snap: 100, tgt: 0, rush: 9.4, news: "Healthy.", ceiling: 33, floor: 16 },

  // RBs
  { id: "cmcaff",  name: "Christian McCaffrey", pos: "RB", team: "SF",  num: 23, age: 29, proj: 21.8, actual: 17.4, rank: 1, prev: 1, ros: 198.6, own: 100, trend: "flat", risk: "med",   adp: 2,   round: 1,  matchup: "@ARI", matchupRating: 4, traj: [19.8, 22.4, 16.2, 20.6, 24.0, 14.8, 21.2, 18.6, 23.8, 17.4], notes: "Workhorse after return; Arizona next.", boom: 52, bust: 15, snap: 76, tgt: 5.2, rush: 17.4, news: "Limited Wed, full Thu/Fri.", ceiling: 31, floor: 12 },
  { id: "bijan",   name: "Bijan Robinson",   pos: "RB", team: "ATL", num: 7,  age: 23, proj: 20.6, actual: 22.8, rank: 2,  prev: 3,  ros: 192.4, own: 99.9, trend: "up",   risk: "low",   adp: 5,   round: 1,  matchup: "@NO",  matchupRating: 5, traj: [14.2, 16.8, 18.4, 17.6, 21.0, 19.2, 20.4, 18.8, 22.6, 22.8], notes: "Touch share elite in 2025.", boom: 55, bust: 11, snap: 84, tgt: 4.6, rush: 18.2, news: "No designation.", ceiling: 30, floor: 12 },
  { id: "saquon",  name: "Saquon Barkley",   pos: "RB", team: "PHI", num: 26, age: 28, proj: 19.8, actual: 14.2, rank: 3,  prev: 2,  ros: 186.2, own: 100, trend: "down", risk: "med",   adp: 4,   round: 1,  matchup: "vs GB",  matchupRating: 4, traj: [18.6, 20.2, 15.4, 19.0, 22.4, 13.8, 18.2, 16.4, 21.0, 14.2], notes: "Eagles run game peaking; shares dip in blowouts.", boom: 47, bust: 21, snap: 72, tgt: 3.0, rush: 16.8, news: "Full practice Thursday.", ceiling: 28, floor: 9 },
  { id: "henry",   name: "Derrick Henry",    pos: "RB", team: "BAL", num: 22, age: 31, proj: 17.2, actual: 19.6, rank: 4,  prev: 5,  ros: 168.8, own: 99.4, trend: "up",   risk: "med",   adp: 11,  round: 2,  matchup: "@CLE", matchupRating: 4, traj: [12.4, 14.8, 16.2, 15.6, 18.8, 11.2, 17.4, 14.0, 20.2, 19.6], notes: "Goal-line volume in Baltimore.", boom: 46, bust: 19, snap: 62, tgt: 1.0, rush: 20.8, news: "Healthy.", ceiling: 27, floor: 8 },
  { id: "jacobs",  name: "Josh Jacobs",      pos: "RB", team: "GB",  num: 8,  age: 27, proj: 16.8, actual: 15.4, rank: 6,  prev: 6,  ros: 154.2, own: 98.2, trend: "up",   risk: "med",   adp: 24,  round: 3,  matchup: "@PHI", matchupRating: 3, traj: [11.8, 13.2, 14.6, 12.8, 16.4, 10.6, 15.0, 13.8, 17.2, 15.4], notes: "Lead back in Green Bay.", boom: 42, bust: 22, snap: 70, tgt: 2.4, rush: 16.2, news: "Probable tag cleared.", ceiling: 25, floor: 7 },
  { id: "kamara",  name: "Alvin Kamara",     pos: "RB", team: "NO",  num: 41, age: 30, proj: 13.8, actual: 11.8, rank: 9,  prev: 8,  ros: 142.6, own: 94.6, trend: "flat", risk: "med",   adp: 32,  round: 4,  matchup: "vs ATL", matchupRating: 2, traj: [10.2, 12.4, 11.0, 13.6, 14.2, 9.8, 12.8, 10.4, 13.0, 11.8], notes: "Receiving floor; tough vs Bijan.", boom: 34, bust: 28, snap: 68, tgt: 5.4, rush: 11.2, news: "Limited in practice early week.", ceiling: 21, floor: 5 },

  // WRs
  { id: "jchase",  name: "Ja'Marr Chase",    pos: "WR", team: "CIN", num: 1,  age: 25, proj: 20.4, actual: 24.6, rank: 1,  prev: 1,  ros: 198.4, own: 100, trend: "up",   risk: "low",   adp: 3,   round: 1,  matchup: "@PIT", matchupRating: 4, traj: [16.8, 18.4, 14.2, 22.6, 24.8, 17.2, 21.4, 19.6, 26.2, 24.6], notes: "Alpha target share; Steelers rematch.", boom: 63, bust: 12, snap: 94, tgt: 11.2, rush: 0, news: "Healthy.", ceiling: 33, floor: 10 },
  { id: "jjeff",   name: "Justin Jefferson", pos: "WR", team: "MIN", num: 18, age: 27, proj: 17.8, actual: 16.4, rank: 2,  prev: 2,  ros: 186.2, own: 99.8, trend: "flat", risk: "low",   adp: 6,   round: 1,  matchup: "@BAL", matchupRating: 3, traj: [15.4, 16.2, 12.8, 18.0, 20.4, 14.6, 17.2, 15.8, 19.4, 16.4], notes: "QB volatility caps spike weeks.", boom: 54, bust: 17, snap: 93, tgt: 9.8, rush: 0, news: "Full practice.", ceiling: 29, floor: 8 },
  { id: "ceeded",  name: "CeeDee Lamb",      pos: "WR", team: "DAL", num: 88, age: 26, proj: 17.2, actual: 11.2, rank: 4,  prev: 3,  ros: 172.8, own: 99.6, trend: "down", risk: "med",   adp: 7,   round: 1,  matchup: "@PHI", matchupRating: 2, traj: [16.2, 14.8, 13.4, 15.6, 18.2, 12.4, 14.0, 13.2, 16.8, 11.2], notes: "Road division game at Philly.", boom: 48, bust: 21, snap: 92, tgt: 10.4, rush: 0.2, news: "Expected to play.", ceiling: 27, floor: 6 },
  { id: "amon",    name: "Amon-Ra St. Brown", pos: "WR", team: "DET", num: 14, age: 26, proj: 17.6, actual: 19.8, rank: 3,  prev: 5,  ros: 178.6, own: 99.7, trend: "up",   risk: "low",   adp: 10,  round: 2,  matchup: "@MIN", matchupRating: 4, traj: [13.6, 15.2, 14.8, 16.4, 18.6, 15.0, 17.8, 16.2, 20.4, 19.8], notes: "Consistent PPR floor in Detroit.", boom: 51, bust: 15, snap: 90, tgt: 9.6, rush: 0, news: "Healthy.", ceiling: 28, floor: 9 },
  { id: "puka",    name: "Puka Nacua",       pos: "WR", team: "LAR", num: 17, age: 24, proj: 18.2, actual: 21.4, rank: 5,  prev: 6,  ros: 168.4, own: 99.5, trend: "up",   risk: "low",   adp: 12,  round: 2,  matchup: "vs MIA", matchupRating: 4, traj: [14.2, 16.8, 13.6, 17.4, 19.2, 12.8, 18.6, 15.4, 22.0, 21.4], notes: "Target hog when active.", boom: 55, bust: 13, snap: 91, tgt: 10.6, rush: 0, news: "Healthy.", ceiling: 30, floor: 9 },
  { id: "hill",    name: "Tyreek Hill",      pos: "WR", team: "MIA", num: 10, age: 31, proj: 12.4, actual: 6.8,  rank: 14, prev: 8,  ros: 118.2, own: 97.2, trend: "down", risk: "high",  adp: 18,  round: 2,  matchup: "@LAR", matchupRating: 3, traj: [14.8, 16.2, 12.4, 15.0, 17.6, 9.2, 11.4, 8.6, 12.0, 6.8],  notes: "Limited role after early-season injury.", boom: 38, bust: 38, snap: 62, tgt: 6.2, rush: 0.4, news: "Snap count managed.", ceiling: 24, floor: 4 },
  { id: "drake",   name: "Drake London",     pos: "WR", team: "ATL", num: 5,  age: 25, proj: 15.2, actual: 14.6, rank: 7,  prev: 9,  ros: 156.8, own: 98.4, trend: "up",   risk: "med",   adp: 20,  round: 3,  matchup: "@NO",  matchupRating: 4, traj: [11.2, 12.8, 10.4, 13.6, 15.8, 11.6, 14.2, 12.4, 15.0, 14.6], notes: "Falcons passing funnel.", boom: 40, bust: 22, snap: 91, tgt: 8.8, rush: 0, news: "Healthy.", ceiling: 23, floor: 7 },
  { id: "smith",   name: "DeVonta Smith",    pos: "WR", team: "PHI", num: 6,  age: 27, proj: 13.6, actual: 10.4, rank: 11, prev: 10, ros: 142.4, own: 95.8, trend: "flat", risk: "med",   adp: 36,  round: 4,  matchup: "vs GB",  matchupRating: 3, traj: [10.8, 11.4, 9.6, 12.2, 14.0, 8.8, 11.2, 10.6, 13.8, 10.4], notes: "TD-dependent but steady routes.", boom: 35, bust: 28, snap: 85, tgt: 7.2, rush: 0, news: "Healthy.", ceiling: 22, floor: 5 },

  // TEs
  { id: "kelce",   name: "Travis Kelce",     pos: "TE", team: "KC",  num: 87, age: 35, proj: 10.8, actual: 11.6, rank: 2,  prev: 1,  ros: 118.6, own: 98.8, trend: "flat", risk: "med",   adp: 38,  round: 4,  matchup: "@BUF", matchupRating: 3, traj: [8.4, 9.6, 7.8, 11.2, 12.4, 9.0, 10.6, 9.8, 12.2, 11.6], notes: "Red-zone looks vs Buffalo.", boom: 38, bust: 24, snap: 82, tgt: 7.4, rush: 0, news: "Healthy.", ceiling: 20, floor: 4 },
  { id: "lakers",  name: "Sam LaPorta",      pos: "TE", team: "DET", num: 87, age: 24, proj: 11.2, actual: 13.4, rank: 1,  prev: 2,  ros: 124.8, own: 98.2, trend: "up",   risk: "low",   adp: 42,  round: 5,  matchup: "vs MIN", matchupRating: 4, traj: [9.2, 10.4, 8.6, 11.8, 13.0, 9.4, 11.6, 10.2, 12.8, 13.4], notes: "RZ role steady in 2025.", boom: 40, bust: 20, snap: 84, tgt: 6.4, rush: 0, news: "Healthy.", ceiling: 19, floor: 4 },
  { id: "andrews", name: "Mark Andrews",     pos: "TE", team: "BAL", num: 89, age: 30, proj: 8.8,  actual: 7.2,  rank: 6,  prev: 5,  ros: 98.4,  own: 94.2, trend: "down", risk: "med",   adp: 54,  round: 6,  matchup: "@CLE", matchupRating: 3, traj: [9.8, 8.4, 10.2, 7.6, 11.4, 6.8, 9.2, 7.4, 9.6, 7.2],  notes: "Split with Likely caps ceiling.", boom: 28, bust: 30, snap: 58, tgt: 4.8, rush: 0, news: "Limited snaps.", ceiling: 16, floor: 3 },
  { id: "gibbs",   name: "Jahmyr Gibbs",     pos: "RB", team: "DET", num: 0, age: 24, proj: 19.4, actual: 0, rank: 5, prev: 5, ros: 180.2, own: 99, trend: "up", risk: "low", adp: 8, round: 1, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 48, bust: 16, snap: 70, tgt: 4.2, rush: 16, news: "", ceiling: 28, floor: 10 },
  { id: "mcbride", name: "Trey McBride",     pos: "TE", team: "ARI", num: 85, age: 26, proj: 12.6, actual: 0, rank: 3, prev: 3, ros: 132.4, own: 97, trend: "up", risk: "low", adp: 28, round: 3, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 36, bust: 18, snap: 86, tgt: 8.2, rush: 0, news: "", ceiling: 20, floor: 6 },
  { id: "aubrey",  name: "Brandon Aubrey",   pos: "K",  team: "DAL", num: 17, age: 31, proj: 9.1, actual: 0, rank: 1, prev: 1, ros: 154.7, own: 90, trend: "flat", risk: "low", adp: 120, round: 10, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 22, bust: 12, snap: 0, tgt: 0, rush: 0, news: "", ceiling: 14, floor: 5 },
  { id: "bowers",  name: "Brock Bowers",     pos: "TE", team: "LV",  num: 89, age: 23, proj: 13.0, actual: 0, rank: 2, prev: 2, ros: 138.0, own: 96, trend: "up", risk: "med", adp: 26, round: 3, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 40, bust: 20, snap: 82, tgt: 8.8, rush: 0, news: "", ceiling: 22, floor: 6 },
  { id: "dadams",  name: "Davante Adams",    pos: "WR", team: "LAR", num: 17, age: 33, proj: 14.6, actual: 0, rank: 12, prev: 12, ros: 148.2, own: 94, trend: "flat", risk: "med", adp: 34, round: 3, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 38, bust: 24, snap: 84, tgt: 8.4, rush: 0, news: "", ceiling: 24, floor: 6 },
  { id: "hou-dst", name: "Houston",          pos: "DST", team: "HOU", num: 0, age: 0, proj: 7.4, actual: 0, rank: 4, prev: 4, ros: 125.8, own: 40, trend: "flat", risk: "med", adp: 140, round: 12, matchup: "TBD", matchupRating: 3, traj: [], notes: "949 marketing roster", boom: 30, bust: 28, snap: 0, tgt: 0, rush: 0, news: "", ceiling: 16, floor: 2 },
];

const MARKETING_STARTER_NAMES = [
  { slot: "QB", name: "Josh Allen", pos: "QB" },
  { slot: "RB", name: "Bijan Robinson", pos: "RB" },
  { slot: "RB", name: "Jahmyr Gibbs", pos: "RB" },
  { slot: "WR", name: "Ja'Marr Chase", pos: "WR" },
  { slot: "WR", name: "Amon-Ra St. Brown", pos: "WR" },
  { slot: "TE", name: "Trey McBride", pos: "TE" },
  { slot: "FLEX", name: "Justin Jefferson", pos: "WR" },
  { slot: "DST", name: "Houston", pos: "DST" },
  { slot: "K", name: "Brandon Aubrey", pos: "K" },
];
const MARKETING_BENCH_NAMES = [
  { name: "Lamar Jackson", pos: "QB" },
  { name: "Christian McCaffrey", pos: "RB" },
  { name: "Davante Adams", pos: "WR" },
  { name: "Brock Bowers", pos: "TE" },
  { name: "Derrick Henry", pos: "RB" },
  { name: "Puka Nacua", pos: "WR" },
];

const ROSTER_IDS = ["jallen", "bijan", "gibbs", "jchase", "amon", "mcbride", "jjeff", "hou-dst", "aubrey", "lamar", "cmcaff", "dadams", "bowers", "henry", "puka"];
const STARTER_IDS = ["jallen", "bijan", "gibbs", "jchase", "amon", "mcbride", "jjeff", "hou-dst", "aubrey"];
const BENCH_IDS = ["lamar", "cmcaff", "dadams", "bowers", "henry", "puka"];

const LEAGUE_SLOTS = ["QB", "RB", "RB", "WR", "WR", "FLEX", "TE", "DST", "K"];

// Your team — weeks 1–10 played (2025); totals sum to ~1277 PF through Week 10
const TEAM_POSITION_WEEKS = [
  { wk: 1,  QB: 24.2, RB1: 22.1, RB2: 14.8, WR1: 18.4, WR2: 12.0, FLEX: 11.2, TE: 13.5, DST: 7.5, K: 4.7, total: 128.4, opp: 112.3, w: true,  proj: 124.0, oppProj: 118.0 },
  { wk: 2,  QB: 28.1, RB1: 25.4, RB2: 16.2, WR1: 21.8, WR2: 14.4, FLEX: 12.8, TE: 11.0, DST: 8.0, K: 5.1, total: 142.8, opp: 98.2,  w: true,  proj: 126.0, oppProj: 119.0 },
  { wk: 3,  QB: 17.4, RB1: 16.8, RB2: 12.4, WR1: 14.2, WR2:  8.6, FLEX:  9.2, TE:  8.8, DST: 5.0, K: 3.7, total: 96.1,  opp: 118.0, w: false, proj: 128.0, oppProj: 115.0 },
  { wk: 4,  QB: 26.5, RB1: 19.2, RB2: 18.6, WR1: 17.4, WR2: 13.2, FLEX: 12.0, TE: 16.1, DST: 6.0, K: 5.2, total: 134.2, opp: 121.5, w: true,  proj: 122.0, oppProj: 117.0 },
  { wk: 5,  QB: 31.8, RB1: 28.0, RB2: 19.4, WR1: 22.6, WR2: 17.8, FLEX: 15.4, TE: 12.4, DST: 4.5, K: 4.8, total: 156.7, opp: 132.0, w: true,  proj: 128.0, oppProj: 121.0 },
  { wk: 6,  QB: 19.6, RB1: 14.2, RB2: 11.0, WR1: 16.8, WR2:  9.4, FLEX: 10.8, TE: 17.0, DST: 6.5, K: 4.1, total: 109.4, opp: 124.6, w: false, proj: 127.0, oppProj: 118.0 },
  { wk: 7,  QB: 25.6, RB1: 24.8, RB2: 15.4, WR1: 19.4, WR2: 12.8, FLEX: 13.6, TE: 14.2, DST: 7.5, K: 4.7, total: 138.0, opp: 105.1, w: true,  proj: 125.0, oppProj: 119.0 },
  { wk: 8,  QB: 22.0, RB1: 21.6, RB2: 13.8, WR1: 18.2, WR2: 11.0, FLEX: 11.4, TE: 11.6, DST: 6.5, K: 5.1, total: 121.2, opp: 119.8, w: true,  proj: 126.0, oppProj: 118.0 },
  { wk: 9,  QB: 29.4, RB1: 26.2, RB2: 17.6, WR1: 22.4, WR2: 15.6, FLEX: 14.0, TE: 13.4, DST: 4.5, K: 4.8, total: 147.9, opp: 110.4, w: true,  proj: 124.0, oppProj: 117.0 },
  { wk: 10, QB: 18.4, RB1: 12.0, RB2:  9.6, WR1: 24.6, WR2:  6.0, FLEX:  8.0, TE: 12.0, DST: 7.0, K: 5.0, total: 102.6, opp: 128.4, w: false, proj: 127.0, oppProj: 120.0 },
];

// Forward from Week 10 snapshot (Weeks 11–14 projections only)
const TEAM_FUTURE_WEEKS = [
  { wk: 11, proj: 131.0, oppProj: 119.0 },
  { wk: 12, proj: 133.5, oppProj: 117.0 },
  { wk: 13, proj: 127.0, oppProj: 121.0 },
  { wk: 14, proj: 130.0, oppProj: 118.0 },
];

// Per-week positional averages through 10 weeks (from TEAM_POSITION_WEEKS)
const POSITION_PERFORMANCE = {
  QB:   { expected: 24.0, actual: 24.3, starters: 1, slotLabel: "QB",   color: "#F2C94C" },
  RB:   { expected: 18.2, actual: 18.8, starters: 2, slotLabel: "RB",   color: "#95F9AE" },
  WR:   { expected: 16.2, actual: 16.1, starters: 2, slotLabel: "WR",   color: "#7BD9F4" },
  TE:   { expected: 12.2, actual: 12.9, starters: 1, slotLabel: "TE",   color: "#D9A6F4" },
  FLEX: { expected: 12.0, actual: 12.2, starters: 1, slotLabel: "FLEX", color: "#D9FFE4" },
  DST:  { expected:  6.5, actual:  6.3, starters: 1, slotLabel: "DST",  color: "#FF9E5E" },
  K:    { expected:  4.8, actual:  4.7, starters: 1, slotLabel: "K",    color: "#A6B7AC" },
};

const LEAGUE_POS_AVG = {
  QB:  [20.4, 22.6, 18.2, 21.8, 23.8, 19.4, 22.2, 20.2, 23.6, 20.8],
  RB:  [14.8, 16.0, 13.2, 15.2, 17.4, 13.6, 15.4, 14.6, 16.2, 14.4],
  WR:  [13.2, 14.6, 12.6, 14.2, 15.8, 12.4, 14.0, 13.4, 14.8, 13.6],
  TE:  [ 9.4, 10.0,  8.6,  9.8, 11.0,  8.8,  9.6,  9.2, 10.2,  9.4],
  FLEX:[11.8, 13.2, 10.6, 12.2, 13.8, 11.2, 12.0, 11.6, 12.8, 11.4],
  DST: [ 6.2,  6.8,  5.6,  6.4,  6.8,  6.0,  6.6,  6.2,  6.4,  6.2],
  K:   [ 6.8,  7.4,  6.0,  7.0,  7.6,  6.4,  7.0,  6.6,  7.2,  6.6],
};

const LEAGUE_STANDINGS = [
  { id: "you",   name: "The Vector Reapers",       owner: "You",        w: 7, l: 3, pf: 1277, pa: 1168, streak: "L1", rank: 2 },
  { id: "t01",   name: "Dynasty Warriors",         owner: "Mike S.",    w: 8, l: 2, pf: 1314, pa: 1142, streak: "W3", rank: 1 },
  { id: "t02",   name: "Saquon Family Robinson",   owner: "Alex P.",    w: 6, l: 4, pf: 1224, pa: 1198, streak: "W1", rank: 3 },
  { id: "t03",   name: "Bijan Mustard",            owner: "Jamie L.",   w: 6, l: 4, pf: 1198, pa: 1184, streak: "W2", rank: 4 },
  { id: "t04",   name: "Allen Wrenches",           owner: "Sam T.",     w: 5, l: 5, pf: 1196, pa: 1212, streak: "L2", rank: 5 },
  { id: "t05",   name: "Multiple Scorgasms",       owner: "Chris D.",   w: 5, l: 5, pf: 1184, pa: 1206, streak: "W1", rank: 6 },
  { id: "t06",   name: "Lamb Chops",               owner: "Pat M.",     w: 5, l: 5, pf: 1172, pa: 1196, streak: "L1", rank: 7 },
  { id: "t07",   name: "Mahomies & Co.",           owner: "Tay R.",     w: 4, l: 6, pf: 1148, pa: 1224, streak: "L3", rank: 8 },
  { id: "t08",   name: "Justifiably Jefferson",    owner: "Quinn K.",   w: 4, l: 6, pf: 1142, pa: 1232, streak: "W1", rank: 9 },
  { id: "t09",   name: "Hurts So Good",            owner: "Robin V.",   w: 3, l: 7, pf: 1102, pa: 1248, streak: "L4", rank: 10 },
  { id: "t10",   name: "Punt God",                 owner: "Casey N.",   w: 3, l: 7, pf: 1088, pa: 1264, streak: "L2", rank: 11 },
  { id: "t11",   name: "Last Place Lizards",       owner: "Drew H.",    w: 2, l: 8, pf: 1042, pa: 1296, streak: "L5", rank: 12 },
];

const NEWS = [
  { tag: "INJ",     team: "MIA", text: "Tyreek Hill limited in practice but expected to play at Rams in Week 11.", time: "18m ago", severity: "warn" },
  { tag: "USAGE",   team: "PHI", text: "Saquon Barkley at 68% snaps in Week 10 — Eagles leaned pass-heavy in second half.", time: "42m ago", severity: "info" },
  { tag: "WEATHER", team: "BUF", text: "Chiefs @ Bills Week 11: lake-effect risk, 15–20 mph gusts possible.", time: "1h ago",  severity: "info" },
  { tag: "TARGET",  team: "CIN", text: "Ja'Marr Chase 31% target share over last three games — still the offense hub.", time: "2h ago",  severity: "good" },
  { tag: "OUT",     team: "MIN", text: "Jordan Addison ruled out; Jefferson remains the clear WR1.", time: "3h ago",  severity: "bad"  },
];

const NFL_TEAM_NICK = {
  SEA: "SEAHAWKS", SF: "49ERS", LV: "RAIDERS", LAR: "RAMS", LAC: "CHARGERS", ARI: "CARDINALS",
  DEN: "BRONCOS", KC: "CHIEFS", DAL: "COWBOYS", HOU: "TEXANS", NO: "SAINTS",
  MIN: "VIKINGS", GB: "PACKERS", CHI: "BEARS", DET: "LIONS", IND: "COLTS",
  TEN: "TITANS", ATL: "FALCONS", CAR: "PANTHERS", JAX: "JAGUARS", TB: "BUCCANEERS", MIA: "DOLPHINS",
  CIN: "BENGALS", CLE: "BROWNS", PIT: "STEELERS", BAL: "RAVENS", WAS: "COMMANDERS",
  PHI: "EAGLES", NYG: "GIANTS", NYJ: "JETS", BUF: "BILLS", NE: "PATRIOTS",
};

const NFL_CITIES = [
  { team: "SEA", city: "Seattle",       stadium: "Lumen Field",              indoor: false, x: 394,  y: 74,   lx: -8,  ly: -28, iconRot: -135 },
  { team: "SF",  city: "Santa Clara",   stadium: "Levi's Stadium",           indoor: false, x: 205,  y: 483,  lx: 8,   ly: -28 },
  { team: "LAR", city: "Inglewood",     stadium: "SoFi Stadium",             indoor: true,  x: 269,  y: 675,  lx: -4,  ly: 34 },
  { team: "LAC", city: "Inglewood",     stadium: "SoFi Stadium",             indoor: true,  x: 269,  y: 675,  lx: -4,  ly: 34 },
  { team: "LV",  city: "Las Vegas",     stadium: "Allegiant Stadium",        indoor: true,  x: 398,  y: 568,  lx: 10,  ly: -26, iconRot: 55 },
  { team: "ARI", city: "Glendale",      stadium: "State Farm Stadium",      indoor: true,  x: 539,  y: 773,  lx: 12,  ly: 32 },
  { team: "DEN", city: "Denver",        stadium: "Empower Field at Mile High", indoor: false, x: 795,  y: 570,  lx: 0,   ly: -28 },
  { team: "KC",  city: "Kansas City",   stadium: "GEHA Field at Arrowhead Stadium", indoor: false, x: 1118, y: 618,  lx: 0,   ly: -28 },
  { team: "MIN", city: "Minneapolis",   stadium: "U.S. Bank Stadium",       indoor: true,  x: 1182, y: 197,  lx: 0,   ly: -28, iconRot: 112 },
  { team: "GB",  city: "Green Bay",     stadium: "Lambeau Field",           indoor: false, x: 1299, y: 250,  lx: 18,  ly: -22 },
  { team: "CHI", city: "Chicago",       stadium: "Soldier Field",           indoor: false, x: 1278, y: 378,  lx: -36, ly: 8, iconRot: 45 },
  { team: "DET", city: "Detroit",       stadium: "Ford Field",              indoor: true,  x: 1418, y: 308,  lx: 36,  ly: -8, iconRot: 37 },
  { team: "IND", city: "Indianapolis",  stadium: "Lucas Oil Stadium",       indoor: true,  x: 1368, y: 468,  lx: 0,   ly: 32 },
  { team: "CIN", city: "Cincinnati",    stadium: "Paycor Stadium",          indoor: false, x: 1516, y: 539,  lx: 28,  ly: 24 },
  { team: "CLE", city: "Cleveland",     stadium: "Cleveland Browns Stadium", indoor: false, x: 1529, y: 467,  lx: 8,   ly: -26 },
  { team: "PIT", city: "Pittsburgh",    stadium: "Acrisure Stadium",        indoor: false, x: 1614, y: 457,  lx: -8,  ly: 32 },
  { team: "BUF", city: "Orchard Park",  stadium: "Highmark Stadium",        indoor: false, x: 1707, y: 338,  lx: 0,   ly: -28 },
  { team: "NE",  city: "Foxborough",    stadium: "Gillette Stadium",        indoor: false, x: 1852, y: 331,  lx: 8,   ly: -26 },
  { team: "NYJ", city: "E. Rutherford", stadium: "MetLife Stadium",         indoor: false, x: 1784, y: 411,  lx: 42,  ly: -8 },
  { team: "NYG", city: "E. Rutherford", stadium: "MetLife Stadium",         indoor: false, x: 1784, y: 411,  lx: 42,  ly: -8 },
  { team: "PHI", city: "Philadelphia",  stadium: "Lincoln Financial Field", indoor: false, x: 1718, y: 438,  lx: 48,  ly: 10 },
  { team: "BAL", city: "Baltimore",     stadium: "M&T Bank Stadium",        indoor: false, x: 1715, y: 483,  lx: 52,  ly: 28 },
  { team: "WAS", city: "Landover",      stadium: "Northwest Stadium",       indoor: false, x: 1712, y: 514,  lx: -8,  ly: 32 },
  { team: "TEN", city: "Nashville",     stadium: "Nissan Stadium",          indoor: false, x: 1425, y: 680,  lx: 0,   ly: -26 },
  { team: "ATL", city: "Atlanta",       stadium: "Mercedes-Benz Stadium",   indoor: true,  x: 1611, y: 847,  lx: 36,  ly: 8 },
  { team: "CAR", city: "Charlotte",     stadium: "Bank of America Stadium", indoor: false, x: 1609, y: 679,  lx: 48,  ly: -6 },
  { team: "JAX", city: "Jacksonville",  stadium: "EverBank Stadium",        indoor: false, x: 1619, y: 919,  lx: 40,  ly: 8 },
  { team: "TB",  city: "Tampa",         stadium: "Raymond James Stadium",   indoor: false, x: 1629, y: 1038, lx: -8,  ly: 32 },
  { team: "MIA", city: "Miami Gardens", stadium: "Hard Rock Stadium",       indoor: false, x: 1688, y: 1027, lx: 8,   ly: 32 },
  { team: "NO",  city: "New Orleans",   stadium: "Caesars Superdome",       indoor: true,  x: 1291, y: 980,  lx: 0,   ly: 32 },
  { team: "HOU", city: "Houston",       stadium: "NRG Stadium",             indoor: true,  x: 1121, y: 951,  lx: 0,   ly: 32 },
  { team: "DAL", city: "Arlington",     stadium: "AT&T Stadium",            indoor: true,  x: 1039, y: 831,  lx: 0,   ly: -28 },
];

// Week 10, 2025-style slate (export name kept for stadium-map.jsx)
const WEEK11_GAMES = {
  BUF: "you",   KC: "you",   BAL: "you",   CIN: "you",   ATL: "you",
  SF:  "you",   LAR: "you",   PHI: "opp",   DAL: "opp",
  DET: "neutral", MIN: "neutral", GB: "neutral", CHI: "neutral",
  MIA: "neutral", HOU: "neutral", NO: "neutral", SEA: "neutral",
  CLE: "off",   PIT: "off",   IND: "off",   LV: "off",   LAC: "off",
  DEN: "off",   CAR: "off",   JAX: "off",   TEN: "off",   TB: "off",
  NE:  "off",   NYJ: "off",   NYG: "off",   ARI: "off",   WAS: "off",
};

const SHARED_STADIUM_GROUPS = [
  { id: "metlife", name: "MetLife Stadium", idleLabel: "GIANTS & JETS", teams: ["NYG", "NYJ"] },
  { id: "sofi", name: "SoFi Stadium", idleLabel: "RAMS & CHARGERS", teams: ["LAR", "LAC"] },
];

const WEEK11_HOME_AT_SHARED = {
  NYG: "NYJ",
  NYJ: "NYJ",
  LAC: "LAR",
  LAR: "LAR",
};

const NFL_CITIES_BY_TEAM = Object.fromEntries(NFL_CITIES.map((c) => [c.team, c]));

function buildNflStadiums() {
  const used = new Set();
  const stadiums = SHARED_STADIUM_GROUPS.map((group) => {
    const site = NFL_CITIES_BY_TEAM[group.teams[0]];
    group.teams.forEach((t) => used.add(t));
    return {
      id: group.id,
      name: group.name,
      idleLabel: group.idleLabel,
      city: site?.city || group.name,
      indoor: !!site?.indoor,
      x: site?.x,
      y: site?.y,
      lx: site?.lx || 0,
      ly: site?.ly || -28,
      iconRot: site?.iconRot ?? -38,
      teams: [...group.teams],
      shared: true,
    };
  });
  NFL_CITIES.forEach((city) => {
    if (used.has(city.team)) return;
    stadiums.push({
      id: city.team,
      name: city.stadium || city.city,
      idleLabel: NFL_TEAM_NICK[city.team] || city.team,
      city: city.city,
      indoor: city.indoor,
      x: city.x,
      y: city.y,
      lx: city.lx || 0,
      ly: city.ly || -28,
      iconRot: city.iconRot ?? -38,
      teams: [city.team],
      shared: false,
    });
  });
  return stadiums;
}

const NFL_STADIUMS = buildNflStadiums();

function stadiumForTeam(team) {
  return NFL_STADIUMS.find((s) => s.teams.includes(team)) || null;
}

function homeTeamsThisWeek(schedule) {
  const homes = new Set();
  (schedule || []).forEach((g) => { if (g.homeTeam) homes.add(g.homeTeam); });
  return homes;
}

function occupantAtStadium(stadium, schedule, preferTeams) {
  const homes = homeTeamsThisWeek(schedule);
  const occupying = (stadium.teams || []).filter((t) => homes.has(t));
  if (!occupying.length) return { occupant: null, conflict: false, alsoHome: [] };
  if (occupying.length === 1) return { occupant: occupying[0], conflict: false, alsoHome: occupying };
  const prefer = preferTeams instanceof Set ? preferTeams : new Set(preferTeams || []);
  const preferred = occupying.filter((t) => prefer.has(t));
  return {
    occupant: preferred[0] || occupying[0],
    conflict: true,
    alsoHome: occupying,
  };
}

function occupantFromHomeOverrides(stadium, homeOverrides) {
  const occupying = (stadium.teams || []).filter((t) => homeOverrides && homeOverrides[t] === t);
  if (occupying.length === 1) return occupying[0];
  if (occupying.length > 1) return occupying[0];
  return null;
}

function mapSiteForTeam(team, homeOverrides) {
  const stadium = stadiumForTeam(team);
  if (!stadium) {
    const c = NFL_CITIES_BY_TEAM[team];
    return c ? { ...c, displayTeam: team } : null;
  }
  const occupant = occupantFromHomeOverrides(stadium, homeOverrides || {}) || (stadium.shared ? null : team);
  return {
    ...NFL_CITIES_BY_TEAM[occupant || stadium.teams[0]],
    x: stadium.x,
    y: stadium.y,
    city: stadium.city,
    indoor: stadium.indoor,
    displayTeam: occupant || stadium.idleLabel,
    stadiumId: stadium.id,
  };
}

function buildMapMarkers(weekStatus, homeOverrides) {
  return NFL_STADIUMS.map((stadium) => {
    const occupant = occupantFromHomeOverrides(stadium, homeOverrides || {});
    return {
      team: occupant || stadium.idleLabel,
      city: stadium.city,
      indoor: stadium.indoor,
      x: stadium.x,
      y: stadium.y,
      teamsAtSite: stadium.teams,
      stadiumId: stadium.id,
      occupant,
    };
  });
}

const NFL_MAP_MARKERS = buildMapMarkers(WEEK11_GAMES, WEEK11_HOME_AT_SHARED);

// Demo/roster surfaces read through these so live API rows win when hydrated,
// and the seeded Week 10 snapshot still renders the mock league views.
const SEEDED_PROTOTYPE_DATA = {
  TEAMS, PLAYERS, NEWS,
  ROSTER_IDS, STARTER_IDS, BENCH_IDS, LEAGUE_SLOTS,
  TEAM_POSITION_WEEKS, TEAM_FUTURE_WEEKS,
  POSITION_PERFORMANCE, LEAGUE_POS_AVG, LEAGUE_STANDINGS,
  NFL_CITIES, WEEK11_GAMES,
};

function getDemoPlayerPool() {
  return window.PLAYERS && window.PLAYERS.length ? window.PLAYERS : PLAYERS;
}

function normalizePlayerName(name) {
  return String(name || "")
    .toLowerCase()
    .replace(/['’.]/g, "")
    .replace(/\s+/g, " ")
    .replace(/\s+(jr|sr|ii|iii|iv)$/i, "")
    .replace(/\s+[a-z]$/i, "")
    .trim();
}

const PLAYER_NAME_ALIASES = {
  "devante adams": "davante adams",
  "amon ra st brown": "amon-ra st brown",
  "jerimyah love": "jeremiyah love",
  "jeremiah love": "jeremiyah love",
};

function findPlayerByName(name, pos) {
  const raw = normalizePlayerName(name);
  const key = PLAYER_NAME_ALIASES[raw] || raw;
  const pools = [window.DRAFT_PLAYERS || [], PLAYERS];
  for (const pool of pools) {
    const hit = pool.find((p) => {
      if (pos && p.pos !== pos) return false;
      const pn = normalizePlayerName(p.name);
      if (pos === "DST" && (key === "houston" || key === "hou") && (p.team === "HOU" || pn.includes("houston"))) return true;
      return pn === key || pn.includes(key) || key.includes(pn);
    });
    if (hit) return hit;
  }
  return null;
}

function resolveCanonicalPlayer(player) {
  if (!player) return null;
  const pool = window.DRAFT_PLAYERS || [];
  const profiles = window.PLAYER_PROFILES || {};
  const byId = pool.find((p) => p.id === player.id) || profiles[player.id];
  if (byId && byId.name) {
    const row = pool.find((p) => p.id === byId.id) || player;
    return { ...row, ...byId, profile: row.profile || (byId.historical ? byId : (profiles[row.id] || row.profile)) };
  }
  const byName = findPlayerByName(player.name, player.pos);
  if (byName) return { ...byName, profile: byName.profile || profiles[byName.id] };
  return {
    ...player,
    profile: profiles[player.id] || player.profile || {
      name: player.name,
      pos: player.pos,
      team: player.team,
      projection: {
        impactRank: player.rank || 0,
        valueGrade: "—",
        sosRank: 0,
        ceiling: (player.proj || 0) * 17,
        floor: 0,
        boomPct: 0,
        bustPct: 0,
      },
      scoring: {
        ppr: { weekly: player.proj || 0, season: player.ros || 0 },
        halfPpr: { weekly: player.proj || 0, season: player.ros || 0 },
        standard: { weekly: player.proj || 0, season: player.ros || 0 },
      },
      historical: { seasons: player.historicalSeasons || [], gameLogs: player.historicalLogs || [] },
      workload: {},
      efficiency: {},
      drivers: [],
      advanced: {},
      risk: player.risk || "med",
    },
  };
}

function getMarketingRoster() {
  const starters = MARKETING_STARTER_NAMES.map((row) => {
    const player = findPlayerByName(row.name, row.pos) || findPlayerByName(row.name);
    return player ? { ...player, lineupSlot: row.slot } : null;
  }).filter(Boolean);
  const starterIds = new Set(starters.map((p) => p.id));
  const bench = MARKETING_BENCH_NAMES.map((row) => {
    const player = findPlayerByName(row.name, row.pos) || findPlayerByName(row.name);
    return player && !starterIds.has(player.id) ? { ...player, lineupSlot: "BENCH" } : null;
  }).filter(Boolean);
  return { starters, bench, roster: starters.concat(bench) };
}

const FANTASY_REG_WEEKS = 13;

function scheduleStorageKey(leagueId) {
  return `949fantasy.league-schedule.v1.${leagueId || "local"}`;
}

function hasLeagueSchedule(leagueId) {
  try {
    const raw = window.localStorage.getItem(scheduleStorageKey(leagueId));
    if (!raw) return false;
    const parsed = JSON.parse(raw);
    return Object.values(parsed || {}).some((row) => Object.values(row || {}).some(Boolean));
  } catch (error) {
    return false;
  }
}

function loadLeagueScheduleGrid(leagueId, teamCount = 12) {
  const grid = {};
  for (let week = 1; week <= FANTASY_REG_WEEKS; week += 1) {
    grid[week] = {};
    for (let slot = 1; slot <= teamCount; slot += 1) grid[week][slot] = null;
  }
  try {
    const raw = window.localStorage.getItem(scheduleStorageKey(leagueId));
    if (!raw) return grid;
    const parsed = JSON.parse(raw);
    for (let week = 1; week <= FANTASY_REG_WEEKS; week += 1) {
      const row = parsed[week] || parsed[String(week)] || {};
      for (let slot = 1; slot <= teamCount; slot += 1) {
        const opp = Number(row[slot] || row[String(slot)]);
        grid[week][slot] = Number.isFinite(opp) && opp >= 1 && opp <= teamCount && opp !== slot ? opp : null;
      }
    }
  } catch (error) {
    return grid;
  }
  return grid;
}

function getDemoRosterPlayers(options = {}) {
  const forceSeed = !!options.forceSeed;
  if (!forceSeed && window.LIVE_LEAGUE?.roster?.length) {
    return window.LIVE_LEAGUE.roster.slice();
  }
  const marketing = getMarketingRoster();
  if (marketing.roster.length) return marketing.roster;
  const source = forceSeed ? PLAYERS : getDemoPlayerPool();
  const ids = !forceSeed && window.ROSTER_IDS && window.ROSTER_IDS.length ? window.ROSTER_IDS : ROSTER_IDS;
  const roster = ids.map(id => source.find(p => p.id === id) || PLAYERS.find(p => p.id === id)).filter(Boolean);
  return roster.length ? roster : ROSTER_IDS.map(id => PLAYERS.find(p => p.id === id)).filter(Boolean);
}

Object.assign(window, {
  TEAMS, PLAYERS, NEWS,
  ROSTER_IDS, STARTER_IDS, BENCH_IDS, LEAGUE_SLOTS,
  TEAM_POSITION_WEEKS, TEAM_FUTURE_WEEKS,
  POSITION_PERFORMANCE, LEAGUE_POS_AVG, LEAGUE_STANDINGS,
  NFL_CITIES, NFL_STADIUMS, SHARED_STADIUM_GROUPS, NFL_TEAM_NICK, WEEK11_GAMES,
  NFL_MAP_MARKERS, mapSiteForTeam, WEEK11_HOME_AT_SHARED,
  stadiumForTeam, occupantAtStadium,
  NFL_DATA_ENDPOINT, PROJECTIONS_ENDPOINTS, ROSTER_STATS_ENDPOINT, SOS_ENDPOINT, SCHEDULE_ENDPOINT,
  hydrateNflTeamsFromApi, hydrateProjectionsFromApi, hydrateWeeklyLogsFromApi, hydrateNflSosFromApi, hydrateNflScheduleFromApi, hydrateRosterStatsFromApi, hydrateDemoDataFromApi,
  restoreCachedProjections, saveCachedProjections,
  syncDraftScoringContext,
  profileToLegacyPlayer,
  applyByeWeeksToPlayers,
  buildMapMarkers, mapSiteForTeam,
  SEEDED_PROTOTYPE_DATA, getDemoPlayerPool, getDemoRosterPlayers,
  getMarketingRoster, findPlayerByName, resolveCanonicalPlayer, normalizePlayerName,
  playerIdAliases, findDraftPoolPlayer,
  applyWeeklyProjectionModel, modeledWeekProjection, weeklyModelRecord, currentSeasonGameLogs, inferLiveCurrentWeekFromLogs,
  hasLeagueSchedule, loadLeagueScheduleGrid, scheduleStorageKey, FANTASY_REG_WEEKS,
});

function useNflDataStatus() {
  const [status, setStatus] = React.useState(() => window.NFL_DATA_STATUS || {});
  React.useEffect(() => {
    const onStatus = (event) => setStatus({ ...(event.detail || window.NFL_DATA_STATUS || {}) });
    window.addEventListener("nfl-data-status", onStatus);
    return () => window.removeEventListener("nfl-data-status", onStatus);
  }, []);
  return status;
}

window.useNflDataStatus = useNflDataStatus;

const CURRENT_WEEK = 1;

function scoreFromStatLine(stats) {
  if (!stats) return null;
  const passYds = Number(stats.passingYards) || 0;
  const passTd = Number(stats.passingTds) || 0;
  const ints = Number(stats.interceptions) || 0;
  const rushYds = Number(stats.rushingYards) || 0;
  const rushTd = Number(stats.rushingTds) || 0;
  const rec = Number(stats.receptions) || 0;
  const recYds = Number(stats.receivingYards) || 0;
  const recTd = Number(stats.receivingTds) || 0;
  if (!(passYds || passTd || ints || rushYds || rushTd || rec || recYds || recTd)) return null;
  const base = passYds / 25 + passTd * 4 - ints * 2 + rushYds / 10 + rushTd * 6 + recYds / 10 + recTd * 6;
  return {
    std: +base.toFixed(2),
    half: +(base + rec * 0.5).toFixed(2),
    ppr: +(base + rec).toFixed(2),
  };
}

function scoringPts(row, scoring) {
  if (!row) return null;
  const scored = scoreFromStatLine(row.stats);
  if (scored) {
    if (scoring === "std") return scored.std;
    if (scoring === "half") return scored.half;
    return scored.ppr;
  }
  if (row.fantasyPoints != null && Number.isFinite(Number(row.fantasyPoints))) return Number(row.fantasyPoints);
  if (scoring === "std") return Number(row.std);
  if (scoring === "half") return Number(row.half);
  return Number(row.ppr);
}

function issuedWeeklyPts(row, scoring) {
  if (!row) return null;
  const issued = scoring === "std"
    ? row.std ?? row.fantasyPointsStd
    : scoring === "half"
      ? row.half ?? row.fantasyPointsHalfPpr
      : row.ppr ?? row.fantasyPointsPpr ?? row.fantasyPoints;
  if (issued != null && Number.isFinite(Number(issued))) return Number(issued);
  return scoringPts(row, scoring);
}

function resolveWeeklyProjections(player) {
  if (!player) return [];
  const named = findPlayerByName(player.name, player.pos) || findPlayerByName(player.name);
  return player.weeklyProjRaw
    || window.WEEKLY_PROJ_BY_ID?.[player.id]
    || window.WEEKLY_PROJ_BY_NAME?.[weeklyLogKey(player.name, player.pos)]
    || window.WEEKLY_PROJ_BY_PLAIN?.[normalizePlayerName(player.name)]
    || (named && (
      named.weeklyProjRaw
      || window.WEEKLY_PROJ_BY_ID?.[named.id]
      || window.WEEKLY_PROJ_BY_NAME?.[weeklyLogKey(named.name, named.pos)]
      || window.WEEKLY_PROJ_BY_PLAIN?.[normalizePlayerName(named.name)]
    ))
    || [];
}

function makeGameLog(player) {
  const season = Number(window.TAPE_SEASON || 2025);
  const scoring = window.ACTIVE_DRAFT_SCORING || "ppr";
  const historical = resolveHistoricalLogs(player)
    .filter((row) => Number(row.season) === season && Number(row.week) > 0)
    .sort((a, b) => (Number(a.week) || 0) - (Number(b.week) || 0));
  const projections = resolveWeeklyProjections(player).filter((row) => Number(row.season) === season);
  const projByWeek = Object.fromEntries(projections.map((row) => [Number(row.week), row]));
  const logByWeek = Object.fromEntries(historical.map((row) => [Number(row.week), row]));
  const byeWeek = Number(player.byeWeek || player.bye) || null;

  return Array.from({ length: 18 }, (_, i) => i + 1).map((wk) => {
    if (byeWeek && wk === byeWeek) {
      return {
        wk, season, opp: null, home: null, indoor: null, kickoff: null, day: null, weather: null, holiday: null,
        proj: null, actual: null, ceiling: null, floor: null, played: false, bye: true, conditionsKnown: false,
      };
    }
    const hist = logByWeek[wk];
    const projRow = projByWeek[wk];
    const actual = hist ? scoringPts(hist, scoring) : null;
    const proj = projRow ? issuedWeeklyPts(projRow, scoring) : null;
    const ceiling = projRow?.ceiling != null ? Number(projRow.ceiling) : null;
    const floor = projRow?.floor != null ? Number(projRow.floor) : null;
    const opp = (hist && hist.opponent) || (projRow && projRow.opponent) || null;
    return {
      wk,
      season,
      opp,
      home: hist?.home == null ? null : !!hist.home,
      indoor: null,
      kickoff: null,
      day: null,
      weather: null,
      holiday: null,
      proj: proj != null && Number.isFinite(proj) ? +Number(proj).toFixed(2) : null,
      actual: actual != null && Number.isFinite(actual) ? +Number(actual).toFixed(2) : null,
      ceiling: ceiling != null && Number.isFinite(ceiling) ? +Number(ceiling).toFixed(2) : null,
      floor: floor != null && Number.isFinite(floor) ? +Number(floor).toFixed(2) : null,
      played: actual != null && Number.isFinite(actual),
      bye: false,
      conditionsKnown: false,
    };
  });
}

function logsFromProfile(profile) {
  return (profile?.historical?.gameLogs || profile?.historicalLogs || []).slice();
}

function resolveHistoricalLogs(player) {
  if (!player) return [];
  const scoring = window.ACTIVE_DRAFT_SCORING || "ppr";
  const raw = player.weeklyLogRaw
    || window.WEEKLY_LOGS_BY_ID?.[player.id]
    || window.WEEKLY_LOGS_BY_NAME?.[weeklyLogKey(player.name, player.pos)]
    || window.WEEKLY_LOGS_BY_PLAIN?.[normalizePlayerName(player.name)];
  if (raw?.length) return materializeWeeklyLogs(raw, scoring);
  const named = findPlayerByName(player.name, player.pos) || findPlayerByName(player.name);
  const namedRaw = named && (
    named.weeklyLogRaw
    || window.WEEKLY_LOGS_BY_ID?.[named.id]
    || window.WEEKLY_LOGS_BY_NAME?.[weeklyLogKey(named.name, named.pos)]
  );
  if (namedRaw?.length) return materializeWeeklyLogs(namedRaw, scoring);
  const bags = [
    logsFromProfile(player.profile),
    (player.historicalLogs || []).slice(),
    logsFromProfile(window.PLAYER_PROFILES?.[player.id]),
  ];
  const scoringMaps = Object.values(window.PLAYER_PROFILES_BY_SCORING || {});
  scoringMaps.forEach((map) => {
    if (map?.[player.id]) bags.push(logsFromProfile(map[player.id]));
  });
  if (named) {
    bags.push((named.historicalLogs || []).slice());
    bags.push(logsFromProfile(named.profile));
    bags.push(logsFromProfile(window.PLAYER_PROFILES?.[named.id]));
    scoringMaps.forEach((map) => {
      if (!map) return;
      const hit = Object.values(map).find((profile) => (
        normalizePlayerName(profile.name) === normalizePlayerName(player.name)
        && (!player.pos || profile.pos === player.pos)
      ));
      if (hit) bags.push(logsFromProfile(hit));
    });
  }
  return bags.sort((a, b) => b.length - a.length)[0] || [];
}

const _logCache = {};
function clearGameLogCache() {
  Object.keys(_logCache).forEach((key) => delete _logCache[key]);
}
function getGameLog(playerId) {
  const raw = (window.DRAFT_PLAYERS || []).find((x) => x.id === playerId)
    || PLAYERS.find((x) => x.id === playerId)
    || { id: playerId };
  const p = resolveCanonicalPlayer(raw) || raw;
  if (!p?.id && !p?.name) return [];
  const resolvedId = p.id || playerId;
  const projState = window.NFL_DATA_STATUS?.projections || "unknown";
  const cacheKey = `${playerId}:${resolvedId}:${projState}:${window.NFL_DATA_STATUS?.weeklyProjectionsLoaded || 0}:${window.NFL_DATA_STATUS?.weeklyLogsLoaded || 0}:${window.ACTIVE_DRAFT_SCORING || "ppr"}:${window.TAPE_SEASON || 2025}`;
  if (_logCache[cacheKey]) return _logCache[cacheKey];
  const log = makeGameLog(p);
  if (!log.length && projState === "loading") return [];
  _logCache[cacheKey] = log;
  return log;
}

Object.assign(window, { CURRENT_WEEK, getGameLog, makeGameLog, clearGameLogCache });
