// SonicForge DAW — desktop shell: spawn/terminate daw_engine sidecar.
//
// QUAN TRONG (fix bản 1.1.1 — engine không chạy trên Windows):
// tauri.conf.json bundle.resources giờ dùng dạng MAP:
//   { "resources/daw_engine": "daw_engine/" }
// vì dạng ARRAY ("resources/daw_engine") copy file tới
// $RESOURCE_DIR/resources/daw_engine/... (giữ tiền tố "resources/"),
// trong khi code cũ tìm ở $RESOURCE_DIR/daw_engine/... -> exists=false.
// Dạng map (Walk mode) giữ nguyên cây thư mục (_internal) dưới đích
// "daw_engine/" — đúng layout lib.rs chờ.
// Ngoài ra lib.rs còn dò THÊM các vị trí fallback (legacy/portable/dev)
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, FilePath};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;

mod shm;

/// App đang tắt (cửa sổ chính đã Destroyed) — pump thread phải dừng ngay,
/// không respawn bridge mới sau khi sidecar đã bị giết (fix: app còn sống
/// sau khi đóng window → pump thấy stall 3s → spawn bridge mới → orphan).
static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
use shm::{Shm, ShmState};

struct EngineProcess(Mutex<Option<CommandChild>>);
struct BridgeProcess(Mutex<Option<CommandChild>>);
struct BridgeSampleRate(Mutex<u32>);

/// Overwrite the managed bridge child. `app.manage` is a NO-OP once the state
/// exists (StateManager::set keeps the first value), so restarts would leave
/// the state at None and the Destroyed handler would never kill the new
/// bridge — orphan daw_vst_bridge.exe. First call still uses manage().
fn set_bridge_child(app: &AppHandle, child: Option<CommandChild>) {
    if let Some(state) = app.try_state::<BridgeProcess>() {
        if let Ok(mut g) = state.0.lock() {
            *g = child;
        }
    } else {
        app.manage(BridgeProcess(Mutex::new(child)));
    }
}

/// Kill the currently managed bridge child (if any) — a restart MUST never
/// leave the old bridge running, else two daw_vst_bridge.exe race on the same
/// SHM (double instrument load, double OPEN_GUI, garbled control queue).
fn kill_bridge(app: &AppHandle) -> bool {
    let state = app.state::<BridgeProcess>();
    let mut guard = match state.0.lock() {
        Ok(g) => g,
        Err(_) => return false,
    };
    match guard.take() {
        Some(child) => {
            let ok = kill_process_tree(&child) || child.kill().is_ok();
            if ok {
                println!("Native Host Bridge killed (restart)");
            } else {
                println!("Bridge kill failed");
            }
            ok
        }
        None => false,
    }
}

/// Kill a child plus its whole process tree via taskkill /T /F. The engine
/// (PyInstaller ONEDIR) and bridge are direct children, but /T guarantees no
/// orphan subprocess stays behind when the DAW window closes.
fn kill_process_tree(child: &CommandChild) -> bool {
    let pid = child.pid();
    std::process::Command::new("taskkill")
        .args(["/PID", &pid.to_string(), "/T", "/F"])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
#[derive(Clone, serde::Serialize)]
struct AudioFrame {
    l: Vec<f32>,
    r: Vec<f32>,
}

/// Các vị trí có thể chứa daw_engine, theo thứ tự ưu tiên.
fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
    let exe_name = if cfg!(windows) { "daw_engine.exe" } else { "daw_engine" };
    vec![
        // 1. Layout chuẩn (resources map): $RESOURCE/daw_engine/daw_engine.exe
        (
            res_dir.join("daw_engine").join(exe_name),
            "resource_dir/daw_engine (map layout)".into(),
        ),
        // 2. Legacy (resources array cũ giữ tiền tố resources/)
        (
            res_dir.join("resources").join("daw_engine").join(exe_name),
            "resource_dir/resources/daw_engine (legacy array)".into(),
        ),
        // 3. Portable: engine đặt cạnh exe
        (
            exe_dir.join("daw_engine").join(exe_name),
            "exe_dir/daw_engine (portable)".into(),
        ),
        // 4. Dev mode: target/{debug,release} -> src-tauri/resources
        (
            exe_dir.join("..").join("resources").join("daw_engine").join(exe_name),
            "exe_dir/../resources/daw_engine (dev)".into(),
        ),
    ]
}

