// ═══════════════════════════════════════════
// ENGINE — data, question generators, streaks
// Pure JS, no React. Everything here is unit-testable.
// ═══════════════════════════════════════════

export const DEF = {
  masculine:{nominative:'der',accusative:'den',dative:'dem',genitive:'des'},
  feminine: {nominative:'die',accusative:'die',dative:'der',genitive:'der'},
  neuter:   {nominative:'das',accusative:'das',dative:'dem',genitive:'des'},
  plural:   {nominative:'die',accusative:'die',dative:'den',genitive:'der'},
};
export const INDEF = {
  masculine:{nominative:'ein', accusative:'einen',dative:'einem',genitive:'eines'},
  feminine: {nominative:'eine',accusative:'eine', dative:'einer',genitive:'einer'},
  neuter:   {nominative:'ein', accusative:'ein',  dative:'einem',genitive:'eines'},
  plural:   {nominative:'keine',accusative:'keine',dative:'keinen',genitive:'keiner'},
};
export const STRONG = {
  masculine:{nominative:'r',accusative:'n',dative:'m',genitive:'n'},
  feminine: {nominative:'e',accusative:'e',dative:'r',genitive:'r'},
  neuter:   {nominative:'s',accusative:'s',dative:'m',genitive:'n'},
  plural:   {nominative:'e',accusative:'e',dative:'n',genitive:'r'},
};
export const WEAK = {
  masculine:{nominative:'e',accusative:'n',dative:'n',genitive:'n'},
  feminine: {nominative:'e',accusative:'e',dative:'n',genitive:'n'},
  neuter:   {nominative:'e',accusative:'e',dative:'n',genitive:'n'},
  plural:   {nominative:'n',accusative:'n',dative:'n',genitive:'n'},
};

export const PREPS = {
  durch:{c:'accusative',en:'through'}, für:{c:'accusative',en:'for'},
  gegen:{c:'accusative',en:'against'}, ohne:{c:'accusative',en:'without'},
  um:{c:'accusative',en:'around'},
  bis:{c:'accusative',en:'until', ex:{de:'bis nächsten Montag', en:'until next Monday'}},
  aus:{c:'dative',en:'out of'}, bei:{c:'dative',en:'at'},
  mit:{c:'dative',en:'with'}, nach:{c:'dative',en:'to/after'},
  seit:{c:'dative',en:'since', ex:{de:'seit einer Woche', en:'for/since a week'}},
  von:{c:'dative',en:'from'},
  zu:{c:'dative',en:'to'}, außer:{c:'dative',en:'except'},
  gegenüber:{c:'dative',en:'opposite'},
  trotz:{c:'genitive',en:'despite'}, während:{c:'genitive',en:'during'},
  wegen:{c:'genitive',en:'because of'}, statt:{c:'genitive',en:'instead of'},
  anstatt:{c:'genitive',en:'instead of'}, außerhalb:{c:'genitive',en:'outside'},
  innerhalb:{c:'genitive',en:'inside'},
  an:{c:'wechsel',en:'at/on',m:'to',l:'at'},
  auf:{c:'wechsel',en:'on',m:'onto',l:'on'},
  hinter:{c:'wechsel',en:'behind',m:'behind',l:'behind'},
  in:{c:'wechsel',en:'in/into',m:'into',l:'in'},
  neben:{c:'wechsel',en:'next to',m:'next to',l:'next to'},
  über:{c:'wechsel',en:'over/above',m:'over',l:'above'},
  unter:{c:'wechsel',en:'under',m:'under',l:'under'},
  vor:{c:'wechsel',en:'in front of',m:'in front of',l:'in front of'},
  zwischen:{c:'wechsel',en:'between',m:'between',l:'between'},
};
export const PREPS_BY_CASE = {
  accusative:['durch','für','gegen','ohne','um','bis'],
  dative:['aus','bei','mit','nach','seit','von','zu','außer','gegenüber'],
  genitive:['trotz','während','wegen','statt','anstatt','außerhalb','innerhalb'],
  wechsel:['an','auf','hinter','neben','unter','über','in','vor','zwischen'],
};
export const COMMON_PREPS = ['mit','von','zu','in','an','auf','für','durch','ohne','um','aus','bei','nach','vor','über','wegen'];
export const RARE_PREPS = ['gegen','bis','seit','außer','gegenüber','hinter','neben','unter','zwischen','trotz','während','statt','anstatt','außerhalb','innerhalb'];
export const ALL_PREPS = [...COMMON_PREPS, ...RARE_PREPS];

// "bis" almost never takes a bare definite article in real German
// (bis nächsten Montag / bis zum Ende) — so it stays in the case-ID
// quizzes but is excluded from fill-the-article sentence levels.
const SENTENCE_ACC_PREPS = ['durch','für','gegen','ohne','um'];

export const WECHSEL_EX = {
  an:       {noun:'Tür',    g:'feminine',  en:'door',   mot:'to',           loc:'at'},
  auf:      {noun:'Tisch',  g:'masculine', en:'table',  mot:'onto',         loc:'on'},
  hinter:   {noun:'Haus',   g:'neuter',    en:'house',  mot:'behind',       loc:'behind'},
  in:       {noun:'Park',   g:'masculine', en:'park',   mot:'into',         loc:'in'},
  neben:    {noun:'Tisch',  g:'masculine', en:'table',  mot:'next to',      loc:'next to'},
  über:     {noun:'Brücke', g:'feminine',  en:'bridge', mot:'over',         loc:'above'},
  unter:    {noun:'Brücke', g:'feminine',  en:'bridge', mot:'under',        loc:'under'},
  vor:      {noun:'Haus',   g:'neuter',    en:'house',  mot:'in front of',  loc:'in front of'},
  zwischen: {noun:'Bäume',  g:'plural',    en:'trees',  mot:'between',      loc:'between', plDat:'n'},
};
export function wechselExample(prepKey, isMotion) {
  const ex = WECHSEL_EX[prepKey];
  const cas = isMotion ? 'accusative' : 'dative';
  const art = DEF[ex.g][cas];
  let n = ex.noun;
  if (ex.g === 'plural' && !isMotion && ex.plDat) n += ex.plDat;
  return { de: `${prepKey} ${art} ${n}`, en: `${isMotion ? ex.mot : ex.loc} the ${ex.en}` };
}

