import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.*;
import java.util.*;

public class LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger {
  enum A { HIT, STAND, DOUBLE, SPLIT }
  enum State { NORMAL, COOLING, CAUTIOUS, BRAKE }
  static class Card {
    final String rank, suit;
    final int value;
    Card(String rank,String suit,int value){this.rank=rank;this.suit=suit;this.value=value;}
    public String toString(){return rank+(suit.isEmpty()?"":suit);}
  }
  static final Scanner sc=new Scanner(System.in);
  static final List<String> log=new ArrayList<>();
  static final double PLATFORM_OFFSET=2400.0;
  static final double PLATFORM_START=2500.0;
  static final double PLATFORM_DEPLETED=2400.0;
  static double bank=PLATFORM_START, peak=PLATFORM_START, trough=PLATFORM_START, maxDD=0, exposure=0;
  static boolean reached130=false;
  static String sessionName="";
  static State state=State.NORMAL;
  static int w=0,l=0,p=0, wagerMatch=0,wagerAbove=0,wagerBelow=0, actionMatch=0,actionDiff=0;
  static int validationWarnings=0, shuffleEvents=0, cardsSinceShuffle=0, totalCardsObserved=0, amendmentsMade=0;
  static int textbookAlignedDecisions=0, frozenResearchOverrideDecisions=0, frozenPrimarySpecialDecisions=0;
  static final List<String> shuffleLog=new ArrayList<>();
  static final List<String> chronologyLog=new ArrayList<>();
  static final List<Card> currentPlayerPostDealCards=new ArrayList<>();
  static final List<Card> currentDealerCards=new ArrayList<>();
  static int chronologicalCardNumber=0;
  static boolean handWasSplit=false;
  static double handCommittedStake=0.0;
  static boolean personalMode=false;
  static int lowSinceShuffle=0, neutralSinceShuffle=0, highSinceShuffle=0;
  static final List<String> handSignatureLog=new ArrayList<>();
  static final Path CUMULATIVE_OUTPUT=Path.of("output.txt");
  static String currentInitialAction="NONE";
  static String lastSavedSessionId=null;
  static Path lastSavedReport=null;
  static final int DEFAULT_SHUFFLE_REPLICATES=5000;
  static final long SHUFFLE_SEED_BASE=202609050001L;

  static class HandSnapshot {
    double bank,peak,trough,maxDD,exposure; boolean reached130; State state;
    int w,l,p,wagerMatch,wagerAbove,wagerBelow,actionMatch,actionDiff;
    int validationWarnings,shuffleEvents,cardsSinceShuffle,totalCardsObserved;
    int textbookAlignedDecisions,frozenResearchOverrideDecisions,frozenPrimarySpecialDecisions;
    int logSize,shuffleLogSize,chronologySize,handSignatureLogSize,chronologicalCardNumber; int lowSinceShuffle,neutralSinceShuffle,highSinceShuffle; Map<String,Integer> counts;
    HandSnapshot(){
      this.bank=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank;
      this.peak=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak;
      this.trough=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough;
      this.maxDD=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD;
      this.exposure=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure;
      this.reached130=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130;
      this.state=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state;
      this.w=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w;
      this.l=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l;
      this.p=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p;
      this.wagerMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch;
      this.wagerAbove=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove;
      this.wagerBelow=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow;
      this.actionMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch;
      this.actionDiff=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff;
      this.validationWarnings=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings;
      this.shuffleEvents=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents;
      this.cardsSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle;
      this.totalCardsObserved=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved;
      this.textbookAlignedDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions;
      this.frozenResearchOverrideDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions;
      this.frozenPrimarySpecialDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions;
      this.logSize=log.size(); this.shuffleLogSize=shuffleLog.size(); this.chronologySize=chronologyLog.size(); this.handSignatureLogSize=handSignatureLog.size(); this.lowSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle; this.neutralSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle; this.highSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle; this.chronologicalCardNumber=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber; this.counts=new LinkedHashMap<>(observedPhysicalCounts);
    }
    void restore(){
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank=bank; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak=peak; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough=trough; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD=maxDD; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure=exposure;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130=reached130; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state=state;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w=w; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l=l; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p=p;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch=wagerMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove=wagerAbove; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow=wagerBelow; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch=actionMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff=actionDiff;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings=validationWarnings; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents=shuffleEvents; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle=cardsSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved=totalCardsObserved;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions=textbookAlignedDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions=frozenResearchOverrideDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions=frozenPrimarySpecialDecisions;
      while(log.size()>logSize) log.remove(log.size()-1); while(shuffleLog.size()>shuffleLogSize) shuffleLog.remove(shuffleLog.size()-1); while(chronologyLog.size()>chronologySize) chronologyLog.remove(chronologyLog.size()-1); while(handSignatureLog.size()>handSignatureLogSize) handSignatureLog.remove(handSignatureLog.size()-1); LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle=lowSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle=neutralSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle=highSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber=chronologicalCardNumber;
      observedPhysicalCounts.clear(); observedPhysicalCounts.putAll(counts);
    }
  }

  public static void main(String[] z)throws Exception{
    System.out.println("DETERMINISTIC BLACKJACK - LIVE + POST-SESSION ANALYSIS");
    System.out.println("Self-contained workflow: L = live capture, S = local shuffle robustness, Q = exit.");
    System.out.println();
    System.out.println("PUBLICATION WORKFLOW REMINDER:");
    System.out.println();
    System.out.println("Provide ChatGPT with:");
    System.out.println("  1. output.txt");
    System.out.println("  2. Session X-1 plate");
    System.out.println();
    System.out.println("Then make this exact request:");
    System.out.println();
    System.out.println("\"Create the Session X plate (X = the latest session recorded in the attached output.txt) using the attached Session X-1 plate as the presentation template. Use output.txt as the authoritative evidence source. Once the Session X plate has been created, verify every reported data value on the completed plate against output.txt. Do not accept or mark the Session X plate COMPLETE until this verification has passed. If further modifications are required, audit the plate before and after each change and confirm that no unrelated data, wording, labels, or layout have changed. If making the requested modification would risk compromising the plate, provide only the specific segment that requires updating so it can be edited manually.\"");
    System.out.println();
    System.out.println("END OF PUBLICATION WORKFLOW REMINDER.");
    while(true){
      String operation=choice("\nMAIN MENU: [L] LIVE SESSION / [S] RUN SHUFFLE ROBUSTNESS / [Q] QUIT: ","LSQ");
      if(operation.equals("Q")){ System.out.println("Program closed."); return; }
      if(operation.equals("S")){ runAutomaticShuffleAnalysis(); continue; }
      if(hasPendingShuffleAnalysis()){
        System.out.println("\n*** POST-SESSION ANALYSIS PENDING ***");
        System.out.println("The most recently completed live session has not yet completed S analysis.");
        if(choice("Run S before starting another live session? [Y/N]: ","YN").equals("Y")){runAutomaticShuffleAnalysis();continue;}
        if(!choice("Defer the previous S analysis and start another live session anyway? [Y/N]: ","YN").equals("Y"))continue;
        appendAnalysisDeferral();
      }
      if(!confirmPreviousSessionPlateBeforeNewLive()) continue;
      runLiveSession();
    }
  }

  static void resetLiveSessionState(){
    log.clear();shuffleLog.clear();chronologyLog.clear();currentPlayerPostDealCards.clear();currentDealerCards.clear();handSignatureLog.clear();observedPhysicalCounts.clear();
    bank=PLATFORM_START;peak=PLATFORM_START;trough=PLATFORM_START;maxDD=0;exposure=0;reached130=false;state=State.NORMAL;w=l=p=wagerMatch=wagerAbove=wagerBelow=actionMatch=actionDiff=0;validationWarnings=shuffleEvents=cardsSinceShuffle=totalCardsObserved=amendmentsMade=0;textbookAlignedDecisions=frozenResearchOverrideDecisions=frozenPrimarySpecialDecisions=0;chronologicalCardNumber=0;handWasSplit=false;handCommittedStake=0;personalMode=false;lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;currentInitialAction="NONE";
  }

