/// DreamStack Incremental Compiler
///
/// Diffs successive compilations to emit minimal JS patches when only
/// signal values change. Falls back to full recompile when structural
/// changes (new/removed declarations, view changes) are detected.

use ds_parser::{Program, Declaration, LetDecl, Lexer, Parser, TokenKind};
use ds_analyzer::SignalGraph;
use ds_codegen::JsEmitter;

/// Result of an incremental compilation.
pub enum IncrementalResult {
    /// Full HTML — either first compile or structural change detected.
    Full(String),
    /// JS patch — only signal value updates, can be eval'd in-place.
    Patch(String),
    /// Compile error.
    Error(String),
}

/// Delta between two programs.
#[derive(Debug)]
pub struct ProgramDelta {
    /// Signals whose initial value expressions changed.
    pub changed_signals: Vec<ChangedSignal>,
    /// Whether structural changes require a full recompile.
    pub needs_full_recompile: bool,
}

/// A signal with a changed initial value.
#[derive(Debug)]
pub struct ChangedSignal {
    pub name: String,
    pub new_expr_js: String,
}

/// Stateful incremental compiler. Holds the previous program AST
/// to diff against.
pub struct IncrementalCompiler {
    prev_source: Option<String>,
    prev_program: Option<Program>,
}

impl IncrementalCompiler {
    pub fn new() -> Self {
        IncrementalCompiler {
            prev_source: None,
            prev_program: None,
        }
    }

    /// Parse source into a Program, returning an error string on failure.
    fn parse_source(source: &str) -> Result<Program, String> {
        let mut lexer = Lexer::new(source);
        let tokens = lexer.tokenize();

        // Check for lexer errors
        let mut errors = Vec::new();
        for tok in &tokens {
            if let TokenKind::Error(msg) = &tok.kind {
                errors.push(format!("Lexer error at line {}: {}", tok.line, msg));
            }
        }
        if !errors.is_empty() {
            return Err(errors.join("\n"));
        }

        let mut parser = Parser::with_source(tokens, source);
        let result = parser.parse_program_resilient();

        if !result.errors.is_empty() {
            let error_msgs: Vec<String> = result.errors.iter()
                .map(|e| e.to_string())
                .collect();
            return Err(error_msgs.join("\n"));
        }

        Ok(result.program)
    }

    /// Full compilation pipeline.
    fn full_compile(program: &Program) -> String {
        let graph = SignalGraph::from_program(program);
        let views = SignalGraph::analyze_views(program);
        JsEmitter::emit_html(program, &graph, &views, false)
    }

    /// Compile source code, returning either a full HTML recompile or a JS patch.
    pub fn compile(&mut self, source: &str) -> IncrementalResult {
        // Fast path: source unchanged
        if self.prev_source.as_deref() == Some(source) {
            return IncrementalResult::Patch(String::new());
        }

        // Parse
        let program = match Self::parse_source(source) {
            Ok(p) => p,
            Err(e) => return IncrementalResult::Error(e),
        };

        // Check if we can do incremental
        let result = if let Some(prev) = &self.prev_program {
            let delta = Self::diff_programs(prev, &program);
            if delta.needs_full_recompile {
                IncrementalResult::Full(Self::full_compile(&program))
            } else if delta.changed_signals.is_empty() {
                // No changes at all — return empty patch
                IncrementalResult::Patch(String::new())
            } else {
                // Only signal values changed → emit patch
                IncrementalResult::Patch(Self::emit_patch(&delta))
            }
        } else {
            // First compile — always full
            IncrementalResult::Full(Self::full_compile(&program))
        };

        // Save state
        self.prev_source = Some(source.to_string());
        self.prev_program = Some(program);

        result
    }