// gen: genitive noun form for masculine/neuter (des Mannes, des Hauses)
export const NOUNS = [
  {w:'Hund',g:'masculine',en:'dog',gen:'Hundes'},
  {w:'Mann',g:'masculine',en:'man',gen:'Mannes'},
  {w:'Tisch',g:'masculine',en:'table',gen:'Tisches'},
  {w:'Park',g:'masculine',en:'park',gen:'Parks'},
  {w:'Arzt',g:'masculine',en:'doctor',gen:'Arztes'},
  {w:'Freund',g:'masculine',en:'friend',gen:'Freundes'},
  {w:'Baum',g:'masculine',en:'tree',gen:'Baumes'},
  {w:'Garten',g:'masculine',en:'garden',gen:'Gartens'},
  {w:'Stuhl',g:'masculine',en:'chair',gen:'Stuhls'},
  {w:'Bruder',g:'masculine',en:'brother',gen:'Bruders'},
  {w:'Lehrer',g:'masculine',en:'teacher',gen:'Lehrers'},
  {w:'Vater',g:'masculine',en:'father',gen:'Vaters'},
  {w:'Zug',g:'masculine',en:'train',gen:'Zuges'},
  {w:'Regen',g:'masculine',en:'rain',gen:'Regens'},
  {w:'Frau',g:'feminine',en:'woman'},
  {w:'Katze',g:'feminine',en:'cat'},
  {w:'Schule',g:'feminine',en:'school'},
  {w:'Stadt',g:'feminine',en:'city'},
  {w:'Straße',g:'feminine',en:'street'},
  {w:'Tür',g:'feminine',en:'door'},
  {w:'Schwester',g:'feminine',en:'sister'},
  {w:'Mutter',g:'feminine',en:'mother'},
  {w:'Tochter',g:'feminine',en:'daughter'},
  {w:'Musik',g:'feminine',en:'music'},
  {w:'Nacht',g:'feminine',en:'night'},
  {w:'Woche',g:'feminine',en:'week'},
  {w:'Arbeit',g:'feminine',en:'work'},
  {w:'Brücke',g:'feminine',en:'bridge'},
  {w:'Kind',g:'neuter',en:'child',gen:'Kindes'},
  {w:'Haus',g:'neuter',en:'house',gen:'Hauses'},
  {w:'Buch',g:'neuter',en:'book',gen:'Buches'},
  {w:'Auto',g:'neuter',en:'car',gen:'Autos'},
  {w:'Mädchen',g:'neuter',en:'girl',gen:'Mädchens'},
  {w:'Fenster',g:'neuter',en:'window',gen:'Fensters'},
  {w:'Hotel',g:'neuter',en:'hotel',gen:'Hotels'},
  {w:'Museum',g:'neuter',en:'museum',gen:'Museums'},
  {w:'Büro',g:'neuter',en:'office',gen:'Büros'},
  {w:'Restaurant',g:'neuter',en:'restaurant',gen:'Restaurants'},
  {w:'Wetter',g:'neuter',en:'weather',gen:'Wetters'},
  {w:'Dach',g:'neuter',en:'roof',gen:'Daches'},
  {w:'Konzert',g:'neuter',en:'concert',gen:'Konzerts'},
];
const NOUN_BY_NAME = Object.fromEntries(NOUNS.map(n => [n.w, n]));
export const nounForm = (noun, cas) =>
  cas === 'genitive' && noun.gen ? noun.gen : noun.w;

const PERSONS = ['Mann','Arzt','Freund','Bruder','Lehrer','Vater','Frau','Schwester','Mutter','Tochter','Kind','Mädchen'];
const PLACES_IN = ['Park','Garten','Schule','Stadt','Haus','Hotel','Museum','Büro','Restaurant'];

// Nouns that read naturally with a possessive ("my …")
const POSSESSABLE = new Set([
  ...PERSONS, 'Hund','Katze','Haus','Garten','Auto','Buch','Tisch','Stuhl',
  'Büro','Schule','Stadt','Fenster','Tür','Musik',
]);

export const PRONOUNS = [
  {key:'ich',    nom:'ich', acc:'mich', dat:'mir',   en:'I',            enObj:'me',           tag:'1st person',     num:'sg'},
  {key:'du',     nom:'du',  acc:'dich', dat:'dir',   en:'you',          enObj:'you',          tag:'2nd informal',   num:'sg'},
  {key:'Sie_sg', nom:'Sie', acc:'Sie',  dat:'Ihnen', en:'you',          enObj:'you',          tag:'2nd formal',     num:'sg'},
  {key:'er',     nom:'er',  acc:'ihn',  dat:'ihm',   en:'he',           enObj:'him',          tag:'3rd masculine',  num:'sg'},
  {key:'sie_sg', nom:'sie', acc:'sie',  dat:'ihr',   en:'she',          enObj:'her',          tag:'3rd feminine',   num:'sg'},
  {key:'es',     nom:'es',  acc:'es',   dat:'ihm',   en:'it',           enObj:'it',           tag:'3rd neuter',     num:'sg'},
  {key:'wir',    nom:'wir', acc:'uns',  dat:'uns',   en:'we',           enObj:'us',           tag:'1st plural',     num:'pl'},
  {key:'ihr',    nom:'ihr', acc:'euch', dat:'euch',  en:"y'all",        enObj:"y'all",        tag:'2nd inf plural', num:'pl'},
  {key:'Sie_pl', nom:'Sie', acc:'Sie',  dat:'Ihnen', en:'you (formal)', enObj:'you (formal)', tag:'2nd formal pl',  num:'pl'},
  {key:'sie_pl', nom:'sie', acc:'sie',  dat:'ihnen', en:'they',         enObj:'them',         tag:'3rd plural',     num:'pl'},
];

export const POSSESSIVES = [
  {stem:'mein',  en:'my'},
  {stem:'dein',  en:'your (informal)'},
  {stem:'sein',  en:'his / its'},
  {stem:'ihr',   en:'her / their'},
  {stem:'unser', en:'our'},
  {stem:'euer',  en:"y'all's"},
];
export const POSS_ENDINGS = {
  masculine:{nominative:'',  accusative:'en', dative:'em', genitive:'es'},
  feminine: {nominative:'e', accusative:'e',  dative:'er', genitive:'er'},
  neuter:   {nominative:'',  accusative:'',   dative:'em', genitive:'es'},
  plural:   {nominative:'e', accusative:'e',  dative:'en', genitive:'er'},
};
export const combinePoss = (stem, ending) => (stem === 'euer' && ending) ? 'eur' + ending : stem + ending;

