/// Dev server command — file watching with hot reload.

use std::fs;
use std::path::Path;
use std::sync::{Arc, Mutex, atomic::{AtomicU64, Ordering}};
use std::time::{Duration, Instant};

use super::build::compile;

/// HMR client script injected into every page served by `dreamstack dev`.
/// Uses Server-Sent Events to receive reload notifications from the dev server.
const HMR_CLIENT_SCRIPT: &str = r#"
<script>
// ── DreamStack HMR (poll-based) ─────────────
(function() {
  let currentVersion = null;
  let polling = false;

  async function poll() {
    if (polling) return;
    polling = true;
    try {
      const res = await fetch('/__hmr');
      const version = await res.text();
      if (currentVersion === null) {
        currentVersion = version;
        console.log('[DS HMR] 🟢 watching (v' + version + ')');
      } else if (version !== currentVersion) {
        console.log('[DS HMR] 🔄 change detected (v' + currentVersion + ' → v' + version + '), reloading...');
        location.reload();
        return;
      }
    } catch(e) {
      // server down — retry silently
    }
    polling = false;
  }

  setInterval(poll, 500);
  poll();
})();
</script>
"#;

pub fn inject_hmr(html: &str) -> String {
    // Inject the HMR script just before </body>
    if let Some(pos) = html.rfind("</body>") {
        format!("{}{}{}", &html[..pos], HMR_CLIENT_SCRIPT, &html[pos..])
    } else {
        // No </body> tag — just append
        format!("{html}{HMR_CLIENT_SCRIPT}")
    }
}