    /// Diff two programs. Detects signal value changes and structural changes.
    fn diff_programs(old: &Program, new: &Program) -> ProgramDelta {
        // If declaration count differs, structural change
        if old.declarations.len() != new.declarations.len() {
            return ProgramDelta {
                changed_signals: vec![],
                needs_full_recompile: true,
            };
        }

        // Check if declaration types match (structure preserved)
        for (o, n) in old.declarations.iter().zip(new.declarations.iter()) {
            if std::mem::discriminant(o) != std::mem::discriminant(n) {
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        let old_lets = Self::extract_lets(old);
        let new_lets = Self::extract_lets(new);

        // Compare let declarations for value changes
        let mut changed = Vec::new();

        for (name, old_expr_dbg) in &old_lets {
            // Find matching new let
            if let Some((_, new_expr_dbg)) = new_lets.iter().find(|(n, _)| n == name) {
                if old_expr_dbg != new_expr_dbg {
                    // Signal value changed — find the new expression and emit JS
                    for decl in &new.declarations {
                        if let Declaration::Let(LetDecl { name: n, value, .. }) = decl {
                            if n == name {
                                let emitter = JsEmitter::default();
                                let expr_js = emitter.emit_expr(value);
                                changed.push(ChangedSignal {
                                    name: name.clone(),
                                    new_expr_js: expr_js,
                                });
                                break;
                            }
                        }
                    }
                }
            } else {
                // Signal removed → full recompile
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        // Check for new signals
        for (name, _) in &new_lets {
            if !old_lets.iter().any(|(n, _)| n == name) {
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        // Check if any view declarations changed (using Debug repr as cheap diff)
        let old_views: Vec<String> = old.declarations.iter()
            .filter_map(|d| if let Declaration::View(v) = d { Some(format!("{:?}", v)) } else { None })
            .collect();
        let new_views: Vec<String> = new.declarations.iter()
            .filter_map(|d| if let Declaration::View(v) = d { Some(format!("{:?}", v)) } else { None })
            .collect();

        let needs_full = old_views != new_views;

        ProgramDelta {
            changed_signals: changed,
            needs_full_recompile: needs_full,
        }
    }

    /// Extract let declarations as (name, debug_string) pairs for comparison.
    fn extract_lets(program: &Program) -> Vec<(String, String)> {
        program.declarations.iter()
            .filter_map(|d| {
                if let Declaration::Let(LetDecl { name, value, .. }) = d {
                    Some((name.clone(), format!("{:?}", value)))
                } else {
                    None
                }
            })
            .collect()
    }

    /// Emit a JS patch that updates signal values without full page reload.
    fn emit_patch(delta: &ProgramDelta) -> String {
        let mut js = String::from("// DreamStack incremental patch\n");
        for sig in &delta.changed_signals {
            js.push_str(&format!(
                "if (typeof DS !== 'undefined' && DS.signals && DS.signals.{name}) {{ DS.signals.{name}.value = {expr}; }}\n",
                name = sig.name,
                expr = sig.new_expr_js,
            ));
        }
        js
    }
}

impl Default for IncrementalCompiler {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_first_compile_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let result = compiler.compile("let x = 42\nview main = column [\n  text x\n]");
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    #[test]
    fn test_same_source_is_empty_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src = "let x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src);
        let result = compiler.compile(src);
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty()),
            _ => panic!("expected empty patch for identical source"),
        }
    }

    #[test]
    fn test_signal_value_change_is_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 99\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.x"));
                assert!(js.contains("99"));
            }
            _ => panic!("expected patch for signal value change"),
        }
    }

    #[test]
    fn test_structural_change_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\nlet y = 10\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    #[test]
    fn test_view_change_triggers_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\nview main = row [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    // ── v0.7 Incremental Compiler Tests ─────────────────────

    #[test]
    fn test_error_then_fix_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        // Introduce a syntax error
        let bad = "let x = !@#\nview main = column [\n  text x\n]";
        let err_result = compiler.compile(bad);
        assert!(matches!(err_result, IncrementalResult::Error(_)));
        // Fix the error with a DIFFERENT valid source — should trigger full
        // (prev_program is from src1, but the new source has structural changes)
        let src3 = "let x = 42\nlet y = 10\nview main = column [\n  text x\n  text y\n]";
        let result = compiler.compile(src3);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "fixing a syntax error with new structure should trigger full recompile");
    }

    #[test]
    fn test_whitespace_only_change() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\n\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        // Whitespace change in between declarations — source changes but AST stays same
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty(), "whitespace-only should be no-op patch"),
            IncrementalResult::Full(_) => {} // also acceptable — parser may differ on spans
            other => panic!("unexpected: {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_comment_only_change() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "-- a comment\nlet x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        // Adding a comment shouldn't change signals or views
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty(), "comment-only should be no-op"),
            IncrementalResult::Full(_) => {} // also acceptable
            other => panic!("unexpected: {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_add_signal_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 1\nview main = text x";
        let src2 = "let x = 1\nlet y = 2\nview main = text x";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "adding a signal should trigger full recompile");
    }

    #[test]
    fn test_remove_signal_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 1\nlet y = 2\nview main = text x";
        let src2 = "let x = 1\nview main = text x";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "removing a signal should trigger full recompile");
    }

    #[test]
    fn test_multi_signal_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let a = 1\nlet b = 2\nlet c = 3\nview main = column [\n  text a\n  text b\n  text c\n]";
        let src2 = "let a = 10\nlet b = 20\nlet c = 3\nview main = column [\n  text a\n  text b\n  text c\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.a"), "should patch signal a");
                assert!(js.contains("DS.signals.b"), "should patch signal b");
                assert!(!js.contains("DS.signals.c"), "should NOT patch unchanged signal c");
            }
            other => panic!("expected patch for multi-signal change, got {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_large_program_incremental() {
        let mut compiler = IncrementalCompiler::new();
        let base = "let s0 = 0\nlet s1 = 1\nlet s2 = 2\nlet s3 = 3\nlet s4 = 4\n\
                    let s5 = 5\nlet s6 = 6\nlet s7 = 7\nlet s8 = 8\nlet s9 = 9\n\
                    view main = column [\n  text s0\n  text s1\n  text s9\n]";
        compiler.compile(base);
        // Change only one signal in a 10-signal program
        let modified = base.replace("let s5 = 5", "let s5 = 55");
        let result = compiler.compile(&modified);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.s5"), "should patch only s5");
                assert!(js.contains("55"), "should contain new value");
            }
            other => panic!("expected patch, got {:?}", std::mem::discriminant(&other)),
        }
    }
}