export const CL = {nominative:'Nominativ',accusative:'Akkusativ',dative:'Dativ',genitive:'Genitiv',wechsel:'Wechsel'};
export const CASE_C = {nominative:'#2563eb',accusative:'#dc2626',dative:'#ea580c',genitive:'#16a34a',wechsel:'#7c3aed'};
export const GENDER_C = {masculine:'#16a34a',feminine:'#db2777',neuter:'#2563eb',plural:'#7c3aed'};

export const VERBS = {
  sein:    {de:{Er:'ist',Sie:'ist',Wir:'sind',Ich:'bin'},          en:{Er:'is',Sie:'is',Wir:'are',Ich:'am'}},
  stehen:  {de:{Er:'steht',Sie:'steht',Wir:'stehen',Ich:'stehe'},  en:{Er:'is standing',Sie:'is standing',Wir:'are standing',Ich:'am standing'}},
  sitzen:  {de:{Er:'sitzt',Sie:'sitzt',Wir:'sitzen',Ich:'sitze'},  en:{Er:'is sitting',Sie:'is sitting',Wir:'are sitting',Ich:'am sitting'}},
  liegen:  {de:{Er:'liegt',Sie:'liegt',Wir:'liegen',Ich:'liege'},  en:{Er:'is lying',Sie:'is lying',Wir:'are lying',Ich:'am lying'}},
  warten:  {de:{Er:'wartet',Sie:'wartet',Wir:'warten',Ich:'warte'},en:{Er:'waits',Sie:'waits',Wir:'wait',Ich:'wait'}},
  wohnen:  {de:{Er:'wohnt',Sie:'wohnt',Wir:'wohnen',Ich:'wohne'},  en:{Er:'lives',Sie:'lives',Wir:'live',Ich:'live'}},
  gehen:   {de:{Er:'geht',Sie:'geht',Wir:'gehen',Ich:'gehe'},      en:{Er:'walks',Sie:'walks',Wir:'walk',Ich:'walk'}},
  laufen:  {de:{Er:'läuft',Sie:'läuft',Wir:'laufen',Ich:'laufe'},  en:{Er:'runs',Sie:'runs',Wir:'run',Ich:'run'}},
  fahren:  {de:{Er:'fährt',Sie:'fährt',Wir:'fahren',Ich:'fahre'},  en:{Er:'drives',Sie:'drives',Wir:'drive',Ich:'drive'}},
  kommen:  {de:{Er:'kommt',Sie:'kommt',Wir:'kommen',Ich:'komme'},  en:{Er:'comes',Sie:'comes',Wir:'come',Ich:'come'}},
  reisen:  {de:{Er:'reist',Sie:'reist',Wir:'reisen',Ich:'reise'},  en:{Er:'travels',Sie:'travels',Wir:'travel',Ich:'travel'}},
  kaufen:  {de:{Er:'kauft',Sie:'kauft',Wir:'kaufen',Ich:'kaufe'},  en:{Er:'buys',Sie:'buys',Wir:'buy',Ich:'buy'}},
  sprechen:{de:{Er:'spricht',Sie:'spricht',Wir:'sprechen',Ich:'spreche'}, en:{Er:'talks',Sie:'talks',Wir:'talk',Ich:'talk'}},
  spielen: {de:{Er:'spielt',Sie:'spielt',Wir:'spielen',Ich:'spiele'}, en:{Er:'plays',Sie:'plays',Wir:'play',Ich:'play'}},
  schlafen:{de:{Er:'schläft',Sie:'schläft',Wir:'schlafen',Ich:'schlafe'}, en:{Er:'sleeps',Sie:'sleeps',Wir:'sleep',Ich:'sleep'}},
  bleiben: {de:{Er:'bleibt',Sie:'bleibt',Wir:'bleiben',Ich:'bleibe'}, en:{Er:'stays',Sie:'stays',Wir:'stay',Ich:'stay'}},
  nehmen:  {de:{Er:'nimmt',Sie:'nimmt',Wir:'nehmen',Ich:'nehme'},  en:{Er:'takes',Sie:'takes',Wir:'take',Ich:'take'}},
  arbeiten:{de:{Er:'arbeitet',Sie:'arbeitet',Wir:'arbeiten',Ich:'arbeite'}, en:{Er:'works',Sie:'works',Wir:'work',Ich:'work'}},
  springen:{de:{Er:'springt',Sie:'springt',Wir:'springen',Ich:'springe'}, en:{Er:'jumps',Sie:'jumps',Wir:'jump',Ich:'jump'}},
  hängen:  {de:{Er:'hängt',Sie:'hängt',Wir:'hängen',Ich:'hänge'},  en:{Er:'hangs',Sie:'hangs',Wir:'hang',Ich:'hang'}},
  legen:   {de:{Er:'legt',Sie:'legt',Wir:'legen',Ich:'lege'},      en:{Er:'puts',Sie:'puts',Wir:'put',Ich:'put'}},
  stellen: {de:{Er:'stellt',Sie:'stellt',Wir:'stellen',Ich:'stelle'}, en:{Er:'puts',Sie:'puts',Wir:'put',Ich:'put'}},
};
export const SUBJ_EN = {Er:'He',Sie:'She',Wir:'We',Ich:'I'};

function mkv(stems) {
  return {
    ich: stems.ich, du: stems.du,
    er: stems.er, sie_sg: stems.er, es: stems.er,
    wir: stems.wir, sie_pl: stems.wir, Sie_sg: stems.wir, Sie_pl: stems.wir,
    ihr: stems.ihr,
  };
}
export const VC = {
  kommen: {de:mkv({ich:'komme',du:'kommst',er:'kommt',wir:'kommen',ihr:'kommt'}),  en:mkv({ich:'come',du:'come',er:'comes',wir:'come',ihr:'come'})},
  sein:   {de:mkv({ich:'bin',du:'bist',er:'ist',wir:'sind',ihr:'seid'}),           en:mkv({ich:'am',du:'are',er:'is',wir:'are',ihr:'are'})},
  haben:  {de:mkv({ich:'habe',du:'hast',er:'hat',wir:'haben',ihr:'habt'}),         en:mkv({ich:'have',du:'have',er:'has',wir:'have',ihr:'have'})},
  sehen:  {de:mkv({ich:'sehe',du:'siehst',er:'sieht',wir:'sehen',ihr:'seht'}),     en:mkv({ich:'see',du:'see',er:'sees',wir:'see',ihr:'see'})},
  kennen: {de:mkv({ich:'kenne',du:'kennst',er:'kennt',wir:'kennen',ihr:'kennt'}),  en:mkv({ich:'know',du:'know',er:'knows',wir:'know',ihr:'know'})},
  lieben: {de:mkv({ich:'liebe',du:'liebst',er:'liebt',wir:'lieben',ihr:'liebt'}),  en:mkv({ich:'love',du:'love',er:'loves',wir:'love',ihr:'love'})},
  helfen: {de:mkv({ich:'helfe',du:'hilfst',er:'hilft',wir:'helfen',ihr:'helft'}),  en:mkv({ich:'help',du:'help',er:'helps',wir:'help',ihr:'help'})},
  geben:  {de:mkv({ich:'gebe',du:'gibst',er:'gibt',wir:'geben',ihr:'gebt'}),       en:mkv({ich:'give',du:'give',er:'gives',wir:'give',ihr:'give'})},
};

