import React, { useContext, useState } from "react";
import { Settings } from "lucide-react";
import { CHIP_CONFIG } from "../utils/fplLogic";
import { PlayerContext } from "../PlayerContext";
import { FixtureMatrixPanel } from "./FixtureMatrixPanel";
export const TabsPanel = ({
solverTab, setSolverTab,
isSolving, isRunningSens, isChipSolving,
runMainSolver, runSensAnalysis, runChipSolve,
setShowAdvancedSettings,
quickSettings, setQuickSettings,
banSearch, setBanSearch, lockSearch, setLockSearch,
globalPlayers, teamData, solveGWLabel,
numSims, setNumSims, sensResults, setSensResults, sensViewGw, setSensViewGw,
chipSolveOptions, setChipSolveOptions, chipSolveSolutions, setChipSolveSolutions, horizonGWs, baselineEv = 0
}) => {
const [lockGwSearch, setLockGwSearch] = useState("");
const [banGwSearch, setBanGwSearch] = useState("");
const { fixtureOverrides, setFixtureOverrides, availableGWs } = useContext(PlayerContext);
const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : "";
const getRelativeEv = (sol) => {
if (baselineEv === undefined || !sol) return "+0.00";
// Prevent 0.0 bug: Use locked baseline if it's the "Last Applied" summary
const base = sol.lockedBaselineEv !== undefined ? sol.lockedBaselineEv : baselineEv;
// Prevent NaN bug: Safely handle if sol is just a raw number by accident
if (typeof sol === "number") {
const diff = sol - base;
return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2);
}
// Prevent NaN bug: If the API didn't return a full detailed plan (like in Chip Solve)
if (!sol.plan || !Array.isArray(sol.plan) || sol.plan.length === 0) {
const fallbackEv = sol.ev !== undefined ? sol.ev : 0;
const diff = fallbackEv - base;
return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2);
}
let pathEV = 0;
let hasValidGw = false;
sol.plan.forEach(gwPlan => {
const gw = gwPlan.gw;
if (gw === undefined) return; // Prevents checking p['undefined_Pts']
hasValidGw = true;
const gwChip = gwPlan.chip;
const gwCapMult = gwChip === "tc" ? 3 : 2;
let gwPts = 0;
const getPlayer = (id) => globalPlayers.find(p => String(p.ID) === String(id));
(gwPlan.lineup || []).forEach(id => {
const p = getPlayer(id);
if (p && !p.isBlank) {
const pts = Number(p[`${gw}_Pts`]) || 0;
gwPts += pts * (String(p.ID) === String(gwPlan.captain) ? gwCapMult : 1);
}
});
let ofIdx = 0;
(gwPlan.bench || []).forEach(id => {
const p = getPlayer(id);
if (p && !p.isBlank) {
const pts = Number(p[`${gw}_Pts`]) || 0;
if (gwChip === "bb") {
gwPts += pts;
} else if (p.Pos === "G") {
gwPts += pts * 0.04;
} else {
gwPts += pts * ([0.17, 0.05, 0.02][ofIdx] || 0.02);
ofIdx++;
}
}
});
pathEV += gwPts - (gwPlan.hits || 0) * 4;
});
// Failsafe in case the plan existed but couldn't be parsed properly
if (!hasValidGw || Number.isNaN(pathEV)) {
const fallbackEv = sol.ev !== undefined ? sol.ev : 0;
const diff = fallbackEv - base;
return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2);
}
const diff = pathEV - base;
return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2);
};
return (
<>
setShowAdvancedSettings(true)} className="absolute top-3 right-3 text-slate-500 hover:text-luigi-400 transition-colors z-dropdown" aria-label="Comprehensive settings">
{["solver", "sensitivity", "chips", "fixtures"].map((tab) => (
setSolverTab(tab)} className={`flex-1 py-4 text-xs font-black uppercase tracking-widest transition-colors ${solverTab === tab ? "text-luigi-400 border-b-2 border-luigi-400" : "text-slate-500 hover:text-slate-300"}`}>
{tab === "solver" ? "Solve" : tab === "sensitivity" ? "Sens Anal" : tab === "chips" ? "Chips" : "Fixtures"}
))}
{solverTab === "solver" && (
Quick Settings
{/* TOOLBAR LAYOUT: Readable sizes, allowed to wrap if screen is small */}
{/* Decay */}
Decay
setQuickSettings({ ...quickSettings, decay: e.target.value })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" />
{/* FT Val */}
FT Val
setQuickSettings({ ...quickSettings, ft_value: Number(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" />
{/* Iters */}
Iters
setQuickSettings({ ...quickSettings, iterations: Number(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500 cursor-pointer">
{[1, 2, 3, 4, 5].map(i => {i} )}
{/* Lock */}
Lock
setLockSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" />
{lockSearch && (
{/* THE FIX: Added (quickSettings.locked || []) */}
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockSearch)) && !(quickSettings.locked || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, locked: [...(prev.locked || []), p] })); setLockSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate">
{p.Name} ({p.Team})
))}
)}
{/* THE FIX: Added (quickSettings.locked || []) */}
{(quickSettings.locked || []).map((p) => (
{p.Name} setQuickSettings((prev) => ({ ...prev, locked: (prev.locked || []).filter((l) => l.ID !== p.ID) }))} className="hover:text-white shrink-0">✕
))}
{/* Ban */}
Ban
setBanSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" />
{banSearch && (
{/* THE FIX: Added (quickSettings.banned || []) */}
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banSearch)) && !(quickSettings.banned || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, banned: [...(prev.banned || []), p] })); setBanSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate">
{p.Name} ({p.Team})
))}
)}
{/* THE FIX: Added (quickSettings.banned || []) */}
{(quickSettings.banned || []).map((p) => (
{p.Name} setQuickSettings((prev) => ({ ...prev, banned: (prev.banned || []).filter((b) => b.ID !== p.ID) }))} className="hover:text-white shrink-0">✕
))}
{/* Lock (This GW) */}
Lock (This GW)
setLockGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" />
{lockGwSearch && (
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockGwSearch)) && !(quickSettings.lock_this_gw || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, lock_this_gw: [...(prev.lock_this_gw || []), p] })); setLockGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate">
{p.Name} ({p.Team})
))}
)}
{(quickSettings.lock_this_gw || []).map((p) => (
{p.Name} setQuickSettings((prev) => ({ ...prev, lock_this_gw: (prev.lock_this_gw || []).filter((l) => l.ID !== p.ID) }))} className="hover:text-white shrink-0">✕
))}
{/* Ban (This GW) */}
Ban (This GW)
setBanGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" />
{banGwSearch && (
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banGwSearch)) && !(quickSettings.ban_this_gw || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, ban_this_gw: [...(prev.ban_this_gw || []), p] })); setBanGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate">
{p.Name} ({p.Team})
))}
)}
{(quickSettings.ban_this_gw || []).map((p) => (
{p.Name} setQuickSettings((prev) => ({ ...prev, ban_this_gw: (prev.ban_this_gw || []).filter((b) => b.ID !== p.ID) }))} className="hover:text-white shrink-0">✕
))}
p.isBlank) || teamData.length === 0 || isSolving} className="mt-auto self-center bg-luigi-500 hover:bg-luigi-400 text-slate-950 font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-xs uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none">
Solve {solveGWLabel ? `(${solveGWLabel})` : ""}
)}
{solverTab === "sensitivity" && (
Runs N randomised solves with per-player EV noise.
Simulations
setNumSims(Math.max(2, Math.min(300, Number(e.target.value))))} className="w-20 bg-slate-900 border border-slate-700 rounded p-2 text-slate-200 font-mono text-sm outline-none focus:border-luigi-500" />
p.isBlank) || teamData.length === 0} className="self-center bg-cyan-700 hover:bg-cyan-600 text-white font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-sm uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed">
{isRunningSens ? `Running...` : `Run Sims ${solveGWLabel ? `(${solveGWLabel})` : ""}`}
{sensResults && (
{sensResults.valid_runs}/{sensResults.num_sims} valid
{ setSensResults(null); setSensViewGw(null); }} className="text-xs text-slate-500 hover:text-red-400 font-bold uppercase">Clear
{(sensResults.horizon_gws || []).map((gw) => {
const isChipFree = sensResults.gw_results?.[String(gw)]?.is_chip_free;
const isActive = sensViewGw === gw;
return (
setSensViewGw(gw)} className={`w-8 h-8 rounded-full text-[11px] font-black transition-colors ${isActive ? isChipFree ? "bg-purple-500 text-white" : "bg-cyan-500 text-slate-950" : isChipFree ? "bg-purple-900/60 text-purple-300 hover:bg-purple-800/60 ring-1 ring-purple-500/40" : "bg-slate-800 text-slate-400 hover:bg-slate-700"}`}>{gw}
);
})}
{sensViewGw && sensResults.gw_results?.[String(sensViewGw)] && (() => {
const gd = sensResults.gw_results[String(sensViewGw)];
if (gd.is_chip_free) {
const POS_NAMES = { G: "Goalkeepers", D: "Defenders", M: "Midfielders", F: "Forwards" };
return (
⚡ Wildcard / Free Hit — Squad Selection
{["G", "D", "M", "F"].map((pos) => {
const rows = gd.players?.[pos] || [];
if (!rows.length) return null;
return (
{POS_NAMES[pos]}
Player Squad Lineup
{rows.map((r, i) => (
{r.squad_pct}% {r.lineup_pct}%
))}
);
})}
);
}
return (
{gd.no_transfer_pct > 0 &&
Hold (no transfer): {gd.no_transfer_pct}% of sims
}
{["moves", "buys", "sells"].map((key) => {
const rows = gd[key] || [];
if (!rows.length) return null;
const titles = { moves: "Moves (Out → In)", buys: "Buys", sells: "Sells" };
return (
{titles[key]}
{rows.slice(0, 10).map((r, i) => (
))}
);
})}
);
})()}
)}
)}
{solverTab === "chips" && (
Select which GWs each chip can be played in, then hit Chip Solve .
{["wc", "fh", "bb", "tc"].map((key) => {
const cfg = CHIP_CONFIG[key];
const sel = chipSolveOptions[key] || [];
return (
{cfg.label}
{sel.length > 0 && {sel.length} GW{sel.length > 1 ? "s" : ""} }
{horizonGWs.map((gw) => {
const active = sel.includes(gw);
return (
setChipSolveOptions((prev) => ({ ...prev, [key]: active ? prev[key].filter((g) => g !== gw) : [...prev[key], gw] }))} className={`text-[10px] px-1.5 py-0.5 rounded font-bold transition-colors ${active ? cfg.activeBg : cfg.inactiveBg}`}>{gw}
);
})}
);
})}
p.isBlank) || teamData.length === 0 || Object.values(chipSolveOptions).every((v) => v.length === 0)} className="self-center mt-2 bg-purple-600 hover:bg-purple-500 text-white font-black px-10 py-2.5 rounded-lg shadow-lg transition-all active:scale-95 text-sm uppercase tracking-widest disabled:opacity-50 disabled:cursor-not-allowed">
{isChipSolving ? `Solving...` : "Chip Solve"}
{chipSolveSolutions.length > 0 && (
Best Chip Combos
setChipSolveSolutions([])} className="text-xs text-slate-500 hover:text-red-400 font-bold uppercase">Clear
{chipSolveSolutions.map((sol, i) => {
const combo = sol.chip_combo || {};
const active = Object.entries(combo)
.filter(([, gws]) => Array.isArray(gws) && gws.length > 0)
.map(([k, gws]) => ({ text: `${k.replace("use_", "").toUpperCase()}${gws[0]}`, gw: gws[0] }))
.sort((a, b) => a.gw - b.gw)
.map(x => x.text);
return (
{/* 1. Added Rank Number (1., 2., 3...) */}
{i + 1}.
{active.length ? active.join(" + ") : "No chips"}
{sol.objective_score != null ? sol.objective_score.toFixed(2) : sol.ev.toFixed(2)}
{/* 2. EV is now bigger (text-[10px]) and colored purple to match! */}
({getRelativeEv(sol)} ev)
);
})}
)}
)}
{solverTab === "fixtures" && (
)}
>
);
};