/**
 * archetypeService — VYNDR player archetype classification (Session 42; renamed
 * to VYNDR Originals in Session 44).
 *
 * Pure logic: categorize a player by their prop-behavior pattern from season
 * averages + usage. Returns a PRIMARY archetype, an optional SECONDARY, and a
 * weighted `blend` (the production-DNA bar in the design's player profile).
 *
 * The archetype NAMES are VYNDR-proprietary (TORCH, BOMBER, ALPHA, …). Each
 * carries `legacyName` (the old descriptive label) for reference — never shown.
 * colors + glyph keys are the contract shared with the frontend `ArchetypeBadge`
 * (web/src/components/vyndr/ArchetypeBadge.tsx) + `archetypes.js`. Keep all three
 * in sync.
 *
 * No API calls, no I/O — feed it a stat object, get a classification.
 */

// ── Archetype registry ──────────────────────────────────────────────
const ARCHETYPES = {
  // ───────── NBA (15) ─────────
  TORCH: {
    legacyName: 'VOLUME SCORER', tag: 'TORCH', sport: 'nba', color: '#FF6B4A', glyph: 'triangle',
    description: 'High usage, shot-dependent scorer',
    propDNA: { reliable: ['points'], volatile: ['assists', 'threes'] },
    education: 'Torches carry a heavy shot diet, so their points props track usage closely. When they get their normal touches, points clear reliably; assists and threes swing with game script.',
  },
  CONDUCTOR: {
    legacyName: 'FLOOR GENERAL', tag: 'CONDUCTOR', sport: 'nba', color: '#6C8CFF', glyph: 'node',
    description: 'Assist-heavy playmaker',
    propDNA: { reliable: ['assists'], volatile: ['points', 'threes'] },
    education: 'Conductors create for others first. Assists are their most stable prop because the offense runs through them; their scoring fluctuates with shot selection and matchup.',
  },
  FORTRESS: {
    legacyName: 'TWO-WAY ANCHOR', tag: 'FORTRESS', sport: 'nba', color: '#4C6FA5', glyph: 'shield',
    description: 'Defense, rebounds, blocks',
    propDNA: { reliable: ['rebounds', 'blocks'], volatile: ['points', 'assists'] },
    education: 'A Fortress generates value on the defensive glass and at the rim. Rebounds and blocks are matchup-resilient; their scoring depends on how much offense flows their way.',
  },
  ARTILLERY: {
    legacyName: 'STRETCH BIG', tag: 'ARTILLERY', sport: 'nba', color: '#C98A5A', glyph: 'target',
    description: 'Floor-spacing shooting big',
    propDNA: { reliable: ['threes', 'rebounds'], volatile: ['assists', 'blocks'] },
    education: 'Artillery bigs space the floor and crash the glass. Threes and rebounds are their bread and butter; assists and blocks are situational.',
  },
  SURGE: {
    legacyName: 'USAGE SPONGE', tag: 'SURGE', sport: 'nba', color: '#FFA24A', glyph: 'uparrow',
    description: 'Usage spikes when stars sit',
    propDNA: { reliable: ['points'], volatile: ['assists', 'rebounds'] },
    education: 'A Surge soaks up shots when a star sits or is injured. Their points props spike on cascade nights — read the injury report before trusting the baseline.',
  },
  'DUAL THREAT': {
    legacyName: 'COMBO GUARD', tag: 'DUAL THREAT', sport: 'nba', color: '#1EA6D8', glyph: 'twin',
    description: 'Scoring + playmaking hybrid',
    propDNA: { reliable: ['points', 'assists'], volatile: ['rebounds'] },
    education: 'Dual Threats score and create in equal measure, so points and assists both stay in play. Rebounds are the volatile leg for their size.',
  },
  CONNECTOR: {
    legacyName: 'ROLE GLUE', tag: 'CONNECTOR', sport: 'nba', color: '#9AB0C4', glyph: 'chain',
    description: 'Low-usage specialist',
    propDNA: { reliable: [], volatile: ['points', 'assists', 'rebounds'] },
    education: 'Connectors do the little things at low usage. Their counting props are thin and matchup-dependent — they reward unders more often than overs.',
  },
  FASTBREAK: {
    legacyName: 'TRANSITION ENGINE', tag: 'FASTBREAK', sport: 'nba', color: '#4AA0E8', glyph: 'chevrons',
    description: 'Pace-pushing fast-break threat',
    propDNA: { reliable: ['points'], volatile: ['assists', 'threes'] },
    education: 'Fastbreaks feast in the open floor. Their points correlate with game pace — target overs in projected up-tempo matchups.',
  },
  'PAINT BOSS': {
    legacyName: 'POST SCORER', tag: 'PAINT BOSS', sport: 'nba', color: '#FF5C5C', glyph: 'postup',
    description: 'Back-to-basket interior scorer',
    propDNA: { reliable: ['points', 'rebounds'], volatile: ['threes', 'assists'] },
    education: 'A Paint Boss operates in the paint. Points and rebounds are reliable against most fronts; perimeter props are noise.',
  },
  LOCKDOWN: {
    legacyName: 'DEFENSIVE SPECIALIST', tag: 'LOCKDOWN', sport: 'nba', color: '#5A6474', glyph: 'shieldCheck',
    description: 'Perimeter stopper, low usage',
    propDNA: { reliable: ['steals'], volatile: ['points', 'assists'] },
    education: 'Lockdowns earn minutes with their on-ball defense. Steals and blocks carry their card; offensive props are low-volume and streaky.',
  },
  SWITCHBOARD: {
    legacyName: 'POINT FORWARD', tag: 'SWITCHBOARD', sport: 'nba', color: '#90A0E8', glyph: 'half',
    description: 'Oversized primary creator',
    propDNA: { reliable: ['points', 'assists', 'rebounds'], volatile: ['threes'] },
    education: 'Switchboards run the offense from a wing or big body, so points, assists, and rebounds all stay live. Their three-point output is the swing factor.',
  },
  ARCHITECT: {
    legacyName: 'SLASHER', tag: 'ARCHITECT', sport: 'nba', color: '#B07CFF', glyph: 'slash',
    description: 'Self-created shot maker, rim-attacking',
    propDNA: { reliable: ['points'], volatile: ['threes', 'assists'] },
    education: 'Architects manufacture their own shot at the rim and the line. Points are stable; their three-point props are volatile because they rarely settle for jumpers.',
  },
  PISTON: {
    legacyName: 'RIM RUNNER', tag: 'PISTON', sport: 'nba', color: '#E87B4A', glyph: 'arc',
    description: 'Lob and putback finisher',
    propDNA: { reliable: ['rebounds'], volatile: ['points', 'assists'] },
    education: 'Pistons finish lobs and putbacks. Rebounds are reliable; their scoring depends entirely on feeds from creators.',
  },
  SENTINEL: {
    legacyName: '3-AND-D', tag: 'SENTINEL', sport: 'nba', color: '#4FA0C4', glyph: 'crosshair',
    description: 'Catch-and-shoot plus defense',
    propDNA: { reliable: ['threes'], volatile: ['points', 'assists'] },
    education: 'Sentinels catch and shoot. Threes are their signature prop; total points swing with how many open looks the offense generates.',
  },
  IGNITER: {
    legacyName: 'SIXTH MAN', tag: 'IGNITER', sport: 'nba', color: '#FF8A3D', glyph: 'bolt',
    description: 'Bench scoring spark',
    propDNA: { reliable: ['points'], volatile: ['rebounds', 'assists'] },
    education: 'Igniters provide instant offense off the bench. Their points props depend on minutes — confirm the rotation before betting overs.',
  },

  // ───────── WNBA-unique (5) ─────────
  DISTRIBUTOR: {
    legacyName: 'POST FACILITATOR', tag: 'DISTRIBUTOR', sport: 'wnba', color: '#7AB8D8', glyph: 'node',
    description: 'Playmaking hub from the post',
    propDNA: { reliable: ['assists', 'rebounds'], volatile: ['threes'] },
    education: 'Distributors orchestrate from the elbow and block. Assists and rebounds are reliable; perimeter shooting is the volatile leg.',
  },
  SHIELD: {
    legacyName: 'TWO-WAY WING', tag: 'SHIELD', sport: 'wnba', color: '#5A8CA8', glyph: 'shieldCheck',
    description: 'Two-way perimeter forward',
    propDNA: { reliable: ['points', 'steals'], volatile: ['assists'] },
    education: 'Shields contribute on both ends. Points and defensive stats stay live; their playmaking is secondary.',
  },
  RANGE: {
    legacyName: 'STRETCH FORWARD', tag: 'RANGE', sport: 'wnba', color: '#C77CE0', glyph: 'target',
    description: 'Floor-spacing forward',
    propDNA: { reliable: ['threes', 'points'], volatile: ['assists', 'blocks'] },
    education: 'Range forwards space the floor from the four. Threes and points are reliable; interior props are matchup-dependent.',
  },
  SPARK: {
    legacyName: 'SLASHING GUARD', tag: 'SPARK', sport: 'wnba', color: '#F0A93D', glyph: 'slash',
    description: 'Downhill scoring guard',
    propDNA: { reliable: ['points'], volatile: ['threes', 'assists'] },
    education: 'Sparks attack downhill. Points are stable; three-point props are the volatile leg since they prioritize the rim.',
  },
  ANCHOR: {
    legacyName: 'INTERIOR ANCHOR', tag: 'ANCHOR', sport: 'wnba', color: '#4C86C4', glyph: 'shield',
    description: 'Dominant paint defender and rebounder',
    propDNA: { reliable: ['rebounds', 'blocks'], volatile: ['points', 'assists'] },
    education: 'Anchors own the paint. Rebounds and blocks are reliable; their scoring depends on post touches.',
  },

  // ───────── MLB (15) ─────────
  BOMBER: {
    legacyName: 'POWER SLUGGER', tag: 'BOMBER', sport: 'mlb', color: '#FF9F45', glyph: 'triangle',
    description: 'Middle-of-the-order power producer',
    propDNA: { reliable: ['total_bases', 'home_runs'], volatile: ['hits'] },
    education: 'Bombers drive the ball over the fence. Total bases and home-run props carry their value; their batting-average-driven props (hits) are the volatile leg from strikeout risk.',
  },
  BRUSH: {
    legacyName: 'CONTACT', tag: 'BRUSH', sport: 'mlb', color: '#E0B84A', glyph: 'crosshair',
    description: 'High average, low strikeout',
    propDNA: { reliable: ['hits'], volatile: ['home_runs', 'total_bases'] },
    education: 'Brush hitters rarely strike out, so their hits props are among the most reliable in baseball. Power props (HR, TB) are the volatile leg.',
  },
  DRIVER: {
    legacyName: 'RUN PRODUCER', tag: 'DRIVER', sport: 'mlb', color: '#5A7D9A', glyph: 'diamond',
    description: 'RBI-dependent, lineup context',
    propDNA: { reliable: ['rbi'], volatile: ['hits', 'home_runs'] },
    education: 'Drivers hit in the heart of the order. RBI props track lineup context — strong with runners on base; their individual hit props are more variable.',
  },
  ALPHA: {
    legacyName: 'ACE', tag: 'ALPHA', sport: 'mlb', color: '#7C5CFF', glyph: 'star',
    description: 'High K/9, low WHIP, deep games',
    propDNA: { reliable: ['strikeouts', 'innings_pitched'], volatile: ['earned_runs'] },
    education: 'Alphas miss bats and go deep. Strikeout and innings props are their most reliable; earned-run props are noisier because one swing can change a line.',
  },
  WHIFF: {
    legacyName: 'STRIKEOUT ARTIST', tag: 'WHIFF', sport: 'mlb', color: '#E86A6A', glyph: 'bolt',
    description: 'Bat-missing arm, high K with traffic',
    propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
    education: 'Whiffs rack up strikeouts but allow more baserunners than an Alpha. The strikeout prop is reliable; earned-run and innings props swing with their traffic.',
  },
  GHOST: {
    legacyName: 'SPEED THREAT', tag: 'GHOST', sport: 'mlb', color: '#3FD8E0', glyph: 'chevrons',
    description: 'Stolen bases, speed score',
    propDNA: { reliable: ['stolen_bases', 'runs'], volatile: ['home_runs'] },
    education: 'Ghosts turn singles into runs. Stolen-base and runs props are their lane; power props rarely clear.',
  },
  HYBRID: {
    legacyName: 'TWO-WAY PLAYER', tag: 'HYBRID', sport: 'mlb', color: '#C88AB0', glyph: 'half',
    description: 'Bats and pitches at elite level',
    propDNA: { reliable: ['total_bases', 'strikeouts'], volatile: ['hits'] },
    education: 'Hybrids produce on both sides of the ball. Read which role they fill that day — their batting and pitching props live on different lines.',
  },
  FLEX: {
    legacyName: 'UTILITY PLAYER', tag: 'FLEX', sport: 'mlb', color: '#A08AC8', glyph: 'plus',
    description: 'Multi-position lineup flex',
    propDNA: { reliable: [], volatile: ['hits', 'total_bases', 'rbi'] },
    education: 'Flex players move around the lineup and the diamond. Their props are matchup- and slot-dependent — confirm they are starting before betting.',
  },
  WORKHORSE: {
    legacyName: 'INNINGS EATER', tag: 'WORKHORSE', sport: 'mlb', color: '#C89B5A', glyph: 'clock',
    description: 'Durable, deep-start arm',
    propDNA: { reliable: ['innings_pitched'], volatile: ['strikeouts', 'earned_runs'] },
    education: 'Workhorses pitch deep without elite stuff. Innings props are reliable; strikeout and earned-run props are more variable since they pitch to contact.',
  },
  CATALYST: {
    legacyName: 'TABLE SETTER', tag: 'CATALYST', sport: 'mlb', color: '#E88AC0', glyph: 'diamondLine',
    description: 'On-base leadoff catalyst',
    propDNA: { reliable: ['hits', 'runs'], volatile: ['rbi', 'home_runs'] },
    education: 'Catalysts get on base and score. Hits and runs props are their lane; RBI and power props sit lower in their profile.',
  },
  MIRROR: {
    legacyName: 'GAP HITTER', tag: 'MIRROR', sport: 'mlb', color: '#B0B6C8', glyph: 'uparrow',
    description: 'Gap-to-gap line-drive bat',
    propDNA: { reliable: ['total_bases', 'hits'], volatile: ['home_runs'] },
    education: 'Mirror hitters spray doubles to both gaps. Total bases and hits are reliable; home-run props are the volatile leg of their extra-base profile.',
  },
  HAMMER: {
    legacyName: 'CLOSER', tag: 'HAMMER', sport: 'mlb', color: '#FF4D6D', glyph: 'lock',
    description: 'Ninth-inning save specialist',
    propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
    education: 'Hammers throw one high-leverage inning. Strikeout props can hit in a clean save; everything else is a one-inning coin flip.',
  },
  SINKER: {
    legacyName: 'SWINGMAN', tag: 'SINKER', sport: 'mlb', color: '#5A9AB0', glyph: 'swap',
    description: 'Groundball spot-starter / long relief',
    propDNA: { reliable: [], volatile: ['strikeouts', 'innings_pitched', 'earned_runs'] },
    education: 'Sinkers bounce between starting and relief and pitch to contact. Their workload is unpredictable, so all of their props carry role risk — confirm the assignment.',
  },
  BRIDGE: {
    legacyName: 'BULLPEN ARM', tag: 'BRIDGE', sport: 'mlb', color: '#6C9CB0', glyph: 'shieldCheck',
    description: 'Setup / high-leverage middle relief',
    propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
    education: 'Bridges throw short, high-leverage outings. Strikeout props can hit in one inning; innings and earned-run props are too small a sample to trust.',
  },
  SWITCH: {
    legacyName: 'DEFENSIVE WIZARD', tag: 'SWITCH', sport: 'mlb', color: '#C0B08A', glyph: 'batball',
    description: 'Platoon-leveraged, glove-first bat',
    propDNA: { reliable: [], volatile: ['hits', 'total_bases', 'rbi'] },
    education: 'Switch bats earn their spot with the glove and favorable platoon splits. Their offensive props are thin and matchup-dependent — lean unders outside their platoon edge.',
  },

  // ───────── Soccer (6) ─────────
  FINISHER: {
    legacyName: 'POACHER', tag: 'FINISHER', sport: 'soccer', color: '#FF5C5C', glyph: 'crosshair',
    description: 'Penalty-box finisher',
    propDNA: { reliable: ['shots_on_target', 'goals'], volatile: ['assists'] },
    education: 'Finishers score inside the box. Shots-on-target and goals are their props; they rarely create for others.',
  },
  MAESTRO: {
    legacyName: 'CREATOR', tag: 'MAESTRO', sport: 'soccer', color: '#D9A441', glyph: 'node',
    description: 'Chance-creating playmaker',
    propDNA: { reliable: ['assists', 'passes'], volatile: ['goals'] },
    education: 'Maestros set the table. Assists and passing props are reliable; their goal output is the volatile leg.',
  },
  TOWER: {
    legacyName: 'TARGET MAN', tag: 'TOWER', sport: 'soccer', color: '#6C9CB0', glyph: 'triangle',
    description: 'Hold-up aerial striker',
    propDNA: { reliable: ['shots', 'shots_on_target'], volatile: ['goals', 'assists'] },
    education: 'Towers win aerial duels and hold the ball up. Shot props are reliable; conversion to goals is variable.',
  },
  MOTOR: {
    legacyName: 'BOX-TO-BOX', tag: 'MOTOR', sport: 'soccer', color: '#E0A03D', glyph: 'chevrons',
    description: 'All-action central midfielder',
    propDNA: { reliable: ['tackles', 'passes'], volatile: ['goals', 'shots'] },
    education: 'Motors cover every blade of grass. Tackles and passing props are reliable; their attacking output swings by role.',
  },
  BLADE: {
    legacyName: 'WING WIZARD', tag: 'BLADE', sport: 'soccer', color: '#B36CFF', glyph: 'slash',
    description: 'Dribbling wide threat',
    propDNA: { reliable: ['shots', 'assists'], volatile: ['goals'] },
    education: 'Blades beat defenders wide. Shots and assists are their lane; goals come in streaks.',
  },
  WALL: {
    legacyName: 'SWEEPER KEEPER', tag: 'WALL', sport: 'soccer', color: '#5A7D9A', glyph: 'shield',
    description: 'Distributing goalkeeper',
    propDNA: { reliable: ['saves', 'passes'], volatile: ['goals_conceded'] },
    education: 'Walls distribute and defend space. Saves and passing props are reliable; goals-conceded depends on the team in front of them.',
  },
};

