/**
 * Simulations-Engine — Generischer Mechanik-Layer
 *
 * Basisklasse für interaktive Simulationen:
 * - Zeit-Loop (real-time mit pause/play/speed)
 * - Tick-System (jeder Tick = 1 simulierte Zeiteinheit, z.B. 1 Jahr/Monat/Sekunde)
 * - Score & Resources
 * - Erfolgs-/Misserfolgs-Zustände
 * - Tutorial-Phasen mit schrittweiser Freischaltung von Features
 * - Save/Resume
 * - Zeitreihen-Daten für Graphen
 *
 * Designprinzipien:
 * - Fachlich korrekt: alle Werte sind echt, keine Fantasiedaten
 * - Erlebbar: schmaler Grat zwischen Erfolg/Misserfolg
 * - Klassenkompatibel: jederzeit abschließbar, speicherbar
 * - Transparent: alle Werte sind sichtbar (keine versteckten Variablen)
 *
 * Hinweis: Klassen- und Variablennamen tragen weiter "Game..." als
 * technische Bezeichnung. User-facing Texte sprechen aber von
 * "Simulation" — siehe architektur.md.
 */

export type GameState = 'tutorial' | 'playing' | 'paused' | 'won' | 'lost' | 'complete'
export type GameSpeed = 0 | 1 | 2 | 4   // 0 = pause, 1 = normal, 2 = schnell, 4 = sehr schnell

export interface GameMeta {
  id: string
  title: string
  description: string

  /** Wie lange dauert ein Tick in echten Millisekunden bei Speed = 1? */
  msPerTick: number
  /** Was repräsentiert ein Tick in der Simulation? z.B. "Monat", "Jahr", "Sekunde" */
  tickUnit: string
  /** Maximale Anzahl Ticks (= Spielende durch Zeit) — 0 = unbegrenzt */
  maxTicks: number

  /** Wieviele Tutorial-Schritte hat das Spiel? */
  tutorialSteps: number
}

export interface Resource {
  id: string
  name: string
  icon: string
  current: number
  initial: number
  min?: number
  max?: number
  unit: string
  format?: (v: number) => string
}

export interface GoalDef {
  id: string
  title: string
  description: string
  /** Bedingung erfüllt? Erhält Game-Snapshot */
  check: (game: GameEngine) => boolean
  /** Optional: numerischer Fortschritt 0-100 */
  progress?: (game: GameEngine) => number
  /** Verpflichtend für Win, oder Bonus? */
  required: boolean
}

export interface TimelineEntry {
  tick: number
  values: Record<string, number>
}

export interface TutorialStep {
  /** Wann zeigt sich dieser Step (welcher Tick) */
  triggerTick: number
  title: string
  text: string
  /** Optional: welche UI-Elemente werden in diesem Step sichtbar */
  unlocks?: string[]
}

export interface GameEvent {
  tick: number
  type: string
  text: string
  severity: 'info' | 'success' | 'warning' | 'danger'
  /** Optional: Schlüssel für ein Info-Overlay (kindgerechter Erklärtext) */
  infoKey?: string
}

/**
 * Eine Bürger-Beschwerde / Forderung mit interaktiver Wahl.
 * Wird vom Spiel ausgelöst und vom Spieler entschieden — die Wahl
 * hat reale Konsequenzen für Resourcen.
 */
export interface CitizenChoice {
  label: string
  description?: string
  /** Wird ausgeführt, wenn der Spieler diese Option wählt */
  apply: (game: GameEngine) => void
}

export interface CitizenEvent {
  id: string
  /** Welcher Bürger meldet sich? Emoji für Avatar */
  character: string
  /** Kurztitel, z.B. „Forderung der Fischer" */
  title: string
  /** Sprechblase / Forderung */
  message: string
  /** 2–3 Optionen */
  choices: CitizenChoice[]
}

export interface GameSnapshot {
  state: GameState
  speed: GameSpeed
  tick: number
  tutorialStep: number
  resources: Record<string, number>
  variables: Record<string, number>
  events: GameEvent[]
  timeline: TimelineEntry[]
  goals: Array<{ id: string; achieved: boolean; progress: number }>
}

/**
 * Basisklasse für ein spielbares GeoGraSim-Modul
 */
export abstract class GameEngine {
  readonly meta: GameMeta

  protected state: GameState = 'tutorial'
  protected speed: GameSpeed = 1
  protected tick = 0
  protected tutorialStep = 0
  protected resources: Map<string, Resource> = new Map()
  protected variables: Record<string, number> = {}
  protected events: GameEvent[] = []
  protected timeline: TimelineEntry[] = []
  protected goals: GoalDef[] = []
  protected unlockedFeatures: Set<string> = new Set()
  protected tutorialSteps: TutorialStep[] = []
  /** Aktuell anstehende Bürger-Beschwerde, bis sie der Spieler beantwortet hat */
  protected pendingCitizenEvent: CitizenEvent | null = null

  private animFrame = 0
  private lastTickTime = 0
  private listeners: Set<() => void> = new Set()

  constructor(meta: GameMeta) {
    this.meta = meta
  }