/// Các vị trí có thể chứa daw_vst_bridge (Native Host Bridge sidecar).
fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
    let exe_name = if cfg!(windows) { "daw_vst_bridge.exe" } else { "daw_vst_bridge" };
    let triple_name = if cfg!(windows) { "daw_vst_bridge-x86_64-pc-windows-msvc.exe" } else { exe_name };
    vec![
        (res_dir.join("binaries").join(exe_name), "resource_dir/binaries (bundle)".into()),
        (res_dir.join(exe_name), "resource_dir (bundle root)".into()),
        (res_dir.join("binaries").join(triple_name), "resource_dir/binaries (triple name)".into()),
        (res_dir.join(triple_name), "resource_dir (triple name, bundle)".into()),
        (exe_dir.join(triple_name), "exe_dir (triple name, bundle)".into()),
        (exe_dir.join(exe_name), "exe_dir (portable)".into()),
        (exe_dir.join("..").join("resources").join("binaries").join(exe_name), "exe_dir/../resources/binaries (dev)".into()),
        (exe_dir.join("..").join("resources").join(exe_name), "exe_dir/../resources (dev)".into()),
    ]
}

/// Spawn daw_vst_bridge.exe sidecar (B3/B8) with SHM + watchdog + audio env.
/// Appends candidate diagnostics to spawn.log; manages `BridgeProcess`.
/// Returns true when the child started.
fn spawn_bridge(
    app: &AppHandle,
    res_dir: &Path,
    exe_dir: &Path,
    log_line: &mut String,
    spawn_log_path: &Path,
) -> bool {
    let candidates = bridge_candidates(res_dir, exe_dir);
    let mut found: Option<(PathBuf, String)> = None;
    for (path, label) in &candidates {
        let exists = path.exists();
        log_line.push_str(&format!("  [bridge {label}] {} exists={}\n", path.display(), exists));
        if found.is_none() && exists {
            found = Some((path.clone(), label.clone()));
        }
    }
    // B3 DoD: dòng tóm tắt trạng thái bridge trong spawn.log
    log_line.push_str(&format!("[daw_vst_bridge] exists={}
", found.is_some()));
    if let Ok(mut f) = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(spawn_log_path)
    {
        use std::io::Write;
        let _ = f.write_all(log_line.as_bytes());
    }
    match found {
        Some((bridge_exe, label)) => {
            let sample_rate = {
                let rate_state = app.state::<BridgeSampleRate>();
                let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
                *g
            };
            match app
                .shell()
                .command(&bridge_exe)
                .env("SF_SHM_NAME", shm::SHM_NAME)
                .env("SF_PARENT_PID", std::process::id().to_string())
                .env("SF_SAMPLE_RATE", sample_rate.to_string())
                .env("SF_BLOCK_SIZE", "256")
                .spawn()
            {
                Ok((mut rx, child)) => {
                    set_bridge_child(app, Some(child));
                    // B3: redirect bridge stdout/stderr → %APPDATA%/SonicForgeDAW/logs/bridge.log
                    // (tauri-shell pipe rồi vứt rx; đọc event ghi file — E5 UI tail được).
                    let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
                    let bridge_log = std::path::Path::new(&log_dir)
                        .join("SonicForgeDAW").join("logs").join("bridge.log");
                    if let Some(parent) = bridge_log.parent() {
                        let _ = std::fs::create_dir_all(parent);
                    }
                    tauri::async_runtime::spawn(async move {
                        use std::io::Write;
                        let mut f = match std::fs::OpenOptions::new().create(true).append(true).open(&bridge_log) {
                            Ok(f) => f,
                            Err(_) => return,
                        };
                        while let Some(ev) = rx.recv().await {
                            let bytes: Vec<u8> = match ev {
                                CommandEvent::Stdout(b) | CommandEvent::Stderr(b) => b,
                                _ => continue,
                            };
                            if let Ok(text) = String::from_utf8(bytes) {
                                let _ = writeln!(f, "{}", text.trim_end());
                            }
                        }
                    });
                    println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
                    true
                }
                Err(e) => {
                    println!("Failed to spawn daw_vst_bridge {bridge_exe:?}: {e}");
                    set_bridge_child(app, None);
                    false
                }
            }
        }
        None => {
            println!("daw_vst_bridge binary not found — app will use WASM fallback");
            set_bridge_child(app, None);
            false
        }
    }
}

fn list_dir_snippet(dir: &Path) -> String {
    let mut s = String::new();
    match std::fs::read_dir(dir) {
        Ok(rd) => {
            let mut n = 0;
            for e in rd.flatten() {
                if n > 0 {
                    s.push_str(", ");
                }
                s.push_str(&e.file_name().to_string_lossy());
                n += 1;
                if n >= 15 {
                    s.push_str(", ...");
                    break;
                }
            }
        }
        Err(_) => s.push_str("<khong doc duoc dir>"),
    }
    s
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![
            push_midi_event,
            load_native_instrument,
            open_vst_gui,
            bridge_status,
            transport_control,
            restart_bridge_with_sample_rate
        ])
        .setup(|app| {
            let res_dir = app
                .path()
                .resource_dir()
                .expect("resource dir not found");
            let exe_dir = std::env::current_exe()
                .ok()
                .and_then(|p| p.parent().map(|d| d.to_path_buf()))
                .unwrap_or_default();
            let candidates = engine_candidates(&res_dir, &exe_dir);

            // Ghi log spawn de chan doan — vao %APPDATA%/SonicForgeDAW/logs/
            // spawn.log (thu muc luon ton tai), KHONG ghi vao engine_dir.
            let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
            let spawn_log_path = std::path::Path::new(&log_dir)
                .join("SonicForgeDAW")
                .join("logs")
                .join("spawn.log");
            if let Some(parent) = spawn_log_path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }

            let mut log_line = format!(
                "resource_dir={}\nexe_dir={}\n",
                res_dir.display(),
                exe_dir.display()
            );
            let mut found: Option<(PathBuf, String)> = None;
            for (path, label) in &candidates {
                let exists = path.exists();
                log_line.push_str(&format!(
                    "  [{label}] {} exists={}\n",
                    path.display(),
                    exists
                ));
                if found.is_none() && exists {
                    found = Some((path.clone(), label.clone()));
                }
            }
            if found.is_none() {
                log_line.push_str(&format!(
                    "  NOT FOUND — resource_dir contents: {}\n  exe_dir contents: {}\n",
                    list_dir_snippet(&res_dir),
                    list_dir_snippet(&exe_dir)
                ));
            }
            if let Ok(mut f) = std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(&spawn_log_path)
            {
                use std::io::Write;
                let _ = f.write_all(log_line.as_bytes());
            }

            match found {
                Some((engine_exe, label)) => {
                    match app
                        .shell()
                        .command(&engine_exe)
                        .env("SF_PARENT_PID", std::process::id().to_string())
                        .spawn()
                    {
                        Ok((_rx, child)) => {
                            app.manage(EngineProcess(Mutex::new(Some(child))));
                            println!(
                                "Python Background Engine started ({label}): {}",
                                engine_exe.display()
                            );
                        }
                        Err(e) => {
                            println!("Failed to spawn daw_engine {engine_exe:?}: {e}");
                            app.manage(EngineProcess(Mutex::new(None)));
                        }
                    }
                }
                None => {
                    println!(
                        "daw_engine binary not found in any candidate path — xem spawn.log de biet chi tiet"
                    );
                    app.manage(EngineProcess(Mutex::new(None)));
                }
            }

            // ── Native Host Bridge (daw_vst_bridge.exe): SHM + sidecar spawn ──
            let shm_created = Shm::create();
            println!(
                "SharedMemory {}: {}",
                shm::SHM_NAME,
                if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
            );
            app.manage(ShmState(Mutex::new(shm_created)));
            app.manage(BridgeSampleRate(Mutex::new(48000)));

            // Append bridge diagnostics to spawn.log and spawn the sidecar (B3).
            let app_handle = app.handle().clone();
            let _ = spawn_bridge(&app_handle, &res_dir, &exe_dir, &mut log_line, &spawn_log_path);

            // ── Audio pump: bridge SHM -> WebView `bridge-audio` events ──
            // B10 health: if bridgeWriteIndex stalls for 3s the bridge is dead —
            // respawn once, then emit `bridge-down` so the UI falls back to WASM.
            let pump_handle = app.handle().clone();
            std::thread::spawn(move || {
                let mut last_index: u32 = 0;
                let mut last_change = std::time::Instant::now();
                let mut down_emitted = false;
                let mut restart_attempts = 0u32;
                loop {
                    // App đang tắt (window Destroyed) — dừng pump, không respawn bridge.
                    if SHUTTING_DOWN.load(Ordering::Relaxed) {
                        break;
                    }
                    let state = pump_handle.state::<ShmState>();
                    let guard = match state.0.lock() {
                        Ok(g) => g,
                        Err(_) => {
                            std::thread::sleep(std::time::Duration::from_millis(10));
                            continue;
                        }
                    };
                    let advanced = guard.as_ref().map(|shm| {
                        let idx = shm.write_index();
                        if idx != last_index {
                            last_index = idx;
                            true
                        } else {
                            false
                        }
                    });
                    if advanced == Some(true) {
                        last_change = std::time::Instant::now();
                        down_emitted = false;
                        // Bridge đã sống lại sau restart — đóng "sự cố" để lần
                        // stall sau lại được hưởng 1 restart (mỗi incident 1 lần).
                        restart_attempts = 0;
                        let (l, r) = guard.as_ref().map(|s| s.read_audio()).unwrap_or(([0f32; shm::AUDIO_BLOCK_SIZE], [0f32; shm::AUDIO_BLOCK_SIZE]));
                        let _ = pump_handle.emit(
                            "bridge-audio",
                            AudioFrame { l: l.to_vec(), r: r.to_vec() },
                        );
                    } else if last_change.elapsed() >= std::time::Duration::from_secs(3) && !down_emitted {
                        // Check lai truoc khi respawn — Destroyed co the vua set flag.
                        if SHUTTING_DOWN.load(Ordering::Relaxed) {
                            break;
                        }
                        down_emitted = true;
                        if restart_attempts == 0 {
                            restart_attempts += 1;
                            let res_dir = pump_handle.path().resource_dir().unwrap_or_default();
                            let exe_dir = std::env::current_exe()
                                .ok()
                                .and_then(|p| p.parent().map(|d| d.to_path_buf()))
                                .unwrap_or_default();
                            let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
                            let log_path = std::path::Path::new(&log_dir)
                                .join("SonicForgeDAW").join("logs").join("spawn.log");
                            let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n");
                            // FIX: kill bridge cũ trước — không được để 2 bridge
                            // cùng map SHM (race control queue / double load).
                            kill_bridge(&pump_handle);
                            if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) {
                                // count restarts into bridge.log (same file as stdout redirect)
                                let bridge_log = std::path::Path::new(&log_dir)
                                    .join("SonicForgeDAW").join("logs").join("bridge.log");
                                if let Ok(mut f) = std::fs::OpenOptions::new()
                                    .create(true).append(true).open(&bridge_log)
                                {
                                    use std::io::Write;
                                    let _ = f.write_all(b"[tauri] bridge restarted (attempt 1)\n");
                                }
                                last_change = std::time::Instant::now();
                                down_emitted = false;
                            } else {
                                let _ = pump_handle.emit("bridge-down", ());
                            }
                        } else {
                            let _ = pump_handle.emit("bridge-down", ());
                        }
                    }
                    drop(guard);
                    std::thread::sleep(std::time::Duration::from_millis(1));
                }
            });

            // ── Native folder picker bridge (folder picker cho Plugin Manager) ──
            // UI chay tren http://127.0.0.1:8000 (engine) — KHONG co __TAURI__
            // (WebView2 cung khong ho tro window.prompt) → engine goi qua file
            // IPC: engine ghi ipc/pick_dir.request → thread nay mo NATIVE dialog
            // (IFileDialog/Explorer — tauri-plugin-dialog) → ghi ket qua vao
            // ipc/pick_dir.response → engine tra ve cho frontend.
            let data_root = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
            let ipc_dir = std::path::Path::new(&data_root)
                .join("SonicForgeDAW")
                .join("ipc");
            if let Ok(()) = std::fs::create_dir_all(&ipc_dir) {
                // Marker: engine biet bridge ton tai (khong phai chay standalone)
                let _ = std::fs::write(ipc_dir.join("tauri_bridge_ready"), "1");
            }
            let ipc_dir_for_thread = ipc_dir.clone();
            let app_handle = app.handle().clone();
            std::thread::spawn(move || {
                loop {
                    let req = ipc_dir_for_thread.join("pick_dir.request");
                    if req.exists() {
                        let _ = std::fs::remove_file(&req);
                        let resp = ipc_dir_for_thread.join("pick_dir.response");
                        let _ = std::fs::remove_file(&resp);
                        // Dialog phai chay tren main thread (GTK/Windows message loop)
                        let handle = app_handle.clone();
                        let resp_for_main = resp.clone();
                        // Clone rieng cho closure: run_on_main_thread(&self, F)
                        // borrow `handle`, closure (move) phai dung ban clone.
                        let handle_for_closure = handle.clone();
                        let _ = handle.run_on_main_thread(move || {
                            // tauri-plugin-dialog v2: blocking_pick_folder() tra
                            // Option<FilePath> (enum Path(PathBuf) | Url(Url))
                            let picked: Option<FilePath> = handle_for_closure
                                .dialog()
                                .file()
                                .blocking_pick_folder();
                            let val = picked
                                .map(|p| match p {
                                    FilePath::Path(pb) => pb.to_string_lossy().to_string(),
                                    FilePath::Url(u) => u.to_string(),
                                })
                                .unwrap_or_default();
                            let _ = std::fs::write(&resp_for_main, val);
                        });
                        // Cho den khi co response (user co the de dialog mo lau)
                        let mut waited_ms = 0u32;
                        while !resp.exists() && waited_ms < 300_000 {
                            std::thread::sleep(std::time::Duration::from_millis(100));
                            waited_ms += 100;
                        }
                    }
                    std::thread::sleep(std::time::Duration::from_millis(150));
                }
            });
            Ok(())
        })
        .on_window_event(|window, event| {
            // Terminate sidecar khi DAW window dong — tranh orphan process
            if let tauri::WindowEvent::Destroyed = event {
                // Dung pump thread: khong respawn bridge sau khi sidecar da bi giet.
                SHUTTING_DOWN.store(true, Ordering::Relaxed);
                let child = window
                    .state::<EngineProcess>()
                    .0
                    .lock()
                    .ok()
                    .and_then(|mut lock| lock.take());
                if let Some(child) = child {
                    kill_process_tree(&child);
                    let _ = child.kill();
                    println!("daw_engine sidecar terminated.");
                }
                // Terminate Native Host Bridge khi DAW window dong
                let bridge_child = window
                    .state::<BridgeProcess>()
                    .0
                    .lock()
                    .ok()
                    .and_then(|mut lock| lock.take());
                if let Some(child) = bridge_child {
                    kill_process_tree(&child);
                    let _ = child.kill();
                    println!("daw_vst_bridge sidecar terminated.");
                }
                // Cua so chinh da dong — ep app thoat han. Neu con window
                // phu/an giu event loop song, Tauri khong tu exit; exit(0)
                // ket thuc dut diem (pump da dung, khong con respawn bridge).
                if window.label() == "main" {
                    window.app_handle().exit(0);
                }
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

// ── Native Host Bridge Tauri commands ──────────────────────────────────────

#[tauri::command]
fn push_midi_event(
    app: AppHandle,
    command: u8,
    channel: u8,
    pitch: u8,
    velocity: u8,
    data2: u8,
    data3: u8,
    sample_offset: u32,
) -> Result<(), String> {
    let state = app.state::<ShmState>();
    let guard = state.0.lock().map_err(|e| e.to_string())?;
    let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
    shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset)
        .then_some(())
        .ok_or_else(|| "midi queue full".to_string())
}

#[tauri::command]
fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, channel: u8) -> Result<(), String> {
    let state = app.state::<ShmState>();
    let guard = state.0.lock().map_err(|e| e.to_string())?;
    let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
    shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
        .then_some(())
        .ok_or_else(|| "control queue full".to_string())
}

#[tauri::command]
fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), String> {
    // B9: bridge tự tạo native Win32 window cho editor VST3 (không qua
    // WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0)
    // với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread.
    let state = app.state::<ShmState>();
    let lock = state.0.lock();
    match lock {
        Ok(guard) => match guard.as_ref() {
            Some(shm) => {
                let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id);
                eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} ok={}", channel, ok);
            }
            None => eprintln!("open_vst_gui: bridge shm unavailable"),
        },
        Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e),
    }
    Ok(())
}

