//! Dünner Adapter um die `bladeink`-Crate, gekoppelt an unseren `kv::Store`.
//!
//! Bei jedem `cont()` läuft ein bidirektionaler Sync:
//!   - vor `continue`: KV-Werte ins `variables_state` der Story spiegeln
//!     (Skript sieht aktuelle Spielvariablen)
//!   - nach `continue`: **alle** im Skript deklarierten `VAR`s zurücklesen
//!     (Auto-Import). So wandern Werte wie `snaks` oder `favor_nails` ohne
//!     Registrierung automatisch ins nächste per `start_ink` verkettete
//!     Skript — die Autorin legt einfach `VAR`s an, die Engine trägt sie.
//!
//! Ausnahme: `VAR`s mit `_`-Prefix sind skript-lokal und werden in keine
//! Richtung synchronisiert. KV-Variablen, die das Skript nicht als `VAR`
//! deklariert, lehnt bladeink beim `set_variable` ab — wir ignorieren das
//! still.
//!
//! bladeink bietet keine öffentliche Enumeration der Globals; die
//! Deklarationsliste wird deshalb beim Laden per Quelltext-Scan aus dem
//! kompilierten JSON gezogen (siehe [`global_var_names`]).

use std::fs::read_to_string;

use bladeink::story::Story as BladeStory;

use crate::engine::kv::Store;

pub enum StoryState {
    Text(String),
    Choice { prompt: String, options: Vec<String> },
    End,
}

pub struct Story {
    inner: BladeStory,
    tags:  Vec<String>,
    /// Im Skript deklarierte globale `VAR`s (ohne `_`-lokale) — die Menge,
    /// die `sync_to_kv` in den KV exportiert.
    vars:  Vec<String>,
    /// Skript-Pfad, nur für Fehlermeldungen.
    path:  String,
}

/// Alle globalen `VAR`-Namen aus kompiliertem Ink-JSON ziehen. Inklecate
/// kodiert jede Global-Zuweisung — die Deklaration in `"global decl"` ebenso
/// wie jedes `~ var = …` — als `{"VAR=":"name"}`; ein simpler Scan über den
/// Quelltext findet also genau die Globals (temp-Variablen kodieren als
/// `"temp="` und Reads als `"VAR?"`). `_`-Prefix = skript-lokal → aussortiert.
fn global_var_names(src: &str) -> Vec<String> {
    const PAT: &str = "\"VAR=\":\"";
    let mut names: Vec<String> = Vec::new();
    let mut rest = src;
    while let Some(i) = rest.find(PAT) {
        rest = &rest[i + PAT.len()..];
        if let Some(end) = rest.find('"') {
            let name = &rest[..end];
            if !name.starts_with('_') && !names.iter().any(|n| n == name) {
                names.push(name.to_string());
            }
            rest = &rest[end..];
        }
    }
    names
}

impl Story {
    /// Lädt und parst ein Ink-Skript. Fehlende oder kaputte Datei liefert
    /// `Err(msg)` statt zu panicken — ein Tippfehler in einem `start_ink`-Tag
    /// darf das laufende Spiel nicht crashen; der Aufrufer meldet den Fehler.
    pub fn load(path: &str, kv: &Store) -> Result<Self, String> {
        let src = read_to_string(path)
            .map_err(|e| format!("ink load {path}: {e}"))?;
        Self::from_src(&src, path, kv)
    }

    /// Wie [`load`], nur direkt aus dem JSON-Quelltext — `path` dient
    /// ausschließlich Fehlermeldungen. Eigener Schritt, damit Tests ohne
    /// Dateisystem arbeiten können.
    fn from_src(src: &str, path: &str, kv: &Store) -> Result<Self, String> {
        let inner = BladeStory::new(src)
            .map_err(|e| format!("ink parse {path}: {e:?}"))?;
        let mut s = Self {
            inner,
            tags: Vec::new(),
            vars: global_var_names(src),
            path: path.to_string(),
        };
        s.sync_from_kv(kv);
        Ok(s)
    }

    pub fn take_tags(&mut self) -> Vec<String> { std::mem::take(&mut self.tags) }

    pub fn cont(&mut self, kv: &mut Store) -> StoryState {
        self.sync_from_kv(kv);

        // continue_maximally ruft intern reset_output() vor jedem cont() —
        // damit überschriebe der Tag der letzten Zeile alle vorherigen.
        // Wir loopen selbst und sammeln Tags nach jedem Schritt ein, sonst
        // gehen z.B. `# set has_key true` am Knoten-Anfang bei mehrzeiligem
        // Text verloren.
        let mut text = String::new();
        while self.inner.can_continue() {
            // Runtime-Fehler (z.B. „ran out of content" bei einem Skript-Pfad
            // ohne Divert) beenden den Dialog und melden nach stderr — ein
            // Autoren-Fehler im Ink darf das laufende Spiel nicht crashen.
            let chunk = match self.inner.cont() {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("[ink] {}: {e:?}", self.path);
                    return StoryState::End;
                }
            };
            text.push_str(&chunk);
            if let Ok(t) = self.inner.get_current_tags() {
                self.tags.extend(t);
            }
        }
        let text = text.trim().to_string();

