/* ============================================================
   GAMERUNTIME — the single interpreter that runs ANY GameDef
   (gamedef.js) across the three screens. It owns NO UI; it writes
   through the existing Game.engine* actions (store.jsx), so it
   rides the same MQTT/RTC/BC sync as everything else — no new
   transport. The screens (engine-views.jsx) just render the synced
   engine state for the current phase.

   Only the ENGINE AUTHORITY (store._isEngineAuthority — Main, else
   host, else the lowest-clientId player) drives timers, auto-advance
   and win checks, so two devices can never double-advance (same
   guarantee as the Beat-Gate single-authority fix). Everyone else
   simply renders engine.phaseId.

   This file holds the reusable rule library (setup directives, exit
   kinds, condition predicates). Built-in games (src/games/*.js) are
   pure data that reference these by name — that's the whole point.
   ============================================================ */
(function(){
  'use strict';
  function S(){ return window.store.getState(); }
  function eng(){ return S().engine; }
  function def(){ const e = eng(); return e && e.def; }
  function players(){ return (S().players || []).filter(function(p){ return !p.spectator; }); }
  function phaseById(id){ const d = def(); return d && (d.phases || []).find(function(p){ return p.id === id; }); }
  function curPhase(){ return phaseById(eng().phaseId); }
  function authority(){ try { return window.store._isEngineAuthority(); } catch(_){ return false; } }
  function shuffle(a){ a = a.slice(); for(var i=a.length-1;i>0;i--){ var j=Math.floor(Math.random()*(i+1)); var t=a[i]; a[i]=a[j]; a[j]=t; } return a; }

  /* ---------- SETUP: picks + hidden roles + private deal ---------- */
  // Interprets def.setup into { vars, deal }. `vars` are shared picks (e.g. the
  // chosen location); `deal[pid]` is the PRIVATE payload for one phone.
  function runSetup(d){
    var vars = {};
    (d.setup.pick || []).forEach(function(pk){
      var table = (d.setup.tables && d.setup.tables[pk.from]) || [];
      if(table.length) vars[pk.id] = table[Math.floor(Math.random() * table.length)];
    });
    var deal = {};
    var ar = d.setup.assignRoles;
    var pids = players().map(function(p){ return p.id; });
    if(ar && pids.length){
      var bag = shuffle(pids), idx = 0, assigned = {};
      (ar.pool || []).forEach(function(slot){
        for(var k = 0; k < (slot.n || 1) && idx < bag.length; k++){ assigned[bag[idx++]] = slot.id; }
      });
      pids.forEach(function(pid){
        var roleId = assigned[pid] || ar.default;
        var payload = window.GameDef.resolvePayload(d.setup.deal[roleId] || {}, vars);
        deal[pid] = Object.assign({ role: roleId }, payload);
      });
    }
    return { vars: vars, deal: deal };
  }

  /* ---------- CONDITION PREDICATES (win rules & condition exits) ----------
     Declarative `when` objects in a GameDef resolve here. Composable via
     and/or/not so games stay pure data. Add kinds as new games need them. */
  function evalCond(cond){
    if(!cond) return false;
    if(typeof cond === 'string') cond = { kind: cond };
    var e = eng(), v = e.vars || {}, vote = S().vote || {};
    switch(cond.kind){
      case 'always': return true;
      case 'flag':      return !!v[cond.key];
      case 'varSet':    return v[cond.key] != null;
      case 'varEquals': return v[cond.key] === window.GameDef.resolveToken(cond.value, v);
      case 'voteClosed': return vote.status === 'closed' && !!vote.result;
      case 'voteResultRole': {                       // the accused's hidden role
        if(vote.status !== 'closed' || !vote.result) return false;
        var dl = e.deal[vote.result];
        var match = !!dl && dl.role === cond.role;
        return cond.is === false ? !match : match;
      }
      case 'and': return (cond.all || []).every(evalCond);
      case 'or':  return (cond.any || []).some(evalCond);
      case 'not': return !evalCond(cond.cond);
      default: return false;
    }
  }

  /* ---------- BLOCK LAUNCH — start a phase's block via Game.* ----------
     Increment 1 implements `vote`; the existing mini-games already have
     Game.* launchers (QuestRun) and slot in here in later milestones. */
  function launchBlock(phase){
    var b = phase.block; if(!b) return;
    if(b.type === 'vote'){
      var opts = (b.options === 'players')
        ? players().map(function(p){ return { id: p.id, label: (p.name||'Player').split(' ')[0] }; })
        : (b.options || []);
      window.Game.pushVote(b.prompt || 'Cast your vote', opts, { secret: b.secret !== false });
    }
    // other block types are wired in M2+ (gate/circuit/etc. via QuestRun map).
  }

  /* ---------- LIFECYCLE ---------- */
  var RT = {};

  RT.start = function(rawDef){
    var d = window.GameDef.normalize(rawDef);
    var check = window.GameDef.validate(d);
    if(!check.ok){ console.warn('[GameRuntime] invalid GameDef:', check.errors); return { ok:false, errors:check.errors }; }
    var s = runSetup(d);
    window.Game.engineStart(d, { deal: s.deal, vars: s.vars, resources: {} });
    RT.enterPhase(d.startPhaseId);
    return { ok:true };
  };

  RT.enterPhase = function(phaseId){
    var phase = phaseById(phaseId); if(!phase) return;
    window.Game.engineEnterPhase(phaseId);
    launchBlock(phase);
    // a freshly-entered terminal phase may already satisfy a win rule
    RT.checkWins();
  };

  // apply an ExitRule: optional vars patch, then move on.
  RT.fireExit = function(rule){
    if(!rule) return;
    if(rule.set) Object.keys(rule.set).forEach(function(k){ window.Game.engineSetVar(k, rule.set[k]); });
    if(rule.toPhaseId) RT.enterPhase(rule.toPhaseId);
  };

  // first satisfied win rule ends the game (idempotent — only ends once).
  RT.checkWins = function(){
    var e = eng(); if(!e.active || e.result) return false;
    var d = e.def, wins = (d && d.winConditions) || [];
    for(var i = 0; i < wins.length; i++){
      if(evalCond(wins[i].when)){
        window.Game.engineEnd(wins[i].outcome || { winner:'', text:'Game over' });
        if(d.endPhaseId && e.phaseId !== d.endPhaseId) RT.enterPhase(d.endPhaseId);
        return true;
      }
    }
    return false;
  };

  RT.stop = function(){ try{ window.Game.closeVoteStage && window.Game.closeVoteStage(); }catch(_){}; window.Game.engineReset(); };

  /* ---------- DRIVE (authority only) ----------
     Called ~4/s by the EngineDriver mounted on every screen; the guard means
     only ONE device actually advances. Handles phase timers, vote auto-close,
     condition/auto exits, and continuous win checks. */
  RT.tick = function(){
    var e = eng(); if(!e.active || e.result) return;
    if(!authority()) return;
    var phase = curPhase(); if(!phase) return;

    // vote blocks with no moderator: auto-close once everyone has voted
    if(phase.block && phase.block.type === 'vote'){
      var vote = S().vote;
      if(vote.open && vote.status === 'live'){
        var ballots = Object.keys(vote.ballots || {}).length;
        if(ballots >= players().length && players().length > 0){ window.Game.closeVote(); }
      }
    }

    // phase timer
    if(phase.duration){
      var elapsed = (Date.now() - (e.phaseStartedAt || 0)) / 1000;
      if(elapsed >= phase.duration){
        var timed = (phase.next || []).find(function(x){ return x.when && x.when.kind === 'timer'; });
        if(timed){ RT.fireExit(timed); return; }
      }
    }
    // condition / auto exits
    var cexit = (phase.next || []).find(function(x){
      return x.when && (x.when.kind === 'auto' || (x.when.kind === 'condition' && evalCond(x.when.cond)));
    });
    if(cexit){ RT.fireExit(cexit); return; }

    RT.checkWins();
  };

  // transient events (vote close, a player's choice, a dice roll) → exits.
  RT.onEvent = function(ev){
    var e = eng(); if(!e.active || e.result || !ev) return;
    if(!authority()) return;
    var phase = curPhase(); if(!phase) return;
    var next = phase.next || [], rule = null;
    if(ev.type === 'vote-close'){
      rule = next.find(function(x){ return x.when && x.when.kind === 'vote'; });
    } else if(ev.type === 'engine-choice'){
      rule = next.find(function(x){ return x.when && x.when.kind === 'choice' && x.when.value === ev.value; });
    } else if(ev.type === 'roll' && ev.dice){
      var total = ev.dice.total;
      rule = next.find(function(x){ return x.when && x.when.kind === 'dice' && total >= (x.when.min||0) && total <= (x.when.max||99); });
    }
    if(rule){ RT.fireExit(rule); }
    RT.checkWins();
  };

  /* ---------- player-facing helpers (called from engine-views.jsx) ---------- */
  // a player asks the table to vote (questions → accuse, a `choice` exit).
  RT.choose = function(value){ window.store.dispatch({ type:'engine-choice', value: value }); };
  // the spy commits a location guess; win rules resolve it.
  RT.setVar = function(k, v){ window.Game.engineSetVar(k, v); };

  // utility for views: the local player's private payload (never read others').
  RT.dealFor = function(pid){ return (eng().deal || {})[pid] || null; };

  window.GameRuntime = RT;
})();