  // ==========================================================
  // Setup (subclass calls these in constructor)
  // ==========================================================

  protected addResource(r: Omit<Resource, 'current'> & { current?: number }): void {
    this.resources.set(r.id, { ...r, current: r.current ?? r.initial })
  }

  protected addGoal(g: GoalDef): void {
    this.goals.push(g)
  }

  protected setTutorial(steps: TutorialStep[]): void {
    this.tutorialSteps = steps
  }

  protected setVariable(name: string, value: number): void {
    this.variables[name] = value
    this.notify()
  }

  // ==========================================================
  // Public API
  // ==========================================================

  getResource(id: string): number {
    return this.resources.get(id)?.current ?? 0
  }

  setResource(id: string, value: number): void {
    const r = this.resources.get(id)
    if (!r) return
    r.current = Math.max(r.min ?? -Infinity, Math.min(r.max ?? Infinity, value))
    this.notify()
  }

  changeResource(id: string, delta: number): void {
    this.setResource(id, this.getResource(id) + delta)
  }

  getVariable(name: string): number {
    return this.variables[name] ?? 0
  }

  /** Spieler ändert eine Variable über UI (Slider, Button) */
  playerSetVariable(name: string, value: number): void {
    this.variables[name] = value
    this.onPlayerAction(name, value)
    this.notify()
  }

  // ==========================================================
  // Bürger-Beschwerden
  // ==========================================================

  /** Subclass kann ein Citizen-Event in die Warteschlange legen */
  protected triggerCitizenEvent(event: CitizenEvent): void {
    if (this.pendingCitizenEvent) return  // nur eines auf einmal
    this.pendingCitizenEvent = event
    this.notify()
  }

  /** Aktuell anstehende Bürger-Beschwerde (oder null) */
  getPendingCitizenEvent(): CitizenEvent | null {
    return this.pendingCitizenEvent
  }

  /** Spieler hat eine Wahl getroffen */
  resolveCitizenEvent(choiceIndex: number): void {
    const ev = this.pendingCitizenEvent
    if (!ev) return
    const choice = ev.choices[choiceIndex]
    if (!choice) return
    choice.apply(this)
    this.addEvent(ev.id, `💬 ${ev.title}: „${choice.label}"`, 'info')
    this.pendingCitizenEvent = null
    this.notify()
  }

  // ==========================================================
  // Game loop
  // ==========================================================

  start(): void {
    if (this.state === 'tutorial' && this.tutorialSteps.length > 0) {
      // Im Tutorial bleiben, manuell weiter
      this.notify()
      return
    }
    this.state = 'playing'
    this.lastTickTime = performance.now()
    this.loop()
  }

  pause(): void {
    if (this.state === 'playing') {
      this.state = 'paused'
      cancelAnimationFrame(this.animFrame)
      this.notify()
    }
  }

  resume(): void {
    if (this.state === 'paused') {
      this.state = 'playing'
      this.lastTickTime = performance.now()
      this.loop()
    }
  }

  setSpeed(s: GameSpeed): void {
    this.speed = s
    if (s === 0) this.pause()
    else if (this.state === 'paused' || this.state === 'tutorial') {
      this.state = 'playing'
      this.lastTickTime = performance.now()
      this.loop()
    }
    this.notify()
  }

  /** Vorzeitig abschließen — speichert finalen Zustand */
  finish(): void {
    cancelAnimationFrame(this.animFrame)
    if (this.state !== 'won' && this.state !== 'lost') {
      this.state = 'complete'
    }
    this.notify()
  }

  /** Tutorial einen Schritt weiter */
  nextTutorialStep(): void {
    this.tutorialStep++
    if (this.tutorialStep >= this.tutorialSteps.length) {
      // Tutorial fertig — Spiel beginnt
      this.state = 'playing'
      this.lastTickTime = performance.now()
      this.loop()
    } else {
      const step = this.tutorialSteps[this.tutorialStep]
      if (step.unlocks) step.unlocks.forEach(u => this.unlockedFeatures.add(u))
    }
    this.notify()
  }

  isFeatureUnlocked(name: string): boolean {
    return this.unlockedFeatures.has(name)
  }

  // ==========================================================
  // Save / Resume
  // ==========================================================

  /** Save-Format-Version. Bei Breaking Changes erhöhen — ältere saves werden dann verworfen. */
  static readonly SAVE_VERSION = 2

  serialize(): string {
    return JSON.stringify({
      v: GameEngine.SAVE_VERSION,
      meta: this.meta.id,
      state: this.state,
      tick: this.tick,
      tutorialStep: this.tutorialStep,
      speed: this.speed,
      variables: this.variables,
      resources: Object.fromEntries(
        Array.from(this.resources.entries()).map(([k, v]) => [k, v.current])
      ),
      events: this.events,
      timeline: this.timeline,
      unlockedFeatures: Array.from(this.unlockedFeatures),
      // Subclass-spezifischer Zustand (interne private Felder)
      sub: this.serializeSubclass(),
    })
  }

