'use client';

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import ProcessingGrade from '@/components/vyndr/ProcessingGrade';
import PriorReads from '@/components/vyndr/PriorReads';
import { AccuracyBadge, Skeleton, SkeletonList } from '@/components/vyndr';
import type { GradeResultData } from '@/components/vyndr/GradeResultCard';
import { mapScanToGradeResult } from '@/lib/gradeAdapter';
import { normalizeName, nameKey } from '@/lib/playerName';
import { markReadComplete } from '@/lib/reads';
import { useAuth } from '@/contexts/AuthContext';
import { useParlay } from '@/contexts/ParlayContext';
import {
  trackScanCompleted,
  trackScanLimitHit,
  trackUpgradeClicked,
} from '@/lib/analytics';
import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
import NoMarketState, { type PricedLine } from '@/components/vyndr/NoMarketState';
import { indexPricedLines, pricedLinesFor } from '@/lib/pricedLines';
import { type HeadshotSport } from '@/lib/playerHeadshot';
import { buildBookLink, SUPPORTED_BOOKS, BOOK_LINK_REL } from '@/lib/bookLinks';

type Sport = 'NBA' | 'MLB' | 'WNBA';

interface Game {
  id: string;
  away: string;
  home: string;
  start_time: string;
  status: 'scheduled' | 'live' | 'final';
  prop_count?: number;
}

interface Player {
  id: string;
  full_name: string;
  team?: string;
  position?: string;
}

interface ScanResponse {
  grade: string;
  // Session 58 (work-order 1.5) — the model refused: no projection, no read.
  insufficient_data?: boolean;
  projection?: number;
  confidence?: number;
  sample_size?: number;
  factors?: Record<string, string>;
  alt_lines?: { line: number; grade: string; hit_rate?: number; edge_pct?: number; base?: boolean }[];
  kelly?: { pct: number; quarter: number; full: number; odds: string };
  kill_conditions?: { code: string; reason: string }[];
  reasoning?: string;
  historical_hit_rate?: number;
  scans_remaining: number | null;
  tier: 'free' | 'analyst' | 'desk';
  error?: string;
  upgrade?: { tier: string; price: number };
  // Session 43/44 — Player-Intelligence fields the engine attaches; the grade
  // card's STAT CONTEXT + VYNDR INTELLIGENCE sections read these.
  season_avg?: number;
  last10_avg?: number;
  vs_opp_avg?: number;
  form?: number;
  usage?: string;
  matchup_grade?: string;
  rest?: string;
  archetype?: string;
  archetype_blend?: { archetype: string; weight: number }[];
  prop_dna?: { reliable: string[]; volatile: string[] };
  // Session 66 — the PRICE LAYER. `model_odds` is derived from p_win by the
  // engine and STRIPPED for unentitled tiers, which is what sets
  // `model_price_locked` — so a locked leg and a missing leg stay distinct.
  book_odds?: number | null;
  fair_odds?: number | null;
  model_odds?: number | null;
  ev_pct?: number | null;
  model_price_locked?: boolean;
}

const NBA_STATS = [
  { id: 'points', label: 'Points' },
  { id: 'rebounds', label: 'Rebounds' },
  { id: 'assists', label: 'Assists' },
  { id: 'threes', label: '3-Pointers' },
  { id: 'steals', label: 'Steals' },
  { id: 'blocks', label: 'Blocks' },
  { id: 'pra', label: 'P+R+A' },
  { id: 'turnovers', label: 'Turnovers' },
];

const MLB_STATS = [
  { id: 'strikeouts', label: 'Strikeouts (P)' },
  { id: 'hits_allowed', label: 'Hits Allowed (P)' },
  { id: 'earned_runs', label: 'Earned Runs (P)' },
  { id: 'innings_pitched', label: 'Innings Pitched (P)' },
  { id: 'hits', label: 'Hits' },
  { id: 'total_bases', label: 'Total Bases' },
  { id: 'rbi', label: 'RBI' },
  { id: 'runs', label: 'Runs' },
  { id: 'home_runs', label: 'Home Runs' },
];

const WNBA_STATS = NBA_STATS;

const SPORT_STATS: Record<Sport, { id: string; label: string }[]> = {
  NBA: NBA_STATS,
  MLB: MLB_STATS,
  WNBA: WNBA_STATS,
};

const SPORT_ACCENT: Record<Sport, string> = {
  NBA: '#E94B3C',
  MLB: '#1E90FF',
  WNBA: '#FFB347',
};

// Sportsbook deep-links — A1 S3: built by lib/bookLinks (organic until the
// affiliate config flips a book on). rel is BOOK_LINK_REL on every anchor.