export const SG_PRONOUN_KEYS = ['ich','du','er','sie_sg','es'];
export const PL_PRONOUN_KEYS = ['wir','ihr','sie_pl'];
export const FORMAL_PRONOUN_KEYS = ['Sie_sg','Sie_pl'];
export const ALL_PRONOUN_KEYS = PRONOUNS.map(p=>p.key);
const SAFE_SUBJECT_KEYS = ['ich','du','er','wir','ihr'];

export const LEVELS = [
  {id:2, tab:'article', name:'Akkusativ', desc:'durch, für, gegen, ohne, um', cases:['accusative'], preps:SENTENCE_ACC_PREPS, type:'article', artType:'definite'},
  {id:3, tab:'article', name:'Dativ', desc:'mit, von, zu, aus, bei, nach, seit...', cases:['dative'], preps:PREPS_BY_CASE.dative, type:'article', artType:'definite'},
  {id:4, tab:'article', name:'Wechselpräpositionen', desc:'Two-way: motion (Akk) vs location (Dat)', cases:['accusative','dative'], preps:PREPS_BY_CASE.wechsel, type:'article', artType:'definite', twoWay:true},
  {id:4.5, tab:'article', name:'Kontraktionen', desc:'Fused forms in context: ins, am, zum…', cases:['accusative','dative'], type:'chunk', artType:'definite'},
  {id:5, tab:'article', name:'Genitiv', desc:'trotz, während, wegen, statt...', cases:['genitive'], preps:PREPS_BY_CASE.genitive, type:'article', artType:'definite'},
  {id:6, tab:'article', name:'Alles gemischt', desc:'All prepositions, definite articles', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'definite', mixed:true},
  {id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true},

  {id:8,  tab:'prep', name:'Häufige Präpositionen', desc:'16 most common prepositions', preps:COMMON_PREPS, type:'prep'},
  {id:9,  tab:'prep', name:'Seltene Präpositionen', desc:'15 less common prepositions', preps:RARE_PREPS,   type:'prep'},
  {id:10, tab:'prep', name:'Alle Präpositionen',    desc:'All 31 prepositions mixed',   preps:ALL_PREPS,    type:'prep'},
  {id:18, tab:'prep', name:'Wechsel im Kontext',    desc:'Real sentences: Akkusativ or Dativ?', preps:PREPS_BY_CASE.wechsel, type:'wechselContext'},
  {id:19, tab:'prep', name:'Kontraktionen',         desc:'Contractions: am, ins, zum, beim…', type:'contraction'},

  {id:11, tab:'poss', name:'Häufige Possessive',   desc:'mein · dein · sein — Akkusativ & Dativ', possessives:['mein','dein','sein'], cases:['accusative','dative'], type:'poss'},
  {id:12, tab:'poss', name:'Schwierige Possessive', desc:'ihr · unser · euer — Akkusativ & Dativ', possessives:['ihr','unser','euer'], cases:['accusative','dative'], type:'poss'},
  {id:13, tab:'poss', name:'Alle gemischt',         desc:'mein, dein, sein, ihr, unser, euer — incl. Genitiv', possessives:['mein','dein','sein','ihr','unser','euer'], cases:['accusative','dative','genitive'], type:'poss'},

  {id:14, tab:'pronoun', name:'Singular Pronomen', desc:'mich, mir, dich, dir…',            pronouns:SG_PRONOUN_KEYS,      cases:['accusative','dative'], type:'pronoun'},
  {id:15, tab:'pronoun', name:'Plural Pronomen',   desc:'uns, euch, ihnen…',                pronouns:PL_PRONOUN_KEYS,      cases:['accusative','dative'], type:'pronoun'},
  {id:16, tab:'pronoun', name:'Sie (formal)',      desc:'Formal Sie — singular and plural', pronouns:FORMAL_PRONOUN_KEYS,  cases:['accusative','dative'], type:'pronoun'},
  {id:17, tab:'pronoun', name:'Alle gemischt',     desc:'All 10 pronouns, Akkusativ & Dativ', pronouns:ALL_PRONOUN_KEYS,   cases:['accusative','dative'], type:'pronoun'},
];

// Contractions (preposition + article). pref:true = preferred even in writing.
export const CONTRACTIONS = [
  {prep:'an', art:'das', form:'ans', pref:true,  c:'wechsel'},
  {prep:'an', art:'dem', form:'am',  pref:true,  c:'wechsel'},
  {prep:'in', art:'das', form:'ins', pref:true,  c:'wechsel'},
  {prep:'in', art:'dem', form:'im',  pref:true,  c:'wechsel'},
  {prep:'auf',art:'das', form:'aufs',pref:true,  c:'wechsel'},
  {prep:'bei',art:'dem', form:'beim',pref:true,  c:'dative'},
  {prep:'von',art:'dem', form:'vom', pref:true,  c:'dative'},
  {prep:'zu', art:'dem', form:'zum', pref:true,  c:'dative'},
  {prep:'zu', art:'der', form:'zur', pref:true,  c:'dative'},
  {prep:'durch',art:'das',form:'durchs',pref:false,c:'accusative'},
  {prep:'für',art:'das', form:'fürs',pref:false, c:'accusative'},
  {prep:'um', art:'das', form:'ums', pref:false, c:'accusative'},
  {prep:'über',art:'das',form:'übers',pref:false,c:'wechsel'},
  {prep:'unter',art:'das',form:'unters',pref:false,c:'wechsel'},
  {prep:'vor',art:'das', form:'vors',pref:false, c:'wechsel'},
  {prep:'hinter',art:'das',form:'hinters',pref:false,c:'wechsel'},
];