#[derive(serde::Serialize)]
struct BridgeStatus {
    connected: bool,
    shm_name: &'static str,
    write_index: u32,
    block_timestamp: u64,
    shm_size_bytes: usize,
    sample_rate: u32,
    block_size: u32,
}

/// %APPDATA%/SonicForgeDAW/ipc — cùng dir engine (plugins.py) đọc cho
/// /api/v1/bridge/status; None khi APPDATA unset (non-Windows).
fn bridge_ipc_dir() -> Option<PathBuf> {
    let appdata = std::env::var("APPDATA").ok()?;
    Some(Path::new(&appdata).join("SonicForgeDAW").join("ipc"))
}

#[tauri::command]
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
    let state = app.state::<ShmState>();
    let guard = state.0.lock().map_err(|e| e.to_string())?;
    let status = match guard.as_ref() {
        Some(shm) => BridgeStatus {
            connected: true,
            shm_name: shm::SHM_NAME,
            write_index: shm.write_index(),
            block_timestamp: shm.block_timestamp(),
            shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
            sample_rate: {
                let rate_state = app.state::<BridgeSampleRate>();
                let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
                *g
            },
            block_size: shm::AUDIO_BLOCK_SIZE as u32,
        },
        None => BridgeStatus {
            connected: false,
            shm_name: shm::SHM_NAME,
            write_index: 0,
            block_timestamp: 0,
            shm_size_bytes: 0,
            sample_rate: 0,
            block_size: 0,
        },
    };
    // E1: ghi JSON → engine /api/v1/bridge/status (badge Plugin Manager phải
    // phản ánh đúng bridge thật — trước đây file không tồn tại → luôn OFF).
    if let Some(dir) = bridge_ipc_dir() {
        let _ = std::fs::create_dir_all(&dir);
        if let Ok(json) = serde_json::to_string(&status) {
            let _ = std::fs::write(dir.join("bridge_status"), json);
        }
    }
    Ok(status)
}

