use serde_json::Value;
use std::io::Write;
use tokio::io::{
    AsyncBufReadExt,
    AsyncReadExt,
    AsyncWriteExt,
    BufReader,
};
use tokio::net::UnixStream;

pub async fn ensure_daemon(socket: &str) {
    if UnixStream::connect(socket).await.is_ok() {
        return;
    }
    let exe = std::env::current_exe().unwrap();
    let child = std::process::Command::new(exe)
        .args(["daemon"])
        .env("T4A_SOCKET", socket)
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("failed to start daemon");
    // Leak the Child so it isn't killed on drop.
    std::mem::forget(child);
    for _ in 0..50 {
        tokio::time::sleep(
            std::time::Duration::from_millis(50),
        )
        .await;
        if UnixStream::connect(socket).await.is_ok() {
            return;
        }
    }
    eprintln!("warning: daemon may not have started");
}

async fn request(
    socket: &str,
    req: &Value,
) -> Result<Value, Box<dyn std::error::Error>> {
    let stream = UnixStream::connect(socket).await?;
    let (reader, mut writer) = stream.into_split();
    let mut buf = serde_json::to_vec(req)?;
    buf.push(b'\n');
    writer.write_all(&buf).await?;

    let mut reader = BufReader::new(reader);
    let mut line = String::new();
    reader.read_line(&mut line).await?;
    if line.is_empty() {
        return Err(format!(
            "daemon closed connection (try rm {socket})"
        ).into());
    }
    let resp: Value = serde_json::from_str(&line)?;
    Ok(resp)
}

fn check(resp: &Value) {
    if resp["ok"].as_bool() != Some(true) {
        let msg = resp["error"]
            .as_str()
            .unwrap_or("unknown error");
        eprintln!("error: {msg}");
        std::process::exit(1);
    }
}

pub(crate) fn unescape(s: &str) -> String {
    let mut out = String::new();
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c == '\\' {
            match chars.next() {
                Some('n') => out.push('\n'),
                Some('t') => out.push('\t'),
                Some('r') => out.push('\r'),
                Some('\\') => out.push('\\'),
                Some('x') => {
                    let h: String =
                        chars.by_ref().take(2).collect();
                    if let Ok(b) =
                        u8::from_str_radix(&h, 16)
                    {
                        out.push(b as char);
                    }
                }
                Some('e') => out.push('\x1b'),
                Some(other) => {
                    out.push('\\');
                    out.push(other);
                }
                None => out.push('\\'),
            }
        } else {
            out.push(c);
        }
    }
    out
}

pub async fn create(
    socket: &str,
    cols: u16,
    rows: u16,
    cmd: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut req = serde_json::json!({
        "cmd": "create",
        "cols": cols,
        "rows": rows,
    });
    if !cmd.is_empty() {
        req["cmd_args"] = serde_json::json!(cmd);
    }
    let resp = request(socket, &req).await?;
    check(&resp);
    let mut out = resp.as_object().unwrap().clone();
    out.insert("socket".into(), socket.into());
    println!("{}", serde_json::to_string(&out)?);
    Ok(())
}

pub async fn list(
    socket: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let req = serde_json::json!({"cmd": "list"});
    let resp = request(socket, &req).await?;
    check(&resp);
    if let Some(arr) = resp["terminals"].as_array() {
        for item in arr {
            println!("{}", serde_json::to_string(item)?);
        }
    }
    Ok(())
}

pub async fn send(
    socket: &str,
    id: &str,
    input: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let input = match input {
        Some(s) => unescape(&s),
        None => {
            let mut buf = String::new();
            std::io::stdin().read_line(&mut buf)?;
            buf
        }
    };
    let req = serde_json::json!({
        "cmd": "send",
        "id": id,
        "input": input,
    });
    let resp = request(socket, &req).await?;
    check(&resp);
    Ok(())
}

pub async fn text(
    socket: &str,
    id: &str,
    lines: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut req = serde_json::json!({
        "cmd": "text",
        "id": id,
    });
    if let Some(ref l) = lines
        && let Some((s, e)) = l.split_once(':')
    {
        req["start"] =
            serde_json::json!(s.parse::<i64>().unwrap());
        req["end"] =
            serde_json::json!(e.parse::<i64>().unwrap());
    }
    let resp = request(socket, &req).await?;
    check(&resp);
    if let Some(lines) = resp["lines"].as_array() {
        for line in lines {
            println!(
                "{}",
                line.as_str().unwrap_or("")
            );
        }
    }
    Ok(())
}

pub async fn cursor(
    socket: &str,
    id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let req =
        serde_json::json!({"cmd": "cursor", "id": id});
    let resp = request(socket, &req).await?;
    check(&resp);
    println!("{}", serde_json::to_string(&resp)?);
    Ok(())
}