// ── Combat archetype registry (Wave 6 — MMA/UFC) ────────────────────
// PINNED by specs/combat-intelligence.md. Kept in a SEPARATE registry
// (NOT merged into ARCHETYPES) for two reasons:
//   1. FINISHER collides with the soccer archetype name — combat FINISHER
//      is a DIFFERENT color/glyph, and the global getArchetype()/ARCHETYPES
//      lookup is keyed by uppercase name with no sport dimension.
//   2. Combat FINISHER's green (#12B886) sits close to the signal green,
//      which the colorContract gate forbids for shared player archetypes.
//      Isolating combat keeps that gate (edge-green purity) intact while
//      honoring the pinned combat palette.
// The frontend mirror lives in web/src/lib/archetypes.js COMBAT_ARCHETYPE_MAP;
// tests/unit/combatArchetypes.test.js asserts the two agree (colors + glyphs),
// same discipline as the cross-sport color-match test.
const COMBAT_ARCHETYPES = {
  STRIKER:  { tag: 'STRIKER',  sport: 'mma', color: '#E8703A', glyph: '✦', axis: 'range',   description: 'Wins on the feet — volume + power at range.' },
  GRAPPLER: { tag: 'GRAPPLER', sport: 'mma', color: '#2FA4E7', glyph: '⊗', axis: 'range',   description: 'Fight hits the mat on his terms — control + subs.' },
  PRESSURE: { tag: 'PRESSURE', sport: 'mma', color: '#E4574C', glyph: '➤', axis: 'tempo',   description: 'Forward, relentless, breaks the pace.' },
  COUNTER:  { tag: 'COUNTER',  sport: 'mma', color: '#8E7BE0', glyph: '◊', axis: 'tempo',   description: 'Patient — punishes what you show him.' },
  FINISHER: { tag: 'FINISHER', sport: 'mma', color: '#12B886', glyph: '▲', axis: 'outcome', description: 'Ends nights — high KO/SUB rate.' },
  GRINDER:  { tag: 'GRINDER',  sport: 'mma', color: '#B0883B', glyph: '▦', axis: 'outcome', description: 'Goes the distance, wins the rounds.' },
};