  deserialize(json: string): boolean {
    try {
      const data = JSON.parse(json)
      if (data.meta !== this.meta.id) return false
      // Alte/inkompatible saves komplett verwerfen
      if (!data.v || data.v < GameEngine.SAVE_VERSION) return false
      this.state = data.state
      this.tick = data.tick
      this.tutorialStep = data.tutorialStep
      this.speed = data.speed
      this.variables = data.variables
      for (const [k, v] of Object.entries(data.resources as Record<string, number>)) {
        const r = this.resources.get(k)
        if (r) r.current = v
      }
      this.events = data.events
      this.timeline = data.timeline
      this.unlockedFeatures = new Set(data.unlockedFeatures)
      // Subclass-spezifische Felder zurücksetzen
      if (data.sub) this.deserializeSubclass(data.sub)
      this.notify()
      return true
    } catch {
      return false
    }
  }

  /** Override in Subklasse, um zusätzliche interne Felder zu serialisieren. */
  protected serializeSubclass(): Record<string, unknown> {
    return {}
  }

  /** Override in Subklasse, um zusätzliche interne Felder wiederherzustellen. */
  protected deserializeSubclass(_data: Record<string, unknown>): void {
    // default: nichts
  }

  // ==========================================================
  // Snapshot for UI
  // ==========================================================

  getSnapshot(): GameSnapshot {
    return {
      state: this.state,
      speed: this.speed,
      tick: this.tick,
      tutorialStep: this.tutorialStep,
      resources: Object.fromEntries(
        Array.from(this.resources.entries()).map(([k, r]) => [k, r.current])
      ),
      variables: { ...this.variables },
      events: [...this.events],
      timeline: [...this.timeline],
      goals: this.goals.map(g => ({
        id: g.id,
        achieved: g.check(this),
        progress: g.progress?.(this) ?? (g.check(this) ? 100 : 0),
      })),
    }
  }

  getResourcesArray(): Resource[] {
    return Array.from(this.resources.values())
  }

  getCurrentTutorialStep(): TutorialStep | null {
    return this.tutorialSteps[this.tutorialStep] || null
  }

  getEvents(limit = 5): GameEvent[] {
    return this.events.slice(-limit).reverse()
  }

  // ==========================================================
  // Subscribe (UI-Updates)
  // ==========================================================

  subscribe(fn: () => void): () => void {
    this.listeners.add(fn)
    return () => this.listeners.delete(fn)
  }

  protected notify(): void {
    this.listeners.forEach(fn => fn())
  }

  // ==========================================================
  // Main loop (internal)
  // ==========================================================

  private loop = (): void => {
    if (this.state !== 'playing') return

    const now = performance.now()
    const dt = now - this.lastTickTime
    const interval = this.meta.msPerTick / this.speed

    if (dt >= interval) {
      this.lastTickTime = now
      this.runTick()
    }

    this.animFrame = requestAnimationFrame(this.loop)
  }

  private runTick(): void {
    this.tick++

    // Subclass simulates one tick
    this.simulateTick()

    // Record timeline
    this.recordTimeline()

    // Check tutorial triggers
    this.checkTutorialTriggers()

    // Check win/loss
    this.checkEndConditions()

    // Max ticks
    if (this.meta.maxTicks > 0 && this.tick >= this.meta.maxTicks) {
      if (this.state !== 'won' && this.state !== 'lost') {
        this.checkEndConditions()  // Final check
        if (this.state === 'playing') this.state = 'complete'
      }
    }

    this.notify()
  }

  protected addEvent(
    type: string,
    text: string,
    severity: GameEvent['severity'] = 'info',
    infoKey?: string,
  ): void {
    this.events.push({ tick: this.tick, type, text, severity, infoKey })
    if (this.events.length > 50) this.events.shift()
  }

  private recordTimeline(): void {
    const values: Record<string, number> = {}
    for (const [k, r] of this.resources) values[k] = r.current
    for (const [k, v] of Object.entries(this.variables)) values[k] = v
    this.timeline.push({ tick: this.tick, values })
    // Cap timeline length
    if (this.timeline.length > 500) this.timeline.shift()
  }

  private checkTutorialTriggers(): void {
    // Skip — tutorial is manual progression
  }

  private checkEndConditions(): void {
    const requiredGoals = this.goals.filter(g => g.required)
    const allRequired = requiredGoals.every(g => g.check(this))
    if (allRequired && requiredGoals.length > 0) {
      this.state = 'won'
      this.addEvent('win', 'Du hast alle Hauptziele erreicht!', 'success')
      cancelAnimationFrame(this.animFrame)
    }

    // Subclass can override loss conditions via checkLossCondition
    if (this.checkLossCondition()) {
      this.state = 'lost'
      cancelAnimationFrame(this.animFrame)
    }
  }

  // ==========================================================
  // Subclass hooks
  // ==========================================================

  /** Wird bei jedem Tick aufgerufen — Subklasse simuliert hier ein Zeitintervall */
  protected abstract simulateTick(): void

  /** Reagiere auf Spieleraktion (z.B. Slider-Bewegung, Bau-Klick) */
  protected onPlayerAction(_name: string, _value: number): void {}

  /** Soll das Spiel verloren sein? Override in Subklasse */
  protected checkLossCondition(): boolean {
    return false
  }
}