  static void runLiveSession()throws Exception{
    resetLiveSessionState();
    say("\nDETERMINISTIC BLACKJACK - LIVE 30-HAND COMPANION");
    say("Manual external-session entry | Platform start £2500 | £2400 = depleted | 30-hand reference");
    say("SOURCE: 247blackjack.com | PLAY-MONEY | configured 6-deck game");
    say("VALIDATION ON: suit required, wager checked, balance arithmetic checked where determinable.");
    say("HAND REVIEW ON: rejected/amended current hands roll back before re-entry.");
    personalMode=choice("Mode: [P] PERSONAL (no Frozen prompts) / [F] FROZEN architecture: ","PF").equals("P");
    while(sessionName.isBlank()){
      System.out.print("Session name / description (e.g. Make 3x Bankroll): ");
      sessionName=sc.nextLine().trim().replace("|","/").replace("\r"," ").replace("\n"," ");
      if(sessionName.length()>80) sessionName=sessionName.substring(0,80).trim();
      if(sessionName.isBlank()) say("Please enter a session name or short description.");
    }
    say("SESSION NAME: "+sessionName);
    say(personalMode ? "PERSONAL MODE: play entirely by your own judgement. Frozen wager/action prompts are suppressed." : "FROZEN MODE: existing Frozen wager/action prompts remain active.");
    say("Cumulative project file: "+CUMULATIVE_OUTPUT.toAbsolutePath()+"\n");
    for(int h=1;h<=30 && bank>PLATFORM_DEPLETED;h++){
      while(true){
      HandSnapshot snapshot=new HandSnapshot();
      updateState(); double ref=wager(h);
      say("\n========== HAND "+h+" / 30 ==========");
      String sh=choice("Visible shuffle immediately BEFORE this hand? [Y/N]: ","YN");
      if(sh.equals("Y")){
        shuffleEvents++;
        shuffleLog.add("Shuffle observed before hand "+h+" after "+cardsSinceShuffle+" recorded cards since previous start/shuffle.");
        say("SHUFFLE RECORDED before hand "+h+" | previous segment cards recorded: "+cardsSinceShuffle);
        cardsSinceShuffle=0; lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;
      }
      double researchBank=researchBankroll();
      if(personalMode){
        say("Bankroll £"+m(bank)+" | PERSONAL MODE");
        showPreWagerRecentHistory();
      } else {
        say("Bankroll £"+m(bank)+" | Research-equivalent available £"+m(researchBank)+" | State "+state+" | FROZEN WAGER £"+m(ref));
      }
      double actual=personalMode ? validatedPersonalWager() : validatedWager(ref);
      exposure+=actual;
      if(!personalMode) compareWager(actual,ref);
      double before=bank;
      handWasSplit=false; handCommittedStake=actual; currentInitialAction="NONE"; currentPlayerPostDealCards.clear(); currentDealerCards.clear();
      say("Initial visible deal entry: PLAYER first card -> DEALER upcard -> PLAYER second card.");
      Card p1=card("Player card 1");
      Card up=card("Dealer upcard");
      Card p2=card("Player card 2");
      List<Card> player=new ArrayList<>(); player.add(p1); player.add(p2);
      say("Player "+ct(player)+" = "+tt(player)+" | Dealer "+rt(up));
      showObservedHandContext(p1,p2,up);
      if(player.size()==2 && total(player)==21){ currentInitialAction="NATURAL"; say("Player 21/natural: no further player decision entered here."); }
      else play("MAIN",player,up,actual,true,true);
      String dealer=dealerSequence("Dealer hidden/draw sequence IN ORDER after upcard (e.g. 6H,10D,5C; or ?): ");
      showDealerTotal(up);

      // Show the reconstructed physical deal order BEFORE result/bankroll entry so the
      // operator can compare our interpretation with the game screen while the hand is fresh.
      showChronologyPreview(h,p1,up,p2);
      if(choice("Does this reconstructed card order correctly represent the hand? [Y/N]: ","YN").equals("N")){
        amendmentsMade++;
        snapshot.restore();
        say("CARD-STREAM REVIEW: hand "+h+" rejected before result/bankroll entry; re-entering the hand.");
        log.add("  CARD-STREAM REVIEW | Hand "+h+" rejected by operator; replacement entry follows.");
        continue;
      }

      String r=validatedResult(player,up); if(r.equals("W"))w++; else if(r.equals("L"))l++; else p++;
      boolean natural=(player.size()==2 && total(player)==21);
      say("BANKROLL CHECK: enter the updated platform balance shown immediately after the on-screen Win/Lose/Push verdict.");
      bank=validatedEndingBankroll(before,actual,handCommittedStake,r,natural,handWasSplit);

      String amend=choice("Hand "+h+" captured. Amend any details before moving on? [Y/N]: ","YN");
      if(amend.equals("Y")){
        amendmentsMade++;
        snapshot.restore();
        say("AMENDMENT: hand "+h+" superseded before commit and will now be re-entered from the beginning.");
        say("Previous hands remain committed. Current-hand counters/cards/exposure were rolled back.");
        log.add("  AMENDMENT AUDIT | Hand "+h+" superseded before commit; replacement entry follows.");
        continue;
      }

      appendChronology(h,p1,up,p2);
      String hist="HAND_HISTORY | "+signature(p1,p2,up)+" | mode "+(personalMode?"PERSONAL":"FROZEN")+" | action "+currentInitialAction+" | result "+r+" | bank "+m(before)+"->"+m(bank)+" | wager "+m(actual);
      handSignatureLog.add(hist);
      log.add("  "+hist);
      peak=Math.max(peak,bank); trough=Math.min(trough,bank); maxDD=Math.max(maxDD,peak-bank); if(bank>=2530) reached130=true;
      log.add(String.format(Locale.ROOT,"HAND %02d | %.2f->%.2f | ref %.2f actual %.2f | committed %.2f | %s | P %s | D-up %s | D-hidden/draw %s | %s",h,before,bank,ref,actual,handCommittedStake,state,ct(player),rt(up),dealer,r));
      say("Recorded | Peak £"+m(peak)+" | Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
      if(bank<=PLATFORM_DEPLETED){
        say("DEPLETION THRESHOLD REACHED: platform balance £"+m(bank)+" <= £2400.00.");
        say("Research-equivalent bankroll: £"+m(bank-PLATFORM_OFFSET)+". Session stops here.");
      }
      break; // current hand committed
      }
      if(h<30 && line("ENTER next hand, or Q to finish: ").equalsIgnoreCase("Q")) break;
    }
    save();
  }

  static void updateState(){
    if(state==State.BRAKE)return;
    if(state==State.COOLING){ if(bank>=2520)state=State.CAUTIOUS; return; }
    if(state==State.CAUTIOUS){ if(bank>=2540)state=State.NORMAL; return; }
    if(reached130 && bank<=2500)state=State.COOLING;
  }
  static double wager(int h){
    if(state!=State.NORMAL)return 15;
    if(h<=8 || bank<2530)return 15;
    if(bank<2550)return 20;
    if(bank<2570)return 30;
    return 15;
  }
  static void compareWager(double a,double r){
    if(Math.abs(a-r)<.001){wagerMatch++;say("Wager: MATCH reference");}
    else if(a>r){wagerAbove++;say("Wager: £"+m(a-r)+" ABOVE reference");}
    else {wagerBelow++;say("Wager: £"+m(r-a)+" BELOW reference");}
  }

  static double researchBankroll(){
    return Math.max(0.0, bank-PLATFORM_OFFSET);
  }

  static double uncommittedResearchBankroll(){
    return Math.max(0.0, researchBankroll()-handCommittedStake);
  }

  static boolean canCommitAdditional(double amount){
    return uncommittedResearchBankroll()+0.001 >= amount;
  }

  static void play(String label,List<Card> c,Card upCard,double stake,boolean first,boolean allowSplit){
    int up=upCard.value;
    while(true){
      if(total(c)>21){say(label+" BUST at "+total(c));return;}
      boolean pair=allowSplit && first && c.size()==2 && c.get(0).value==c.get(1).value;
      boolean canD=first && canCommitAdditional(stake);
      boolean canP=pair && canCommitAdditional(stake);

      A rec=null;
      if(!personalMode){
        rec=recommend(c,up,first,pair,canD,canP);
        String provenance=policyProvenance(c,up,first,pair,rec);
        say(label+" "+ct(c)+" = "+tt(c)+" | FROZEN POLICY: "+rec+" ["+provenance+"]");
        log.add("  POLICY SOURCE | "+label+" | "+ct(c)+" vs "+rt(upCard)+" | "+rec+" | "+provenance);
      } else {
        say(label+" "+ct(c)+" = "+tt(c)+" | PERSONAL DECISION");
      }

      if(first && !canD){
        say("DOUBLE unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }
      if(pair && !canP){
        say("SPLIT unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }

      String opts="HS"+(canD?"D":"")+(canP?"P":"");
      A act=parse(choice("Actual action ["+opts+"]: ",opts));
      if(label.equals("MAIN") && first && currentInitialAction.equals("NONE")) currentInitialAction=act.toString();
      if(!personalMode){
        if(act==rec)actionMatch++; else actionDiff++;
        log.add("  "+label+" | "+ct(c)+" | frozen "+rec+" | actual "+act);
      } else {
        log.add("  "+label+" | "+ct(c)+" | personal actual "+act);
      }

      if(act==A.STAND)return;
      if(act==A.DOUBLE){
        // Defensive guard as well as removing D from the allowed options.
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("DOUBLE REJECTED: insufficient research-equivalent bankroll after the £2400 platform offset.");
          continue;
        }
        exposure+=stake; handCommittedStake+=stake;
        c.add(card(label+" double card")); say(label+" after DOUBLE: "+ct(c)+" = "+tt(c)); return;
      }
      if(act==A.SPLIT){
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("SPLIT REJECTED: insufficient research-equivalent bankroll after the £2400 platform offset.");
          continue;
        }
        handWasSplit=true; exposure+=stake; handCommittedStake+=stake; Card x=c.get(0),y=c.get(1);
        List<Card>a=new ArrayList<>(List.of(x,card("Split A added card")));
        List<Card>b=new ArrayList<>(List.of(y,card("Split B added card")));
        say("Split A "+ct(a)+" = "+tt(a)); say("Split B "+ct(b)+" = "+tt(b));
        if(x.value==11&&y.value==11){say("Split Aces: one additional card each; stop.");return;}
        play("SPLIT A",a,upCard,stake,true,false); play("SPLIT B",b,upCard,stake,true,false); return;
      }
      c.add(card(label+" hit card")); first=false; allowSplit=false;
    }
  }

  static double validatedPersonalWager(){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The £2400 platform offset is protected and cannot be wagered in the research session.");
        continue;
      }
      return a;
    }
  }

  static String rankOnly(Card q){ return q.rank; }
  static int bucket(Card q){ return (q.value>=2&&q.value<=6)?-1:(q.value>=10?1:0); }
  static String signature(Card p1,Card p2,Card up){
    String a=rankOnly(p1), b=rankOnly(p2);
    if(a.compareTo(b)>0){String t=a;a=b;b=t;}
    return a+","+b+" vs "+rankOnly(up);
  }
  static int previousOccurrences(String sig){
    if(!Files.exists(CUMULATIVE_OUTPUT)) return 0;
    try{
      int n=0; String needle="HAND_SIGNATURE | "+sig;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)) if(x.trim().equals(needle)) n++;
      return n;
    }catch(Exception e){ validationWarnings++; return 0; }
  }
  static List<String> previousOutcomeLines(String sig){
    // HAND_HISTORY is intentionally present in more than one report section.
    // De-duplicate exact history records so one historical occurrence is shown once.
    LinkedHashSet<String> unique=new LinkedHashSet<>();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return new ArrayList<>();
    try{
      String needle="HAND_HISTORY | "+sig+" |";
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(t.startsWith(needle)) unique.add(t);
      }
    }catch(Exception e){ validationWarnings++; }
    return new ArrayList<>(unique);
  }

  static void showPreWagerRecentHistory(){
    boolean recentHandsRequested=choice("Before wagering, show previous hands played on this table/session? [Y/N]: ","YN").equals("Y");
    if(recentHandsRequested){
      int recentCount=Integer.parseInt(choice("How many recent hands? [1/2/3]: ","123"));
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | REQUESTED BEFORE WAGER | last "+recentCount);
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | Y | BEFORE_WAGER | requested "+recentCount);
      showRecentTableHands(recentCount);
    } else {
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | NOT REQUESTED BEFORE WAGER");
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | N | BEFORE_WAGER");
      say("RECENT TABLE-HAND HISTORY: not shown before wager.");
    }
  }

  static void showObservedHandContext(Card p1,Card p2,Card up){
    String sig=signature(p1,p2,up);
    int prev=previousOccurrences(sig);
    int cl=0,cn=0,ch=0;
    for(Card q:List.of(p1,p2,up)){int b=bucket(q);if(b<0)cl++;else if(b>0)ch++;else cn++;}
    int bl=lowSinceShuffle-cl, bn=neutralSinceShuffle-cn, bh=highSinceShuffle-ch;
    say("OBSERVED HAND (suits excluded): "+sig+" | appeared before in project output: "+prev+" time(s)");

    boolean sameHandRequested=false;

    if(personalMode){
      sameHandRequested=choice("View previous outcomes for this starting hand? [Y/N]: ","YN").equals("Y");
      log.add("  BEHAVIOURAL INFO REQUEST | matching starting-hand history | "+(sameHandRequested?"REQUESTED":"NOT REQUESTED")+" | signature "+sig);
      handSignatureLog.add("  HISTORY_REQUESTED | MATCHING_START_HAND | "+(sameHandRequested?"Y":"N")+" | signature "+sig);
      if(sameHandRequested) showMatchingHandHistory(sig);
      else say("MATCHING-HAND HISTORY: not shown.");
    } else {
      // Preserve the established Frozen-mode display. It is observational only and
      // never enters the deterministic policy calculation.
      showMatchingHandHistory(sig);
    }

    say("CARD MIX before current visible hand since last recorded shuffle: Low "+Math.max(0,bl)+" | Neutral "+Math.max(0,bn)+" | High "+Math.max(0,bh));
    say("CARD MIX current visible hand (P1 + P2 + dealer up): Low "+cl+" | Neutral "+cn+" | High "+ch);
    say("Low=2-6 | Neutral=7-9 | High=10/J/Q/K/A | observational only");
    handSignatureLog.add("HAND_SIGNATURE | "+sig);
    handSignatureLog.add("  PRIOR_OCCURRENCES | "+prev+" | BEFORE_LNH "+Math.max(0,bl)+"/"+Math.max(0,bn)+"/"+Math.max(0,bh)+" | CURRENT_VISIBLE_LNH "+cl+"/"+cn+"/"+ch);
  }

  static void showMatchingHandHistory(String sig){
    List<String> priorOutcomes=previousOutcomeLines(sig);
    say("CURRENT STARTING HAND | "+sig);
    if(priorOutcomes.isEmpty()){
      say("PREVIOUS MATCHES | none recorded");
      say("ALL PREVIOUS OCCURRENCES | TOTAL 0 | W 0 (0.0%) | L 0 (0.0%) | P 0 (0.0%)");
      handSignatureLog.add("  HISTORY_MATCHES_SHOWN | 0 | signature "+sig);
      return;
    }

    // Keep the console as restrained as the replay GUI: current state + at most
    // the three most recent matching historical occurrences. Aggregate statistics
    // below still use every eligible previous occurrence, not only the displayed three.
    int shown=Math.min(3,priorOutcomes.size());
    say("PREVIOUS MATCHES | showing most recent "+shown+" of "+priorOutcomes.size());
    for(int i=priorOutcomes.size()-shown;i<priorOutcomes.size();i++){
      String x=priorOutcomes.get(i);
      String detail=x.substring(("HAND_HISTORY | "+sig+" | ").length());
      say("  PREVIOUS "+(i-(priorOutcomes.size()-shown)+1)+" | "+detail);
    }

    int wHist=0,lHist=0,pHist=0;
    for(String x:priorOutcomes){
      if(x.contains(" | result W |")) wHist++;
      else if(x.contains(" | result L |")) lHist++;
      else if(x.contains(" | result P |")) pHist++;
    }
    int total=wHist+lHist+pHist;
    double wp=total==0?0.0:100.0*wHist/total;
    double lp=total==0?0.0:100.0*lHist/total;
    double pp=total==0?0.0:100.0*pHist/total;
    say(String.format(Locale.ROOT,
      "ALL PREVIOUS OCCURRENCES | TOTAL %d | W %d (%.1f%%) | L %d (%.1f%%) | P %d (%.1f%%)",
      total,wHist,wp,lHist,lp,pHist,pp));
    handSignatureLog.add("  HISTORY_MATCHES_SHOWN | "+shown+" | total eligible "+priorOutcomes.size()+" | signature "+sig);
  }

  static void showRecentTableHands(int requested){
    List<String> prior=new ArrayList<>();
    for(String x:handSignatureLog) if(x.startsWith("HAND_HISTORY | ")) prior.add(x);
    int n=Math.min(Math.min(requested,3),prior.size());
    if(n==0){
      say("RECENT TABLE HANDS | none yet in this session");
      handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | 0");
      return;
    }
    say("RECENT TABLE HANDS | showing "+n+" most recent from current session");
    int display=1;
    for(int i=prior.size()-n;i<prior.size();i++){
      String x=prior.get(i);
      String detail=x.startsWith("HAND_HISTORY | ")?x.substring("HAND_HISTORY | ".length()):x;
      say("  PREVIOUS "+(display++)+" | "+detail);
    }
    handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | "+n+" | requested "+requested);
  }

  static String policyProvenance(List<Card> c,int up,boolean first,boolean pair,A rec){
    if(first && pair && c.size()==2 && c.get(0).value==10 && c.get(1).value==10 && up==10 && rec==A.STAND){
      frozenResearchOverrideDecisions++;
      return "FROZEN RESEARCH OVERRIDE: prior SPLIT -> STAND; also TEXTBOOK";
    }
    if(first && total(c)==11 && !soft(c) && up==10 && rec==A.DOUBLE){
      frozenPrimarySpecialDecisions++;
      return "FROZEN PRIMARY: DOUBLE; TEXTBOOK-aligned";
    }
    textbookAlignedDecisions++;
    return "TEXTBOOK / FROZEN-ALIGNED";
  }

  static A recommend(List<Card>c,int up,boolean first,boolean pair,boolean canD,boolean canP){
    int t=total(c); if(t>=21)return A.STAND;
    if(pair){int q=c.get(0).value;
      if(q==10)return A.STAND;
      if(canP){
        if(q==11||q==8)return A.SPLIT;
        if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;
        if(q==7&&between(up,2,7))return A.SPLIT;
        if(q==6&&between(up,2,6))return A.SPLIT;
        if(q==4&&(up==5||up==6))return A.SPLIT;
        if((q==3||q==2)&&between(up,2,7))return A.SPLIT;
      }
      // If a split is unaffordable (or not called for), continue into the ordinary
      // hard/soft policy for the same cards instead of inventing a forced HIT.
    }
    if(soft(c)){
      if(t>=19)return A.STAND;
      if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE; if(up==2||up==7||up==8)return A.STAND; return A.HIT;}
      if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}
      if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}
      if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}
      return A.HIT;
    }
    if(t>=17)return A.STAND; if(t>=13)return between(up,2,6)?A.STAND:A.HIT; if(t==12)return between(up,4,6)?A.STAND:A.HIT;
    if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;
    if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;
    if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;
    return A.HIT;
  }

  static int total(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a-->0)t-=10;return t;}
  static boolean soft(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a>0){t-=10;a--;}return a>0;}
  static String tt(List<Card>c){return (soft(c)?"soft ":"")+total(c);}
  static String ct(List<Card>c){List<String>x=new ArrayList<>();for(Card q:c)x.add(q.toString());return String.join(",",x);}
  static String rt(Card q){return q.toString();}
  // Stable ASCII rendering for chronology/card-stream review.
  // Avoids console/font substitution of suit glyphs (e.g. ♠ appearing as ?).
  static String streamCard(Card q){
    String s=q.suit;
    String a=s.equals("♠")?"S":s.equals("♥")?"H":s.equals("♦")?"D":s.equals("♣")?"C":s.equals("?")?"U":s;
    return q.rank+a;
  }
  static boolean between(int x,int a,int b){return x>=a&&x<=b;} static boolean in(int x,int...v){for(int q:v)if(x==q)return true;return false;}
  static A parse(String s){return switch(s){case"H"->A.HIT;case"S"->A.STAND;case"D"->A.DOUBLE;default->A.SPLIT;};}
  static final Map<String,Integer> observedPhysicalCounts=new LinkedHashMap<>();

  static Card card(String p){
    while(true){
      String raw=line(p+": ").trim().toUpperCase(Locale.ROOT);
      Card q=parseCard(raw);
      if(q==null){ say("INVALID CARD. Use rank+suit such as 8H, KS, A♦, 10C, or 4U if suit genuinely unknown."); continue; }
      if(q.suit.isEmpty()){
        validationWarnings++;
        say("SUIT MISSING - entry rejected. Re-enter with S/H/D/C, ♠/♥/♦/♣, or U for explicitly unknown.");
        continue;
      }
      trackPhysicalCard(q);
      String lp=p.toLowerCase(Locale.ROOT);
      if(lp.contains("hit card") || lp.contains("double card") || lp.contains("split a added card") || lp.contains("split b added card")) currentPlayerPostDealCards.add(q);
      return q;
    }
  }
  static Card parseCard(String raw){
    String x=raw.trim().toUpperCase(Locale.ROOT).replace(" ",""), suit="";
    if(x.endsWith("♠")||x.endsWith("S")){suit="♠";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♥")||x.endsWith("H")){suit="♥";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♦")||x.endsWith("D")){suit="♦";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♣")||x.endsWith("C")){suit="♣";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("U")){suit="?";x=x.substring(0,x.length()-1);}
    int v;
    if(x.equals("A"))v=11; else if(x.matches("10|J|Q|K"))v=10;
    else {try{v=Integer.parseInt(x);}catch(Exception e){return null;}if(v<2||v>9)return null;}
    return new Card(x,suit,v);
  }
  static void trackPhysicalCard(Card q){
    if(q.suit.isEmpty()) return;
    totalCardsObserved++;
    cardsSinceShuffle++;
    if(!q.suit.equals("?")) observedPhysicalCounts.merge(q.rank+q.suit,1,Integer::sum);
  }
  static String dealerSequence(String prompt){
    currentDealerCards.clear();
    while(true){
      String raw=line(prompt).trim(); 
      if(raw.equals("?")){validationWarnings++; say("Dealer sequence explicitly recorded as UNKNOWN."); return "?";}
      String[] a=raw.split(","); List<String> out=new ArrayList<>(); boolean ok=true;
      for(String x:a){
        Card q=parseCard(x);
        if(q==null || q.suit.isEmpty()){ok=false;break;}
        trackPhysicalCard(q); currentDealerCards.add(q); out.add(q.toString());
      }
      if(ok&&!out.isEmpty())return String.join(",",out);
      validationWarnings++;
      say("VALIDATION FAILED. Every dealer card needs a suit (or U), e.g. 6H,10D,5C.");
    }
  }


  static void showDealerTotal(Card up){
    if(currentDealerCards.isEmpty()){
      say("Dealer total: UNKNOWN (dealer hidden/draw sequence not fully recorded)");
      return;
    }
    List<Card> full=new ArrayList<>();
    full.add(up);
    full.addAll(currentDealerCards);
    int t=total(full);
    String status=t>21 ? " BUST" : (t==21 ? " 21" : "");
    say("Dealer "+ct(full)+" = "+tt(full)+status);
  }

  static String validatedResult(List<Card> player, Card up){
    while(true){
      String entered=choice("Result W/L/P: ","WLP");

      // Split rounds can contain mixed child outcomes (for example WIN + PUSH).
      // The present round-level W/L/P field cannot prove a single expected result.
      if(handWasSplit){
        say("RESULT CHECK: split hand - automatic single-result validation is not forced because child outcomes may differ.");
        if(choice("Confirm the entered round result "+entered+" matches the website/your chosen round-level convention? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++;
        continue;
      }

      if(currentDealerCards.isEmpty()){
        say("RESULT CHECK: dealer sequence is UNKNOWN, so card-based result validation cannot be completed.");
        if(choice("Confirm entered result "+entered+" from the website? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++;
        continue;
      }

      List<Card> dealer=new ArrayList<>();
      dealer.add(up); dealer.addAll(currentDealerCards);
      int pt=total(player), dt=total(dealer);
      boolean playerNatural=(player.size()==2 && pt==21);
      boolean dealerNatural=(dealer.size()==2 && dt==21);
      String expected;
      if(pt>21) expected="L";
      else if(dealerNatural) expected=playerNatural?"P":"L";
      else if(playerNatural) expected="W";
      else if(dt>21) expected="W";
      else if(pt>dt) expected="W";
      else if(pt<dt) expected="L";
      else expected="P";

      if(entered.equals(expected)){
        say("RESULT VALIDATED ✓ cards imply "+resultWord(expected)+" | Player "+pt+" vs Dealer "+dt);
        return entered;
      }

      validationWarnings++;
      say("*** RESULT VALIDATION WARNING ***");
      say("Player "+ct(player)+" = "+pt+" | Dealer "+ct(dealer)+" = "+dt);
      say("Cards imply "+resultWord(expected)+" ("+expected+") but entered result is "+resultWord(entered)+" ("+entered+").");
      String c=choice("[R] re-enter result / [A] accept discrepancy with explanation: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/explanation (check the game screen first): ");
      log.add("  RESULT VALIDATION OVERRIDE | cards imply "+expected+" entered "+entered+" | "+note);
      return entered;
    }
  }

  static String resultWord(String r){
    return r.equals("W")?"PLAYER WIN":r.equals("L")?"PLAYER LOSS":"PUSH";
  }

  static double validatedWager(double ref){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The £2400 platform offset is protected and cannot be wagered in the research session.");
        continue;
      }
      if(Math.abs(a-ref)>.001){
        validationWarnings++;
        say("CHECK: frozen reference is £"+m(ref)+" but entered wager is £"+m(a)+".");
        String c=choice("Is £"+m(a)+" definitely the wager placed? [Y/N]: ","YN");
        if(c.equals("N"))continue;

        // Preserve the actual wager as evidence, but classify why it differed.
        String reason=choice("Reason for wager deviation: [A] accidental entry/website amount / [I] intentional deviation / [O] other: ","AIO");
        String reasonText=reason.equals("A") ? "ACCIDENTAL" : reason.equals("I") ? "INTENTIONAL" : "OTHER";
        String note="";
        if(reason.equals("O")) note=line("Brief reason/note: ");
        log.add("  WAGER DEVIATION | frozen_reference "+m(ref)+" | actual "+m(a)+" | difference "+m(a-ref)+" | reason "+reasonText+(note.isEmpty()?"":" | note "+note));
      }
      return a;
    }
  }

  static double validatedEndingBankroll(double before,double base,double committed,String result,boolean natural,boolean split){
    while(true){
      double entered=money("Bankroll AFTER hand £: ");
      if(split){
        say("Split hand: automatic exact balance calculation is intentionally not forced because child outcomes may differ.");
        if(choice("Confirm website balance £"+m(entered)+" is correct? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++; continue;
      }
      double expected;
      if(result.equals("P")) expected=before;
      else if(result.equals("L")) expected=before-committed;
      else expected=before+(natural ? 1.5*base : committed);

      if(Math.abs(entered-expected)<0.011){
        say("BALANCE VALIDATED ✓ expected £"+m(expected));
        return entered;
      }

      validationWarnings++;
      say("*** BALANCE VALIDATION WARNING ***");
      say("Start £"+m(before)+" | committed stake £"+m(committed)+" | result "+result+(natural?" | natural blackjack":""));
      say("Expected £"+m(expected)+" | entered £"+m(entered)+" | difference £"+m(entered-expected));
      String c=choice("[R] re-enter balance / [A] accept discrepancy with note: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/note (may be brief): ");
      log.add("  VALIDATION OVERRIDE | expected "+m(expected)+" entered "+m(entered)+" | "+note);
      return entered;
    }
  }


  static void showChronologyPreview(int hand,Card p1,Card up,Card p2){
    say("\n--- CARD STREAM REVIEW: HAND "+hand+" ---");
    say("Reconstructed physical deal order (not the order you typed hidden cards):");
    say("Suit key: S=Spades, H=Hearts, D=Diamonds, C=Clubs, U=Unknown. Suits are shown in ASCII so they cannot disappear in the console.");
    int n=chronologicalCardNumber;
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 1",streamCard(p1)));
    if(currentDealerCards.isEmpty()){
      say("[UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER hole card",streamCard(currentDealerCards.get(0))));
    }
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 2",streamCard(p2)));
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER upcard",streamCard(up)));
    for(Card q:currentPlayerPostDealCards)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER post-deal card",streamCard(q)));
    for(int i=1;i<currentDealerCards.size();i++)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER draw "+i,streamCard(currentDealerCards.get(i))));
    if(handWasSplit)
      say("SPLIT NOTE: post-deal split cards are shown in operator-entry order; verify this sub-order against the screen.");
    say("--- END CARD STREAM REVIEW ---\n");
  }

  static void appendChronology(int hand,Card p1,Card up,Card p2){
    chronologyLog.add("HAND "+hand+" DEAL-ORDER RECONSTRUCTION");
    addChron(hand,"PLAYER card 1",p1);
    if(currentDealerCards.isEmpty()){
      chronologyLog.add("  [UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      addChron(hand,"DEALER hole card",currentDealerCards.get(0));
    }
    addChron(hand,"PLAYER card 2",p2);
    addChron(hand,"DEALER upcard",up);
    for(Card q:currentPlayerPostDealCards) addChron(hand,"PLAYER post-deal card",q);
    for(int i=1;i<currentDealerCards.size();i++) addChron(hand,"DEALER draw "+i,currentDealerCards.get(i));
    chronologyLog.add("");
  }
  static void addChron(int hand,String role,Card q){
    chronologicalCardNumber++;
    chronologyLog.add(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",chronologicalCardNumber,hand,role,q));
  }

  static double money(String p){while(true)try{return Double.parseDouble(line(p).replace("£","").trim());}catch(Exception e){say("Enter numeric amount.");}}
  static String choice(String p,String allowed){while(true){String s=line(p).toUpperCase();if(s.length()==1&&allowed.contains(s))return s;say("Allowed: "+allowed);}}
  static String line(String p){System.out.print(p);return sc.nextLine().trim();}
  static void say(String s){System.out.println(s);log.add(s);}
  static String m(double x){return String.format(Locale.UK,"%.2f",x);}

  static class LedgerTotals { int sessions=0, hands=0; double cumulativePL=0.0; }

  static LedgerTotals readLedgerTotals(){
    LedgerTotals t=new LedgerTotals();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return t;
    try{
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String q=x.trim();
        if(!q.startsWith("LEDGER_ENTRY |")) continue;
        t.sessions++;
        for(String part:q.split("\\|")){
          String z=part.trim();
          if(z.startsWith("hands ")) t.hands+=Integer.parseInt(z.substring(6).trim());
          else if(z.startsWith("session_pl ")) t.cumulativePL+=Double.parseDouble(z.substring(11).trim());
        }
      }
    }catch(Exception e){ validationWarnings++; }
    return t;
  }

  static int committedHands(){ return w+l+p; }

  // Explicitly distinguishes player decision events from hands played.
  static int totalDecisionEvents(){
    return textbookAlignedDecisions+frozenResearchOverrideDecisions+frozenPrimarySpecialDecisions;
  }

  static String ledgerEntry(String stamp){
    double researchFinal=bank-PLATFORM_OFFSET;
    double sessionPL=researchFinal-100.0;
    return "LEDGER_ENTRY | session "+stamp+" | mode "+(personalMode?"PERSONAL":"FROZEN")+
      " | hands "+committedHands()+" | start 100.00 | final "+m(researchFinal)+" | session_pl "+m(sessionPL)+
      " | total_decision_events "+totalDecisionEvents()+
      " | wager_match "+wagerMatch+" | wager_above "+wagerAbove+" | wager_below "+wagerBelow+
      " | action_match "+actionMatch+" | action_different "+actionDiff;
  }

  static List<String> ledgerSummaryLines(LedgerTotals prior,double thisPL,int thisHands){
    int sessions=prior.sessions+1, hands=prior.hands+thisHands;
    double cumulative=prior.cumulativePL+thisPL;
    double cumulativePct=100.0*cumulative/(100.0*sessions);
    double runningProjectBalance=100.0+cumulative;
    List<String> x=new ArrayList<>();
    x.add("PROJECT LEDGER SUMMARY");
    x.add("Completed sessions: "+sessions+" | Total hands: "+hands);
    x.add("Cumulative project P/L: £"+m(cumulative));
    x.add("Cumulative P/L vs £100 per-session starting capital: "+m(cumulativePct)+"%");
    x.add("Project-equivalent running balance (initial £100 + cumulative P/L): £"+m(runningProjectBalance));
    x.add("Ledger basis: each live session independently starts at research-equivalent £100; cumulative P/L sums session profits/losses.");
    return x;
  }


  static void save()throws Exception{
    String stamp=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); Path f=Path.of("blackjack_live_session_"+stamp+".txt");
    List<String>r=new ArrayList<>(); r.add("DETERMINISTIC BLACKJACK - LIVE SESSION REPORT");
    r.add("SESSION NAME: "+sessionName);
    r.add("PLATFORM BALANCE BASIS: Start £2500.00 | £2400.00 = research-equivalent £0/depleted");
    r.add("Platform Final £"+m(bank)+" | Platform Peak £"+m(peak)+" | Platform Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
    r.add("OFFSET RESEARCH-EQUIVALENT: Start £100.00 | Final £"+m(bank-PLATFORM_OFFSET)+" | Peak £"+m(peak-PLATFORM_OFFSET)+" | Trough £"+m(trough-PLATFORM_OFFSET));
    r.add("OFFSET RULE: research-equivalent bankroll = platform balance - £2400.00");
    r.add("AFFORDABILITY RULE: £2400 platform offset is protected; wager/double/split affordability uses research-equivalent bankroll only.");
    r.add("MODE: "+(personalMode?"PERSONAL - no Frozen wager/action prompts":"FROZEN ARCHITECTURE")); r.add("W/L/P "+w+"/"+l+"/"+p); if(!personalMode){r.add("Wager match/above/below "+wagerMatch+"/"+wagerAbove+"/"+wagerBelow);r.add("Action match/different "+actionMatch+"/"+actionDiff);}
    r.add("SOURCE: 247blackjack.com");
    r.add("SOURCE TYPE: Online play-money blackjack, configured 6-deck game");
    r.add("CARD GENERATION: computational/RNG implementation; visible six-deck shuffle events recorded from interface");
    r.add("Shuffle events explicitly observed: "+shuffleEvents);
    r.add("Total cards recorded: "+totalCardsObserved);
    r.add("Cards since most recent observed shuffle/start: "+cardsSinceShuffle);
    r.add("Exact known rank+suit identities observed: "+observedPhysicalCounts.size());
    r.add("Validation warnings/overrides encountered: "+validationWarnings);
    r.add("Hands re-entered through amendment review: "+amendmentsMade);
    r.add("Decision provenance counts:");
    r.add("  TOTAL DECISION EVENTS: "+totalDecisionEvents()+" (decision events, not hands)");
    r.add("  TEXTBOOK / FROZEN-ALIGNED: "+textbookAlignedDecisions);
    r.add("  FROZEN RESEARCH OVERRIDE events: "+frozenResearchOverrideDecisions);
    r.add("  FROZEN PRIMARY HARD11-vs-10 events: "+frozenPrimarySpecialDecisions);
    r.add("Policy note: Pair20-vs-10 STAND is a research override of prior aggressive SPLIT and also agrees with textbook.");
    r.add("Policy note: HARD11-vs-10 primary remains DOUBLE; Cooling->HIT is a capital-protection alternate, not the primary action used here.");
    r.add("Shuffle log: "+(shuffleLog.isEmpty()?"none":String.join(" | ",shuffleLog)));
    r.add("Penetration is not inferred unless a visible shuffle boundary is recorded; card counts are observational.");
    r.add("Boundary: frozen wagers are historical reference wagers, not proven optimal stakes.");r.add("Controller note: exact mature BRAKE predicate is not invented; only explicit handover transitions are used.");
    r.add(""); r.add("PERSONAL-MODE HISTORY ACCESS: recent-table-hand history is hidden by default and, if requested, shown BEFORE the wager; matching-hand history is available only after the current starting cards are known and before the player action.");
    r.add("RECENT TABLE-HAND WINDOW: user-selectable 1-3 previously committed hands from the current session only; the request itself is retained as behavioural evidence and occurs before capital is committed.");
    r.add(""); r.add("OBSERVED INITIAL-HAND INDEX + HISTORICAL OUTCOMES (SUITS EXCLUDED)"); r.addAll(handSignatureLog); r.add(""); r.add("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)");
    r.add("Order rule observed on 247 animation: Player1 -> Dealer hole -> Player2 -> Dealer up -> subsequent player cards -> subsequent dealer draws.");
    r.add("Dealer hole cards are entered later when revealed but are retrospectively placed as the dealer's first dealt card, matching the observed 247 animation.");
    r.add("Split/post-deal cards are retained in operator-entry order; if platform split dealing order differs, that sub-order must be reviewed before deterministic replay.");
    r.addAll(chronologyLog);
    r.add("");r.add("AUDIT LOG");r.addAll(log);Files.write(f,r);
     LedgerTotals priorLedger=readLedgerTotals();
     double thisPL=(bank-PLATFORM_OFFSET)-100.0; int thisHands=committedHands();
     List<String> cumulative=new ArrayList<>();
     cumulative.add("\n================ SESSION "+stamp+" ================"); cumulative.addAll(r);
     cumulative.add(ledgerEntry(stamp)); cumulative.addAll(ledgerSummaryLines(priorLedger,thisPL,thisHands));
     cumulative.add("================ END SESSION "+stamp+" ================\n");
     cumulative.add("SHUFFLE_ANALYSIS_STATUS | session "+stamp+" | PENDING");
     cumulative.add("PLATE_STATUS | session "+stamp+" | PENDING");
     Files.write(CUMULATIVE_OUTPUT,cumulative,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
     lastSavedSessionId=stamp; lastSavedReport=f;
     System.out.println("\nReport written: "+f.toAbsolutePath());
     System.out.println("Cumulative project output updated: "+CUMULATIVE_OUTPUT.toAbsolutePath());
     System.out.println("Ledger updated: cumulative project P/L £"+m(priorLedger.cumulativePL+thisPL)+" | project-equivalent running balance £"+m(100.0+priorLedger.cumulativePL+thisPL));
  }

  // ==================== SELF-CONTAINED POST-SESSION SHUFFLE ENGINE ====================
  // Replay rules: 247 physical initial deal order P1 -> dealer hole -> P2 -> dealer up; clean peek; dealer stands on all 17s; blackjack 3:2; no insurance;
  // one split level; split aces receive one card; double receives one card.
  // Fixed Casual profile: £15 flat wager; conventional Hit/Stand; double hard 10/11
  // versus dealer 2-9 when affordable; split A-A and 8-8 when affordable.

  enum ShuffleMethod { A, B, C1, C2, C3, C4, D, E }
  static class ReplayDecision { final String key; final A action; ReplayDecision(String k,A a){key=k;action=a;} }
  static class ReplayResult {
    double bank=100,peak=100,trough=100,maxDD=0,exposure=0;
    int hands=0,w=0,l=0,p=0,cards=0; boolean reached200=false,sourceExhausted=false,reached130Replay=false;
    State replayState=State.NORMAL; String reason="COMPLETED"; final List<ReplayDecision> decisions=new ArrayList<>();
  }
  static class PairDiff { int comparable=0,different=0; }
  static class MethodAggregate {
    final ShuffleMethod method; final int reps; final List<Double> frozenFinal=new ArrayList<>(),casualFinal=new ArrayList<>();
    double frozenExposure=0,casualExposure=0,frozenDD=0,casualDD=0,pairedDelta=0; int frozenReach=0,casualReach=0,frozenComplete=0,casualComplete=0,bothComplete=0;
    long comparableActions=0,differentActions=0; MethodAggregate(ShuffleMethod m,int n){method=m;reps=n;}
  }
  static class ReplayShoe {
    final ShuffleMethod method; final Random rng; final List<Card> original; List<Card> cards; int pos=0,segmentStart=0;
    ReplayShoe(ShuffleMethod m,List<Card> reservoir,long seed){method=m;rng=new Random(seed);original=copyCards(reservoir);cards=(m==ShuffleMethod.B)?fullSixDeck():copyCards(reservoir);if(m==ShuffleMethod.D)physicalShuffle(cards,rng);else Collections.shuffle(cards,rng);}
    void beforeHand(){
      if(method==ShuffleMethod.C1||method==ShuffleMethod.C2||method==ShuffleMethod.C3||method==ShuffleMethod.C4){
        double t=switch(method){case C1->.50;case C2->.65;case C3->.75;default->.80;};
        if(pos-segmentStart>=Math.ceil(original.size()*t)&&pos<cards.size()){Collections.shuffle(cards.subList(pos,cards.size()),rng);segmentStart=pos;}
      }
    }
    Card draw()throws EOFException{if(pos>=cards.size())throw new EOFException("source exhausted");return cards.get(pos++);}
    void afterHand(){if(method==ShuffleMethod.E){cards=copyCards(original);Collections.shuffle(cards,rng);pos=0;segmentStart=0;}}
  }
  static class HandCtx { double committed; HandCtx(double x){committed=x;} }
  static class PlayedHand { List<Card> cards; double stake; PlayedHand(List<Card> c,double s){cards=c;stake=s;} }

  static List<Card> copyCards(List<Card> src){List<Card>x=new ArrayList<>();for(Card c:src)x.add(new Card(c.rank,c.suit,c.value));return x;}
  static List<Card> fullSixDeck(){List<Card>x=new ArrayList<>();String[]su={"S","H","D","C"},ra={"A","2","3","4","5","6","7","8","9","10","J","Q","K"};for(int d=0;d<6;d++)for(String s:su)for(String r:ra){int v=r.equals("A")?11:(r.matches("10|J|Q|K")?10:Integer.parseInt(r));x.add(new Card(r,s,v));}return x;}
  static Card replayDraw(ReplayResult r,ReplayShoe s)throws EOFException{Card c=s.draw();r.cards++;return c;}

  // Declared physical-style mathematical proxy; not a proprietary casino-shuffler claim.
  static void physicalShuffle(List<Card> deck,Random rng){
    for(int cycle=0;cycle<3;cycle++){
      int cut=Math.max(1,Math.min(deck.size()-1,deck.size()/2+rng.nextInt(11)-5));List<Card>L=new ArrayList<>(deck.subList(0,cut)),R=new ArrayList<>(deck.subList(cut,deck.size())),mix=new ArrayList<>();int i=0,j=0;
      while(i<L.size()||j<R.size()){boolean left=i<L.size()&&(j>=R.size()||rng.nextBoolean());int packet=1+rng.nextInt(3);for(int k=0;k<packet;k++){if(left&&i<L.size())mix.add(L.get(i++));else if(!left&&j<R.size())mix.add(R.get(j++));else if(i<L.size())mix.add(L.get(i++));else if(j<R.size())mix.add(R.get(j++));}}
      deck.clear();deck.addAll(mix);List<Card>strip=new ArrayList<>();int at=0;while(at<deck.size()){int n=Math.min(deck.size()-at,2+rng.nextInt(5));strip.addAll(0,new ArrayList<>(deck.subList(at,at+n)));at+=n;}deck.clear();deck.addAll(strip);if(deck.size()>1)Collections.rotate(deck,-(1+rng.nextInt(deck.size()-1)));
    }
  }

  static A frozenReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canD){
    int t=total(c);if(t>=21)return A.STAND;if(pair){int q=c.get(0).value;if(q==10)return A.STAND;if(canD){if(q==11||q==8)return A.SPLIT;if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;if(q==7&&between(up,2,7))return A.SPLIT;if(q==6&&between(up,2,6))return A.SPLIT;if(q==4&&(up==5||up==6))return A.SPLIT;if((q==3||q==2)&&between(up,2,7))return A.SPLIT;}}
    if(soft(c)){if(t>=19)return A.STAND;if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE;if(up==2||up==7||up==8)return A.STAND;return A.HIT;}if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}return A.HIT;}
    if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;return A.HIT;
  }
  static A casualReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canExtra){
    int t=total(c);if(t>=21)return A.STAND;if(first&&pair&&canExtra&&(c.get(0).value==11||c.get(0).value==8))return A.SPLIT;if(!soft(c)&&first&&canExtra&&(t==10||t==11)&&between(up,2,9))return A.DOUBLE;
    if(soft(c)){if(t>=19)return A.STAND;if(t==18&&(up==2||up==7||up==8))return A.STAND;return A.HIT;}if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;return A.HIT;
  }
  static String decisionKey(int hand,String branch,int ord,List<Card>c,int up){List<String>r=new ArrayList<>();for(Card q:c)r.add(q.rank);Collections.sort(r);return hand+"|"+branch+"|"+ord+"|"+String.join(",",r)+"|UP"+up;}

  static void playReplayBranch(ReplayResult rr,ReplayShoe shoe,List<PlayedHand> out,List<Card> cards,int up,double stake,boolean frozen,int hand,String branch,boolean allowSplit,HandCtx hc)throws EOFException{
    boolean first=true;int ord=0;
    while(true){
      if(total(cards)>21){out.add(new PlayedHand(cards,stake));return;}
      boolean pair=allowSplit&&first&&cards.size()==2&&cards.get(0).value==cards.get(1).value;boolean canExtra=rr.bank-hc.committed+0.001>=stake;
      A act=frozen?frozenReplayAction(cards,up,first,pair,canExtra):casualReplayAction(cards,up,first,pair,canExtra);
      rr.decisions.add(new ReplayDecision(decisionKey(hand,branch,ord++,cards,up),act));
      if(act==A.STAND){out.add(new PlayedHand(cards,stake));return;}
      if(act==A.DOUBLE&&canExtra){hc.committed+=stake;List<Card>d=new ArrayList<>(cards);d.add(replayDraw(rr,shoe));out.add(new PlayedHand(d,stake*2));return;}
      if(act==A.SPLIT&&pair&&canExtra){hc.committed+=stake;Card a=cards.get(0),b=cards.get(1);List<Card>x=new ArrayList<>(List.of(a,replayDraw(rr,shoe))),y=new ArrayList<>(List.of(b,replayDraw(rr,shoe)));if(a.value==11&&b.value==11){out.add(new PlayedHand(x,stake));out.add(new PlayedHand(y,stake));return;}playReplayBranch(rr,shoe,out,x,up,stake,frozen,hand,branch+"A",false,hc);playReplayBranch(rr,shoe,out,y,up,stake,frozen,hand,branch+"B",false,hc);return;}
      cards.add(replayDraw(rr,shoe));first=false;allowSplit=false;
    }
  }

  static void updateReplayState(ReplayResult r){if(r.replayState==State.BRAKE)return;if(r.replayState==State.COOLING){if(r.bank>=120)r.replayState=State.CAUTIOUS;return;}if(r.replayState==State.CAUTIOUS){if(r.bank>=140)r.replayState=State.NORMAL;return;}if(r.reached130Replay&&r.bank<=100)r.replayState=State.COOLING;}
  static double frozenReplayWager(ReplayResult r,int h){updateReplayState(r);if(r.replayState!=State.NORMAL)return 15;if(h<=8||r.bank<130)return 15;if(r.bank<150)return 20;if(r.bank<170)return 30;return 15;}

  static ReplayResult runReplay(List<Card> reservoir,ShuffleMethod method,long seed,boolean frozen){
    ReplayResult rr=new ReplayResult();ReplayShoe shoe=new ReplayShoe(method,reservoir,seed);
    try{
      for(int h=1;h<=30;h++){
        double base=frozen?frozenReplayWager(rr,h):15.0;if(rr.bank+0.001<15||rr.bank+0.001<base){rr.reason="BANKROLL";break;}shoe.beforeHand();
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole));double before=rr.bank;HandCtx hc=new HandCtx(base);List<PlayedHand>ph=new ArrayList<>();
        boolean pn=total(player)==21,dn=total(dealer)==21;double net=0;
        if(dn){net=pn?0:-base;ph.add(new PlayedHand(player,base));}
        else if(pn){net=1.5*base;ph.add(new PlayedHand(player,base));}
        else{
          playReplayBranch(rr,shoe,ph,player,up.value,base,frozen,h,"MAIN",true,hc);
          boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;
          if(anyLive)while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));int dt=total(dealer);
          for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}
        }
        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;rr.peak=Math.max(rr.peak,rr.bank);rr.trough=Math.min(rr.trough,rr.bank);rr.maxDD=Math.max(rr.maxDD,rr.peak-rr.bank);if(rr.bank>=130)rr.reached130Replay=true;if(rr.bank>=200)rr.reached200=true;shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    return rr;
  }

  static PairDiff compareReplayDecisions(ReplayResult f,ReplayResult c){
    Map<String,A> fm=new LinkedHashMap<>();for(ReplayDecision d:f.decisions)fm.putIfAbsent(d.key,d.action);PairDiff pd=new PairDiff();for(ReplayDecision d:c.decisions){A x=fm.get(d.key);if(x!=null){pd.comparable++;if(x!=d.action)pd.different++;}}return pd;
  }
  static String methodDescription(ShuffleMethod m){return switch(m){case A->"Observed-card permutation: shuffle only the captured session-card reservoir; no cards added.";case B->"Full six-deck RNG: generate and uniformly shuffle a fresh 312-card six-deck shoe.";case C1->"Penetration 50%: captured reservoir; reshuffle only remaining cards between hands at 50%.";case C2->"Penetration 65%: captured reservoir; reshuffle only remaining cards between hands at 65%.";case C3->"Penetration 75%: captured reservoir; reshuffle only remaining cards between hands at 75%.";case C4->"Penetration 80%: captured reservoir; reshuffle only remaining cards between hands at 80%.";case D->"Physical-style proxy: captured reservoir with deterministic riffle/strip/cut-style mixing.";case E->"Automatic/continuous proxy: captured reservoir; after each complete hand, used cards return and the full reservoir is remixed.";};}
  static double mean(List<Double>x){double s=0;for(double v:x)s+=v;return x.isEmpty()?Double.NaN:s/x.size();}
  static double median(List<Double>x){if(x.isEmpty())return Double.NaN;List<Double>y=new ArrayList<>(x);Collections.sort(y);int n=y.size();return n%2==1?y.get(n/2):(y.get(n/2-1)+y.get(n/2))/2;}

  static MethodAggregate runMethod(List<Card> reservoir,ShuffleMethod method,int reps){
    MethodAggregate a=new MethodAggregate(method,reps);System.out.println("\n"+method+" - "+methodDescription(method));
    for(int i=0;i<reps;i++){
      long seed=SHUFFLE_SEED_BASE+1000003L*method.ordinal()+i;ReplayResult f=runReplay(reservoir,method,seed,true),c=runReplay(reservoir,method,seed,false);PairDiff d=compareReplayDecisions(f,c);
      a.frozenFinal.add(f.bank);a.casualFinal.add(c.bank);a.frozenExposure+=f.exposure;a.casualExposure+=c.exposure;a.frozenDD+=f.maxDD;a.casualDD+=c.maxDD;if(f.reached200)a.frozenReach++;if(c.reached200)a.casualReach++;if(f.hands==30)a.frozenComplete++;if(c.hands==30)a.casualComplete++;if(f.hands==30&&c.hands==30){a.bothComplete++;a.pairedDelta+=f.bank-c.bank;}a.comparableActions+=d.comparable;a.differentActions+=d.different;
    }
    System.out.println("Completed "+reps+" paired replays | mean comparable-action differences "+String.format(Locale.ROOT,"%.4f",(double)a.differentActions/reps));return a;
  }

  static String latestPendingSessionId(){
    if(!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      LinkedHashMap<String,String> status=new LinkedHashMap<>();
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("SHUFFLE_ANALYSIS_STATUS | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<3)continue;
        String sid=p[1].trim().replaceFirst("^session\\s+","");
        String st=p[2].trim();
        status.put(sid,st);
      }
      // Process the oldest unresolved session first so a deferred analysis cannot
      // become hidden by a newer live session.
      for(Map.Entry<String,String> e:status.entrySet())
        if(e.getValue().equals("PENDING")||e.getValue().startsWith("DEFERRED"))return e.getKey();
      return null;
    }catch(Exception e){return null;}
  }
  static boolean hasPendingShuffleAnalysis(){return latestPendingSessionId()!=null;}
  static void appendAnalysisDeferral()throws Exception{String sid=latestPendingSessionId();if(sid!=null)Files.writeString(CUMULATIVE_OUTPUT,"SHUFFLE_ANALYSIS_STATUS | session "+sid+" | DEFERRED_BY_OPERATOR\n",StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);}


  static String latestLiveSessionId(){
    if(!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      String latest=null;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("LEDGER_ENTRY | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<2)continue;
        latest=p[1].trim().replaceFirst("^session\\s+","");
      }
      return latest;
    }catch(Exception e){return null;}
  }

  static String plateStatusForSession(String sid){
    if(sid==null||!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      String status=null;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("PLATE_STATUS | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<3)continue;
        String found=p[1].trim().replaceFirst("^session\\s+","");
        if(found.equals(sid)) status=p[2].trim();
      }
      return status;
    }catch(Exception e){return null;}
  }

  static void appendPlateStatus(String sid,String status)throws Exception{
    if(sid==null)return;
    String ts=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    Files.writeString(CUMULATIVE_OUTPUT,
      "PLATE_STATUS | session "+sid+" | "+status+" | operator_confirmation "+ts+"\n",
      StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
  }

  static boolean confirmPreviousSessionPlateBeforeNewLive()throws Exception{
    String sid=latestLiveSessionId();
    if(sid==null)return true;
    String status=plateStatusForSession(sid);
    if("COMPLETE".equals(status))return true;

    System.out.println("\n*** SESSION PLATE PUBLICATION CHECK ***");
    System.out.println("Latest completed live session in output.txt: "+sid);
    System.out.println("Before another live session, the publication plate should normally be created.");
    System.out.println("Provide ChatGPT output.txt and the latest plate and ask for creation of the latest session plate.");

    if(choice("Has the session plate for "+sid+" been created and checked? [Y/N]: ","YN").equals("Y")){
      appendPlateStatus(sid,"COMPLETE");
      System.out.println("Plate confirmation recorded in output.txt.");
      return true;
    }

    System.out.println("Plate remains outstanding for session "+sid+".");
    if(!choice("Defer the plate and start another live session anyway? [Y/N]: ","YN").equals("Y"))return false;
    appendPlateStatus(sid,"DEFERRED_BY_OPERATOR");
    System.out.println("Plate deferral recorded in output.txt.");
    return true;
  }

  static Path reportForSession(String sid){
    Path direct=Path.of("blackjack_live_session_"+sid+".txt");
    if(Files.exists(direct))return direct;
    // lastSavedReport is safe only when it actually belongs to the requested SID.
    // Otherwise fall back to the matching session block in output.txt.
    if(lastSavedReport!=null&&Files.exists(lastSavedReport)&&
       lastSavedReport.getFileName().toString().equals("blackjack_live_session_"+sid+".txt"))return lastSavedReport;
    return CUMULATIVE_OUTPUT;
  }
  static List<Card> loadCapturedReservoir(Path source,String sid)throws Exception{
    List<String> lines=Files.readAllLines(source);List<Card> cards=new ArrayList<>();boolean in=false;String sessionMarker="================ SESSION "+sid+" ================";
    if(source.equals(CUMULATIVE_OUTPUT)){boolean found=false;for(String line:lines){if(line.trim().equals(sessionMarker)){found=true;in=false;}else if(found&&line.startsWith("================ END SESSION "))break;else if(found&&line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)"))in=true;else if(found&&in&&line.trim().equals("AUDIT LOG"))break;else if(found&&in)parseChronCard(line,cards);} }
    else {for(String line:lines){if(line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)")){in=true;continue;}if(in&&line.trim().equals("AUDIT LOG"))break;if(in)parseChronCard(line,cards);}}
    if(cards.isEmpty())throw new IOException("No chronological captured-card dataset found for session "+sid);return cards;
  }
  static void parseChronCard(String line,List<Card> out){String t=line.trim();if(!t.matches("#\\d+.*\\|.*"))return;String raw=t.substring(t.lastIndexOf('|')+1).trim();Card c=parseCard(raw);if(c!=null&&!c.suit.isEmpty()&&!c.suit.equals("?"))out.add(c);}

  static void runAutomaticShuffleAnalysis()throws Exception{
    String sid=latestPendingSessionId();if(sid==null){System.out.println("\nNo PENDING live-session shuffle analysis was found in output.txt.");return;}Path source=reportForSession(sid);List<Card> reservoir=loadCapturedReservoir(source,sid);
    System.out.println("\nPOST-SESSION SHUFFLE ROBUSTNESS - SESSION "+sid);System.out.println("Captured reservoir: "+reservoir.size()+" cards | paired replays per method: "+DEFAULT_SHUFFLE_REPLICATES);System.out.println("Frozen and fixed Casual receive the same seeded starting source per replicate and then consume independently.");
    List<MethodAggregate> all=new ArrayList<>();for(ShuffleMethod m:ShuffleMethod.values())all.add(runMethod(reservoir,m,DEFAULT_SHUFFLE_REPLICATES));
    List<String> out=new ArrayList<>();out.add("");out.add("================ AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");out.add("ENGINE VERSION: integrated-post-session-v3 | paired_replays_per_method "+DEFAULT_SHUFFLE_REPLICATES+" | seed_base "+SHUFFLE_SEED_BASE);out.add("Captured-card reservoir: "+reservoir.size()+" cards");out.add("Comparable-action definition: same replay hand, branch, decision ordinal, player-rank state and dealer upcard in both independently consumed paths.");
    for(MethodAggregate a:all){double md=(double)a.differentActions/a.reps;out.add(a.method+" | "+methodDescription(a.method));out.add(String.format(Locale.ROOT,"  Frozen median %.2f | Casual median %.2f | Frozen mean %.2f | Casual mean %.2f",median(a.frozenFinal),median(a.casualFinal),mean(a.frozenFinal),mean(a.casualFinal)));out.add(String.format(Locale.ROOT,"  £200 reach Frozen %.2f%% | Casual %.2f%% | Mean exposure Frozen %.2f | Casual %.2f | Mean maxDD Frozen %.2f | Casual %.2f",100.0*a.frozenReach/a.reps,100.0*a.casualReach/a.reps,a.frozenExposure/a.reps,a.casualExposure/a.reps,a.frozenDD/a.reps,a.casualDD/a.reps));out.add("  30-hand completion Frozen "+a.frozenComplete+" | Casual "+a.casualComplete+" | Both "+a.bothComplete);out.add(String.format(Locale.ROOT,"  Mean paired final-bankroll difference Frozen-Casual (both completed): %s",a.bothComplete==0?"N/A":String.format(Locale.ROOT,"%.2f",a.pairedDelta/a.bothComplete)));out.add("  Comparable action decisions total: "+a.comparableActions+" | Different actions total: "+a.differentActions+" | Mean Comparable Action Differences: "+String.format(Locale.ROOT,"%.4f",md));}
    out.add("Method E boundary: after each COMPLETE hand, consumed cards return to the captured reservoir and the entire reservoir is remixed; no captured card is added, removed or substituted.");out.add("Method D boundary: declared mathematical riffle/strip/cut proxy; not a proprietary casino-shuffler reproduction.");out.add("SHUFFLE_ANALYSIS_STATUS | session "+sid+" | COMPLETE");out.add("================ END AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");Files.write(CUMULATIVE_OUTPUT,out,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);Path rf=Path.of("blackjack_shuffle_analysis_"+sid+".txt");Files.write(rf,out,StandardCharsets.UTF_8);System.out.println("\nShuffle analysis complete. Results appended to "+CUMULATIVE_OUTPUT.toAbsolutePath());System.out.println("Standalone analysis report: "+rf.toAbsolutePath());
    System.out.println("PUBLICATION NEXT STEP: provide ChatGPT output.txt and the latest session plate,");
    System.out.println("then ask for creation of the latest session plate.");
  }

}