// Discipline pedigree tags — VERIFIABLE credentials only (rendered separately
// from the archetype blend). Never inferred/guessed: absent when unknown.
const DISCIPLINE_PEDIGREES = [
  'Combat Sambo', 'Dagestan Wrestling', 'BJJ', 'Wrestling Base',
  'Kickboxing', 'Muay Thai', 'Boxing',
];

const num = (v) => (typeof v === 'number' && !Number.isNaN(v) ? v : 0);
// Strict presence check — an ABSENT stat must not score an axis (Number(null)
// === 0 would fabricate a "0 output" claim). Only a real finite number counts.
const has = (v) => typeof v === 'number' && Number.isFinite(v);
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);

/** NBA scorers — VYNDR Original keys. */
function scoreNBA(s) {
  const ppg = num(s.ppg), rpg = num(s.rpg), apg = num(s.apg), bpg = num(s.bpg),
    spg = num(s.spg), threes = num(s.threes), usg = num(s.usg), fg3a = num(s.fg3a);
  const pos = (s.pos || '').toUpperCase();
  const isBig = pos === 'C' || pos === 'F-C' || pos === 'C-F';
  return {
    TORCH: ppg >= 22 ? (ppg - 16) / 14 + (usg >= 28 ? 0.3 : 0) : 0,
    CONDUCTOR: apg >= 6 ? (apg - 3) / 7 : apg >= 4 ? 0.2 : 0,
    FORTRESS: (bpg >= 1.3 ? bpg / 3 : 0) + (rpg >= 8 ? (rpg - 6) / 8 : 0),
    ARTILLERY: isBig && threes >= 1.2 ? 0.5 + threes / 6 : 0,
    SURGE: s.bench && usg >= 24 ? 0.5 : 0,
    'DUAL THREAT': ppg >= 15 && apg >= 3 && apg < 7 ? 0.4 + apg / 20 : 0,
    CONNECTOR: usg > 0 && usg < 16 && ppg < 10 ? 0.5 : 0,
    FASTBREAK: ppg >= 16 && spg >= 1.2 ? 0.3 : 0,
    'PAINT BOSS': isBig && ppg >= 16 && threes < 1 ? 0.5 + ppg / 50 : 0,
    LOCKDOWN: spg >= 1.4 && usg < 18 ? 0.5 + spg / 6 : 0,
    SWITCHBOARD: apg >= 5 && (pos === 'F' || pos === 'G-F' || pos === 'F-G') && ppg >= 16 ? 0.6 + apg / 16 : 0,
    ARCHITECT: ppg >= 16 && fg3a < 4 && !isBig ? 0.35 : 0,
    PISTON: isBig && rpg >= 7 && ppg < 16 && threes < 0.5 ? 0.45 : 0,
    SENTINEL: threes >= 1.6 && usg < 20 && spg >= 0.9 ? 0.5 + threes / 8 : 0,
    IGNITER: s.bench && ppg >= 12 ? 0.4 + ppg / 40 : 0,
  };
}