export const CONTRACTABLE = {};
CONTRACTIONS.filter(c=>c.pref).forEach(c => { (CONTRACTABLE[c.prep] = CONTRACTABLE[c.prep] || {})[c.art] = c.form; });
export const CHUNK_PREPS = new Set(Object.keys(CONTRACTABLE)); // an, in, auf, bei, von, zu
export const WECHSEL_SET = new Set(PREPS_BY_CASE.wechsel);

// ═══════════════════════════════════════════
// SENTENCE FRAMES
// Per-preposition verb/noun pairings so generated sentences are
// semantically plausible ("Er kauft das Geschenk für den Vater",
// not "Wir reisen für den Hund").
//
// frame: {verbs, nouns, mid?, fixedSubj?, enPrep?, enDet?, post?, enVerb?, poss?}
//   mid       — object between verb and preposition ({de,en})
//   fixedSubj — literal subject instead of Er/Sie/Wir/Ich
//               ({de, en, vkey?, enVkey?} — conjugation keys, default 'Er')
//   enPrep    — English preposition override (default PREPS[p].en or m/l)
//   enDet     — English determiner (default 'the'; '' drops it)
//   post      — trailing text after the noun ({accusative, dative, genitive, en})
//   enVerb    — English verb override map (Er/Sie/Wir/Ich)
//   poss      — false: skip this frame for possessive questions
// Wechsel preps use {motion:[...], location:[...]} instead of a flat list.
// ═══════════════════════════════════════════
export const PREP_FRAMES = {
  durch: [{verbs:['gehen','laufen','fahren'], nouns:['Park','Stadt','Straße','Garten','Tür']}],
  für:   [{verbs:['kaufen'], mid:{de:'das Geschenk',en:'the gift'}, nouns:[...PERSONS,'Hund','Katze']}],
  gegen: [{verbs:['fahren'], nouns:['Baum','Haus','Tür','Fenster','Auto'], enPrep:'into'}],
  ohne:  [{verbs:['kommen','reisen','gehen'], nouns:[...PERSONS,'Hund','Katze','Buch','Auto']}],
  um:    [{verbs:['laufen','gehen'], nouns:['Tisch','Haus','Baum','Park','Garten']}],
  aus:   [{verbs:['kommen','laufen'], nouns:['Haus','Schule','Büro','Museum','Hotel','Stadt','Garten','Restaurant']}],
  bei:   [{verbs:['wohnen','sein','warten'], nouns:PERSONS}],
  mit: [
    {verbs:['sprechen'], nouns:PERSONS},
    {verbs:['spielen'], nouns:['Hund','Katze','Kind']},
    {verbs:['fahren'], nouns:['Auto','Zug']},
  ],
  nach:  [{verbs:['kommen'], nouns:['Schule','Arbeit','Nacht','Woche'], enPrep:'after', enDet:'', poss:false}],
  seit:  [{verbs:['wohnen'], mid:{de:'hier',en:'here'}, nouns:['Nacht','Woche'], enPrep:'since', enDet:'that',
           enVerb:{Er:'has lived',Sie:'has lived',Wir:'have lived',Ich:'have lived'}, poss:false}],
  von:   [{verbs:['kommen'], nouns:[...PERSONS,'Arbeit','Schule','Büro']}],
  zu:    [{verbs:['gehen','fahren'], nouns:[...PERSONS,'Schule','Park','Museum','Büro','Restaurant','Hotel']}],
  außer: [{fixedSubj:{de:'Alle',en:'Everyone',vkey:'Wir',enVkey:'Er'}, verbs:['kommen'], nouns:PERSONS, enPrep:'except'}],
  gegenüber: [{verbs:['wohnen'], nouns:['Schule','Park','Museum','Hotel','Restaurant']}],
  trotz: [{verbs:['schlafen','arbeiten'], nouns:['Musik','Regen','Wetter'], poss:false}],
  während: [
    {verbs:['schlafen'], nouns:['Nacht','Konzert'], poss:false},
    {verbs:['arbeiten'], nouns:['Woche','Nacht'], poss:false},
  ],
  wegen: [{verbs:['bleiben'], nouns:['Regen','Wetter','Kind','Hund','Katze']}],
  statt: [{verbs:['nehmen'], mid:{de:'den Bus',en:'the bus'}, nouns:['Auto','Zug']}],
  anstatt: [{verbs:['nehmen'], mid:{de:'den Bus',en:'the bus'}, nouns:['Auto','Zug']}],
  außerhalb: [{verbs:['wohnen','arbeiten'], nouns:['Stadt']}],
  innerhalb: [{verbs:['wohnen','arbeiten'], nouns:['Stadt']}],

  an: {
    motion:  [{verbs:['gehen','laufen'], nouns:['Tür','Fenster']}],
    location:[{verbs:['stehen','warten','sitzen'], nouns:['Tür','Fenster']}],
  },
  auf: {
    motion: [
      {verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl']},
      {verbs:['gehen'], nouns:['Straße']},
      {verbs:['springen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Dach']},
    ],
    location: [
      {verbs:['sitzen'], nouns:['Stuhl']},
      {verbs:['liegen'], fixedSubj:{de:'Das Buch',en:'The book'}, nouns:['Tisch']},
      {verbs:['stehen','warten'], nouns:['Straße']},
    ],
  },
  hinter: {
    motion:  [{verbs:['gehen','laufen'], nouns:['Haus','Baum','Garten','Schule']}],
    location:[{verbs:['stehen','warten'], nouns:['Haus','Baum','Tür','Schule']}],
  },
  in: {
    motion: [{verbs:['gehen','fahren','kommen','laufen'], nouns:PLACES_IN}],
    location: [
      {verbs:['wohnen'], nouns:['Stadt','Haus','Hotel']},
      {verbs:['sein','warten','sitzen','arbeiten'], nouns:PLACES_IN},
    ],
  },
  neben: {
    motion: [
      {verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl','Tür']},
      {verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Stuhl','Tisch']},
    ],
    location: [
      {verbs:['stehen','sitzen'], nouns:['Tisch','Tür','Fenster']},
      {verbs:['wohnen'], nouns:['Schule','Park','Museum']},
    ],
  },
  über: {
    motion: [
      {verbs:['gehen','laufen'], nouns:['Straße','Brücke']},
      {verbs:['hängen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch']},
    ],
    location:[{verbs:['hängen'], fixedSubj:{de:'Die Lampe',en:'The lamp'}, nouns:['Tisch']}],
  },
  unter: {
    motion: [
      {verbs:['legen'], mid:{de:'das Buch',en:'the book'}, nouns:['Tisch','Stuhl']},
      {verbs:['laufen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Baum']},
    ],
    location: [
      {verbs:['sitzen','stehen','schlafen'], nouns:['Baum','Brücke']},
      {verbs:['schlafen'], fixedSubj:{de:'Die Katze',en:'The cat'}, nouns:['Tisch','Stuhl']},
    ],
  },
  vor: {
    motion:  [{verbs:['fahren','gehen'], nouns:['Haus','Tür','Schule','Hotel']}],
    location:[{verbs:['stehen','warten'], nouns:['Tür','Haus','Schule','Hotel','Museum','Restaurant']}],
  },
  zwischen: {
    motion: [{verbs:['stellen'], mid:{de:'die Lampe',en:'the lamp'}, nouns:['Tisch','Stuhl'],
              post:{accusative:' und das Fenster', dative:' und dem Fenster', en:' and the window'}}],
    location: [{verbs:['stehen','sitzen'], nouns:['Tisch','Stuhl'],
              post:{accusative:' und das Fenster', dative:' und dem Fenster', en:' and the window'}}],
  },
};

// ═══════════════════════════════════════════
// RANDOM HELPERS + SESSION STATE
// ═══════════════════════════════════════════

export const pick = a => a[Math.floor(Math.random()*a.length)];
export const cap = s => s.charAt(0).toUpperCase() + s.slice(1);

const pickAvoid = (arr, avoid) => {
  if (!arr.length) return undefined;
  if (arr.length === 1) return arr[0];
  const filtered = arr.filter(x => x !== avoid);
  return pick(filtered.length ? filtered : arr);
};

const shuffle = a => {
  const r = [...a];
  for (let i = r.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [r[i], r[j]] = [r[j], r[i]];
  }
  return r;
};

// Shuffle-bag: deal every item from the pool once before any repeats,
// and never let a refill produce the same item twice in a row.
function drawBag(state, key, pool, avoid) {
  let bag = state.bags[key];
  if (!bag || !bag.length) {
    bag = shuffle(pool);
    if (bag.length > 1 && bag[bag.length - 1] === avoid) {
      bag.pop();
      bag.unshift(avoid);
    }
    state.bags[key] = bag;
  }
  return bag.pop();
}

export function newState() {
  return { bags:{}, last:{ prep:null, verb:null, noun:null, subj:null } };
}

// A practice session: one shuffle-bag + anti-repeat memory per round.
export function createSession(levelId) {
  const state = newState();
  return { next: () => gen(levelId, state), state };
}

// ═══════════════════════════════════════════
// FRAME REALIZATION
// ═══════════════════════════════════════════

function framesFor(prepKey, tw) {
  const f = PREP_FRAMES[prepKey];
  if (!f) return [];
  if (Array.isArray(f)) return f;
  return tw ? f.motion : f.location;
}

// Resolve a frame into concrete sentence parts. Returns null if the
// preposition has no usable frame for the given constraints.
function realizeFrame(prepKey, cas, tw, state, { genderFilter, possOnly } = {}) {
  let frames = framesFor(prepKey, tw);
  if (possOnly) {
    frames = frames
      .filter(f => f.poss !== false)
      .map(f => ({...f, nouns: f.nouns.filter(n => POSSESSABLE.has(n))}))
      .filter(f => f.nouns.length);
  }
  if (genderFilter) {
    frames = frames
      .map(f => ({...f, nouns: f.nouns.filter(n => genderFilter.includes(NOUN_BY_NAME[n].g))}))
      .filter(f => f.nouns.length);
  }
  if (!frames.length) return null;

  // Prefer frames that can avoid repeating the previous question's verb.
  const fresh = frames.filter(f => f.verbs.some(v => v !== state.last.verb));
  const frame = pick(fresh.length ? fresh : frames);
  const nounName = pickAvoid(frame.nouns, state.last.noun);
  const noun = NOUN_BY_NAME[nounName];
  const verbKey = pickAvoid(frame.verbs, state.last.verb);
  state.last.noun = nounName;
  state.last.verb = verbKey;

  let subjDe, subjEn, deVkey, enVkey;
  if (frame.fixedSubj) {
    subjDe = frame.fixedSubj.de;
    subjEn = frame.fixedSubj.en;
    deVkey = frame.fixedSubj.vkey || 'Er';
    enVkey = frame.fixedSubj.enVkey || deVkey;
  } else {
    const subj = pickAvoid(['Er','Sie','Wir','Ich'], state.last.subj);
    state.last.subj = subj;
    subjDe = subj; subjEn = SUBJ_EN[subj];
    deVkey = subj; enVkey = subj;
  }

  const p = PREPS[prepKey];
  let enPrep = frame.enPrep || p.en;
  if (p.c === 'wechsel' && !frame.enPrep) enPrep = tw ? p.m : p.l;

  const verbDe = VERBS[verbKey].de[deVkey];
  const verbEn = (frame.enVerb || VERBS[verbKey].en)[enVkey];
  const midDe = frame.mid ? ` ${frame.mid.de}` : '';
  const midEn = frame.mid ? ` ${frame.mid.en}` : '';
  const postDe = frame.post ? (frame.post[cas] || '') : '';
  const postEn = frame.post ? frame.post.en : '';
  const enDet = frame.enDet !== undefined ? frame.enDet : 'the';

  return { noun, verbKey, subjDe, subjEn, verbDe, verbEn, midDe, midEn, enPrep, postDe, postEn, enDet };
}

const enNoun = (r, det) => `${det}${det ? ' ' : ''}${r.noun.en}`;

// ═══════════════════════════════════════════
// QUESTION GENERATORS
// ═══════════════════════════════════════════

function articleChoices(ans, artType) {
  const pool = artType === 'indefinite'
    ? ['ein','eine','einen','einem','eines','einer','keine','keinen','keiner']
    : ['der','die','das','den','dem','des'];
  const rem = pool.filter(a => a !== ans);
  const picked = [];
  while (picked.length < 3 && rem.length) picked.push(rem.splice(Math.floor(Math.random()*rem.length), 1)[0]);
  return [ans, ...picked].sort(() => Math.random() - 0.5);
}

export function genArticleQ(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  const prepKey = drawBag(state, 'prep', lv.preps, state.last.prep);
  state.last.prep = prepKey;
  const pc = PREPS[prepKey].c;
  let tw = null, cas;
  if (pc === 'wechsel') { tw = Math.random() > 0.5; cas = tw ? 'accusative' : 'dative'; }
  else cas = pc;

  // Mixed definite lessons: contraction-capable prep → fused "chunk" question.
  if (lv.mixed && lv.artType === 'definite' && CHUNK_PREPS.has(prepKey)) {
    return makeChunkQ(prepKey, cas, tw, state);
  }

  const r = realizeFrame(prepKey, cas, tw, state);
  const tbl = lv.artType === 'indefinite' ? INDEF : DEF;
  const ans = tbl[r.noun.g][cas];
  const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} ${prepKey} _____ ${nounForm(r.noun, cas)}${r.postDe}.`;
  const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${enNoun(r, r.enDet)}${r.postEn}.`;
  const rule = pc === 'wechsel'
    ? (tw ? `„${prepKey}" + motion (wohin?) → Akkusativ` : `„${prepKey}" + location (wo?) → Dativ`)
    : `„${prepKey}" → always ${CL[cas]}`;
  return {type:'article', noun:r.noun, cas, prepKey, ans, sentence, translation, rule, tw, artType:lv.artType, choices:articleChoices(ans, lv.artType)};
}

export function genPrepQ(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  const prepKey = drawBag(state, 'prep', lv.preps, state.last.prep);
  state.last.prep = prepKey;
  const p = PREPS[prepKey];
  if (p.ex) {
    return {type:'prep', prepKey, prepEn:p.en, ans:p.c, example:p.ex.de, exampleEn:p.ex.en, exampleFull:true};
  }
  const exCase = p.c === 'wechsel' ? 'dative' : p.c;
  const frames = framesFor(prepKey, false);
  const nounName = frames.length ? pick(pick(frames).nouns) : pick(NOUNS).w;
  const noun = NOUN_BY_NAME[nounName];
  return {type:'prep', prepKey, prepEn:p.en, ans:p.c,
    example:`${prepKey} ${DEF[noun.g][exCase]} ${nounForm(noun, exCase)}`, exampleEn:noun.en};
}

export function genWechselContextQ(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  const prepKey = drawBag(state, 'prep', lv.preps, state.last.prep);
  state.last.prep = prepKey;
  const isMotion = Math.random() > 0.5;
  const cas = isMotion ? 'accusative' : 'dative';
  const r = realizeFrame(prepKey, cas, isMotion, state);
  const art = DEF[r.noun.g][cas];
  const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} ${prepKey} ${art} ${nounForm(r.noun, cas)}${r.postDe}.`;
  const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${enNoun(r, r.enDet)}${r.postEn}.`;
  return {type:'wechselContext', prepKey, ans:cas, isMotion, verb:r.verbKey, sentence, translation, nounEn:r.noun.en};
}

const POSS_ACC_TEMPLATES = [
  {de:'sehe', en:'see', nouns:[...PERSONS,'Hund','Katze','Haus','Garten','Auto']},
  {de:'kenne', en:'know', nouns:PERSONS},
  {de:'liebe', en:'love', nouns:[...PERSONS,'Hund','Katze','Stadt','Auto']},
  {de:'höre', en:'hear', nouns:['Musik','Hund','Katze','Kind']},
];

export function genPossQ(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  const stem = pickAvoid(lv.possessives, state.last.stem);
  state.last.stem = stem;
  const possMeaning = POSSESSIVES.find(p=>p.stem===stem).en;
  const wantPrep = Math.random() > 0.5;

  if (wantPrep) {
    const pool = lv.cases.includes('genitive')
      ? [...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.wechsel,...PREPS_BY_CASE.genitive]
      : [...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.wechsel];
    // Draw until we find a prep with a possessive-friendly frame.
    for (let i = 0; i < pool.length; i++) {
      const prepKey = drawBag(state, 'prep', pool, state.last.prep);
      const pc = PREPS[prepKey].c;
      let tw = null, cas;
      if (pc === 'wechsel') { tw = Math.random() > 0.5; cas = tw ? 'accusative' : 'dative'; } else cas = pc;
      if (!lv.cases.includes(cas)) continue;
      const r = realizeFrame(prepKey, cas, tw, state, { possOnly:true });
      if (!r) continue;
      state.last.prep = prepKey;
      const ans = combinePoss(stem, POSS_ENDINGS[r.noun.g][cas]);
      const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} ${prepKey} _____ ${nounForm(r.noun, cas)}${r.postDe}.`;
      const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${possMeaning} ${r.noun.en}${r.postEn}.`;
      const rule = `${stem}- + ${CL[cas]} + ${r.noun.g} → ${ans}`;
      return {type:'poss', noun:r.noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey, tw};
    }
  }

  // No preposition: simple object sentence.
  const cas = pick(lv.cases.filter(c => c !== 'genitive')) || lv.cases[0];
  let noun, sentence, translation;
  if (cas === 'accusative') {
    const t = pick(POSS_ACC_TEMPLATES);
    noun = NOUN_BY_NAME[pickAvoid(t.nouns, state.last.noun)];
    sentence = `Ich ${t.de} _____ ${noun.w}.`;
    translation = `I ${t.en} ${possMeaning} ${noun.en}.`;
  } else {
    noun = NOUN_BY_NAME[pickAvoid(PERSONS, state.last.noun)];
    sentence = `Ich helfe _____ ${noun.w}.`;
    translation = `I help ${possMeaning} ${noun.en}.`;
  }
  state.last.noun = noun.w;
  const ans = combinePoss(stem, POSS_ENDINGS[noun.g][cas]);
  const rule = `${stem}- + ${CL[cas]} + ${noun.g} → ${ans}`;
  return {type:'poss', noun, stem, possMeaning, cas, ans, sentence, translation, rule, prepKey:null, tw:null};
}