#[tauri::command]
fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Result<(), String> {
    let state = app.state::<ShmState>();
    let guard = state.0.lock().map_err(|e| e.to_string())?;
    let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
    // arg0: 0=STOP (flush), 1=PLAY, 2=SET_POSITION (seek, no flush)
    let (arg0, arg1) = match kind.as_str() {
        "play" => (1, playhead.unwrap_or(0)),
        "set_position" => (2, playhead.unwrap_or(0)),
        _ => (0, playhead.unwrap_or(0)), // stop / panic
    };
    shm.push_control(3, arg0, arg1, 0, "")
        .then_some(())
        .ok_or_else(|| "control queue full".to_string())
}

#[tauri::command]
fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> {
    println!("restart_bridge_with_sample_rate: {}", sample_rate);
    {
        let rate_state = app.state::<BridgeSampleRate>();
        {
            let mut g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
            *g = sample_rate;
        }
    }
    kill_bridge(&app);
    let res_dir = app.path().resource_dir().unwrap_or_default();
    let exe_dir = std::env::current_exe()
        .ok()
        .and_then(|p| p.parent().map(|d| d.to_path_buf()))
        .unwrap_or_default();
    let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
    let spawn_log_path = std::path::Path::new(&log_dir)
        .join("SonicForgeDAW")
        .join("logs")
        .join("spawn.log");
    let mut log_line = format!("[tauri] restarting bridge with sample rate {}\n", sample_rate);
    let ok = spawn_bridge(&app, &res_dir, &exe_dir, &mut log_line, &spawn_log_path);
    if ok {
        Ok(())
    } else {
        Err("Failed to spawn bridge with new sample rate".into())
    }
}