pub async fn resize(
    socket: &str,
    id: &str,
    cols: u16,
    rows: u16,
) -> Result<(), Box<dyn std::error::Error>> {
    let req = serde_json::json!({
        "cmd": "resize",
        "id": id,
        "cols": cols,
        "rows": rows,
    });
    let resp = request(socket, &req).await?;
    check(&resp);
    Ok(())
}

pub async fn screen(
    socket: &str,
    id: &str,
    output: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let req = serde_json::json!({
        "cmd": "screenshot",
        "id": id,
    });

    let stream = UnixStream::connect(socket).await?;
    let (reader, mut writer) = stream.into_split();
    let mut buf = serde_json::to_vec(&req)?;
    buf.push(b'\n');
    writer.write_all(&buf).await?;

    let mut reader = BufReader::new(reader);
    let mut line = String::new();
    reader.read_line(&mut line).await?;
    let header: Value = serde_json::from_str(&line)?;
    check(&header);

    let len = header["len"].as_u64().unwrap() as usize;
    let mut png = vec![0u8; len];
    reader.read_exact(&mut png).await?;

    match output {
        Some(path) => {
            std::fs::write(&path, &png)?;
            eprintln!(
                "wrote {} bytes to {path}",
                png.len()
            );
        }
        None => {
            std::io::stdout().write_all(&png)?;
        }
    }
    Ok(())
}

pub async fn kill(
    socket: &str,
    id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let req =
        serde_json::json!({"cmd": "kill", "id": id});
    let resp = request(socket, &req).await?;
    check(&resp);
    Ok(())
}

pub async fn attach(
    socket: &str,
    id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    use tokio::io::AsyncReadExt;

    let req = serde_json::json!({
        "cmd": "attach", "id": id
    });

    let stream = UnixStream::connect(socket).await?;
    let (reader, mut writer) = stream.into_split();
    let mut buf = serde_json::to_vec(&req)?;
    buf.push(b'\n');
    writer.write_all(&buf).await?;

    let mut reader = BufReader::new(reader);

    // Put local terminal in raw mode
    let tty = std::fs::File::open("/dev/tty")?;
    let orig =
        nix::sys::termios::tcgetattr(&tty).unwrap();
    let mut raw = orig.clone();
    nix::sys::termios::cfmakeraw(&mut raw);
    nix::sys::termios::tcsetattr(
        &tty,
        nix::sys::termios::SetArg::TCSANOW,
        &raw,
    )
    .unwrap();

    // Bidirectional pipe: stdin -> PTY, PTY -> stdout
    let mut stdin =
        tokio::io::stdin();
    let mut stdout = tokio::io::stdout();
    let mut local_buf = [0u8; 4096];
    let mut remote_buf = [0u8; 4096];

    loop {
        tokio::select! {
            n = reader.read(&mut remote_buf) => {
                match n {
                    Ok(0) | Err(_) => break,
                    Ok(n) => {
                        use tokio::io::AsyncWriteExt;
                        if stdout.write_all(
                            &remote_buf[..n]
                        ).await.is_err() {
                            break;
                        }
                        let _ = stdout.flush().await;
                    }
                }
            }
            n = stdin.read(&mut local_buf) => {
                match n {
                    Ok(0) | Err(_) => break,
                    Ok(n) => {
                        if writer.write_all(
                            &local_buf[..n]
                        ).await.is_err() {
                            break;
                        }
                    }
                }
            }
        }
    }

    // Restore terminal
    nix::sys::termios::tcsetattr(
        &tty,
        nix::sys::termios::SetArg::TCSANOW,
        &orig,
    )
    .unwrap();

    Ok(())
}

pub async fn events(
    socket: &str,
    terminal: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut req = serde_json::json!({"cmd": "events"});
    if let Some(ref t) = terminal {
        req["terminal"] = serde_json::json!(t);
    }

    let stream = UnixStream::connect(socket).await?;
    let (reader, mut writer) = stream.into_split();
    let mut buf = serde_json::to_vec(&req)?;
    buf.push(b'\n');
    writer.write_all(&buf).await?;

    let mut reader = BufReader::new(reader);
    let mut line = String::new();
    loop {
        line.clear();
        let n = reader.read_line(&mut line).await?;
        if n == 0 {
            break;
        }
        print!("{line}");
    }
    Ok(())
}

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

    #[test]
    fn unescape_newline() {
        assert_eq!(unescape(r"\n"), "\n");
    }

    #[test]
    fn unescape_tab() {
        assert_eq!(unescape(r"\t"), "\t");
    }

    #[test]
    fn unescape_hex() {
        assert_eq!(unescape(r"\x03"), "\x03");
    }

    #[test]
    fn unescape_esc() {
        assert_eq!(unescape(r"\e"), "\x1b");
    }

    #[test]
    fn unescape_backslash() {
        assert_eq!(unescape(r"\\"), "\\");
    }

    #[test]
    fn unescape_passthrough() {
        assert_eq!(unescape("hello world"), "hello world");
    }
}