// Session 80 — PRICED-LINE NUDGE gate + freshness. The flag is the reversibility
// lever: false → the nudge disappears and the scanner falls back to S6's
// link-only empty state (Scan A / the working triplet are untouched either way).
// STALE_MS matches /api/snapshot's 30s cache — no point re-fetching more often.
const PRICED_NUDGE_ENABLED = true;
const PRICED_STALE_MS = 30_000;

export default function ScanPage() {
  const router = useRouter();
  const { user, session, tier, scansRemaining, canScan, loading: authLoading, bumpScanCount } = useAuth();
  const { addLeg, legCount, open } = useParlay();

  const [sport, setSport] = useState<Sport>('NBA');
  const [games, setGames] = useState<Game[] | null>(null);
  const [gameId, setGameId] = useState<string>('');
  const [playerQuery, setPlayerQuery] = useState('');
  const [playerSuggestions, setPlayerSuggestions] = useState<Player[]>([]);
  const [selectedPlayer, setSelectedPlayer] = useState<string>('');
  // Wave 2A — the MLBAM id of the player picked from search (MLB only; numeric).
  // Feeds the grade card's real headshot. null → team-colored monogram.
  const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
  const [stat, setStat] = useState<string>('points');
  const [line, setLine] = useState<string>('');
  const [direction, setDirection] = useState<'over' | 'under'>('over');
  const [scanning, setScanning] = useState(false);
  const [result, setResult] = useState<ScanResponse | null>(null);
  // Session 79 — the CURRENT snapshot's priced lines, indexed by player+stat, so
  // a marketless scan can surface REAL priced lines (never suggested/nearest).
  const [pricedIndex, setPricedIndex] = useState<Map<string, PricedLine[]> | null>(null);
  // Session 80 — freshness clock: when the held snapshot was last fetched, so a
  // long-open scanner re-fetches instead of surfacing hour-stale priced lines.
  const [pricedFetchedAt, setPricedFetchedAt] = useState(0);
  // One-tap re-scan of a surfaced priced line: bump this to re-run runScan AFTER
  // line/direction state has committed.
  const [rescanKey, setRescanKey] = useState(0);
  const [error, setError] = useState('');
  // Session 19 — tonight's players grid. Pulled from the odds proxy
  // (props array) so the chip set is real, not hard-coded. Each entry
  // unique by name + the set of stats that player has props for, so
  // clicking a chip can prefill the stat dropdown intelligently.
  const [tonightsPlayers, setTonightsPlayers] = useState<Array<{ name: string; stats: string[] }> | null>(null);

  // Auth gate — push anonymous users to signup
  useEffect(() => {
    if (!authLoading && !user) router.replace('/signup?next=/scan');
  }, [authLoading, user, router]);

  // Session 79 — the REAL priced lines for the EXACT selected player+stat. Empty
  // for a player/stat the board didn't price — never suggested or interpolated.
  const pricedForSelection = useMemo(
    () => (PRICED_NUDGE_ENABLED && pricedIndex && selectedPlayer && stat
      ? pricedLinesFor(pricedIndex, selectedPlayer, stat) : []),
    [pricedIndex, selectedPlayer, stat],
  );

  // One-tap re-scan after a surfaced priced line commits its line/direction.
  useEffect(() => {
    if (rescanKey === 0) return;
    void runScan();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rescanKey]);

  // Reset stat selection when sport changes
  useEffect(() => {
    const list = SPORT_STATS[sport];
    if (!list.some((s) => s.id === stat)) setStat(list[0].id);
  }, [sport, stat]);

  // Load tonight's slate
  useEffect(() => {
    let cancelled = false;
    setGames(null);
    setGameId('');
    fetch(`/api/games/tonight?sport=${sport}`)
      .then((r) => r.json())
      .then((data: { games: Game[] }) => {
        if (!cancelled) setGames(Array.isArray(data?.games) ? data.games : []);
      })
      .catch(() => !cancelled && setGames([]));
    return () => {
      cancelled = true;
    };
  }, [sport]);

  // Session 79/80 — index the current snapshot's priced lines for the surfacer,
  // kept FRESH. /api/snapshot is public + 30s-cached and re-validated
  // server-side at scan time (so a stale chip that's tapped degrades to the
  // honest empty state, never a vanishing triplet). The display is kept current
  // by re-fetching when it's older than PRICED_STALE_MS at the moment of use —
  // on selection change and on window focus — so a long-open page never shows
  // an hour-stale priced line.
  const refreshPriced = useCallback(() => {
    if (!PRICED_NUDGE_ENABLED) return;
    fetch(`/api/snapshot/${sport.toLowerCase()}`)
      .then((r) => (r.ok ? r.json() : null))
      .then((data: { grades?: unknown[] } | null) => {
        setPricedIndex(indexPricedLines((data && data.grades) || []));
        setPricedFetchedAt(Date.now());
      })
      .catch(() => { setPricedIndex(new Map()); setPricedFetchedAt(Date.now()); });
  }, [sport]);

  // Sport change: clear (never show the old sport's lines) then fetch fresh.
  useEffect(() => {
    setPricedIndex(null);
    setPricedFetchedAt(0);
    refreshPriced();
  }, [refreshPriced]);

  // Selection change: re-fetch only if the held snapshot has gone stale, so the
  // chips shown for the new selection come from current data, not a mount copy.
  useEffect(() => {
    if (!selectedPlayer) return;
    if (Date.now() - pricedFetchedAt > PRICED_STALE_MS) refreshPriced();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedPlayer, stat]);

  // Long-open page returning to focus: refresh if stale.
  useEffect(() => {
    const onFocus = () => { if (Date.now() - pricedFetchedAt > PRICED_STALE_MS) refreshPriced(); };
    window.addEventListener('focus', onFocus);
    return () => window.removeEventListener('focus', onFocus);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pricedFetchedAt]);

  // Session 19 — fetch tonight's players from the odds proxy. The
  // odds endpoint returns the canonical list of players who have
  // props posted, which is exactly what the scan UI should surface
  // as quick-fill chips. Empty array on failure → the section
  // hides itself (we don't want a sad "couldn't load" stripe when
  // odds-api is rate-limited).
  useEffect(() => {
    let cancelled = false;
    setTonightsPlayers(null);
    const sportPath = sport.toLowerCase();
    fetch(`/api/odds/${sportPath}`)
      .then(async (r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then((data: { props?: Array<{ player?: string; stat_type?: string }> }) => {
        if (cancelled) return;
        // Session 48 — group by the normalized name key so variants
        // ("A.J."/"AJ", "Matt"/"Matthew", "Jazz Chisholm"/"Jr.") show as ONE
        // tile; display the normalized (longest) name.
        const byPlayer = new Map<string, { name: string; stats: Set<string> }>();
        for (const p of data.props || []) {
          if (!p.player || !p.stat_type) continue;
          const key = nameKey(p.player);
          const disp = normalizeName(p.player).display || p.player;
          const entry = byPlayer.get(key) || { name: disp, stats: new Set<string>() };
          if (disp.length > entry.name.length) entry.name = disp;
          entry.stats.add(p.stat_type);
          byPlayer.set(key, entry);
        }
        const list = Array.from(byPlayer.values())
          .map(({ name, stats }) => ({ name, stats: Array.from(stats) }))
          .sort((a, b) => a.name.localeCompare(b.name));
        setTonightsPlayers(list);
      })
      .catch(() => {
        if (!cancelled) setTonightsPlayers([]);
      });
    return () => { cancelled = true; };
  }, [sport]);

  // Debounced player search — narrow to selected game when set
  const searchPlayers = useCallback(
    async (query: string) => {
      if (query.trim().length < 2) {
        setPlayerSuggestions([]);
        return;
      }
      try {
        const params = new URLSearchParams({ sport, q: query });
        if (gameId) params.set('game_id', gameId);
        const res = await fetch(`/api/players/search?${params}`);
        if (!res.ok) return;
        const data = (await res.json()) as { players: Player[] };
        setPlayerSuggestions((data.players || []).slice(0, 8));
      } catch {
        setPlayerSuggestions([]);
      }
    },
    [sport, gameId],
  );

  useEffect(() => {
    const t = setTimeout(() => void searchPlayers(playerQuery), 200);
    return () => clearTimeout(t);
  }, [playerQuery, searchPlayers]);

  const canSubmit = useMemo(
    () => selectedPlayer && stat && line !== '' && !scanning && canScan,
    [selectedPlayer, stat, line, scanning, canScan],
  );

  const runScan = async () => {
    if (!canSubmit) {
      if (!canScan) {
        trackScanLimitHit({ current_scan_count: 5, tier });
      }
      return;
    }
    setScanning(true);
    setError('');
    setResult(null);
    try {
      // DS1 (§17 — scan→ledger persistence). The authoritative bearer token is
      // the live Supabase session's access_token (set for EVERY sign-in method).
      // The legacy `localStorage['sb-token']` key is written ONLY by the OAuth
      // callback — so email/password users sent NO Authorization header, the
      // /api/scan route saw an anonymous request, and the completed read was
      // silently dropped from the ledger (the write is gated on an authed user).
      // Prefer the session token; keep the legacy key as a fallback.
      const token =
        session?.access_token ||
        (typeof window !== 'undefined' ? localStorage.getItem('sb-token') : null);
      const res = await fetch('/api/scan', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(token ? { Authorization: `Bearer ${token}` } : {}),
        },
        body: JSON.stringify({
          sport,
          player: selectedPlayer,
          stat,
          line: Number(line),
          direction,
          book: 'draftkings',
        }),
      });
      const data = (await res.json()) as ScanResponse;
      if (!res.ok) {
        setError(data.error || 'The engine hit a wall. Try that read again.');
        if (res.status === 402) trackScanLimitHit({ current_scan_count: 5, tier });
        return;
      }
      setResult(data);
      // Session 58 — a refused read (insufficient data) doesn't burn a scan.
      if (!data.insufficient_data) bumpScanCount();
      trackScanCompleted({
        sport,
        player: selectedPlayer,
        stat,
        line: Number(line),
        grade: data.grade,
        tier,
      });
    } catch {
      setError('The engine hit a wall. Try that read again.');
    } finally {
      setScanning(false);
    }
  };

  // Count a completed read once per prop per session (drives the Install/Push
  // prompt gates) — preserved from the legacy GradeCard's reveal effect.
  useEffect(() => {
    if (!result || typeof window === 'undefined') return;
    const readKey = `vyndr_read_${sport}_${selectedPlayer}_${stat}_${line}_${direction}`;
    if (!window.sessionStorage.getItem(readKey)) {
      window.sessionStorage.setItem(readKey, '1');
      markReadComplete();
    }
  }, [result, sport, selectedPlayer, stat, line, direction]);

  const reset = () => {
    setResult(null);
    setError('');
    setPlayerQuery('');
    setSelectedPlayer('');
    setSelectedPlayerId(null);
    setLine('');
  };

  if (authLoading || !user) {
    // DS1 (§4) — layout-matched skeleton of the scan form, not a text wall.
    return (
      <section style={{ maxWidth: 720, margin: '0 auto', padding: '32px 16px 96px' }} aria-busy="true">
        <Skeleton height={30} width={200} radius={8} style={{ marginBottom: 8 }} />
        <Skeleton height={16} width={320} radius={6} style={{ marginBottom: 28 }} />
        <SkeletonList count={3} height={64} />
        <Skeleton height={52} radius={12} style={{ marginTop: 20 }} />
      </section>
    );
  }

  return (
    <section
      className="diagonal-cut animate-fade-up"
      style={{ maxWidth: 720, margin: '0 auto', padding: '32px 16px 96px', position: 'relative' }}
    >
      {/* Header */}
      <header style={{ marginBottom: 24 }}>
        <h1 style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em', marginBottom: 6 }}>
          Grade a prop.
        </h1>
        <p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
          Pick a sport, find the player, set the line. We grade it in seconds.
        </p>
      </header>

      {/* Scan counter */}
      {tier === 'free' && scansRemaining != null && (
        <div
          style={{
            marginBottom: 24,
            padding: '12px 16px',
            background: 'var(--bg-surface)',
            border: '1px solid var(--border)',
            borderRadius: 12,
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            gap: 12,
          }}
        >
          <span className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)', letterSpacing: '0.05em' }}>
            {scansRemaining} OF 5 FREE READS REMAINING THIS MONTH
          </span>
          <div style={{ flex: 1, maxWidth: 160, height: 4, background: 'var(--border)', borderRadius: 2, overflow: 'hidden' }}>
            <div
              style={{
                width: `${(scansRemaining / 5) * 100}%`,
                height: '100%',
                background: scansRemaining <= 1 ? 'var(--grade-c)' : 'var(--grade-a)',
                transition: 'width 200ms ease',
              }}
            />
          </div>
        </div>
      )}

      {/* Sport tabs */}
      <div role="tablist" aria-label="Sport" style={{ display: 'flex', gap: 4, marginBottom: 24, borderBottom: '1px solid var(--border)' }}>
        {(Object.keys(SPORT_STATS) as Sport[]).map((s) => {
          const active = s === sport;
          return (
            <button
              key={s}
              role="tab"
              aria-selected={active}
              onClick={() => setSport(s)}
              style={{
                padding: '12px 20px',
                background: 'transparent',
                border: 'none',
                borderBottom: `2px solid ${active ? SPORT_ACCENT[s] : 'transparent'}`,
                color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
                fontFamily: 'inherit',
                fontWeight: active ? 600 : 500,
                fontSize: 14,
                cursor: 'pointer',
                transition: 'color 200ms ease',
                marginBottom: -1,
                boxShadow: active ? `0 4px 16px ${SPORT_ACCENT[s]}26` : 'none',
              }}
            >
              {s}
            </button>
          );
        })}
      </div>

      {/* Game selector */}
      <div style={{ marginBottom: 16 }}>
        <label className="mono" style={labelStyle}>Tonight&apos;s slate</label>
        {games === null ? (
          <div style={shimmerStyle} />
        ) : games.length === 0 ? (
          <p className="surface" style={{ padding: 16, fontSize: 13, color: 'var(--text-secondary)' }}>
            No games posted yet. Check back soon.
          </p>
        ) : (
          <select
            value={gameId}
            onChange={(e) => setGameId(e.target.value)}
            className="input-field"
            aria-label="Game"
          >
            <option value="">All games</option>
            {games.map((g) => (
              <option key={g.id} value={g.id}>
                {g.away} @ {g.home} · {formatTime(g.start_time)}
              </option>
            ))}
          </select>
        )}
      </div>

      {/* Session 19 — tonight's players chip grid. Above the search
          input so the user sees who's actually playing before having
          to think about what to type. Tapping a chip prefills the
          player and, when only one stat is available, the stat too. */}
      {tonightsPlayers && tonightsPlayers.length > 0 && (
        <div style={{ marginBottom: 20 }}>
          <label className="mono" style={labelStyle}>Tonight&apos;s Players</label>
          <div
            style={{
              display: 'grid',
              gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
              gap: 8,
              maxHeight: 220,
              overflowY: 'auto',
              padding: 2,
            }}
          >
            {tonightsPlayers.map((p) => {
              const selected = selectedPlayer === p.name;
              return (
                <button
                  key={p.name}
                  type="button"
                  onClick={() => {
                    setSelectedPlayer(p.name);
                    setSelectedPlayerId(null); // tonight chips carry no id → monogram
                    setPlayerQuery(p.name);
                    setPlayerSuggestions([]);
                    // If the player has exactly one stat type with
                    // props, prefill it — saves a tap for single-stat
                    // pitcher props (ERs/Ks) etc.
                    if (p.stats.length === 1 && SPORT_STATS[sport].some((s) => s.id === p.stats[0])) {
                      setStat(p.stats[0]);
                    }
                  }}
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: 8,
                    padding: '6px 10px',
                    background: selected ? 'rgba(0,212,160,0.08)' : 'var(--bg-surface)',
                    border: `1px solid ${selected ? 'var(--grade-a)' : 'var(--border)'}`,
                    borderRadius: 999,
                    color: 'var(--text-primary)',
                    cursor: 'pointer',
                    fontSize: 12,
                    fontFamily: 'inherit',
                    textAlign: 'left',
                    minWidth: 0,
                  }}
                >
                  {/* Wave 2A — tonight's players carry no id → team-colored
                      monogram (branded, never a gray silhouette). */}
                  <PlayerAvatar name={p.name} sport={sport.toLowerCase() as HeadshotSport} size={24} />
                  <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>
                    {p.name}
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      )}

      {/* Player search */}
      <div style={{ marginBottom: 16, position: 'relative' }}>
        <label className="mono" style={labelStyle}>Player</label>
        <input
          className="input-field"
          placeholder={`Search ${sport} players`}
          value={playerQuery}
          onChange={(e) => {
            setPlayerQuery(e.target.value);
            setSelectedPlayer('');
            setSelectedPlayerId(null);
          }}
          autoComplete="off"
        />
        {/* Session 17 — show "no results" when the search ran but
            returned nothing. Audit reported a silent dropdown failure;
            this gives the user feedback when the upstream player
            service is offline or the spelling didn't match. */}
        {playerQuery.trim().length >= 2 && playerSuggestions.length === 0 && playerQuery !== selectedPlayer && (
          <div
            className="surface-elevated"
            style={{
              position: 'absolute', top: '100%', left: 0, right: 0,
              marginTop: 4, zIndex: 20, padding: 12,
              fontSize: 12, color: 'var(--text-tertiary)',
            }}
          >
            No {sport} players matched &ldquo;{playerQuery}&rdquo;. Check spelling or try a partial name.
          </div>
        )}
        {playerSuggestions.length > 0 && playerQuery !== selectedPlayer && (
          <div
            className="surface-elevated"
            style={{
              position: 'absolute',
              top: '100%',
              left: 0,
              right: 0,
              marginTop: 4,
              zIndex: 20,
              padding: 4,
              maxHeight: 280,
              overflowY: 'auto',
            }}
          >
            {playerSuggestions.map((p) => (
              <button
                key={p.id}
                onMouseDown={(e) => {
                  e.preventDefault();
                  setSelectedPlayer(p.full_name);
                  setSelectedPlayerId(sport.toLowerCase() === 'mlb' && /^\d+$/.test(String(p.id)) ? String(p.id) : null);
                  setPlayerQuery(p.full_name);
                  setPlayerSuggestions([]);
                }}
                style={suggestionStyle}
              >
                {/* Wave 2A — real headshot in search suggestions. MLB's
                    /api/players/search carries the MLBAM id on p.id (numeric);
                    NBA/WNBA ids are synthetic → guarded out → team-colored
                    monogram. Never a gray silhouette. */}
                <span style={{ marginRight: 10, display: 'inline-flex' }}>
                  <PlayerAvatar
                    name={p.full_name}
                    sport={sport.toLowerCase() as HeadshotSport}
                    playerId={sport.toLowerCase() === 'mlb' && /^\d+$/.test(String(p.id)) ? p.id : undefined}
                    team={p.team}
                    size={28}
                  />
                </span>
                <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.full_name}</span>
                {p.team && (
                  <span className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                    {p.team}
                  </span>
                )}
              </button>
            ))}
          </div>
        )}
      </div>

      {/* Stat + line + direction */}
      <div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 0.8fr', gap: 12, marginBottom: 24 }}>
        <div>
          <label className="mono" style={labelStyle}>Stat</label>
          <select className="input-field" value={stat} onChange={(e) => setStat(e.target.value)}>
            {SPORT_STATS[sport].map((s) => (
              <option key={s.id} value={s.id}>{s.label}</option>
            ))}
          </select>
        </div>
        <div>
          <label className="mono" style={labelStyle}>Line</label>
          <input
            type="number"
            step="0.5"
            inputMode="decimal"
            className="input-field"
            placeholder="0.0"
            value={line}
            onChange={(e) => setLine(e.target.value)}
          />
          {/* Session 79 — HELP, not restriction: the board's REAL priced lines
              for this exact player+stat. Tap to pre-fill a line that will yield
              a real triplet. The scanner still accepts any free-typed line. */}
          {pricedForSelection.length > 0 && (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
              <span className="mono" style={{ fontSize: 9, color: 'var(--text-3)', letterSpacing: '0.1em', alignSelf: 'center' }}>
                PRICED TONIGHT:
              </span>
              {pricedForSelection.map((l, i) => (
                <button
                  key={`${l.direction}-${l.line}-${i}`}
                  type="button"
                  onClick={() => { setLine(String(l.line)); setDirection(l.direction); }}
                  className="mono"
                  style={{ cursor: 'pointer', fontSize: 11, fontWeight: 700, padding: '4px 9px', borderRadius: 999, border: '1px solid var(--border-hi)', background: 'transparent', color: 'var(--text-1)', fontVariantNumeric: 'tabular-nums' }}
                >
                  {l.direction === 'under' ? 'U' : 'O'} {l.line}
                </button>
              ))}
            </div>
          )}
        </div>
        <div>
          <label className="mono" style={labelStyle}>Side</label>
          <div style={{ display: 'flex', borderRadius: 12, overflow: 'hidden', border: '1px solid var(--border)' }}>
            {(['over', 'under'] as const).map((d) => (
              <button
                key={d}
                onClick={() => setDirection(d)}
                aria-pressed={direction === d}
                style={{
                  flex: 1,
                  padding: '12px 0',
                  background: direction === d ? 'var(--accent)' : 'transparent',
                  color: direction === d ? 'var(--text-primary)' : 'var(--text-secondary)',
                  border: 'none',
                  fontFamily: 'inherit',
                  fontWeight: 600,
                  fontSize: 13,
                  textTransform: 'capitalize',
                  cursor: 'pointer',
                }}
              >
                {d}
              </button>
            ))}
          </div>
        </div>
      </div>

      {/* Grade button OR upgrade trigger */}
      {!canScan ? (
        <div className="surface diagonal-cut tex-scan" style={{ padding: 32, textAlign: 'center', maxWidth: 440, margin: '0 auto' }}>
          <p className="lbl" style={{ color: 'var(--grade-c)', marginBottom: 12 }}>SIGNAL EXHAUSTED</p>
          <h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>
            You&apos;ve used your 5 free reads this month.
          </h2>
          <p style={{ color: 'var(--text-1)', fontSize: 14, marginBottom: 20 }}>
            Unlock unlimited reads — plus kill conditions, alt lines, and the full intelligence layer.
          </p>
          <p className="num" style={{
            fontSize: 32, color: 'var(--grade-a)', marginBottom: 4,
            textShadow: '0 0 14px rgba(0, 212, 160, 0.7)',
          }}>
            $14.99<span style={{ fontSize: 14, color: 'var(--text-1)' }}>/mo</span>
          </p>
          <p style={{ color: 'var(--grade-c)', fontSize: 13, fontWeight: 600, marginBottom: 20 }}>
            Locked for life. This rate disappears June 15.
          </p>
          <button
            className="btn-primary"
            style={{ width: '100%', padding: 14 }}
            onClick={() => {
              trackUpgradeClicked({ current_tier: tier, target_tier: 'analyst', trigger_location: 'scan_limit' });
              router.push('/api/checkout?tier=analyst');
            }}
          >
            Upgrade Now
          </button>
          <p style={{ color: 'var(--text-2)', fontSize: 12, marginTop: 12 }}>
            Or come back next month for 5 more free reads.
          </p>
        </div>
      ) : (
        <button
          onClick={runScan}
          disabled={!canSubmit}
          className={scanning ? 'shimmer-loading' : 'btn-primary'}
          style={{ width: '100%', padding: 16, fontSize: 15, color: 'var(--text-primary)', border: 'none', borderRadius: 12, fontWeight: 600, cursor: canSubmit ? 'pointer' : 'not-allowed', opacity: canSubmit ? 1 : 0.4 }}
        >
          {scanning ? 'Running the model…' : 'Read It'}
        </button>
      )}

      {/* Inline error */}
      {error && (
        <div
          style={{
            marginTop: 16,
            padding: 14,
            borderRadius: 12,
            background: 'rgba(255,107,107,0.10)',
            border: '1px solid rgba(255,107,107,0.30)',
            color: 'var(--grade-d)',
            fontSize: 13,
          }}
        >
          {error}
        </div>
      )}

      {/* Session 58 (work-order 1.5) — the honest refusal. When the model has
          no projection there is NO read: no grade letter, no fake +0% edge,
          and nothing writes to the ledger. A refused read builds more trust
          than a hollow one. */}
      {result && result.insufficient_data && (
        <div
          className="surface scanlines"
          style={{ marginTop: 32, padding: 32, textAlign: 'center', border: '1px solid var(--border-hi)', borderRadius: 10, display: 'grid', gap: 10, justifyItems: 'center' }}
        >
          <p className="mono" style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.14em', color: 'var(--amber)' }}>
            INSUFFICIENT DATA — NO READ
          </p>
          <p style={{ color: 'var(--text-1)', fontSize: 14, maxWidth: 460 }}>
            The model has no projection for this prop, so it refuses to grade it.
            No number gets invented here — that&apos;s the deal.
          </p>
          <button onClick={reset} className="btn-ghost" style={{ marginTop: 6, padding: '10px 18px' }}>
            Read another prop →
          </button>
        </div>
      )}

      {/* Grade result — VYNDR 2.0 ProcessingGrade → GradeResultCard (Session 35).
          Engine output is mapped to the §7 contract and tier-gated by the adapter. */}
      {result && !result.insufficient_data && (
        <div style={{ marginTop: 32, display: 'grid', gap: 16 }}>
          <ProcessingGrade
            key={`${selectedPlayer}-${stat}-${line}-${direction}`}
            data={mapScanToGradeResult({
              player: selectedPlayer,
              playerId: selectedPlayerId,
              sport,
              stat,
              line: Number(line),
              direction,
              grade: result.grade,
              projection: result.projection,
              confidence: result.confidence,
              sample_size: result.sample_size,
              factors: result.factors,
              alt_lines: result.alt_lines,
              kelly: result.kelly,
              kill_conditions: result.kill_conditions,
              tier,
              // Session 44 — forward the engine's intel fields so the grade
              // card's STAT CONTEXT + VYNDR INTELLIGENCE sections populate.
              season_avg: result.season_avg,
              last10_avg: result.last10_avg,
              vs_opp_avg: result.vs_opp_avg,
              form: result.form,
              usage: result.usage,
              matchup_grade: result.matchup_grade,
              rest: result.rest,
              archetype: result.archetype,
              archetype_blend: result.archetype_blend,
              prop_dna: result.prop_dna,
              // Session 66 — the PRICE LAYER. Without these forwarded the
              // triplet stays hidden no matter what the engine computed
              // (the Session-44 lesson: this call site is a hardcoded
              // whitelist, not a spread).
              book_odds: result.book_odds,
              fair_odds: result.fair_odds,
              model_odds: result.model_odds,
              ev_pct: result.ev_pct,
              model_price_locked: result.model_price_locked,
            }) as GradeResultData}
            onAddToParlay={() => {
              addLeg({
                sport,
                player: selectedPlayer,
                stat,
                line: Number(line),
                direction,
                grade: result.grade,
                confidence: result.confidence ?? 50,
              });
              open();
            }}
            onReadAnother={reset}
          />

          {/* Session 79 — HONEST EMPTY STATE. A marketless scan (the board never
              priced this exact player+stat+line) renders no triplet on the card
              above; this says so truthfully and surfaces the REAL priced lines
              (one-tap to a genuine triplet), or points at the live board. The
              working card/join/triplet is byte-identical — this is a separate
              block that only appears when book_odds/fair_odds came back null. */}
          {(result.book_odds == null || result.fair_odds == null) && (
            <NoMarketState
              player={selectedPlayer}
              stat={stat}
              line={line}
              pricedLines={pricedForSelection}
              onPick={(l) => {
                setLine(String(l.line));
                setDirection(l.direction);
                setRescanKey((k) => k + 1);
              }}
            />
          )}

          {/* Session 60 (4.3) — the model's public history on this player.
              Deferred-render: shows only when real ledger rows exist. */}
          <PriorReads player={selectedPlayer} stat={stat} />

          {/* Session 55 — the self-learning loop's track record for this sport.
              "The system learns" — real hit rate on graded props, misses shown. */}
          <div style={{ display: 'flex', justifyContent: 'center' }}>
            <AccuracyBadge variant="chip" sport={sport?.toLowerCase()} />
          </div>

          {/* Sportsbook hand-off (preserved feature) */}
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center' }}>
            {SUPPORTED_BOOKS.map((b) => {
              const link = buildBookLink({ book: b.id, player: selectedPlayer, sport });
              if (!link) return null;
              return (
                <a
                  key={b.id}
                  href={link.url}
                  target="_blank"
                  rel={BOOK_LINK_REL}
                  className="mono"
                  style={{ padding: '7px 13px', fontSize: 11, fontWeight: 700, borderRadius: 6, border: '1px solid var(--border-hi)', color: 'var(--text-1)', textDecoration: 'none' }}
                >
                  {b.label} ↗
                </a>
              );
            })}
          </div>

          {/* Free-tier nudge — full paywall treatment returns in Phase G */}
          {tier === 'free' && (
            <button
              onClick={() => {
                trackUpgradeClicked({ current_tier: tier, target_tier: 'analyst', trigger_location: 'grade_card_teaser' });
                router.push('/api/checkout?tier=analyst');
              }}
              className="mono"
              style={{ padding: '12px 16px', borderRadius: 8, border: '1px solid rgba(255,179,71,.4)', background: 'rgba(255,179,71,.06)', color: 'var(--amber)', fontWeight: 700, fontSize: 13, cursor: 'pointer' }}
            >
              Unlock every signal + kill conditions — $14.99/mo
            </button>
          )}

          <div style={{ display: 'flex', gap: 12 }}>
            <button onClick={reset} className="btn-ghost" style={{ flex: 1 }}>
              Read another prop
            </button>
            <a href="/dashboard" className="btn-ghost" style={{ flex: 1 }}>
              Back to slate
            </a>
          </div>
        </div>
      )}

      {/* Helpful sticky parlay indicator */}
      {legCount > 0 && (
        <button
          onClick={open}
          className="mono"
          style={{
            position: 'fixed',
            bottom: 88,
            right: 16,
            zIndex: 30,
            padding: '10px 16px',
            borderRadius: 999,
            background: 'var(--accent)',
            border: '1px solid var(--accent-light)',
            color: 'var(--text-primary)',
            fontWeight: 700,
            fontSize: 12,
            boxShadow: '0 8px 24px var(--accent-glow)',
            cursor: 'pointer',
          }}
        >
          Parlay · {legCount} leg{legCount === 1 ? '' : 's'}
        </button>
      )}
    </section>
  );
}

const labelStyle: React.CSSProperties = {
  display: 'block',
  fontSize: 11,
  fontWeight: 700,
  letterSpacing: '0.08em',
  textTransform: 'uppercase',
  color: 'var(--text-tertiary)',
  marginBottom: 8,
};

const shimmerStyle: React.CSSProperties = {
  height: 44,
  borderRadius: 12,
  background:
    'linear-gradient(90deg, var(--bg-surface) 0%, var(--bg-surface-hover) 50%, var(--bg-surface) 100%)',
  backgroundSize: '200% 100%',
  animation: 'shimmer 1.5s linear infinite',
};

const suggestionStyle: React.CSSProperties = {
  width: '100%',
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
  padding: '10px 12px',
  background: 'transparent',
  border: 'none',
  color: 'var(--text-primary)',
  fontFamily: 'inherit',
  fontSize: 14,
  cursor: 'pointer',
  borderRadius: 8,
  textAlign: 'left',
};

function formatTime(iso: string): string {
  try {
    return new Date(iso).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
  } catch {
    return iso;
  }
}