export function genPronounQ(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  const pronKey = drawBag(state, 'pron', lv.pronouns, state.last.pron);
  state.last.pron = pronKey;
  const pron = PRONOUNS.find(p=>p.key===pronKey);
  const cas = pick(lv.cases);
  const ans = pron[{accusative:'acc',dative:'dat'}[cas]];

  const subjCandidates = SAFE_SUBJECT_KEYS.filter(k => k !== pronKey);
  const subjKey = pick(subjCandidates);
  const subj = PRONOUNS.find(p=>p.key===subjKey);
  let tpl;
  if (cas === 'accusative') {
    tpl = pick([{v:'sehen'}, {v:'kennen'}, {v:'lieben'}]);
  } else {
    tpl = pick([{v:'helfen'}, {v:'geben', dePost:' das Buch', enPost:' the book'}]);
  }
  const sDe = VC[tpl.v].de[subjKey];
  const sEn = VC[tpl.v].en[subjKey];
  const dePost = tpl.dePost || '';
  const enPost = tpl.enPost || '';
  const sentence = `${cap(subj.nom)} ${sDe} _____${dePost}.`;
  const translation = `${cap(subj.en)} ${sEn} ${pron.enObj}${enPost}.`;
  return {type:'pronoun', pronKey, pron, cas, ans, sentence, translation};
}

