/**
 * Klima-Wächter — Engine (Single Source of Truth für 2D und 3D)
 * ------------------------------------------------------------------
 * Headless Spiellogik: kein DOM, keine kwAudio-Aufrufe, keine Renderings.
 * Konsumenten (game-2d.html, game-3d.html) rufen die Engine-Methoden auf
 * und verarbeiten die zurückgegebenen Events (Toast, Sound) selbst.
 *
 * Exportiert als globales `window.KlimaEngine` (kein ES-Module, kein Bundler).
 *
 * Die Engine-Funktionen sind zustandsfrei gegenüber dem Modul — sie operieren
 * auf einem `game`-Objekt, das du mit `createGame(levelId)` erzeugst.
 *
 * Event-Pattern: mutierende Aktionen (buyMeasure, tick, applyCitizenChoice)
 * pushen fire-and-forget Events in einen internen Puffer `game._pendingEvents`
 * und geben beim Aufruf die in DIESEM Aufruf frisch hinzugekommenen Events
 * zurück, damit die View Sound/Toast ansteuern kann. Alle Events werden
 * ausserdem permanent in `game.events` (Event-Stapel) gehalten.
 */
(function () {
  'use strict';

  /* ============================================================
     KONSTANTEN — Maßnahmen, Schwierigkeit, Achievements, Events
     ============================================================ */

  const MEASURES = [
    { id:'forest',      name:'Wald aufforsten',      emoji:'🌲', description:'Bäume binden langfristig CO₂. Sehr günstig im Unterhalt — die Naturlösung.',
      cost:12,  upkeep:0.5, co2Reduction:0.04, protection:0 },
    { id:'solar',       name:'Solaranlage',          emoji:'☀️', description:'Klein und günstig — sofort kaufbar. Wartung ist mittelhoch.',
      cost:40,  upkeep:2,   co2Reduction:0.14, protection:0, powerOutput:1 },
    { id:'wind',        name:'Windpark',             emoji:'🌬️', description:'Großes Windrad — hohe Anschaffung, dafür viel sauberer Strom.',
      cost:150, upkeep:4,   co2Reduction:0.50, protection:0, powerOutput:3 },
    { id:'green-roof',  name:'Gründächer',           emoji:'🏡', description:'Begrünte Dächer kühlen die Stadt. Keine jährlichen Kosten.',
      cost:30,  upkeep:0,   co2Reduction:0.04, protection:0 },
    { id:'bikes',       name:'Radwege-Netz',         emoji:'🚲', description:'Macht die Insel fahrradfreundlich. Günstig, kein Strom nötig.',
      cost:18,  upkeep:0.5, co2Reduction:0.06, protection:0 },
    { id:'mangrove',    name:'Mangrovenwald',        emoji:'🪸', description:'Bindet CO₂ UND schützt die Küste vor Erosion — beides in einem.',
      cost:35,  upkeep:1,   co2Reduction:0.06, protection:6 },
    { id:'sand-fill',   name:'Sand-Aufschüttung',    emoji:'🏖', description:'Künstlicher Strand. Sofort Schutz — wird aber jährlich vom Meer weggespült.',
      cost:50,  upkeep:0,   co2Reduction:0,    protection:18 },
    { id:'dike',        name:'Deich bauen',          emoji:'🌊', description:'Niedrige Hürde — schnell baubar, aber teuer pro cm Schutz.',
      cost:80,  upkeep:3,   co2Reduction:0,    protection:12 },
    { id:'sea-wall',    name:'Hochwasserschutz',     emoji:'🛡️', description:'Teuer, aber deutlich effizienter pro Mio €. Sehr starker Schutz.',
      cost:240, upkeep:6,   co2Reduction:0,    protection:50 },
    { id:'coal',        name:'Kohlekraftwerk',       emoji:'🏭', description:'Liefert viel Strom — billig und sofort. Aber viel CO₂-Ausstoß!',
      cost:20,  upkeep:1,   co2Reduction:-0.38, protection:0, powerOutput:4 },
    { id:'airport',     name:'Tourismus-Flughafen',  emoji:'✈️', description:'Bringt Touristen — aber Flugverkehr bedeutet viel CO₂.',
      cost:62,  upkeep:3,   co2Reduction:-0.22, protection:0 },
    { id:'cloud-seed',  name:'Wolken-Impfung',       emoji:'🌤️', description:'Geo-Engineering. Klingt nach Wundermittel — wirkt aber kaum.',
      cost:125, upkeep:4.5, co2Reduction:0.04, protection:0 },
  ];

  const DIFFICULTY = {
    1: { id:1, label:'Lernen', emoji:'🟢',
         startBudget:600, startPopulation:6000, incomePer10k:220,
         blackoutGrace:10, treesPerBlackout:1, co2BlackoutPerYear:0.15,
         climateDamageMul:0.35, popLossMul:0.35,
         skipCitizenEvents:['citizen-scientist','citizen-farmer','citizen-tourism','citizen-youth','citizen-industry','citizen-mountain'],
         citizenEventTickOffset:20 },
    2: { id:2, label:'Üben', emoji:'🟡',
         startBudget:480, startPopulation:6000, incomePer10k:155,
         blackoutGrace:0, treesPerBlackout:1, co2BlackoutPerYear:0.4,
         climateDamageMul:1.2, popLossMul:1.1 },
    3: { id:3, label:'Profi', emoji:'🔴',
         startBudget:220, startPopulation:7000, incomePer10k:110,
         blackoutGrace:0, treesPerBlackout:2, co2BlackoutPerYear:0.7,
         climateDamageMul:2.2, popLossMul:1.8 }
  };

  const ACHIEVEMENTS = [
    { key:'first_build',        icon:'🔨', title:'Erste Maßnahme',         desc:'Deine erste Klimaschutz-Maßnahme gebaut.' },
    { key:'first_wind',         icon:'🌬️', title:'Windkraft-Pionier',      desc:'Ersten Windpark errichtet.' },
    { key:'first_coast',        icon:'🛡️', title:'Küstenschützer*in',      desc:'Deich oder Hochwasserschutz gebaut.' },
    { key:'renewable_100',      icon:'☀️', title:'100 % Erneuerbar',      desc:'Keine fossilen Kraftwerke, Stromdeckung gesichert.' },
    { key:'under_1_5',          icon:'🌍', title:'Unter 1,5 °C',           desc:'Temperatur bleibt im Pariser Zielkorridor.' },
    { key:'forester',           icon:'🌲', title:'Baummeister*in',         desc:'Mindestens 10 Wald-Aufforstungen.' },
    { key:'survivor',           icon:'🏆', title:'Überlebenskünstler*in',  desc:'Das Jahr 2175 erreicht.' },
    { key:'frugal',             icon:'💰', title:'Sparfuchs',              desc:'Am Ende noch > 100 Mio € übrig.' },
  ];

  // Aufklärungs-Events bei zweifelhaften Maßnahmen, einmal pro Maßnahme.
  const FIRST_BUY_HINTS = {
    'cloud-seed': { icon:'⚠️', text:'Wolken-Impfung klingt groß — Forschung zeigt: kaum Klima-Wirkung. Klick für Details.', infoKey:'cloud_seeding' },
    'coal':       { icon:'🏭', text:'Kohle liefert schnell Strom — aber jede Tonne Kohle = sehr viel CO₂. Klick für Details.',   infoKey:'coal_power' },
    'airport':    { icon:'✈️', text:'Flughafen bringt Geld — Flugverkehr ist aber sehr CO₂-intensiv. Klick für Details.',        infoKey:'aviation' },
  };

  const CITIZEN_EVENTS = [
    { id:'citizen-fisher', tick:30, cond:g => true,
      char:'🎣', name:'Lina, Fischerin',
      msg:'Bürgermeister*in! Das Wasser wird wärmer, die Fische ziehen weg. Mein Boot ist alt — wir brauchen Hilfe, oder wir müssen aufgeben.',
      choices:[
        { title:'120 Mio € für moderne Boote',  desc:'Bevölkerung stabil. Budget −120 Mio €.',
          apply:(g, ctx) => { g.budget -= 120; } },
        { title:'Auf Tourismus umstellen',       desc:'Kostenlos, Extra-Einnahmen solange Strand. Mehr CO₂. Strand weg → Kollaps.',
          apply:(g, ctx) => { g.population -= 200; g.tourismMode = true; ctx.pushEvent('🏖','Die Insel stellt sich auf Tourismus um.','neutral'); } },
        { title:'Nichts tun',                    desc:'−400 Bevölkerung, aber kein Geldverlust.',
          apply:(g, ctx) => { g.population -= 400; } },
      ] },
    { id:'citizen-scientist', tick:18, cond:g => true,
      char:'👩‍🔬', name:'Dr. Hassan, Klimaforscherin',
      msg:'Wir haben ein neues Verfahren entwickelt — mit einem Forschungszentrum werden alle künftigen Maßnahmen 20 % billiger.',
      choices:[
        { title:'Forschungszentrum bauen (300 Mio €)', desc:'Alle künftigen Maßnahmen 20 % billiger.',
          apply:(g, ctx) => { g.budget -= 300; g.researchDiscount = 0.8; } },
        { title:'Antrag ablehnen', desc:'Nichts passiert.',
          apply:(g, ctx) => {} },
      ] },
    { id:'citizen-farmer', tick:28, cond:g => g.currentTemp > 15.8,
      char:'👨‍🌾', name:'Yusuf, Bauer',
      msg:'Die Felder vertrocknen! Letztes Jahr hatten wir kaum Ernte. Brauchen wir Bewässerung?',
      choices:[
        { title:'Bewässerungssystem bauen (180 Mio €)', desc:'Bevölkerung stabil.',
          apply:(g, ctx) => { g.budget -= 180; } },
        { title:'Salzresistente Sorten subventionieren (60 Mio €)', desc:'−100 Menschen, langfristig stabiler.',
          apply:(g, ctx) => { g.budget -= 60; g.population -= 100; } },
        { title:'Nichts tun', desc:'−500 Menschen ziehen weg.',
          apply:(g, ctx) => { g.population -= 500; } },
      ] },
    { id:'citizen-tourism', tick:40, cond:g => g.seaLevelCm > 15,
      char:'🏖️', name:'Maria, Hotelbesitzerin',
      msg:'Der Strand wird kleiner. Meine Hotels stehen halbleer. Wir müssen etwas gegen den Meeresspiegel tun!',
      choices:[
        { title:'Strand künstlich aufschütten (50 Mio €)', desc:'+5 cm Küstenschutz.',
          apply:(g, ctx) => { g.budget -= 50; g.protection += 5; g.hasSandBeach = true; } },
        { title:'„Wir können das Meer nicht aufhalten"', desc:'−300 Menschen wandern ab.',
          apply:(g, ctx) => { g.population -= 300; } },
      ] },
    { id:'citizen-youth', tick:56, cond:g => g.co2Reduction < 1,
      char:'👧', name:'Lia, 14 Jahre',
      msg:'Sie planen unsere Zukunft! Wir haben heute gestreikt. Machen Sie endlich ernst mit dem Klimaschutz.',
      choices:[
        { title:'„Ich verspreche, mehr zu tun" — 1 Solar gratis', desc:'+1 kostenlose Solaranlage.',
          apply:(g, ctx) => {
            if (!g.ownedMeasures.solar) g.ownedMeasures.solar = { count:0, instances:[] };
            g.ownedMeasures.solar.count++;
            const extra = ctx.resolveMeta ? ctx.resolveMeta('solar') : null;
            g.ownedMeasures.solar.instances.push(Object.assign({ builtAt: g.tick }, extra || {}));
            recalcEffects(g);
          } },
        { title:'Streik aussitzen', desc:'−150 Bevölkerung (Jugend wandert ab).',
          apply:(g, ctx) => { g.population -= 150; } },
      ] },
    { id:'citizen-industry', tick:76, cond:g => true,
      char:'🏭', name:'Konzernchef Vogel',
      msg:'Wir bringen Arbeitsplätze — wenn Sie uns Steuererleichterungen geben. Sonst wandern wir ab.',
      choices:[
        { title:'Subventionen zahlen (250 Mio €)', desc:'+500 Bevölkerung, +5 ppm CO₂.',
          apply:(g, ctx) => { g.budget -= 250; g.population += 500; g.co2Ppm += 5; } },
        { title:'Klar nein', desc:'−200 Bevölkerung verlässt die Insel.',
          apply:(g, ctx) => { g.population -= 200; } },
        { title:'Nur klimaneutral', desc:'Konzern lehnt ab, nichts passiert.',
          apply:(g, ctx) => {} },
      ] },
    { id:'citizen-mountain', tick:100, cond:g => g.currentTemp > 16.5,
      char:'🏔', name:'Anna vom Bergdorf',
      msg:'Der Gletscher schmilzt — bei Starkregen fließt das Wasser in unser Dorf. Wir brauchen Schutzwälle!',
      choices:[
        { title:'Lawinen-Schutzwall bauen (55 Mio €)', desc:'+4 cm Küstenschutz, Dorf sicher.',
          apply:(g, ctx) => { g.budget -= 55; g.protection += 4; g.hasMountainShield = true; } },
        { title:'Bergdorf evakuieren', desc:'−400 Menschen, aber Geld gespart.',
          apply:(g, ctx) => { g.population -= 400; } },
      ] },
  ];

  const CO2_FLOOR  = 350;
  const TEMP_FLOOR = 15.0;

  /* ============================================================
     HILFEN
     ============================================================ */

  function findMeasure(id) { return MEASURES.find(m => m.id === id); }

  function pushEvent(game, icon, text, type, infoKey) {
    const ev = { icon, text, type: type || 'neutral', tick: game.tick, infoKey: infoKey || null };
    game.events.unshift(ev);
    if (game.events.length > 40) game.events.pop();
    game._pendingEvents.push(ev);
    return ev;
  }

  function eventFiredOnce(game, id) {
    if (game.firedEvents.has(id)) return true;
    game.firedEvents.add(id);
    return false;
  }

  /* ============================================================
     KLIMA-PHYSIK
     ============================================================ */

  function computeTemperatureFromCO2(co2ppm) {
    const PRE = 280, CS = 3.0;
    const SOLAR = 1361, STEF = 5.67e-8, ALBEDO = 0.3;
    const absorbed = (SOLAR / 4) * (1 - ALBEDO);
    const tempNoGH = Math.pow(absorbed / STEF, 0.25) - 273.15;
    const NATURAL_GH = 33;
    return tempNoGH + NATURAL_GH + CS * Math.log2(co2ppm / PRE);
  }

  function computeClimate(game) {
    game.targetTemp = computeTemperatureFromCO2(game.co2Ppm);
    if (game.currentTemp <= 0) game.currentTemp = game.targetTemp;
    else game.currentTemp += (game.targetTemp - game.currentTemp) * 0.08;
    if (game.currentTemp < TEMP_FLOOR) game.currentTemp = TEMP_FLOOR;

    const deltaT = Math.max(0, game.currentTemp - 15);

    // Schneller Anteil (thermische Ausdehnung + schnelle Gletscherschmelze)
    const fastTarget = deltaT * 15;
    if (fastTarget > game.seaFastCm) game.seaFastCm += (fastTarget - game.seaFastCm) * 0.04;
    else                             game.seaFastCm += (fastTarget - game.seaFastCm) * 0.003;
    if (game.seaFastCm < 0) game.seaFastCm = 0;

    // Committed sea level rise (Eisschilde, Tiefenozean — irreversibel)
    game.committedSeaCm += deltaT * 0.15;

    game.seaLevelCm = game.seaFastCm + game.committedSeaCm;

    const effectiveRise = Math.max(0, game.seaLevelCm - game.protection);
    game.floodedPct = effectiveRise > 35 ? Math.min(100, (effectiveRise - 35) * 0.9) : 0;
  }

  /* ============================================================
     ABGELEITETE WERTE
     ============================================================ */

  function recalcEffects(game) {
    let co2Red=0, prot=0, upkeep=0, power=0, renewablePower=0;
    for (const id in game.ownedMeasures) {
      const m = findMeasure(id);
      if (!m) continue;
      const entry = game.ownedMeasures[id];
      co2Red += m.co2Reduction * entry.count;
      upkeep += m.upkeep * entry.count;
      power  += (m.powerOutput || 0) * entry.count;
      if (id !== 'coal') renewablePower += (m.powerOutput || 0) * entry.count;
      if (id === 'sand-fill') {
        for (const inst of entry.instances) {
          const age = Math.max(0, game.tick - inst.builtAt);
          prot += Math.max(0, m.protection - age * 2);
        }
      } else {
        prot += m.protection * entry.count;
      }
    }
    game.co2Reduction   = co2Red;
    game.protection     = prot;
    game.upkeepTotal    = upkeep;
    game.powerOutput    = power;
    game.renewablePower = renewablePower;
  }

  function getEmissionsThisYear(game) {
    const startEm = 2.5, peakEm = 3.2, ramp = 50;
    if (game.tick < ramp) return startEm + (peakEm - startEm) * (game.tick / ramp);
    return peakEm;
  }

  function getPowerDemand(game) {
    let demand = Math.max(0, Math.round(game.population / 1000));
    const airport = game.ownedMeasures.airport;
    if (airport && airport.count > 0) demand += airport.count * 3;
    const cs = game.ownedMeasures['cloud-seed'];
    if (cs && cs.count > 0) demand += cs.count * 2;
    return demand;
  }

  function getIncomeThisYear(game) {
    const popRatio = game.population / 10000;
    return Math.round(game.diff.incomePer10k * popRatio);
  }

  function getYearlyBalance(game) {
    const popRatio = game.population / 10000;
    const income = Math.round(game.diff.incomePer10k * popRatio);
    const tourism = (game.tourismMode && game.seaLevelCm < 40) ? Math.round(35 * popRatio) : 0;
    const upkeep = game.upkeepTotal;
    let climate = 0;
    if (game.seaLevelCm > 25) {
      const sev = Math.min(1, (game.seaLevelCm - 25) / 40);
      climate = Math.round(sev * 12 * game.diff.climateDamageMul);
    }
    return { income, tourism, upkeep, climate, net: income + tourism - upkeep - climate };
  }

  /* ============================================================
     ACHIEVEMENTS
     ============================================================ */

  function tryUnlockAchievement(game, key) {
    if (game.achievements.has(key)) return null;
    const a = ACHIEVEMENTS.find(x => x.key === key);
    if (!a) return null;
    game.achievements.add(key);
    game._pendingAchievements.push(a);
    return a;
  }

  function checkRenewable100(game) {
    if (game.achievements.has('renewable_100')) return;
    const hasCoal = game.ownedMeasures.coal && game.ownedMeasures.coal.count > 0;
    const hasAir  = game.ownedMeasures.airport && game.ownedMeasures.airport.count > 0;
    if (hasCoal || hasAir) return;
    const demand = getPowerDemand(game);
    if (game.powerOutput >= demand && demand > 0 && game.tick >= 5) tryUnlockAchievement(game, 'renewable_100');
  }

  /* ============================================================
     BEVÖLKERUNGSWACHSTUM
     ============================================================ */

  function computePopulationGrowth(game) {
    let measureCount = 0;
    for (const id in game.ownedMeasures) measureCount += game.ownedMeasures[id].count;
    const capBonus = Math.min(1.0, measureCount * 0.05);
    const popCap = game.diff.startPopulation * (1 + capBonus);
    if (game.population >= popCap) return;

    const tempFactor  = game.currentTemp < 16.5 ? 1 : game.currentTemp < 17.5 ? 0.4 : 0;
    const floodFactor = game.floodedPct   < 5   ? 1 : game.floodedPct   < 15   ? 0.5 : 0;
    const demand = getPowerDemand(game);
    const powerFactor  = demand === 0 ? 1 : (game.powerOutput >= demand ? 1 : 0.3);
    const budgetFactor = game.budget > 0 ? 1 : 0.2;

    const attractiveness = tempFactor * floodFactor * powerFactor * budgetFactor;
    if (attractiveness <= 0) return;

    const gap = popCap - game.population;
    const growth = Math.round(gap * 0.02 * attractiveness);
    if (growth > 0) game.population += growth;
  }

  /* ============================================================
     AKTIONEN — kaufen, abreissen
     ============================================================ */

  /**
   * buyMeasure(game, id, options = { resolveMeta, forceNegBalance })
   * Rückgabe:
   *   { ok: true, cost, events: [...], unlockedAchievements: [...],
   *     firstBuyHint: { icon, text, type, infoKey } | null }
   *   { ok: false, reason: 'not-enough-budget' | 'unknown-measure' | 'negative-balance' }
   *
   * Bei `negative-balance` entscheidet die View (Confirm-Dialog) und ruft
   * `buyMeasure(game, id, { forceNegBalance: true, resolveMeta })` erneut.
   */
  function buyMeasure(game, id, options) {
    options = options || {};
    const m = findMeasure(id);
    if (!m) return { ok: false, reason: 'unknown-measure' };

    const cost = Math.round(m.cost * game.researchDiscount);
    if (game.budget < cost) {
      return { ok: false, reason: 'not-enough-budget', cost };
    }

    // Einmalige Negativ-Bilanz-Warnung — View muss confirmieren
    if (!game.negBalanceWarningShown && !options.forceNegBalance) {
      const bal = getYearlyBalance(game);
      const netAfter = bal.income + bal.tourism - (bal.upkeep + m.upkeep) - bal.climate;
      if (netAfter < 0) {
        return { ok: false, reason: 'negative-balance', cost, netAfter, measure: m };
      }
    }
    if (options.forceNegBalance) game.negBalanceWarningShown = true;

    game._beginEvents();
    game._beginAchievements();

    game.budget -= cost;
    if (!game.ownedMeasures[id]) game.ownedMeasures[id] = { count:0, instances:[] };
    game.ownedMeasures[id].count++;
    const extra = options.resolveMeta ? options.resolveMeta(id) : null;
    game.ownedMeasures[id].instances.push(Object.assign({ builtAt: game.tick }, extra || {}));
    game.actionLog.push({ tick: game.tick, action:'buy', item:id, cost });
    recalcEffects(game);

    const evType = m.co2Reduction >= 0.05 ? 'good' : (m.co2Reduction < 0 ? 'bad' : 'neutral');
    pushEvent(game, m.emoji, m.name + ' gebaut (−' + cost + ' Mio €)', evType);

    if (!game.achievements.has('first_build')) tryUnlockAchievement(game, 'first_build');
    if (id === 'wind' && !game.achievements.has('first_wind')) tryUnlockAchievement(game, 'first_wind');
    if ((id === 'dike' || id === 'sea-wall' || id === 'mangrove') && !game.achievements.has('first_coast')) tryUnlockAchievement(game, 'first_coast');
    if (id === 'forest' && game.ownedMeasures.forest && game.ownedMeasures.forest.count >= 10 && !game.achievements.has('forester')) tryUnlockAchievement(game, 'forester');
    checkRenewable100(game);

    // First-Buy-Hint bei zweifelhaften Maßnahmen (einmal pro Maßnahme)
    let firstBuyHint = null;
    if (game.ownedMeasures[id].count === 1) {
      const trap = FIRST_BUY_HINTS[id];
      if (trap && !eventFiredOnce(game, 'first-buy-' + id)) {
        firstBuyHint = trap;
        pushEvent(game, trap.icon, trap.text, 'bad', trap.infoKey);
      }
    }

    return {
      ok: true,
      cost,
      measure: m,
      events: game._drainEvents(),
      unlockedAchievements: game._drainAchievements(),
      firstBuyHint,
    };
  }

  function demolishMeasure(game, id) {
    const m = findMeasure(id);
    if (!m) return { ok: false, reason: 'unknown-measure' };
    const entry = game.ownedMeasures[id];
    if (!entry || entry.count <= 0) return { ok: false, reason: 'nothing-to-demolish' };

    game._beginEvents();

    entry.count--;
    entry.instances.pop();
    if (entry.count === 0) delete game.ownedMeasures[id];
    const refund = Math.round(m.cost * game.researchDiscount * 0.5);
    game.budget += refund;
    game.actionLog.push({ tick: game.tick, action:'demolish', item:id, refund });
    recalcEffects(game);
    pushEvent(game, '🗑', m.name + ' abgerissen (+' + refund + ' Mio €)', 'neutral');

    return {
      ok: true,
      refund,
      events: game._drainEvents(),
    };
  }

  /* ============================================================
     BÜRGER-EVENTS
     ============================================================ */

  function maybeCitizenEvent(game) {
    if (game.pendingCitizenEventId) return null;
    const offset = game.diff.citizenEventTickOffset || 0;
    const skipped = new Set(game.diff.skipCitizenEvents || []);
    for (const ev of CITIZEN_EVENTS) {
      if (skipped.has(ev.id)) continue;
      if (game.citizenEventsFired.has(ev.id)) continue;
      if (game.tick !== ev.tick + offset) continue;
      if (!ev.cond(game)) continue;
      // Pending markieren, damit der gleiche Event nicht im nächsten Tick
      // erneut gezeigt wird. `citizenEventsFired` wird erst in
      // applyCitizenChoice gefüllt — so überlebt ein Refresh mitten im
      // Dialog: die View kann den Event wieder zeigen, solange der
      // Spieler:in noch nicht entschieden hat.
      game.pendingCitizenEventId = ev.id;
      return ev;
    }
    return null;
  }

  /**
   * applyCitizenChoice(game, eventId, choiceIndex, options = { resolveMeta })
   * Rückgabe: { ok, events, unlockedAchievements, choiceTitle, eventMeta }
   */
  function applyCitizenChoice(game, eventId, choiceIndex, options) {
    options = options || {};
    const ev = CITIZEN_EVENTS.find(e => e.id === eventId);
    if (!ev) return { ok: false, reason: 'unknown-citizen-event' };
    const choice = ev.choices[choiceIndex];
    if (!choice) return { ok: false, reason: 'invalid-choice' };

    game._beginEvents();
    game._beginAchievements();

    const ctx = {
      resolveMeta: options.resolveMeta,
      pushEvent: (icon, text, type, infoKey) => pushEvent(game, icon, text, type, infoKey),
    };
    choice.apply(game, ctx);
    game.actionLog.push({ tick: game.tick, action:'citizen', id: ev.id, choice: choiceIndex });
    recalcEffects(game);

    pushEvent(game, ev.char, ev.name + ': „' + choice.title + '"', 'neutral');
    // Erst nach der Entscheidung als endgültig erledigt markieren; so kann
    // ein Refresh mitten im Dialog den Event erneut anzeigen.
    game.citizenEventsFired.add(ev.id);
    game.pendingCitizenEventId = null;

    return {
      ok: true,
      choiceTitle: choice.title,
      eventMeta: { id: ev.id, char: ev.char, name: ev.name },
      events: game._drainEvents(),
      unlockedAchievements: game._drainAchievements(),
    };
  }

  function getCitizenEvent(eventId) {
    return CITIZEN_EVENTS.find(e => e.id === eventId) || null;
  }

  /* ============================================================
     NARRATIVE EVENTS (tickweise)
     ============================================================ */

  function tickNarrativeEvents(game) {
    if (game.tick === 4 && !eventFiredOnce(game, 'model-hint'))
      pushEvent(game, '📚', 'Diese Insel steht stellvertretend für das gesamte Weltklima.', 'neutral', 'wedge');
    if (game.tick === 10 && !eventFiredOnce(game, 'warning-1'))
      pushEvent(game, '⚠️', 'Wissenschaftler*innen warnen: CO₂ steigt weiter!', 'bad', 'co2');
    if (game.tick === 30 && game.currentTemp > 15.5 && !eventFiredOnce(game, 'warning-temp1'))
      pushEvent(game, '🌡️', 'Die Temperatur ist um 0,5 °C gestiegen.', 'bad', 'temperature');
    if (game.tick === 50 && game.currentTemp > 16 && !eventFiredOnce(game, 'warning-temp2'))
      pushEvent(game, '🌡️', '+1 °C: Hitzewellen werden häufiger.', 'bad', 'temperature');
    if (game.tick === 80 && game.currentTemp > 17 && !eventFiredOnce(game, 'warning-temp3'))
      pushEvent(game, '🔥', '+2 °C: Pariser Klimaziel überschritten!', 'bad', 'temperature');
    if (game.floodedPct > 5 && game.floodedPct < 10 && !eventFiredOnce(game, 'flood-1'))
      pushEvent(game, '🌊', 'Erste Häuser an der Küste sind betroffen.', 'bad', 'sealevel');
    if (game.floodedPct > 20 && !eventFiredOnce(game, 'flood-2'))
      pushEvent(game, '🌊', 'Massive Überflutung — viele verlieren ihr Zuhause!', 'bad', 'sealevel');
    if (game.seaLevelCm > 20 && !eventFiredOnce(game, 'vegetation-dying'))
      pushEvent(game, '🌿', 'Mangroven und Küstenwälder sterben — Salzwasser versalzt die Böden.', 'bad', 'vegetation');
    if (game.seaLevelCm > 35 && !eventFiredOnce(game, 'drinking-water'))
      pushEvent(game, '💧', 'Die Brunnen liefern immer mehr salziges Wasser.', 'bad', 'drinking_water');
    if (game.currentTemp > 16.8 && !eventFiredOnce(game, 'glacier-melt'))
      pushEvent(game, '🏔', 'Der Gletscher am Vulkan schrumpft merklich.', 'bad', 'glacier');
    if (game.tick === 60 && game.budget > 500 && !eventFiredOnce(game, 'praise-1'))
      pushEvent(game, '👏', 'Die Bevölkerung lobt deine kluge Haushaltsführung.', 'good');
    if (game.tick === 100 && game.co2Reduction > 3 && !eventFiredOnce(game, 'praise-co2'))
      pushEvent(game, '🌿', 'Deine Klimaschutzmaßnahmen zeigen Wirkung!', 'good');
    if (game.tick === 120 && game.currentTemp < 16 && !eventFiredOnce(game, 'praise-temp'))
      pushEvent(game, '🏆', 'Internationale Anerkennung für deine Klimapolitik!', 'good');

    const cs = game.ownedMeasures['cloud-seed'];
    if (cs && cs.count > 0 && cs.instances.length > 0 && !eventFiredOnce(game, 'cloud-seed-verdict')) {
      const builtAt = cs.instances[0].builtAt;
      if (game.tick - builtAt >= 30) {
        pushEvent(game, '☁️',
          'Wolken-Impfung nach 30 Jahren: reine Scheinlösung. Wirkung minimal, Geld besser in echte Klimaschutzmaßnahmen investieren.',
          'bad', 'cloud_seeding');
      }
    }
    const ap = game.ownedMeasures['airport'];
    if (ap && ap.count > 0 && ap.instances.length > 0 && !eventFiredOnce(game, 'airport-verdict')) {
      const builtAt = ap.instances[0].builtAt;
      if (game.tick - builtAt >= 20) {
        pushEvent(game, '✈️',
          'Tourismus-Bilanz: Einnahmen stehen gegen zusätzlichen CO₂-Ausstoß. Die Temperatur-Kurve wäre ohne Flughafen niedriger.',
          'bad', 'aviation');
      }
    }
    const coal = game.ownedMeasures['coal'];
    if (coal && coal.count > 0 && coal.instances.length > 0 && !eventFiredOnce(game, 'coal-verdict')) {
      const builtAt = coal.instances[0].builtAt;
      if (game.tick - builtAt >= 16) {
        pushEvent(game, '🏭',
          'Kohle-Bilanz: dein Kraftwerk hat jetzt '+(coal.count*Math.abs(-0.38).toFixed(2))+' ppm/Jahr zusätzliches CO₂ in die Luft gepumpt. Abschalten und auf Wind/Solar umsteigen?',
          'bad', 'coal_power');
      }
    }
  }

  /* ============================================================
     TICK — Ein Jahr simulieren
     ============================================================ */

  /**
   * tick(game) — Rückgabe:
   * {
   *   newEvents: [...],
   *   unlockedAchievements: [...],
   *   citizenEvent: citizenEventObject | null,
   *   endReason: 'won' | 'pleite' | 'ueberflutet' | null,
   *   timelineSample: { tick, co2, temp, budget, sea, power, demand }
   * }
   */
  function tick(game) {
    if (game.levelCompleted) return { newEvents: [], unlockedAchievements: [], citizenEvent: null, endReason: null };

    game._beginEvents();
    game._beginAchievements();

    recalcEffects(game);

    const popRatio = game.population / 10000;
    game.budget += Math.round(game.diff.incomePer10k * popRatio);

    if (game.tourismMode) {
      if (game.seaLevelCm < 40) {
        game.budget += Math.round(35 * popRatio);
        game.co2Ppm += 0.6;
      } else {
        game.population -= Math.round(60 * popRatio);
        if (!eventFiredOnce(game, 'tourism-collapse')) {
          pushEvent(game, '🏖', 'Der Strand ist vom Meer verschluckt — der Tourismus bricht zusammen!', 'bad', 'sealevel');
        }
      }
    }

    game.budget -= game.upkeepTotal;

    const emissions = getEmissionsThisYear(game);
    const reduction = game.co2Reduction;
    game.co2Ppm = Math.max(CO2_FLOOR, game.co2Ppm + emissions - reduction);

    computeClimate(game);

    // Strom-Bilanz / Blackout
    const powerDemand = getPowerDemand(game);
    if (powerDemand > game.powerOutput) {
      game.blackoutStreak++;
      if (!eventFiredOnce(game, 'blackout-first')) {
        pushEvent(game, '⚡', 'Zu wenig Strom! Baue schnell ein Kraftwerk, sonst wird es bald kalt.', 'bad', 'blackout');
      }
      if (game.blackoutStreak > game.diff.blackoutGrace) {
        game.treesChoppedForHeat += game.diff.treesPerBlackout;
        game.co2Ppm += game.diff.co2BlackoutPerYear;
        const forest = game.ownedMeasures.forest;
        if (forest && forest.count > 0) {
          const kill = Math.min(forest.count, game.diff.treesPerBlackout);
          forest.count -= kill;
          for (let k=0; k<kill; k++) forest.instances.pop();
          if (forest.count === 0) delete game.ownedMeasures.forest;
        }
      }
      const thr = game.diff.blackoutGrace + 3;
      if (game.blackoutStreak >= thr) {
        const drift = Math.round(25 * popRatio * game.diff.popLossMul * (game.blackoutStreak - thr + 1));
        game.population -= drift;
      }
    } else {
      if (game.blackoutStreak > game.diff.blackoutGrace && !eventFiredOnce(game, 'blackout-recovered')) {
        pushEvent(game, '💡', 'Der Strom ist wieder da. Die gefällten Bäume kommen aber nicht zurück.', 'neutral');
      }
      game.blackoutStreak = 0;
    }

    // Überflutungs-Schäden
    if (game.floodedPct > 0) {
      game.population -= Math.round(game.floodedPct * 5 * game.diff.popLossMul);
    }
    if (game.seaLevelCm > 25) {
      const sev = Math.min(1, (game.seaLevelCm - 25) / 40);
      game.budget -= Math.round(sev * 12 * game.diff.climateDamageMul);
      if (Math.random() < sev * game.diff.popLossMul) {
        game.population -= Math.round(sev * 15 * game.diff.popLossMul);
      }
    }

    if (game.population < 0) game.population = 0;

    tickNarrativeEvents(game);

    const citizenEv = maybeCitizenEvent(game);

    computePopulationGrowth(game);

    const timelineSample = {
      tick: game.tick,
      co2: game.co2Ppm,
      temp: game.currentTemp,
      budget: game.budget,
      sea: game.seaLevelCm,
      power: game.powerOutput,
      demand: getPowerDemand(game),
    };
    game.timeline.push(timelineSample);

    checkRenewable100(game);
    if (game.currentTemp < 16.5 && game.tick > 30 && !game.achievements.has('under_1_5')) tryUnlockAchievement(game, 'under_1_5');

    game.tick++;

    // End-Bedingungen
    let endReason = null;
    const budgetLoss = (game.levelConfig && typeof game.levelConfig.budgetLoss === 'number') ? game.levelConfig.budgetLoss : -500;
    if (game.tick >= game.maxTick) {
      endReason = 'won';
      game.levelCompleted = true;
      game.levelWon = true;
      tryUnlockAchievement(game, 'survivor');
      if (game.budget > 100) tryUnlockAchievement(game, 'frugal');
    } else if (game.budget < budgetLoss) {
      endReason = 'pleite';
      game.levelCompleted = true;
      game.levelWon = false;
    } else if (game.floodedPct > 50) {
      endReason = 'ueberflutet';
      game.levelCompleted = true;
      game.levelWon = false;
    }

    return {
      newEvents: game._drainEvents(),
      unlockedAchievements: game._drainAchievements(),
      citizenEvent: citizenEv,
      endReason,
      timelineSample,
    };
  }

  /* ============================================================
     SPIEL ERZEUGEN / ZURÜCKSETZEN
     ============================================================ */

  function createGame(levelId, opts) {
    opts = opts || {};
    const diff = DIFFICULTY[levelId] || DIFFICULTY[1];
    const game = {
      // Simulations-State
      tick: 0,
      maxTick: opts.maxTick || 150,
      startYear: opts.startYear || 2025,
      difficulty: levelId,
      diff,
      levelConfig: opts.levelConfig || null,

      budget: diff.startBudget,
      population: diff.startPopulation,
      co2Ppm: 425,
      currentTemp: 15.5,
      targetTemp: 15.5,
      seaLevelCm: 0, floodedPct: 0,
      seaFastCm: 0, committedSeaCm: 0,

      ownedMeasures: {},
      co2Reduction: 0, protection: 0, upkeepTotal: 0, powerOutput: 0, renewablePower: 0,

      // Stapel + Historie
      timeline: [],
      actionLog: [],
      events: [],
      firedEvents: new Set(),
      citizenEventsFired: new Set(),
      achievements: new Set(),

      // Flags
      researchDiscount: 1.0,
      tourismMode: false, hasMountainShield: false, hasSandBeach: false,
      negBalanceWarningShown: false,
      blackoutStreak: 0, treesChoppedForHeat: 0,

      pendingCitizenEventId: null,
      levelCompleted: false, levelWon: false,
      startTime: Date.now(),
      lastProgressTick: -1,

      // Event-/Achievement-Puffer, pro Aufruf geleert (internal)
      _pendingEvents: [],
      _pendingAchievements: [],
      _beginEvents: function()       { this._pendingEvents = []; },
      _drainEvents: function()       { const out = this._pendingEvents; this._pendingEvents = []; return out; },
      _beginAchievements: function() { this._pendingAchievements = []; },
      _drainAchievements: function() { const out = this._pendingAchievements; this._pendingAchievements = []; return out; },
    };

    // Klima konsistent initialisieren
    game.currentTemp = computeTemperatureFromCO2(game.co2Ppm);
    game.targetTemp  = game.currentTemp;
    recalcEffects(game);
    return game;
  }

  /* ============================================================
     SERIALIZE / DESERIALIZE
     ============================================================ */

  function serialize(game) {
    return {
      v: 4,
      tick: game.tick,
      maxTick: game.maxTick,
      startYear: game.startYear,
      difficulty: game.difficulty,
      levelConfig: game.levelConfig,
      budget: game.budget, population: game.population,
      co2Ppm: game.co2Ppm, currentTemp: game.currentTemp, targetTemp: game.targetTemp,
      seaLevelCm: game.seaLevelCm, floodedPct: game.floodedPct,
      seaFastCm: game.seaFastCm, committedSeaCm: game.committedSeaCm,
      ownedMeasures: game.ownedMeasures,
      timeline: game.timeline, events: game.events,
      firedEvents: Array.from(game.firedEvents),
      citizenEventsFired: Array.from(game.citizenEventsFired),
      achievements: Array.from(game.achievements),
      pendingCitizenEventId: game.pendingCitizenEventId,
      researchDiscount: game.researchDiscount,
      tourismMode: game.tourismMode, hasMountainShield: game.hasMountainShield,
      hasSandBeach: game.hasSandBeach,
      negBalanceWarningShown: game.negBalanceWarningShown,
      blackoutStreak: game.blackoutStreak,
      treesChoppedForHeat: game.treesChoppedForHeat,
      lastProgressTick: game.lastProgressTick,
      levelCompleted: game.levelCompleted, levelWon: game.levelWon,
      savedAt: Date.now(),
    };
  }

  function deserialize(data) {
    if (!data || typeof data !== 'object') return null;
    const game = createGame(data.difficulty || 1, {
      maxTick: data.maxTick,
      startYear: data.startYear,
      levelConfig: data.levelConfig,
    });
    game.tick = data.tick | 0;
    game.budget = data.budget || 0;
    game.population = data.population || 0;
    game.co2Ppm = data.co2Ppm || 425;
    game.currentTemp = data.currentTemp || 15.5;
    game.targetTemp  = data.targetTemp  || game.currentTemp;
    game.seaLevelCm = data.seaLevelCm || 0;
    game.seaFastCm = data.seaFastCm || 0;
    game.committedSeaCm = data.committedSeaCm || 0;
    game.floodedPct = data.floodedPct || 0;
    game.ownedMeasures = data.ownedMeasures || {};
    game.timeline = Array.isArray(data.timeline) ? data.timeline : [];
    game.events   = Array.isArray(data.events)   ? data.events   : [];
    game.firedEvents        = new Set(data.firedEvents || []);
    game.citizenEventsFired = new Set(data.citizenEventsFired || []);
    game.achievements       = new Set(data.achievements || []);
    game.pendingCitizenEventId = data.pendingCitizenEventId || null;
    game.researchDiscount   = data.researchDiscount != null ? data.researchDiscount : 1.0;
    game.tourismMode        = !!data.tourismMode;
    game.hasMountainShield  = !!data.hasMountainShield;
    game.hasSandBeach       = !!data.hasSandBeach;
    game.negBalanceWarningShown = !!data.negBalanceWarningShown;
    game.blackoutStreak     = data.blackoutStreak || 0;
    game.treesChoppedForHeat= data.treesChoppedForHeat || 0;
    game.lastProgressTick   = data.lastProgressTick != null ? data.lastProgressTick : -1;
    game.levelCompleted     = !!data.levelCompleted;
    game.levelWon           = !!data.levelWon;
    recalcEffects(game);
    return game;
  }

  /* ============================================================
     PUBLIC API
     ============================================================ */

  window.KlimaEngine = {
    // Konstanten
    MEASURES,
    DIFFICULTY,
    ACHIEVEMENTS,
    CITIZEN_EVENTS,
    FIRST_BUY_HINTS,
    CO2_FLOOR,
    TEMP_FLOOR,

    // Lebenszyklus
    createGame,
    serialize,
    deserialize,

    // Ableitung
    recalcEffects,
    computeTemperatureFromCO2,
    computeClimate,
    getEmissionsThisYear,
    getPowerDemand,
    getIncomeThisYear,
    getYearlyBalance,
    computePopulationGrowth,

    // Aktionen
    buyMeasure,
    demolishMeasure,

    // Tick
    tick,

    // Bürger-Events
    maybeCitizenEvent,
    applyCitizenChoice,
    getCitizenEvent,

    // Achievements
    checkRenewable100,
    tryUnlockAchievement,

    // Helpers
    findMeasure,
    pushEvent,
  };
})();
