/* ══════════════════════════════════════════════════════════
   STRICTLY BUSINESS — AI OPPONENT ENGINE
   ══════════════════════════════════════════════════════════
   Loaded after game.jsx. Reads shared enums from
   window.SB_EXPORTS and attaches its public API to
   window.SB_AI. game.jsx's App component calls this from
   a useEffect to drive bot turns.

   Architecture:
   - Traits assigned once at game start via assignBotTraits()
   - botTakeTurn() runs async with 1-3s delays between decisions
   - useEffect in App drives reactions / pending choices for bots
   ══════════════════════════════════════════════════════════ */

(function () {
  if (typeof window === "undefined" || !window.SB_EXPORTS) {
    console.error("[ai] SB_EXPORTS not found — game.jsx must load first.");
    return;
  }
  var SB = window.SB_EXPORTS;
  var CT  = SB.CT;
  var AS  = SB.AS;
  var ACT = SB.ACT;
  var PH  = SB.PH;

  // ── Utilities ────────────────────────────────────────────

  function rand(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  function pick(arr) {
    if (!arr || !arr.length) return null;
    return arr[Math.floor(Math.random() * arr.length)];
  }

  function weightedRandom(options) {
    var total = options.reduce(function(s, o) { return s + o.w; }, 0);
    var r = Math.random() * total;
    for (var i = 0; i < options.length; i++) {
      r -= options[i].w;
      if (r <= 0) return options[i].id;
    }
    return options[options.length - 1].id;
  }

  // ── Trait Definitions ────────────────────────────────────

  var TRAITS = {
    buyingAggression: [
      { id:"safe",        w:50 },
      { id:"competitive", w:35 },
      { id:"aggressive",  w:15 },
    ],
    buyingPreference: [
      { id:"safe",           w:43 },
      { id:"competitive",    w:45 },
      { id:"aggressive",     w:10 },
      { id:"very_aggressive",w:2  },
    ],
    actionOptions: [
      { id:"seller",     w:15 },
      { id:"risk_taker", w:15 },
      { id:"balanced",   w:35 },
      { id:"competitive",w:35 },
    ],
    cardPreference: [
      { id:"safe",       w:45 },
      { id:"competitive",w:35 },
      { id:"aggressive", w:20 },
    ],
    reactionLikelihood: [
      { id:"safe",       w:30 },
      { id:"competitive",w:50 },
      { id:"aggressive", w:20 },
    ],
    reactionChain: [
      { id:"safe",       w:30 },
      { id:"competitive",w:50 },
      { id:"aggressive", w:20 },
    ],
    targetPriority: [
      { id:"safe",    w:55 },
      { id:"rival",   w:5  },
      { id:"greedy",  w:35 },
      { id:"ruthless",w:5  },
    ],
    assetActivFreq: [
      { id:"safe",       w:30 },  // 80%
      { id:"competitive",w:50 },  // 90%
      { id:"aggressive", w:20 },  // 100%
    ],
    turnEndThreshold: [
      { id:"safe",       w:20 },  // 15% early end
      { id:"competitive",w:80 },  // never end early
    ],
    moneyFloor: [
      { id:"safe",       w:30 },  // $1k
      { id:"competitive",w:60 },  // -$2k
      { id:"aggressive", w:10 },  // none
    ],
    comebackAggression: [
      { id:"safe",       w:20 },
      { id:"competitive",w:60 },
      { id:"aggressive", w:20 },
    ],
    discardBehavior: [
      { id:"safe",       w:20 },  // random
      { id:"competitive",w:80 },  // lowest value
    ],
    briberyBehavior: [
      { id:"passive",    w:30 },
      { id:"competitive",w:55 },
      { id:"aggressive", w:15 },
    ],
  };

  function assignBotTraits(rivalId) {
    var traits = {};
    Object.keys(TRAITS).forEach(function(k) {
      traits[k] = weightedRandom(TRAITS[k]);
    });
    traits.rivalId = rivalId || null;
    return traits;
  }

  // ── Helper: effective traits with comeback modifier ──────

  function isSignificantlyBehind(st, bot) {
    var others = st.players.filter(function(p){ return p.id !== bot.id; });
    if (!others.length) return false;
    var maxMoney = Math.max.apply(null, others.map(function(p){ return p.money; }));
    if (maxMoney <= 0) return false;
    var pct = (maxMoney - bot.money) / Math.abs(maxMoney);
    var t = bot.botTraits;
    if (t.comebackAggression === "aggressive"  && pct >= 0.25) return true;
    if (t.comebackAggression === "competitive" && pct >= 0.30) return true;
    return false;
  }

  var COMEBACK_OVERRIDES = {
    buyingAggression:"aggressive", buyingPreference:"very_aggressive",
    actionOptions:"competitive",   cardPreference:"aggressive",
    reactionLikelihood:"aggressive",reactionChain:"aggressive",
    assetActivFreq:"aggressive",
  };

  function getTraits(st, bot) {
    var t = bot.botTraits || {};
    if (!isSignificantlyBehind(st, bot)) return t;
    var out = Object.assign({}, t);
    if (t.comebackAggression === "aggressive") {
      Object.assign(out, COMEBACK_OVERRIDES);
    } else if (t.comebackAggression === "competitive") {
      var keys = Object.keys(COMEBACK_OVERRIDES);
      var chosen = keys.slice().sort(function(){ return Math.random()-0.5; }).slice(0,3);
      chosen.forEach(function(k){ out[k] = COMEBACK_OVERRIDES[k]; });
    }
    return out;
  }

  // ── Targeting ────────────────────────────────────────────

  function pickTarget(st, bot, traits) {
    var others = st.players.filter(function(p){ return p.id !== bot.id; });
    if (!others.length) return null;
    switch (traits.targetPriority) {
      case "rival": {
        var rv = others.find(function(p){ return p.id === traits.rivalId; });
        return rv || pick(others);
      }
      case "greedy":  return others.reduce(function(a,b){ return a.money > b.money ? a : b; });
      case "ruthless":return others.reduce(function(a,b){ return a.money < b.money ? a : b; });
      default:        return pick(others);
    }
  }

  // ── Card selection ───────────────────────────────────────

  function selectCard(cards, cardPref) {
    if (!cards || !cards.length) return null;
    if (cardPref === "competitive") {
      return Math.random() < 0.60
        ? cards.reduce(function(a,b){ return a.value > b.value ? a : b; })
        : pick(cards);
    }
    if (cardPref === "aggressive") {
      return Math.random() < 0.75
        ? cards.reduce(function(a,b){ return a.value > b.value ? a : b; })
        : pick(cards);
    }
    return pick(cards);
  }

  function lowestCard(cards) {
    if (!cards || !cards.length) return null;
    return cards.reduce(function(a,b){ return a.value < b.value ? a : b; });
  }

  function highestCard(cards) {
    if (!cards || !cards.length) return null;
    return cards.reduce(function(a,b){ return a.value > b.value ? a : b; });
  }

  function discardN(cards, n, behavior) {
    if (!cards || !cards.length) return [];
    var sorted = cards.slice();
    if (behavior === "competitive") {
      sorted.sort(function(a,b){ return a.value - b.value; });
    } else {
      sorted.sort(function(){ return Math.random() - 0.5; });
    }
    return sorted.slice(0, n);
  }

  // ── Activation frequency ─────────────────────────────────

  function shouldActivate(traits) {
    switch (traits.assetActivFreq) {
      case "safe":       return Math.random() < 0.80;
      case "competitive":return Math.random() < 0.90;
      case "aggressive": return true;
      default:           return true;
    }
  }

  // ── Money floor ──────────────────────────────────────────

  function getFloor(traits) {
    switch (traits.moneyFloor) {
      case "safe":       return 1;
      case "competitive":return -2;
      case "aggressive": return -Infinity;
      default:           return -2;
    }
  }

  // ── Asset activation decision ────────────────────────────

  function shouldActivateAsset(st, bot, asset, traits) {
    if (asset.status !== "READY" || asset.disabled) return false;
    var h = asset.hook;
    var money = bot.money;
    var hand  = bot.hand || [];

    switch (h) {
      case "a_4day_work_week":  return true;
      case "a_extra_shift":     return true;
      case "a_coming_soon":     return false;

      case "a_lets_make_a_deal": {
        if (!asset.lockedCard) return hand.length > 0 ||
          st.players.some(function(p){ return p.id !== bot.id && p.assets.some(function(a){ return !a.disabled; }); });
        return Math.random() < (Math.random() < 0.5 ? 0.10 : 0.30);
      }
      case "a_extra_income": {
        if (!asset.lockedCard) return true;
        return Math.random() < (Math.random() < 0.5 ? 0.10 : 0.30);
      }
      case "a_risk_vs_reward": {
        if (!asset.lockedCard) return true;
        return Math.random() < (Math.random() < 0.5 ? 0.10 : 0.30);
      }

      case "a_action_plan": {
        var ap = asset;
        var stored = ap.lockedCard;
        var toks = (ap.tokens || []).length;
        if (stored && toks >= stored.value) return true;
        if (!stored) return hand.some(function(c){ return c.type === CT.ACTION; });
        return false;
      }

      case "a_severance_pay":
        return (asset.tokens || []).length >= (asset.tmax || 12);

      case "a_monopoly": {
        if (!asset.lockedCard) return hand.length > 0;
        var eligible = hand.filter(function(c){ return c.value !== asset.lockedCard.value; });
        return eligible.length > 0 && Math.random() < 0.60;
      }

      case "a_credit_line":
        return (asset.tokens || []).length > 0;

      case "a_rnd_budget":
        return (asset.tokens || []).length >= (asset.tmax || 3);

      case "a_retainer":
        return !asset.lockedCard && hand.some(function(c){ return c.type === CT.REACTION; });

      case "a_markup":
        return hand.length > 0;

      case "a_closed_for_remodeling":
        return st.players.some(function(p){
          return p.id !== bot.id && p.assets.some(function(a){ return !a.disabled; });
        });

      case "a_sneak_peek":
        return Math.random() < 0.70;

      case "a_trade_in": {
        var ta = hand.filter(function(c){ return c.type === CT.ACTION; });
        var tr = hand.filter(function(c){ return c.type === CT.REACTION; });
        if (!ta.length || !tr.length) return false;
        if (money < 0) return Math.random() < 0.90;
        var la = ta.reduce(function(a,b){ return a.value < b.value ? a : b; }).value;
        var lr = tr.reduce(function(a,b){ return a.value < b.value ? a : b; }).value;
        if (la < 3 && lr < 3) return Math.random() < 0.75;
        if (la + lr <= 5) return Math.random() < 0.65;
        if (la === 3 && lr === 3) return Math.random() < 0.25;
        return false;
      }

      case "a_product_update": {
        var pu = bot.assets.filter(function(a){ return a.id !== asset.id && !a.disabled; });
        return pu.length > 0;
      }

      case "a_rebranding": {
        var rb = bot.assets.filter(function(a){ return !a.disabled && (a.origVal||a.val||0) >= 5 && (a.origVal||a.val||0) <= 7; });
        return rb.length >= 1;
      }

      case "a_multi_tool": {
        var mt = bot.assets.filter(function(a){ return a.status === "USED" && !a.disabled && a.id !== asset.id; });
        return mt.length > 0;
      }

      case "a_bribery": {
        var hasTarget = st.players.some(function(p){
          return p.id !== bot.id && p.assets.some(function(a){ return !a.disabled; });
        });
        if (!hasTarget) return false;
        var bb = traits.briberyBehavior;
        if (bb === "passive") {
          var hv = hand.reduce(function(s,c){ return s + c.value; }, 0);
          return hv > 8 && Math.random() < 0.60;
        }
        return Math.random() < (bb === "aggressive" ? 0.80 : 0.50);
      }

      case "a_three_of_kind": {
        var tok = hand.filter(function(c){ return c.value === 2; });
        return tok.length >= 3;
      }

      case "a_exchange": {
        var shop = st.shop || [];
        if (shop.some(function(a){ return (a.origVal||a.val||0) >= 9; })) return Math.random() < 0.85;
        if (shop.some(function(a){ return (a.origVal||a.val||0) >= 6; })) return Math.random() < 0.40;
        return false;
      }

      case "a_eye_for_eye": {
        var allOppAssets = [];
        st.players.forEach(function(p){
          if (p.id !== bot.id) p.assets.forEach(function(a){ if (!a.disabled) allOppAssets.push(a); });
        });
        var max = allOppAssets.reduce(function(m,a){ return Math.max(m, a.origVal||a.val||0); }, 0);
        if (max >= 9) return Math.random() < 0.85;
        if (max >= 7) return Math.random() < 0.50;
        return Math.random() < 0.15;
      }

      case "a_bogo": {
        var bogoOpts = bot.assets.filter(function(a){ return a.id !== asset.id && !a.disabled && !a._isBogo; });
        if (!bogoOpts.length) return false;
        var bogoMax = bogoOpts.reduce(function(m,a){ return Math.max(m, a.origVal||a.val||0); }, 0);
        if (bogoMax >= 9) return Math.random() < 0.80;
        if (bogoMax >= 7) return Math.random() < 0.60;
        return Math.random() < 0.20;
      }

      case "a_franchise": {
        var franOpts = bot.assets.filter(function(a){
          return a.id !== asset.id && !a.disabled && !a._franchised && a.sub === AS.PASSIVE;
        });
        if (!franOpts.length) return false;
        var franMax = franOpts.reduce(function(m,a){ return Math.max(m, a.origVal||a.val||0); }, 0);
        if (franMax >= 9) return Math.random() < 0.80;
        if (franMax >= 7) return Math.random() < 0.60;
        return Math.random() < 0.20;
      }

      default: return false;
    }
  }

  // ── Pending choice resolver ──────────────────────────────

  function resolvePendingChoice(st, pc, botId) {
    var bot = st.players.find(function(p){ return p.id === botId; });
    if (!bot) return null;
    var traits = getTraits(st, bot);
    var hand = bot.hand || [];
    var opts = pc.options || [];

    switch (pc.type) {

      case "DISCARD_TO_LIMIT": {
        var dtl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
        if (!dtl) return null;
        var toDiscard = discardN(dtl.hand, pc.needed || 1, traits.discardBehavior);
        return { id: toDiscard.map(function(c){ return c.id; }).join(",") };
      }

      case "DISCARD_SELF": {
        var dsSelf = st.players.find(function(p){ return p.id === pc.srcPlayer; });
        if (!dsSelf || !dsSelf.hand.length) return null;
        return { id: lowestCard(dsSelf.hand).id };
      }

      case "OPPONENT_DISCARD": {
        var odTgt = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
        if (!odTgt || !odTgt.hand.length) return null;
        return { id: lowestCard(odTgt.hand).id };
      }

      case "CLIENT_THEFT": {
        var ctTgt = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
        if (!ctTgt || !ctTgt.hand.length) return null;
        return { id: lowestCard(ctTgt.hand).id };
      }

      case "MONOPOLY_SELECT": {
        if (!opts.length) return null;
        var stored = (function(){ var a = bot.assets.find(function(a){ return a.id===pc.assetId; }); return a && a.lockedCard; })();
        var eligible = stored ? opts.filter(function(c){ return c.value !== stored.value; }) : opts;
        if (!eligible.length) return null;
        var r = Math.random();
        var chosen;
        if (r < 0.50) chosen = pick(eligible);
        else if (r < 0.80) chosen = lowestCard(eligible);
        else chosen = highestCard(eligible);
        return { id: chosen.id };
      }

      case "RETAINER_PLACE": {
        if (!opts.length) return null;
        var ret = Math.random() < 0.50 ? pick(opts) : highestCard(opts);
        return { id: ret.id };
      }

      case "CREDIT_LINE_CHOICE": {
        var cl = pc.currentTokens || 0;
        if (!cl) return null;
        var money = bot.money;
        var p;
        if (money < 0)      p = [0.80, 0.15, 0.05];
        else if (money <= 3)p = [0.30, 0.60, 0.10];
        else if (money <= 8)p = [0.15, 0.75, 0.10];
        else                p = [0.10, 0.70, 0.20];
        var rr = Math.random(), rem;
        if (rr < p[0]) rem = 1;
        else if (rr < p[0]+p[1]) rem = 2;
        else rem = 3;
        return { id: String(Math.min(rem, cl)) };
      }

      case "RND_ACTIVATE":
        return { id: "activate" };

      case "TRADE_IN_SELECT": {
        var ta2 = pc.actionOptions || [];
        var tr2 = pc.reactionOptions || [];
        if (!ta2.length || !tr2.length) return null;
        var act = lowestCard(ta2), rxn = lowestCard(tr2);
        return act && rxn ? { id: act.id + ":" + rxn.id } : null;
      }

      case "AP_ACTIVATE_SELECT":
        return { id: "activate" };

      case "SNEAK_PEEK_SELECT": {
        var spCards = pc.cards || [];
        if (spCards.length < 2) return { id: "keep", order: spCards.map(function(c){ return c.id; }) };
        var sorted = spCards.slice().sort(function(a,b){ return b.value - a.value; });
        return { id: "reorder", order: sorted.map(function(c){ return c.id; }) };
      }

      case "BOGO_SELECT": {
        if (!opts.length) return null;
        var bogoChosen = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: bogoChosen.id };
      }

      case "FRANCHISE_SELECT": {
        if (!opts.length) return null;
        var franChosen = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: franChosen.id };
      }

      case "EXCHANGE_SELECT": {
        if (!opts.length) return null;
        var ex9 = opts.filter(function(a){ return (a.origVal||a.val||0) >= 9; });
        var ex6 = opts.filter(function(a){ return (a.origVal||a.val||0) >= 6; });
        var exPool = ex9.length ? ex9 : (ex6.length ? ex6 : opts);
        return { id: pick(exPool).id };
      }

      case "EYE_SELECT": {
        if (pc.phase === "opponent") {
          var eyeOpps = pc.opponents || [];
          if (!eyeOpps.length) return null;
          var eyeTgt = pickTarget(st, bot, traits);
          var eyeFound = eyeTgt && eyeOpps.find(function(p){ return p.id === eyeTgt.id; });
          return { id: (eyeFound || pick(eyeOpps)).id };
        }
        // Asset phase
        var eyeAssets = pc.assets || [];
        if (!eyeAssets.length) return null;
        var eyeMax = eyeAssets.reduce(function(m,a){ return Math.max(m, a.origVal||a.val||0); }, 0);
        var eyeMin = eyeMax >= 9 ? 9 : eyeMax >= 7 ? 7 : 0;
        var eyePool = eyeAssets.filter(function(a){ return (a.origVal||a.val||0) >= eyeMin; });
        return { id: pick(eyePool || eyeAssets).id };
      }

      case "BRIBERY_ASSET_SELECT": {
        if (!opts.length) return null;
        var bribChosen = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: bribChosen.id };
      }

      case "BRIBERY_CARD_SELECT": {
        var threshold = pc.threshold || 0;
        var bribOpts = pc.options || [];
        if (!bribOpts.length) return null;
        var sorted2 = bribOpts.slice().sort(function(a,b){ return a.value - b.value; });
        var sel = [], total = 0;
        for (var bi = 0; bi < sorted2.length && total <= threshold; bi++) {
          sel.push(sorted2[bi].id);
          total += sorted2[bi].value;
        }
        if (total <= threshold) return null;
        return { id: sel.join(",") };
      }

      case "THREE_OF_KIND_SELECT": {
        var tplVal   = pc.tripleValue || 2;
        var tplCount = pc.tripleCount || 3;
        var tplBot   = st.players.find(function(p){ return p.id===botId; });
        var tplCards = tplBot ? tplBot.hand.filter(function(c){ return c.value===tplVal; }).slice(0,tplCount) : [];
        if (tplCards.length < tplCount) return null;
        return { id: tplCards.map(function(c){ return c.id; }).join(",") };
      }

      case "PU_ASSET_SELECT": {
        if (!opts.length) return null;
        var puChosen = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: puChosen.id };
      }
      case "PU_FIELD_SELECT":
        return opts.length ? { id: pick(opts).id } : null;
      case "PU_DIRECTION_SELECT":
        return { id: "increase" };

      case "REBRANDING_SELECT": {
        if (!opts.length) return null;
        var rbIds = opts.filter(function(a){ return (a.origVal||a.val||0) >= 5 && (a.origVal||a.val||0) <= 7; })
                        .map(function(a){ return a.id; });
        return rbIds.length ? { id: rbIds.join(",") } : null;
      }

      case "MARKUP_DIRECTION":
        return { id: "increase" };

      case "MARKUP_CARD": {
        if (!opts.length) return null;
        return { id: highestCard(opts).id };
      }

      case "CFR_SELECT": {
        if (!opts.length) return null;
        if (pc.phase === "opponent" || !pc.phase) {
          var cfrTgt = pickTarget(st, bot, traits);
          var cfrFound = cfrTgt && opts.find(function(p){ return p.id === cfrTgt.id; });
          return { id: (cfrFound || pick(opts)).id };
        }
        // Asset phase: most valuable
        var cfrA = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: cfrA.id };
      }

      case "MULTI_TOOL_SELECT":
        return opts.length ? { id: pick(opts).id } : null;

      case "SF_SELECT": {
        if (pc.phase === "opponent") {
          var sfOpps = pc.opponents || [];
          var sfTgt = pickTarget(st, bot, traits);
          var sfFound = sfTgt && sfOpps.find(function(p){ return p.id === sfTgt.id; });
          return { id: (sfFound || pick(sfOpps)).id };
        }
        // Asset phase
        var sfAssets = pc.assets || [];
        if (!sfAssets.length) return null;
        var sfChosen = sfAssets.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: sfChosen.id };
      }

      case "RISK_REWARD_SET_TARGET":
      case "LETS_DEAL_SET_TARGET":
      case "EXTRA_INCOME_SET_TARGET":
      case "BP_TARGET_SELECT":
      case "COVER_COSTS_TARGET": {
        if (!opts.length) return null;
        var tTgt = pickTarget(st, bot, traits);
        var tFound = tTgt && opts.find(function(p){ return p.id === tTgt.id; });
        return { id: (tFound || pick(opts)).id };
      }

      // Choices where the bot is the TARGET (opponent forced them to respond)
      case "HELP_WANTED_GIVE": {
        var hwPlayer = st.players.find(function(p){ return p.id === (pc.tgtPlayer || pc.srcPlayer); });
        if (!hwPlayer || !hwPlayer.hand.length) return null;
        return { id: lowestCard(hwPlayer.hand).id };
      }

      case "INTERVIEWS_PICK": {
        if (!opts.length) return null;
        return { id: highestCard(opts).id };
      }

      case "KICKSTARTER_CHOICE": {
        var ksPlayer = st.players.find(function(p){ return p.id === (pc.tgtPlayer || pc.srcPlayer); });
        if (!ksPlayer) return null;
        // If in negatives, prefer to gain money (action cards are worth more sold)
        var ksHand = ksPlayer.hand || [];
        if (!ksHand.length) return { id: "draw" };
        return ksPlayer.money < 0 ? { id: "draw" } : { id: (highestCard(ksHand) || ksHand[0]).id };
      }

      case "DOUBLE_SELL": {
        if (!opts.length) return null;
        return { id: highestCard(opts).id };
      }

      case "DISCOUNT_BUY":
      case "DISCOUNT_CHOICE": {
        if (!opts.length) return null;
        var dcChosen = opts.reduce(function(a,b){ return (a.origVal||a.val||0) > (b.origVal||b.val||0) ? a : b; });
        return { id: dcChosen.id };
      }

      case "MR_SHOW_CARD": {
        if (!opts.length) return null;
        return { id: lowestCard(opts).id };
      }

      case "TOTAL_LOSS_PICK_VALUE":
        // Pick mid-range value that opponents are likely to have
        return { id: "3" };

      case "QUICK_EXCHANGE_DISCARD": {
        if (!opts.length) return null;
        // Find a valid pair (equal value cards)
        var qeValid = pc.validValues || [];
        if (!qeValid.length) return null;
        var qeVal = qeValid[0];
        var qePair = opts.filter(function(c){ return c.value === qeVal; }).slice(0, 2);
        return qePair.length >= 2 ? { id: qePair.map(function(c){ return c.id; }).join(",") } : null;
      }

      case "SHRINKAGE_SELECT": {
        // Bot must pick a card from tgtPlayer's hand to discard
        var shrTgt = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
        if (!shrTgt || !shrTgt.hand.length) return null;
        return { id: lowestCard(shrTgt.hand).id };
      }

      case "MINOR_LOSS_DISCARD": {
        if (!opts.length) return null;
        return { id: lowestCard(opts).id };
      }

      case "OUT_OF_ORDER_SELECT": {
        if (pc.phase === "opponent") {
          var oooOpps = pc.opponents || [];
          if (!oooOpps.length) return null;
          var oooTgt = pickTarget(st, bot, traits);
          var oooFound = oooTgt && oooOpps.find(function(p){ return p.id === oooTgt.id; });
          return { id: (oooFound || pick(oooOpps)).id };
        }
        var oooAssets = pc.assets || [];
        if (!oooAssets.length) return null;
        return { id: pick(oooAssets).id };
      }

      case "UPGRADE_DISCARD":
      case "UPGRADE_GAIN": {
        if (!opts.length) return null;
        return { id: pick(opts).id };
      }

      case "NEG_REACTION_SELECT":
      case "LET_GO_SELECT":
      case "OUTSIDE_HIRE_SELECT":
      case "POCKET_CHANGE_SELECT": {
        if (!opts.length) return null;
        var pcTgt = pickTarget(st, bot, traits);
        var pcFound = pcTgt && opts.find(function(p){ return p.id === pcTgt.id; });
        return { id: (pcFound || pick(opts)).id };
      }

      case "RECYCLE_CHOICE": {
        if (!opts.length) return null;
        return { id: highestCard(opts).id };
      }

      case "BTB_SELECT": {
        if (pc.phase === "opponent") {
          var btbOpps = pc.opponents || [];
          if (!btbOpps.length) return null;
          var btbTgt = pickTarget(st, bot, traits);
          var btbFound = btbTgt && btbOpps.find(function(p){ return p.id === btbTgt.id; });
          return { id: (btbFound || pick(btbOpps)).id };
        }
        var btbAssets = pc.assets || [];
        if (!btbAssets.length) return null;
        return { id: pick(btbAssets).id };
      }

      case "PRICE_CHECK_PROMPT": {
        // Play the drawn card or discard - if in positives and card has value, play it
        var pcp = pc.drawnCard;
        if (!pcp) return { id: "discard" };
        return bot.money >= 0 ? { id: "play" } : { id: "discard" };
      }

      case "VALUATION_PICK": {
        if (!opts.length) return null;
        return { id: pick(opts).id };
      }

      case "GIVE_BACK_TIE":
      case "REFRESH_ASSET": {
        if (!opts.length) return null;
        return { id: pick(opts).id };
      }

      case "TAXES_SELECT_ASSET": {
        // Use "auto" so the game picks randomly — assets use origVal not value, keep it simple
        return { id: "auto" };
      }

      case "POCKET_CHANGE_REVEAL":
        return { id: "reveal" };

      case "FULL_REFUND_PROMPT":
      case "FULL_REFUND_TARGET":
        if (!opts.length) return null;
        return { id: pick(opts).id };

      // Read-only / auto-dismiss
      case "PEEK":
      case "COMING_SOON_PEEK":
        return { id: "dismiss" };

      default:
        return null; // will cause DISMISS_CHOICE
    }
  }

  // ── Buying logic ─────────────────────────────────────────

  function chooseToBuy(st, bot, traits) {
    var shop = st.shop || [];
    if (!shop.length || bot.hasBoughtAsset) return null;

    var pref = traits.buyingPreference;
    var buyType;
    var r = Math.random() * 100;

    if (pref === "safe") {
      if (r < 40) buyType = "random";
      else if (r < 80) buyType = "cheapest";
      else if (r < 85) buyType = "expensive";
      else buyType = "blind";
    } else if (pref === "competitive") {
      if (r < 30) buyType = "random";
      else if (r < 50) buyType = "cheapest";
      else if (r < 75) buyType = "expensive";
      else buyType = "blind";
    } else if (pref === "aggressive") {
      if (r < 20) buyType = "random";
      else if (r < 25) buyType = "cheapest";
      else if (r < 70) buyType = "expensive";
      else buyType = "blind";
    } else {
      if (r < 10) buyType = "random";
      else if (r < 15) buyType = "cheapest";
      else if (r < 85) buyType = "expensive";
      else buyType = "blind";
    }

    if (buyType === "blind") {
      // Always try blind buy; aggression check still applies vs. rough estimate
      var floorB = getFloor(traits);
      if ((bot.money - 5) < floorB) return null; // rough cost estimate $5k
      return { type:"BUY_DECK" };
    }

    var candidate;
    if (buyType === "cheapest")  candidate = shop.reduce(function(a,b){ return (a.origVal||a.val||0)<(b.origVal||b.val||0)?a:b; });
    else if (buyType === "expensive") candidate = shop.reduce(function(a,b){ return (a.origVal||a.val||0)>(b.origVal||b.val||0)?a:b; });
    else candidate = pick(shop);
    if (!candidate) return null;

    var cost  = candidate.origVal || candidate.val || 0;
    var floor = getFloor(traits);
    var canAfford;
    switch (traits.buyingAggression) {
      case "safe":       canAfford = (bot.money - cost) > 1; break;
      case "competitive":canAfford = (bot.money - cost) >= Math.ceil(cost * 0.5); break;
      default:           canAfford = (bot.money - cost) >= floor; break;
    }
    if (!canAfford) return null;
    return { type:"BUY_SHOP", p:{ assetId:candidate.id } };
  }

  // ── Turn action decision ─────────────────────────────────

  function decideNextAction(st, bot, traits) {
    var money  = bot.money;
    var hand   = bot.hand || [];
    var taken  = st.actionsTaken || [];
    var left   = st.actionsLeft || 0;
    var bpFree = bot.assets.some(function(a){ return a.hook==="p_blueprint"&&!a.disabled; }) && !taken.includes(ACT.ACTION);
    var canAct = left > 0 || bpFree;
    var canSell= left > 0 && !taken.includes(ACT.SELL);
    var canDraw= left > 0 && !taken.includes(ACT.DRAW);
    var acts   = hand.filter(function(c) {
      if (c.type !== CT.ACTION) return false;
      // Pre-check conditions that would cause handlePlayCard to reject the card silently,
      // leaving the action un-consumed and triggering the retry loop.
      if (c.hook === "give_back") {
        var gb_max = Math.max.apply(null, st.players.map(function(p){ return p.money; }));
        if (bot.money >= gb_max) return false;
      }
      if (c.hook === "outside_hire") {
        if (bot.money < 0) return false;
        var oh_ok = st.players.some(function(p){ return p.id !== bot.id && p.assets.length >= 2; });
        if (!oh_ok) return false;
      }
      if (c.hook === "market_research" || c.hook === "water_damage" || c.hook === "cost_of_business") {
        if (hand.filter(function(h){ return h.id !== c.id; }).length === 0) return false;
      }
      if (c.hook === "valuation") {
        var vd = (st.assetDeck||[]).length + (st.assetDiscard||[]).length;
        if (vd === 0) return false;
      }
      if (c.hook === "refresh") {
        var AS_ONCE = window.SB_EXPORTS && window.SB_EXPORTS.AS && window.SB_EXPORTS.AS.ONCE;
        var usedAssets = bot.assets.filter(function(a){ return a.status === "USED" && a.sub !== AS_ONCE && !a.disabled; });
        if (!usedAssets.length) return false;
      }
      return true;
    });

    if (traits.turnEndThreshold === "safe" && Math.random() < 0.15) return { type:"END_TURN" };
    if (!canAct && !canSell && !canDraw) return { type:"END_TURN" };

    switch (traits.actionOptions) {

      case "seller": {
        if (money < 0 && canSell && hand.length) return { type:"SELL", p:{ card:highestCard(hand), cardId:highestCard(hand).id } };
        if (canDraw && hand.length < 3) return { type:"DRAW" };
        if (canDraw && !hand.length) return { type:"DRAW" };
        if (canAct && acts.length) { var c=selectCard(acts,traits.cardPreference); return { type:"PLAY_ACTION", p:{ card:c, cardId:c.id } }; }
        if (canDraw) return { type:"DRAW" };
        return { type:"END_TURN" };
      }

      case "risk_taker": {
        if (money <= -3 && canSell) {
          var rt = hand.filter(function(c){ return c.value>=4; });
          if (rt.length) return { type:"SELL", p:{ card:rt[0], cardId:rt[0].id } };
        }
        if (money >= -2 && canAct && acts.length) { var rc=selectCard(acts,traits.cardPreference); return { type:"PLAY_ACTION", p:{ card:rc, cardId:rc.id } }; }
        if (canDraw && !hand.length) return { type:"DRAW" };
        return { type:"END_TURN" };
      }

      case "balanced": {
        if (hand.length >= 5) {
          if (Math.random() < 0.5 && canSell) { var bsh=highestCard(hand); if(bsh) return { type:"SELL", p:{ card:bsh, cardId:bsh.id } }; }
          if (canAct && acts.length) { var bph=selectCard(acts,traits.cardPreference); return { type:"PLAY_ACTION", p:{ card:bph, cardId:bph.id } }; }
        }
        var bopts = [];
        if (canDraw) bopts.push("draw");
        if (canSell && hand.length) bopts.push("sell");
        if (canAct && acts.length) bopts.push("play");
        if (!bopts.length) return { type:"END_TURN" };
        if (money < 0) {
          bopts = bopts.filter(function(o){ return o!=="draw"; });
          if (!bopts.length) return canDraw ? { type:"DRAW" } : { type:"END_TURN" };
        }
        var bc = pick(bopts);
        if (bc === "draw") return { type:"DRAW" };
        if (bc === "sell") { var bs=highestCard(hand); return bs?{ type:"SELL", p:{ card:bs, cardId:bs.id } }:{ type:"DRAW" }; }
        var bp=selectCard(acts,traits.cardPreference); return bp?{ type:"PLAY_ACTION", p:{ card:bp, cardId:bp.id } }:{ type:"DRAW" };
      }

      default: // competitive
      {
        if (hand.length <= 1 && canDraw) return { type:"DRAW" };
        if (money < 0) {
          if (canSell && Math.random() < 0.30 && hand.length) { var cs=highestCard(hand); return { type:"SELL", p:{ card:cs, cardId:cs.id } }; }
          if (canAct && acts.length) { var ca=acts.reduce(function(a,b){ return a.value>b.value?a:b; }); return { type:"PLAY_ACTION", p:{ card:ca, cardId:ca.id } }; }
          if (canSell && hand.length) { var cs2=highestCard(hand); return { type:"SELL", p:{ card:cs2, cardId:cs2.id } }; }
          if (canDraw) return { type:"DRAW" };
          return { type:"END_TURN" };
        }
        var cr = Math.random();
        if (cr < 0.55 && canAct && acts.length) { var cp=selectCard(acts,traits.cardPreference); return { type:"PLAY_ACTION", p:{ card:cp, cardId:cp.id } }; }
        if (cr < 0.80 && canSell && hand.length) { var css=highestCard(hand); return { type:"SELL", p:{ card:css, cardId:css.id } }; }
        if (canDraw) return { type:"DRAW" };
        return { type:"END_TURN" };
      }
    }
  }

  // ── Action Plan card selection helper ────────────────────

  function chooseActionPlanCard(bot, asset, traits) {
    var hand   = bot.hand || [];
    var actCards = hand.filter(function(c){ return c.type === CT.ACTION; });
    if (!actCards.length) return null;
    var stored = asset.lockedCard;
    var pref   = traits.cardPreference;

    if (pref === "safe") return pick(actCards);
    if (pref === "competitive") {
      var low3 = actCards.filter(function(c){ return c.value <= 3; });
      return low3.length ? pick(low3) : pick(actCards);
    }
    // aggressive
    var low2 = actCards.filter(function(c){ return c.value <= 2; });
    if (low2.length) return pick(low2);
    return lowestCard(actCards);
  }

  // ── Main bot turn controller ─────────────────────────────

  var _botActive = null;

  function botTakeTurn(st, dispatch, botId) {
    if (_botActive) return;
    _botActive = botId;

    function getState() { return window.SB_AI._lastState; }

    function wait(ms) {
      return new Promise(function(r){ setTimeout(r, ms || rand(2000,4000)); });
    }

    // Wait until state is unblocked (no pendingChoice/reactionWindow)
    function waitUnblocked(maxMs) {
      var started = Date.now();
      return new Promise(function(r){
        function check() {
          var ns = getState();
          if (!ns) { r(); return; }
          var blocked = ns.pendingChoice || ns.reactionWindow || ns.pendingPWChoice;
          if (!blocked) { r(); return; }
          // Choices can require human input — give them up to 5 minutes before giving up.
          // Reaction windows have their own timers so the default 15s is fine for those.
          var effectiveMax = (ns.pendingChoice || ns.pendingPWChoice) ? 300000 : (maxMs || 15000);
          if (Date.now()-started > effectiveMax) { r(); return; }
          setTimeout(check, 300);
        }
        check();
      });
    }

    // Execute a step: wait random delay, wait for unblocked, then run fn
    function step(fn) {
      return wait().then(function(){ return waitUnblocked(); }).then(function(){
        var ns = getState();
        if (!ns || ns.gameOver) return false;
        return fn(ns);
      });
    }

    var turnDone = false;

    (function run() {
      (async function() {
        try {
          var ns, bot, traits;

          // ── 1. Extra Shift first ──────────────────────────
          await step(function(ns0) {
            bot = ns0.players.find(function(p){ return p.id===botId; });
            if (!bot) return;
            var es = bot.assets.find(function(a){ return a.hook==="a_extra_shift" && a.status==="READY" && !a.disabled; });
            if (es) dispatch({ type:"ACTIVATE", p:{ assetId:es.id, ownerId:botId, _skipAnimation:true } });
          });

          // ── 2. Other DURING/ONCE assets (not Multi-Tool) ──
          for (var i = 0; i < 12; i++) {
            var activated = false;
            await step(function(ns1) {
              bot = ns1.players.find(function(p){ return p.id===botId; });
              if (!bot || ns1.phase !== PH.BP) return;
              traits = getTraits(ns1, bot);
              if (!shouldActivate(traits)) return;

              var toAct = bot.assets.find(function(a) {
                return a.hook !== "a_extra_shift" && a.hook !== "a_multi_tool" &&
                       (a.sub === AS.DURING || a.sub === AS.ONCE) &&
                       shouldActivateAsset(ns1, bot, a, traits);
              });
              if (toAct) {
                activated = true;
                // Action Plan: store a card if no card stored yet
                if (toAct.hook === "a_action_plan" && !toAct.lockedCard) {
                  // Activate triggers AP_ACTIVATE_SELECT or similar - handled by useEffect
                }
                dispatch({ type:"ACTIVATE", p:{ assetId:toAct.id, ownerId:botId, _skipAnimation:true } });
              }
            });
            if (!activated) break;
            await waitUnblocked(10000);
          }

          // ── 3. Multi-Tool (last) ──────────────────────────
          await step(function(ns2) {
            bot = ns2.players.find(function(p){ return p.id===botId; });
            if (!bot || ns2.phase !== PH.BP) return;
            traits = getTraits(ns2, bot);
            var mt = bot.assets.find(function(a){ return a.hook==="a_multi_tool" && a.status==="READY" && !a.disabled; });
            if (mt && shouldActivateAsset(ns2, bot, mt, traits)) {
              dispatch({ type:"ACTIVATE", p:{ assetId:mt.id, ownerId:botId, _skipAnimation:true } });
            }
          });
          await waitUnblocked(10000);

          // ── 4. Buy asset ──────────────────────────────────
          await step(function(ns3) {
            bot = ns3.players.find(function(p){ return p.id===botId; });
            if (!bot || ns3.phase !== PH.BP || bot.hasBoughtAsset) return;
            traits = getTraits(ns3, bot);
            var buy = chooseToBuy(ns3, bot, traits);
            if (buy) {
              if (buy.type === "BUY_SHOP") dispatch({ type:"BUY_SHOP", p:buy.p });
              else dispatch({ type:"BUY_DECK" });
            }
          });

          // ── 5. Play actions ───────────────────────────────
          for (var j = 0; j < 12; j++) {
            var tookAction = false;
            var endedTurn  = false;

            await step(function(ns4) {
              bot = ns4.players.find(function(p){ return p.id===botId; });
              if (!bot || ns4.phase !== PH.BP || ns4.gameOver) return;
              if (ns4.players[ns4.curIdx].id !== botId) { turnDone = true; return; }
              traits = getTraits(ns4, bot);
              var action = decideNextAction(ns4, bot, traits);

              if (action.type === "END_TURN") {
                endedTurn = true;
                return; // dispatch happens below with a natural pause
              }
              tookAction = true;

              if (action.type === "DRAW") {
                dispatch({ type:"DRAW" });
              } else if (action.type === "SELL") {
                dispatch({ type:"SELL", p:{ cardId:action.p.cardId } });
              } else if (action.type === "PLAY_ACTION") {
                var card = action.p.card;
                if (card && card.target === "opponent") {
                  var tgt = pickTarget(ns4, bot, traits);
                  if (tgt) dispatch({ type:"PLAY_ACTION", p:{ cardId:card.id, targetId:tgt.id, _skipAnimation:true } });
                  else dispatch({ type:"END_TURN" });
                } else {
                  dispatch({ type:"PLAY_ACTION", p:{ cardId:action.p.cardId, _skipAnimation:true } });
                }
              }
            });

            await waitUnblocked(15000);

            if (endedTurn) {
              // Natural pause before ending the turn so it doesn't feel abrupt
              await wait(3000);
              var preEndSt = getState();
              if (preEndSt && !preEndSt.gameOver && preEndSt.phase === PH.BP &&
                  preEndSt.players[preEndSt.curIdx] && preEndSt.players[preEndSt.curIdx].id === botId) {
                dispatch({ type:"END_TURN" });
              }
              break;
            }

            var curSt = getState();
            if (turnDone || !curSt || curSt.gameOver) break;
            if (!curSt.players[curSt.curIdx] || curSt.players[curSt.curIdx].id !== botId) break;
            if (curSt.phase !== PH.BP) break;
            if (!tookAction) break;
          }

          // ── Release lock before safety wait ──────────────
          // The bot's real work is done. Clearing _botActive now lets the next bot's
          // turn start without waiting for this 1.5s safety buffer to expire.
          // (Race: advanceTurn fires ~400ms after END_TURN, which is inside this window.)
          _botActive = null;

          // ── Safety END_TURN if still in bot's BP ─────────
          await wait(1500);
          var finalSt = getState();
          if (finalSt && !finalSt.gameOver && finalSt.phase === PH.BP &&
              finalSt.players[finalSt.curIdx] && finalSt.players[finalSt.curIdx].id === botId) {
            dispatch({ type:"END_TURN" });
          }

        } catch(e) {
          console.error("[AI] turn error:", e);
        } finally {
          _botActive = null;
        }
      })();
    })();
  }

  // ── Reaction window handler ──────────────────────────────

  var _reactHandled = {};

  function botHandleReaction(st, dispatch) {
    var rw = st.reactionWindow;
    if (!rw) return;
    if (st.playAnimation) return; // card animation in progress — wait for it to finish before reacting

    rw.reactors.forEach(function(r) {
      if (r.decision !== "PENDING") return;
      var bot = st.players.find(function(p){ return p.id === r.pid && p.isBot; });
      if (!bot) return;

      // Deduplicate using window id + reactor pid
      var key = (rw.id || rw.tid || "") + "_" + r.pid;
      if (_reactHandled[key]) return;
      _reactHandled[key] = true;

      var traits = getTraits(st, bot);
      var isChain = (st.reactionStack && st.reactionStack.length > 0);

      var prob;
      if (!r.canReact) {
        prob = 0; // auto-pass
      } else if (isChain) {
        prob = { safe:0.20, competitive:0.50, aggressive:0.95 }[traits.reactionChain] || 0.50;
      } else {
        prob = { safe:0.50, competitive:0.70, aggressive:0.95 }[traits.reactionLikelihood] || 0.70;
      }

      setTimeout(function() {
        delete _reactHandled[key];
        if (!r.canReact || Math.random() > prob) {
          dispatch({ type:"ENG_PASS", pid:r.pid });
          return;
        }
        // Choose a reaction card
        var cards = r.cards || [];
        var card = selectCard(cards, traits.cardPreference);
        if (card) {
          dispatch({ type:"ENG_REACT", pid:r.pid, cardId:card.id });
          return;
        }
        // Check anytime assets (Paid Work, Worth The Price)
        var anytime = bot.assets.find(function(a){
          return (a.hook==="a_paid_work" || a.hook==="a_wtp") && !a.disabled && a.status==="READY";
        });
        if (anytime) {
          dispatch({ type:"ENG_REACT", pid:r.pid, assetId:anytime.id });
          return;
        }
        dispatch({ type:"ENG_PASS", pid:r.pid });
      }, rand(1000, 3000));
    });
  }

  // ── Paid Work (pendingPWChoice) handler ──────────────────

  function botHandlePWChoice(st, dispatch) {
    var pw = st.pendingPWChoice;
    if (!pw) return;
    var bot = st.players.find(function(p){ return p.id===pw.srcPlayer && p.isBot; });
    if (!bot) return;

    var affected = pw.affectedPlayer || pw.pid;
    var isSelf  = affected === bot.id;
    var isGain  = pw.isGain;
    var amount  = Math.abs(pw.amount || 0);

    var prob = 0;
    if (isSelf && isGain)   prob = 0.80;
    else if (isSelf && !isGain) prob = amount <= 1 ? 0 : 0.85;
    else if (!isSelf && isGain) prob = 0.25;
    else                    prob = 0.50;

    setTimeout(function() {
      dispatch({ type:"RESOLVE_PW_CHOICE", id: Math.random() < prob ? "activate" : "pass" });
    }, rand(800, 2000));
  }

  // ── General pending choice handler ───────────────────────

  var _pcHandled = {};

  // Choice types where tgtPlayer is the decision-maker (not srcPlayer)
  var TGT_MAKES_DECISION = [
    "HELP_WANTED_GIVE","SHRINKAGE_SELECT","LET_GO_SELECT","NEG_REACTION_SELECT",
    "POCKET_CHANGE_SELECT","KICKSTARTER_CHOICE","REFRESH_ASSET","GIVE_BACK_TIE",
    "OUTSIDE_HIRE_SELECT","UPGRADE_DISCARD","UPGRADE_GAIN","BTB_SELECT",
    "OPPONENT_DISCARD","CLIENT_THEFT",
  ];

  function botHandlePendingChoice(st, dispatch, botId) {
    var pc = st.pendingChoice;
    if (!pc) return;

    // Determine which player should be making this choice
    var deciderId = (TGT_MAKES_DECISION.indexOf(pc.type) !== -1)
      ? (pc.tgtPlayer || pc.srcPlayer)
      : (pc.srcPlayer || pc.tgtPlayer);

    // Only handle if this bot is the decision-maker
    if (deciderId !== botId) return;

    var key = pc.type + "_" + (pc.srcPlayer||"") + "_" + (pc.assetId||"") + "_" + (pc.timerEnd||Date.now());
    if (_pcHandled[key]) return;
    _pcHandled[key] = true;

    setTimeout(function() {
      delete _pcHandled[key];
      var ns = window.SB_AI._lastState;
      if (!ns || !ns.pendingChoice || ns.pendingChoice.type !== pc.type) return;

      var result = resolvePendingChoice(ns, ns.pendingChoice, botId);
      if (result) {
        dispatch({ type:"RESOLVE_CHOICE", id:result.id, order:result.order });
      } else {
        dispatch({ type:"DISMISS_CHOICE" });
      }
    }, rand(900, 2500));
  }

  // ── Public API ───────────────────────────────────────────

  window.SB_AI = {
    _lastState:  null,
    _botActive:  null,
    assignBotTraits:       assignBotTraits,
    botTakeTurn:           botTakeTurn,
    botHandleReaction:     botHandleReaction,
    botHandlePWChoice:     botHandlePWChoice,
    botHandlePendingChoice:botHandlePendingChoice,
  };

  if (window.SB_EXPORTS) window.SB_EXPORTS.AI = window.SB_AI;

  console.log("[ai] Strictly Business AI loaded.");
})();