export function genContractionQ(state = newState()) {
  const form = drawBag(state, 'contr', CONTRACTIONS.map(c=>c.form), state.last.contr);
  state.last.contr = form;
  const c = CONTRACTIONS.find(x => x.form === form);
  return {type:'contraction', prep:c.prep, art:c.art, ans:c.form, pref:c.pref, cas:c.c};
}

// Build the 4 choices for a fused prep+article question: always a BLEND of
// contracted and two-word forms, so the shape never reveals whether it contracts.
export function buildChunkChoices(prepKey, article) {
  const map = CONTRACTABLE[prepKey] || {};
  const contracts = !!map[article];
  const correct = contracts ? map[article] : `${prepKey} ${article}`;
  const cand = new Set();
  Object.values(map).forEach(f => cand.add(f));                 // contraction shapes
  Object.keys(map).forEach(a => cand.add(`${prepKey} ${a}`));   // their two-word forms
  cand.add(`${prepKey} ${article}`);                            // two-word of the real article
  cand.delete(correct);
  const pool = [...cand].sort(() => Math.random() - 0.5);
  const choices = [correct, ...pool.slice(0, 3)];
  const filler = ['das','dem','der','die','den'].map(a => `${prepKey} ${a}`);
  for (let i = 0; i < filler.length && choices.length < 4; i++) {
    if (!choices.includes(filler[i])) choices.push(filler[i]);
  }
  return { correct, contracts, choices: choices.slice(0, 4).sort(() => Math.random() - 0.5) };
}