function scoreWNBA(s) {
  const ppg = num(s.ppg), rpg = num(s.rpg), apg = num(s.apg), bpg = num(s.bpg),
    spg = num(s.spg), threes = num(s.threes), usg = num(s.usg);
  const pos = (s.pos || '').toUpperCase();
  const isBig = pos === 'C' || pos === 'F' || pos === 'F-C';
  return {
    TORCH: ppg >= 18 ? (ppg - 12) / 12 + (usg >= 26 ? 0.25 : 0) : 0,
    CONDUCTOR: apg >= 5 ? (apg - 2) / 6 : 0,
    'DUAL THREAT': ppg >= 14 && apg >= 3 && apg < 6 ? 0.4 + apg / 18 : 0,
    DISTRIBUTOR: isBig && apg >= 3 && rpg >= 7 ? 0.6 + apg / 12 : 0,
    SHIELD: !isBig && ppg >= 12 && spg >= 1.2 ? 0.5 + spg / 6 : 0,
    RANGE: (pos === 'F' || isBig) && threes >= 1.2 ? 0.5 + threes / 5 : 0,
    SPARK: pos.startsWith('G') && ppg >= 14 && threes < 1.5 ? 0.45 : 0,
    ANCHOR: isBig && (bpg >= 1 || rpg >= 8) ? 0.5 + rpg / 16 : 0,
    LOCKDOWN: spg >= 1.5 && usg < 18 ? 0.45 : 0,
  };
}