pub fn cmd_dev(file: &Path, port: u16) {
    use notify::{Watcher, RecursiveMode};
    use std::sync::mpsc;
    use std::thread;

    println!("🚀 DreamStack dev server");
    println!("   watching: {}", file.display());
    println!("   serving:  http://localhost:{port}");
    println!();

    // Shared state: compiled HTML + version counter
    let version = Arc::new(AtomicU64::new(1));
    let compiled_html = Arc::new(Mutex::new(String::new()));

    // Initial compile
    let source = match fs::read_to_string(file) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("❌ Could not read {}: {}", file.display(), e);
            std::process::exit(1);
        }
    };

    let start = Instant::now();
    let base_dir = file.parent().unwrap_or(Path::new("."));
    match compile(&source, base_dir, false) {
        Ok(html) => {
            let ms = start.elapsed().as_millis();
            let html_with_hmr = inject_hmr(&html);
            *compiled_html.lock().unwrap() = html_with_hmr;
            println!("✅ Compiled in {ms}ms ({} bytes)", html.len());
        }
        Err(e) => {
            eprintln!("⚠️  Compile error: {e}");
            let error_html = format!(
                r#"<!DOCTYPE html>
<html><head><meta charset="UTF-8"><style>
body {{ background: #0a0a0f; color: #ef4444; font-family: 'JetBrains Mono', monospace; padding: 40px; }}
pre {{ white-space: pre-wrap; line-height: 1.7; }}
h2 {{ color: #f87171; margin-bottom: 16px; }}
</style></head><body>
<h2>── COMPILE ERROR ──</h2>
<pre>{e}</pre>
</body></html>"#
            );
            *compiled_html.lock().unwrap() = inject_hmr(&error_html);
        }
    }

    // ── File Watcher Thread ─────────────────────────
    let file_path = fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
    let watch_dir = file_path.parent().unwrap().to_path_buf();
    let watch_file = file_path.clone();
    let v_watcher = Arc::clone(&version);
    let html_watcher = Arc::clone(&compiled_html);

    thread::spawn(move || {
        let (tx, rx) = mpsc::channel();

        let mut watcher = notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
            if let Ok(event) = res {
                let _ = tx.send(event);
            }
        }).expect("Failed to create file watcher");

        watcher.watch(&watch_dir, RecursiveMode::Recursive)
            .expect("Failed to watch directory");

        // Also watch project root (for registry/components etc.)
        // Walk up from watch_dir to find a directory containing examples/ or registry/
        let mut project_root = watch_dir.clone();
        for _ in 0..5 {
            if project_root.join("registry").is_dir() || project_root.join("examples").is_dir() {
                if project_root != watch_dir {
                    let _ = watcher.watch(&project_root, RecursiveMode::Recursive);
                    println!("👁  Also watching {} (project root)", project_root.display());
                }
                break;
            }
            if let Some(parent) = project_root.parent() {
                project_root = parent.to_path_buf();
            } else {
                break;
            }
        }

        println!("👁  Watching {} for changes (recursive)", watch_dir.display());
        println!();

        // Debounce: coalesce rapid events
        let mut last_compile = Instant::now();

        loop {
            match rx.recv_timeout(Duration::from_millis(100)) {
                Ok(event) => {
                    // Only recompile for .ds file changes
                    let dominated = event.paths.iter().any(|p| {
                        p == &watch_file ||
                        p.extension().map_or(false, |ext| ext == "ds")
                    });

                    if !dominated { continue; }

                    // Debounce: skip if less than 100ms since last compile
                    if last_compile.elapsed() < Duration::from_millis(100) {
                        continue;
                    }

                    // Recompile
                    if let Ok(src) = fs::read_to_string(&watch_file) {
                        let start = Instant::now();
                        match compile(&src, watch_file.parent().unwrap_or(Path::new(".")), false) {
                            Ok(html) => {
                                let ms = start.elapsed().as_millis();
                                let new_version = v_watcher.fetch_add(1, Ordering::SeqCst) + 1;
                                *html_watcher.lock().unwrap() = inject_hmr(&html);
                                println!("🔄 Recompiled in {ms}ms (v{new_version}, {} bytes)", html.len());
                                last_compile = Instant::now();
                            }
                            Err(e) => {
                                let new_version = v_watcher.fetch_add(1, Ordering::SeqCst) + 1;
                                let error_html = format!(
                                    r#"<!DOCTYPE html>
<html><head><meta charset="UTF-8"><style>
body {{ background: #0a0a0f; color: #ef4444; font-family: 'JetBrains Mono', monospace; padding: 40px; }}
pre {{ white-space: pre-wrap; line-height: 1.7; }}
h2 {{ color: #f87171; margin-bottom: 16px; }}
</style></head><body>
<h2>── COMPILE ERROR ──</h2>
<pre>{e}</pre>
</body></html>"#
                                );
                                *html_watcher.lock().unwrap() = inject_hmr(&error_html);
                                eprintln!("❌ v{new_version}: {e}");
                                last_compile = Instant::now();
                            }
                        }
                    }
                }
                Err(mpsc::RecvTimeoutError::Timeout) => {
                    // No events — loop and check again
                    continue;
                }
                Err(mpsc::RecvTimeoutError::Disconnected) => break,
            }
        }
    });

    // ── HTTP Server ─────────────────────────────────
    let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();
    println!("✅ Server running at http://localhost:{port}");
    println!("   Press Ctrl+C to stop");
    println!();

    // Auto-open browser
    let url = format!("http://localhost:{port}");
    #[cfg(target_os = "linux")]
    { let _ = std::process::Command::new("xdg-open").arg(&url).spawn(); }
    #[cfg(target_os = "macos")]
    { let _ = std::process::Command::new("open").arg(&url).spawn(); }
    #[cfg(target_os = "windows")]
    { let _ = std::process::Command::new("cmd").args(["/C", "start", &url]).spawn(); }

    for request in server.incoming_requests() {
        let url = request.url().to_string();

        if url == "/__hmr" {
            // Version endpoint for HMR polling
            let v = version.load(Ordering::SeqCst);
            let response = tiny_http::Response::from_string(format!("{v}"))
                .with_header(
                    tiny_http::Header::from_bytes(
                        &b"Content-Type"[..],
                        &b"text/plain"[..],
                    ).unwrap(),
                )
                .with_header(
                    tiny_http::Header::from_bytes(
                        &b"Cache-Control"[..],
                        &b"no-cache, no-store"[..],
                    ).unwrap(),
                );
            let _ = request.respond(response);
        } else {
            // Serve the compiled HTML
            let html = compiled_html.lock().unwrap().clone();
            let response = tiny_http::Response::from_string(&html)
                .with_header(
                    tiny_http::Header::from_bytes(
                        &b"Content-Type"[..],
                        &b"text/html; charset=utf-8"[..],
                    ).unwrap(),
                );
            let _ = request.respond(response);
        }
    }
}

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

    #[test]
    fn test_inject_hmr_with_body_tag() {
        let html = "<html><body><p>hello</p></body></html>";
        let result = inject_hmr(html);
        assert!(result.contains("DS HMR"), "should inject HMR script");
        assert!(result.contains("</body>"), "should preserve </body>");
        // HMR should appear before </body>
        let hmr_pos = result.find("DS HMR").unwrap();
        let body_pos = result.find("</body>").unwrap();
        assert!(hmr_pos < body_pos, "HMR script should be before </body>");
    }

    #[test]
    fn test_inject_hmr_without_body_tag() {
        let html = "<p>no body tag</p>";
        let result = inject_hmr(html);
        assert!(result.contains("DS HMR"), "should inject HMR script");
        assert!(result.starts_with("<p>"), "should preserve original content");
    }
}

