use anyhow::{anyhow, Result};
use serde_json::{json, Value};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::Mutex;
use uuid::Uuid;

// ── Binary location ───────────────────────────────────────────────────────────

fn find_sidecar_binary() -> Result<PathBuf> {
    // Dev override via env var
    if let Ok(p) = std::env::var("ASW_SIDECAR_PATH") {
        let path = PathBuf::from(&p);
        if path.exists() {
            return Ok(path);
        }
        return Err(anyhow!("ASW_SIDECAR_PATH set but binary not found at: {}", p));
    }

    // Walk upward from the current executable to find the project root,
    // then look for the sidecar in known build output locations.
    // In dev mode the exe is at: <project>/src-tauri/target/debug/<bin>
    // In production the exe is at: <install>/<bin>
    if let Ok(exe) = std::env::current_exe() {
        // Collect ancestor directories up to 8 levels deep
        let mut dir = exe.parent().map(|p| p.to_path_buf());
        for _ in 0..8 {
            if let Some(ref d) = dir {
                // Production (MSI install): Tauri places sidecar next to the exe.
                // Tauri bundles with the target-triple suffix; check both forms.
                for name in &[
                    "asw-sidecar-x86_64-pc-windows-msvc.exe",
                    "asw-sidecar.exe",
                ] {
                    let candidate = d.join(name);
                    if candidate.exists() {
                        return Ok(candidate);
                    }
                }
                // Dev: check binaries/ subfolder and sidecar publish output
                for suffix in &[
                    "binaries/asw-sidecar-x86_64-pc-windows-msvc.exe",
                    "binaries/asw-sidecar.exe",
                    "sidecar/bin/Release/net8.0/win-x64/publish/asw-sidecar.exe",
                    "sidecar/bin/Debug/net8.0/win-x64/asw-sidecar.exe",
                ] {
                    let candidate = d.join(suffix);
                    if candidate.exists() {
                        return Ok(candidate);
                    }
                }
                dir = d.parent().map(|p| p.to_path_buf());
            } else {
                break;
            }
        }
    }

    Err(anyhow!(
        "asw-sidecar binary not found. Build it first:\n\
         cd sidecar && dotnet publish -c Release -r win-x64 --self-contained\n\
         Or set ASW_SIDECAR_PATH env var."
    ))
}

// ── Handle (inner, behind Mutex) ─────────────────────────────────────────────

struct SidecarHandle {
    _child: Child,
    stdin: BufWriter<ChildStdin>,
    stdout: BufReader<ChildStdout>,
}

impl SidecarHandle {
    fn spawn() -> Result<Self> {
        let binary = find_sidecar_binary()?;

        let mut child = Command::new(&binary)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::null())
            .spawn()
            .map_err(|e| anyhow!("Failed to spawn sidecar {:?}: {}", binary, e))?;

        let stdin = BufWriter::new(child.stdin.take().ok_or(anyhow!("No stdin"))?);
        let stdout = BufReader::new(child.stdout.take().ok_or(anyhow!("No stdout"))?);

        Ok(SidecarHandle { _child: child, stdin, stdout })
    }

    fn send(&mut self, command: &str, args: Value) -> Result<Value> {
        let id = Uuid::new_v4().to_string();
        let request = json!({ "id": id, "command": command, "args": args });

        let line = serde_json::to_string(&request)?;
        writeln!(self.stdin, "{}", line)?;
        self.stdin.flush()?;

        // Read lines until we get the response matching this request ID
        let mut buf = String::new();
        loop {
            buf.clear();
            let n = self.stdout.read_line(&mut buf)?;
            if n == 0 {
                return Err(anyhow!("Sidecar closed stdout unexpectedly"));
            }
            let trimmed = buf.trim();
            if trimmed.is_empty() {
                continue;
            }
            let response: Value = match serde_json::from_str(trimmed) {
                Ok(v) => v,
                Err(_) => continue,
            };
            if response["id"].as_str() == Some(&id) {
                return if response["ok"].as_bool().unwrap_or(false) {
                    Ok(response["data"].clone())
                } else {
                    Err(anyhow!(
                        "{}",
                        response["error"].as_str().unwrap_or("sidecar error")
                    ))
                };
            }
        }
    }
}

// ── Public state type ─────────────────────────────────────────────────────────

pub struct SidecarState {
    handle: Mutex<Option<SidecarHandle>>,
}

impl SidecarState {
    pub fn new() -> Self {
        SidecarState { handle: Mutex::new(None) }
    }

    /// Ensure the sidecar is running and send a command.
    pub fn send(&self, command: &str, args: Value) -> Result<Value> {
        let mut guard = self.handle.lock().map_err(|_| anyhow!("Sidecar lock poisoned"))?;

        // Spawn if not running
        if guard.is_none() {
            *guard = Some(SidecarHandle::spawn()?);
        }

        guard.as_mut().unwrap().send(command, args)
    }

    /// Kill and restart the sidecar (e.g. after a re-index).
    pub fn restart(&self) -> Result<()> {
        let mut guard = self.handle.lock().map_err(|_| anyhow!("Sidecar lock poisoned"))?;
        *guard = Some(SidecarHandle::spawn()?);
        Ok(())
    }
}