export function makeChunkQ(prepKey, cas, tw, state = newState(), genderFilter) {
  const r = realizeFrame(prepKey, cas, tw, state, { genderFilter });
  const article = DEF[r.noun.g][cas];
  const { correct, contracts, choices } = buildChunkChoices(prepKey, article);
  const sentence = `${r.subjDe} ${r.verbDe}${r.midDe} _____ ${nounForm(r.noun, cas)}${r.postDe}.`;
  const translation = `${r.subjEn} ${r.verbEn}${r.midEn} ${r.enPrep} ${enNoun(r, r.enDet)}${r.postEn}.`;
  const pref = contracts ? (CONTRACTIONS.find(x => x.prep === prepKey && x.art === article)?.pref ?? true) : null;
  return { type:'chunk', prepKey, noun:r.noun, cas, ans:correct, choices, sentence, translation, tw, contracts, pref };
}

// Dedicated "Kontraktionen" level: answer ALWAYS contracts (preferred forms only).
export function genContractionContextQ(state = newState()) {
  const prefForms = CONTRACTIONS.filter(x => x.pref && CHUNK_PREPS.has(x.prep));
  const form = drawBag(state, 'contr', prefForms.map(c=>c.form), state.last.contr);
  state.last.contr = form;
  const c = prefForms.find(x => x.form === form);
  const wechsel = WECHSEL_SET.has(c.prep);
  let cas, genders, tw = null;
  if (c.art === 'das')      { cas='accusative'; genders=['neuter'];              tw = wechsel ? true  : null; }
  else if (c.art === 'dem') { cas='dative';     genders=['masculine','neuter'];  tw = wechsel ? false : null; }
  else                      { cas='dative';     genders=['feminine'];            tw = wechsel ? false : null; }
  return makeChunkQ(c.prep, cas, tw, state, genders);
}

export function gen(level, state = newState()) {
  const lv = LEVELS.find(l=>l.id===level);
  if (lv.type==='chunk')           return genContractionContextQ(state);
  if (lv.type==='contraction')     return genContractionQ(state);
  if (lv.type==='prep')            return genPrepQ(level, state);
  if (lv.type==='wechselContext')  return genWechselContextQ(level, state);
  if (lv.type==='poss')            return genPossQ(level, state);
  if (lv.type==='pronoun')         return genPronounQ(level, state);
  return genArticleQ(level, state);
}

// ═══════════════════════════════════════════
// STREAKS
// Local dates (not UTC) so a late-evening session counts as the
// day the user actually experienced.
// ═══════════════════════════════════════════

export function localDateStr(d = new Date()) {
  const p = n => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`;
}
const dayDiff = (a, b) => Math.round((Date.parse(b) - Date.parse(a)) / 864e5);

// Only the trailing window needed for the 7-day dots is kept.
function pruneDays(days, today) {
  const keep = {};
  for (const d of Object.keys(days)) {
    const diff = dayDiff(d, today);
    if (diff >= 0 && diff < 14) keep[d] = true;
  }
  return keep;
}

// Called on every answered question. Idempotent within a day.
export function bumpStreak(st, today = localDateStr()) {
  if (st.ld === today) {
    if (st.days?.[today]) return st;
    return { ...st, days: pruneDays({ ...(st.days || {}), [today]: true }, today) };
  }
  const streak = st.ld && dayDiff(st.ld, today) === 1 ? (st.streak || 0) + 1 : 1;
  const days = pruneDays({ ...(st.days || {}), [today]: true }, today);
  return { ...st, streak, ld: today, days };
}

// The last 7 days (oldest first, today last), flagged with whether the
// user practiced on each — drives the little dots next to the streak.
export function weekDots(st, today = localDateStr()) {
  return Array.from({ length: 7 }, (_, i) => {
    const key = new Date(Date.parse(today) - (6 - i) * 864e5).toISOString().slice(0, 10);
    return { date: key, practiced: !!st?.days?.[key], isToday: i === 6 };
  });
}

// What to display: a streak is only alive if the last practice was
// today or yesterday — otherwise it shows 0 (it has lapsed).
export function currentStreak(st, today = localDateStr()) {
  if (!st?.ld) return 0;
  const d = dayDiff(st.ld, today);
  return d >= 0 && d <= 1 ? (st.streak || 0) : 0;
}