/**
 * MLB scorers. Hitters and pitchers are disjoint.
 * Hitter inputs: avg, hr, rbi, sb, ops, runs, k_rate, doubles.
 * Pitcher inputs: era, k9, whip, ip_per_start, saves, role ('SP'|'RP'|'CL').
 */
function scoreMLB(s) {
  const role = (s.role || '').toUpperCase();
  const isPitcher = s.isPitcher || role === 'SP' || role === 'RP' || role === 'CL' ||
    num(s.era) > 0 || num(s.k9) > 0;
  if (isPitcher) {
    const era = num(s.era), k9 = num(s.k9), whip = num(s.whip),
      ip = num(s.ip_per_start), saves = num(s.saves);
    return {
      ALPHA: k9 >= 9.5 && era <= 3.6 && ip >= 5.5 ? 0.6 + k9 / 30 : k9 >= 9 && era <= 3.6 ? 0.3 : 0,
      WHIFF: k9 >= 10.5 && era > 3.6 ? 0.6 + k9 / 30 : k9 >= 9.5 && era > 3.8 ? 0.35 : 0,
      WORKHORSE: ip >= 6 && k9 < 9 ? 0.55 + ip / 20 : 0,
      HAMMER: role === 'CL' || saves >= 10 ? 0.7 + saves / 60 : 0,
      BRIDGE: role === 'RP' && saves < 10 ? 0.55 : ip > 0 && ip < 3 ? 0.4 : 0,
      SINKER: role === 'SP' && ip < 5 && ip > 0 ? 0.4 : 0,
      HYBRID: s.twoWay ? 0.8 : 0,
    };
  }
  const avg = num(s.avg), hr = num(s.hr), rbi = num(s.rbi), sb = num(s.sb),
    ops = num(s.ops), runs = num(s.runs), kRate = num(s.k_rate), doubles = num(s.doubles);
  return {
    // BOMBER is the single power archetype — fires for any high-HR bat (incl.
    // high-strikeout sluggers like Judge). Session 47: thresholds prorated for
    // mid-season HR totals (a 15-HR June pace is full-season slugger territory),
    // so power leads over DRIVER for a slugger who also drives in runs.
    BOMBER: hr >= 15 ? 0.6 + hr / 60 : hr >= 10 ? 0.35 : 0,
    BRUSH: avg >= 0.28 && kRate < 16 ? 0.6 + (avg - 0.25) * 2 : avg >= 0.29 ? 0.4 : 0,
    DRIVER: rbi >= 50 && hr >= 12 ? 0.5 + rbi / 200 : 0,
    GHOST: sb >= 15 ? 0.6 + sb / 60 : sb >= 10 ? 0.35 : 0,
    CATALYST: runs >= 50 && sb >= 8 && hr < 15 ? 0.5 + runs / 200 : 0,
    MIRROR: doubles >= 25 && hr < 20 ? 0.5 + doubles / 80 : 0,
    FLEX: s.utility ? 0.5 : 0,
    SWITCH: s.glove && avg < 0.25 ? 0.5 : 0,
    HYBRID: s.twoWay ? 0.8 : 0,
  };
}

