'use client'; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; export interface ParlayLeg { id: string; sport: 'NBA' | 'MLB' | 'WNBA'; player: string; stat: string; line: number; direction: 'over' | 'under'; grade: string; confidence: number; // Session 50 — needed for the correlation model. team?: string; game?: string; archetype?: string; } // Session 50 — the combined analysis from /api/parlay/grade. export interface ParlayCombined { grade: string; score: number; penalty: number } export interface ParlayCorrelation { max: number; avg: number; warning: string | null } export interface ParlayPayout { amount: number; multiplier: number; fairMultiplier?: number; discount?: number } interface ParlayContextValue { legs: ParlayLeg[]; legCount: number; isOpen: boolean; open: () => void; close: () => void; toggle: () => void; addLeg: (leg: Omit) => void; removeLeg: (id: string) => void; clear: () => void; // Session 50 — live combined metrics (auto-graded when legs ≥ 2). combined: ParlayCombined | null; correlation: ParlayCorrelation | null; payout: ParlayPayout | null; grading: boolean; hasLeg: (key: string) => boolean; // Session 50 — tier-aware leg cap (set by the panel from useAuth). maxLegs: number; setMaxLegs: (n: number) => void; atCap: boolean; } const STORAGE_KEY = 'bbk:parlay'; const MAX_LEGS = 6; // Session 50 — Desk cap; lower tiers gated in the UI. /** Stable de-dupe key for a leg (player|stat|line|direction). */ export function legKey(l: { player: string; stat: string; line: number; direction: string }) { return `${l.player}|${l.stat}|${l.line}|${l.direction}`; } const ParlayContext = createContext(null); export default function ParlayProvider({ children }: { children: React.ReactNode }) { const [legs, setLegs] = useState([]); const [isOpen, setOpen] = useState(false); // Session 50 — tier cap (default Desk 6; the panel lowers it for free/analyst). const [maxLegs, setMaxLegs] = useState(MAX_LEGS); const maxLegsRef = useRef(MAX_LEGS); useEffect(() => { maxLegsRef.current = maxLegs; }, [maxLegs]); // Restore from localStorage so a refresh doesn't drop the tray useEffect(() => { if (typeof window === 'undefined') return; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) setLegs(parsed); } } catch { /* ignore */ } }, []); useEffect(() => { if (typeof window === 'undefined') return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(legs)); } catch { /* ignore quota */ } }, [legs]); const addLeg = useCallback((leg: Omit) => { setLegs((prev) => { if (prev.length >= maxLegsRef.current) return prev; // De-dupe by player+stat+line+direction const key = `${leg.player}|${leg.stat}|${leg.line}|${leg.direction}`; if (prev.some((p) => `${p.player}|${p.stat}|${p.line}|${p.direction}` === key)) return prev; const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`; const next = [...prev, { ...leg, id }]; // Fire-and-forget: tell the backend so most-parlayed counts get bumped void fetch('/api/parlay/add-leg', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sport: leg.sport, player: leg.player, stat: leg.stat, line: leg.line, direction: leg.direction, }), }).catch(() => {}); return next; }); }, []); const removeLeg = useCallback((id: string) => { setLegs((prev) => prev.filter((l) => l.id !== id)); }, []); const clear = useCallback(() => setLegs([]), []); const hasLeg = useCallback((key: string) => legs.some((l) => legKey(l) === key), [legs]); // Session 50 — auto-grade the slip whenever the legs change (≥ 2 legs). // Debounced so rapid adds make one request; clears when below 2. const [combined, setCombined] = useState(null); const [correlation, setCorrelation] = useState(null); const [payout, setPayout] = useState(null); const [grading, setGrading] = useState(false); useEffect(() => { if (legs.length < 2) { setCombined(null); setCorrelation(null); setPayout(null); return; } let active = true; setGrading(true); const t = setTimeout(() => { fetch('/api/parlay/grade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ legs: legs.map((l) => ({ player: l.player, team: l.team || '', game: l.game || '', stat: l.stat, line: l.line, grade: l.grade, })), betAmount: 10, }), }) .then((r) => (r.ok ? r.json() : null)) .then((d) => { if (!active || !d || d.error) return; setCombined(d.combined ?? null); setCorrelation(d.correlation ?? null); setPayout(d.payout ?? null); }) .catch(() => { /* keep last-known on failure */ }) .finally(() => { if (active) setGrading(false); }); }, 250); return () => { active = false; clearTimeout(t); setGrading(false); }; }, [legs]); const value = useMemo(() => ({ legs, legCount: legs.length, isOpen, open: () => setOpen(true), close: () => setOpen(false), toggle: () => setOpen((o) => !o), addLeg, removeLeg, clear, combined, correlation, payout, grading, hasLeg, maxLegs, setMaxLegs, atCap: legs.length >= maxLegs, }), [legs, isOpen, addLeg, removeLeg, clear, combined, correlation, payout, grading, hasLeg, maxLegs]); return {children}; } export function useParlay(): ParlayContextValue { const ctx = useContext(ParlayContext); if (!ctx) { // Provide a noop fallback so components can render outside the provider // (e.g. during prerender of marketing pages). return { legs: [], legCount: 0, isOpen: false, open: () => {}, close: () => {}, toggle: () => {}, addLeg: () => {}, removeLeg: () => {}, clear: () => {}, combined: null, correlation: null, payout: null, grading: false, hasLeg: () => false, maxLegs: 6, setMaxLegs: () => {}, atCap: false, }; } return ctx; }