        self.sync_to_kv(kv);

        let choices = self.inner.get_current_choices();
        if !choices.is_empty() {
            let options = choices.iter().map(|c| c.text.clone()).collect();
            return StoryState::Choice { prompt: text, options };
        }
        if !text.is_empty() { return StoryState::Text(text); }
        StoryState::End
    }

    pub fn choose(&mut self, i: usize) {
        // Fehler (z.B. Index außerhalb der Choices) nur melden — der nächste
        // `cont()` präsentiert dann denselben Zustand erneut.
        if let Err(e) = self.inner.choose_choice_index(i) {
            eprintln!("[ink] {}: choose {i}: {e:?}", self.path);
        }
    }

    fn sync_from_kv(&mut self, kv: &Store) {
        for (name, value) in kv {
            if name.starts_with('_') { continue; } // skript-lokal
            // set_variable schlägt fehl wenn der Name kein deklariertes
            // VAR im Skript ist — still ignorieren.
            let _ = self.inner.set_variable(name, value);
        }
    }

    /// Auto-Import: alle deklarierten `VAR`s des Skripts in den KV übernehmen
    /// (siehe Modul-Doc). Der KV wächst damit um jede neue Skript-Variable —
    /// genau so wandern Werte in per `start_ink` verkettete Folgeskripte.
    fn sync_to_kv(&self, kv: &mut Store) {
        for name in &self.vars {
            if let Some(v) = self.inner.get_variable(name) {
                kv.insert(name.clone(), v);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bladeink::value_type::ValueType;

    // Handkompiliertes Minimal-Ink (inklecate-Format wie cube.ink.json):
    //   VAR snaks = 0
    //   VAR _local = 0
    //   S={snaks}          → gibt den (ggf. aus dem KV gesyncten) Wert aus
    //   ~ snaks = 1
    //   ~ _local = 5
    //   -> END
    const MINIMAL: &str = r##"{"inkVersion":21,"root":[["^S=","ev",{"VAR?":"snaks"},"out","/ev","\n","ev",1,{"VAR=":"snaks","re":true},"/ev","ev",5,{"VAR=":"_local","re":true},"/ev","end",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"snaks"},0,{"VAR=":"_local"},"/ev","end",null]}],"listDefs":{}}"##;

    #[test]
    fn global_var_names_scans_dedupes_and_filters_locals() {
        let names = global_var_names(MINIMAL);
        assert_eq!(names, vec!["snaks"]); // dedupliziert, `_local` gefiltert
    }

    #[test]
    fn declared_vars_auto_import_into_kv() {
        // KV kennt `snaks` vorher nicht — nach dem Lauf steht es trotzdem
        // drin (Auto-Import), `_local` bleibt draußen.
        let mut kv = Store::new();
        let mut s = Story::from_src(MINIMAL, "test", &kv).unwrap();
        assert!(matches!(s.cont(&mut kv), StoryState::Text(_)));
        assert!(matches!(kv["snaks"], ValueType::Int(1)));
        assert!(!kv.contains_key("_local"), "_-Prefix ist skript-lokal");
    }

    #[test]
    fn kv_values_flow_into_the_script() {
        // Vorbelegter KV-Wert ist im Skript sichtbar (Sync vor `continue`).
        let mut kv = Store::new();
        kv.insert("snaks".into(), ValueType::Int(7));
        let mut s = Story::from_src(MINIMAL, "test", &kv).unwrap();
        match s.cont(&mut kv) {
            StoryState::Text(t) => assert_eq!(t, "S=7"),
            _ => panic!("erwartete Textzeile"),
        }
    }

    #[test]
    fn runtime_error_ends_story_instead_of_panicking() {
        // Flow läuft ohne `done`/`end` aus (wie ein Skript-Pfad ohne Divert)
        // → bladeink meldet „ran out of content". Erwartung: End, kein Panic.
        let src = r#"{"inkVersion":21,"root":[["^Hi","\n",null],null],"listDefs":{}}"#;
        let mut kv = Store::new();
        let mut s = Story::from_src(src, "test", &kv).unwrap();
        assert!(matches!(s.cont(&mut kv), StoryState::End));
    }

    #[test]
    fn invalid_choice_is_reported_not_fatal() {
        let mut kv = Store::new();
        let mut s = Story::from_src(MINIMAL, "test", &kv).unwrap();
        s.choose(3); // keine Choices vorhanden → Meldung, kein Panic
        assert!(matches!(s.cont(&mut kv), StoryState::Text(_)));
    }
}