/**
 * MMA fighter scorer. Style is a BLEND across three axes:
 *   range   → STRIKER (strikes at distance) ↔ GRAPPLER (mat control + subs)
 *   tempo   → PRESSURE (forward volume) ↔ COUNTER (patient, high defense)
 *   outcome → FINISHER (KO/SUB rate) ↔ GRINDER (goes the distance)
 *
 * Best-effort from what the ESPN feed exposes; ESPN's striking/grappling
 * granularity is THINNER than ufcstats. Every axis scores ONLY when its
 * inputs are real finite numbers — thin data yields fewer style claims,
 * never a fabricated one. Inputs (all optional):
 *   slpm/sapm      sig strikes landed/absorbed per min
 *   strAcc/strDef  striking accuracy / defense (0-1)
 *   tdAvg          takedowns per 15
 *   subAvg         sub attempts per 15
 *   koRate/subRate/decRate  fraction of WINS by method (0-1)
 *   koWins/subWins/decWins  method counts (rates derived if rates absent)
 */
function scoreMMA(s = {}) {
  const out = {};

  // ── range axis ──
  if (has(s.slpm)) {
    let v = clamp01((s.slpm - 2) / 4);           // ~2/min floor, ~6/min elite
    if (has(s.tdAvg) && s.tdAvg < 1) v += 0.15;  // low takedown reliance = pure striker
    if (has(s.strAcc)) v += clamp01((s.strAcc - 0.4) * 1.2) * 0.15;
    if (v > 0) out.STRIKER = clamp01(v);
  }
  if (has(s.tdAvg) || has(s.subAvg)) {
    let v = has(s.tdAvg) ? clamp01(s.tdAvg / 4) : 0; // 4 TD/15 ≈ elite control
    if (has(s.subAvg)) v += clamp01(s.subAvg / 3) * 0.5;
    if (v > 0) out.GRAPPLER = clamp01(v);
  }

  // ── tempo axis ──
  if (has(s.slpm) && has(s.sapm)) {
    const vol = clamp01((s.slpm + s.sapm - 6) / 6); // heavy two-way volume = forward pressure
    if (vol > 0) out.PRESSURE = vol;
  }
  if (has(s.strDef) || has(s.strAcc)) {
    let v = has(s.strDef) ? clamp01((s.strDef - 0.55) * 2.2) * 0.6 : 0;
    if (has(s.strAcc)) v += clamp01((s.strAcc - 0.45) * 2.2) * 0.4;
    if (has(s.slpm) && s.slpm > 4.5) v -= 0.2; // a high-output striker isn't a patient counter
    if (v > 0) out.COUNTER = clamp01(v);
  }

  // ── outcome axis ──
  const koR  = has(s.koRate)  ? s.koRate  : deriveRate(s.koWins, s);
  const subR = has(s.subRate) ? s.subRate : deriveRate(s.subWins, s);
  const decR = has(s.decRate) ? s.decRate : deriveRate(s.decWins, s);
  if (koR != null || subR != null) {
    const finish = (koR || 0) + (subR || 0);
    if (finish > 0) out.FINISHER = clamp01(finish);
  }
  if (decR != null && decR > 0) out.GRINDER = clamp01(decR);

  return out;
}

