use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use serde_json::Value;
use spacesh_proto::codec::{read_frame, write_frame};
use spacesh_proto::{Cmd, Envelope};
use tokio::net::UnixStream;

pub fn socket_path() -> PathBuf {
    if let Ok(p) = std::env::var("SPACESH_SOCK") {
        if !p.is_empty() {
            return PathBuf::from(p);
        }
    }
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home).join(".spacesh").join("sock")
}

/// Connect, lazy-starting the daemon if the socket is absent.
async fn connect_or_start() -> Result<UnixStream> {
    let sock = socket_path();
    if let Ok(s) = UnixStream::connect(&sock).await {
        return Ok(s);
    }
    // Locate the daemon next to this binary and spawn it.
    let exe = std::env::current_exe().context("current_exe")?;
    let daemon = exe.with_file_name("spaceshd");
    let _ = std::process::Command::new(daemon).spawn();
    for _ in 0..100 {
        if let Ok(s) = UnixStream::connect(&sock).await {
            return Ok(s);
        }
        tokio::time::sleep(std::time::Duration::from_millis(30)).await;
    }
    Err(anyhow!("daemon unavailable"))
}

/// One-shot request/response. Skips any interleaved events; returns `data` on ok.
pub async fn request(cmd: Cmd) -> Result<Value> {
    let mut stream = connect_or_start().await?;
    send_and_read(&mut stream, cmd).await
}

/// Best-effort status notify: connect only (no spawn); silently succeed if absent.
pub async fn notify(cmd: Cmd) -> Result<()> {
    let sock = socket_path();
    let Ok(mut stream) = UnixStream::connect(&sock).await else {
        return Ok(()); // no daemon — best-effort no-op
    };
    let _ = send_and_read(&mut stream, cmd).await;
    Ok(())
}

async fn send_and_read(stream: &mut UnixStream, cmd: Cmd) -> Result<Value> {
    write_frame(stream, &Envelope::Req { id: 1, cmd }).await.map_err(|e| anyhow!(e.to_string()))?;
    loop {
        match read_frame(stream).await.map_err(|e| anyhow!(e.to_string()))? {
            Some(Envelope::Res { id: 1, ok, data, error }) => {
                if ok {
                    return Ok(data);
                }
                let (code, msg) = error.map(|e| (e.code, e.msg)).unwrap_or_else(|| ("ERROR".into(), "error".into()));
                return Err(anyhow!("{code}: {msg}"));
            }
            Some(_) => continue, // events / non-matching res
            None => return Err(anyhow!("connection closed")),
        }
    }
}