// Derive a method rate from a win count when explicit rates are absent.
// Returns null (not 0) when totals are unknown — absent, never fabricated.
function deriveRate(count, s) {
  if (!has(count)) return null;
  const total = has(s.totalWins)
    ? s.totalWins
    : (has(s.koWins) ? s.koWins : 0) + (has(s.subWins) ? s.subWins : 0) + (has(s.decWins) ? s.decWins : 0);
  if (!total || total <= 0) return null;
  return clamp01(count / total);
}

const SCORERS = { nba: scoreNBA, wnba: scoreWNBA, mlb: scoreMLB, mma: scoreMMA };

/** Look up an archetype descriptor by VYNDR name OR legacy name (case-insensitive). */
function getArchetype(name) {
  if (!name) return null;
  const key = String(name).toUpperCase();
  if (ARCHETYPES[key]) return { name: key, ...ARCHETYPES[key] };
  // Legacy-name fallback so old references still resolve.
  const byLegacy = Object.entries(ARCHETYPES).find(([, a]) => a.legacyName === key);
  return byLegacy ? { name: byLegacy[0], ...byLegacy[1] } : null;
}

/** Look up a COMBAT archetype descriptor by name (case-insensitive). */
function getCombatArchetype(name) {
  if (!name) return null;
  const key = String(name).toUpperCase();
  return COMBAT_ARCHETYPES[key] ? { name: key, ...COMBAT_ARCHETYPES[key] } : null;
}

/**
 * Classify a player. Returns:
 *   { sport, primary, secondary|null, blend: [{archetype, weight}] }
 */
function classify(sport, stats = {}) {
  const sp = String(sport || 'nba').toLowerCase();
  // An explicit null does NOT trigger the default parameter, and every scorer
  // dereferences its argument. Guard it here rather than in four scorers.
  const input = stats && typeof stats === 'object' ? stats : {};
  const scorer = SCORERS[sp];
  if (!scorer) return { sport: sp, primary: null, secondary: null, blend: [] };

  const scores = scorer(input);
  const ranked = Object.entries(scores)
    .filter(([, v]) => v > 0)
    .sort((a, b) => b[1] - a[1]);

  // Combat is HONEST-empty on thin data: no forced fallback archetype (the
  // other sports fall back to a low-usage role, but inventing a fighter's
  // style from no data would be a fabrication — spec §STYLE-MATCHUP).
  const resolver = sp === 'mma' ? getCombatArchetype : getArchetype;
  if (ranked.length === 0) {
    // Session 69 — THE FALLBACK IS GONE. It used to return FLEX (mlb) / SHIELD
    // (wnba) / CONNECTOR (nba) at weight 1.0, i.e. "we could not classify this
    // player" rendered as a fully-confident classification of a real archetype,
    // complete with descriptive education copy. 8 of 18 MLB players carried it.
    // MMA already did the honest thing; every sport does now: unclassified is
    // ABSENT, and the multi-axis classifier (services/archetypeAxes.js) makes a
    // single fallback bucket meaningless anyway — a player is unremarkable on
    // an AXIS, not as a whole.
    return { sport: sp, primary: null, secondary: null, blend: [] };
  }

  const top = ranked.slice(0, 4);
  const total = top.reduce((sum, [, v]) => sum + v, 0) || 1;
  const blend = top.map(([name, v]) => ({ archetype: name, weight: +(v / total).toFixed(3) }));

  const primary = resolver(ranked[0][0]);
  const secondary = ranked.length > 1 && ranked[1][1] >= ranked[0][1] * 0.4
    ? resolver(ranked[1][0])
    : null;

  return { sport: sp, primary, secondary, blend };
}

/** Weight of an archetype within a blend (0 when absent). */
function blendWeight(blend, name) {
  const hit = (blend || []).find((b) => b.archetype === name);
  return hit ? hit.weight : 0;
}

// Accept either a classify() result ({ blend }) or a raw blend array.
function asBlend(x) {
  if (Array.isArray(x)) return x;
  if (x && Array.isArray(x.blend)) return x.blend;
  return [];
}

/**
 * styleMatchup(a, b) — a DESCRIPTIVE MODEL style-edge read (the mockup's
 * CENTER VERDICT). NOT a settled grade, NOT an edge %, NO fabricated
 * confidence. Compares two style blends; when the data is too thin or the
 * styles are too close to call, it says so honestly.
 *
 * a/b may be classify('mma', …) results or raw blend arrays.
 * Returns { verdict, edgeSide: 'a'|'b'|null, summary }.
 */
const STYLE_AXES = ['GRAPPLER', 'STRIKER', 'PRESSURE', 'COUNTER', 'FINISHER', 'GRINDER'];
const MIN_EDGE = 0.2; // below this stylistic gap → too close to call

function styleMatchup(a, b) {
  const A = asBlend(a);
  const B = asBlend(b);
  if (A.length === 0 || B.length === 0) {
    return {
      verdict: 'INSUFFICIENT READ',
      edgeSide: null,
      summary: 'Not enough style data to call this matchup — a MODEL read needs both fighters profiled.',
    };
  }

  let best = null;
  for (const ax of STYLE_AXES) {
    const diff = blendWeight(A, ax) - blendWeight(B, ax);
    if (!best || Math.abs(diff) > Math.abs(best.diff)) best = { ax, diff };
  }

  if (!best || Math.abs(best.diff) < MIN_EDGE) {
    return {
      verdict: 'STYLES EVEN',
      edgeSide: null,
      summary: 'Two closely matched styles — no clear stylistic edge. A MODEL read, not a graded pick.',
    };
  }

  const edgeSide = best.diff > 0 ? 'a' : 'b';
  return {
    verdict: `${best.ax} EDGE`,
    edgeSide,
    summary: `${best.ax} advantage tilts this on style — a MODEL read, not a settled grade.`,
  };
}

const classifyNBA = (stats) => classify('nba', stats);
const classifyWNBA = (stats) => classify('wnba', stats);
const classifyMLB = (stats) => classify('mlb', stats);

const classifyMMA = (stats) => classify('mma', stats);

module.exports = {
  ARCHETYPES,
  COMBAT_ARCHETYPES,
  DISCIPLINE_PEDIGREES,
  getArchetype,
  getCombatArchetype,
  classify,
  classifyNBA,
  classifyWNBA,
  classifyMLB,
  classifyMMA,
  styleMatchup,
};
