//! Local socket IPC server. JSON-lines + binary payloads.
//! Cross-platform via `interprocess` (Unix sockets / Windows named pipes).

use std::sync::Arc;

use anyhow::Result;
use interprocess::local_socket::{
    tokio::prelude::*,
    GenericFilePath, GenericNamespaced, ListenerOptions, ToFsName, ToNsName,
};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tracing::{debug, info, warn};

use crate::node::MemoNode;

// -- Protocol types ---------------------------------------------------------

#[derive(Debug, Deserialize)]
#[serde(tag = "op")]
pub enum Request {
    #[serde(rename = "ping")]
    Ping,
    #[serde(rename = "get")]
    Get { ns: String, key: String },
    /// Metadata-only lookup: returns Hit { size } or Miss, no blob bytes.
    #[serde(rename = "probe")]
    Probe { ns: String, key: String },
    /// Combined probe+get in one round-trip. Returns local data if cached,
    /// Miss otherwise. Does NOT trigger network fetch (use get for that).
    #[serde(rename = "check")]
    Check { ns: String, key: String },
    #[serde(rename = "put")]
    Put {
        ns: String,
        key: String,
        size: u64,
        #[serde(default)]
        meta: PutMeta,
    },
    /// Import a file from disk as a chunked HashSeq. No bytes cross the socket.
    #[serde(rename = "put_file")]
    PutFile {
        ns: String,
        key: String,
        path: String,
        #[serde(default)]
        meta: PutMeta,
    },
    /// Materialize a HashSeq (by blob hash) to a file. Downloads missing chunks from peers.
    #[serde(rename = "get_file")]
    GetFile {
        ns: String,
        hash: String,
        path: String,
    },
    #[serde(rename = "join")]
    Join { ns: String },
    #[serde(rename = "status")]
    Status { ns: String },
    #[serde(rename = "evict")]
    Evict { ns: String, tag: String },
}

#[derive(Debug, Default, Deserialize)]
pub struct PutMeta {
    #[serde(default)]
    pub compute_ns: u64,
    #[serde(default)]
    pub ops: u64,
    #[serde(default)]
    pub mem_bytes: u64,
    #[serde(default)]
    pub peak_mem: u64,
    #[serde(default)]
    pub tag: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum Response {
    #[serde(rename = "pong")]
    Pong,
    #[serde(rename = "hit")]
    Hit { size: u64 },
    #[serde(rename = "probe_hit")]
    ProbeHit { size: u64, ops: u64, mem_bytes: u64, peak_mem: u64 },
    #[serde(rename = "miss")]
    Miss,
    #[serde(rename = "ok")]
    Ok { message: String },
    #[serde(rename = "stored")]
    Stored { hash: String, size: u64 },
    #[serde(rename = "info")]
    Info {
        node_id: String,
        node_id_hex: String,
        ns: String,
        namespaces: usize,
    },
    #[serde(rename = "error")]
    Error { message: String },
}

// -- Server -----------------------------------------------------------------

pub async fn serve(node: Arc<MemoNode>) -> Result<()> {
    let name = crate::config::socket_name();

    #[cfg(unix)]
    { let _ = std::fs::remove_file(&name); }

    let listener = if cfg!(windows) {
        ListenerOptions::new()
            .name(name.as_str().to_ns_name::<GenericNamespaced>()?)
            .create_tokio()?
    } else {
        ListenerOptions::new()
            .name(name.as_str().to_fs_name::<GenericFilePath>()?)
            .create_tokio()?
    };

    info!(socket = %name, "IPC server listening");
    println!("IROH_MEMO_SOCKET={name}");

    loop {
        match listener.accept().await {
            Ok(stream) => {
                let node = node.clone();
                tokio::spawn(async move {
                    if let Err(e) = handle_client(stream, &node).await {
                        debug!("client disconnect: {e}");
                    }
                });
            }
            Err(e) => warn!("accept error: {e}"),
        }
    }
}

async fn handle_client(
    stream: impl AsyncRead + AsyncWrite + Unpin,
    node: &Arc<MemoNode>,
) -> Result<()> {
    let (reader, mut writer) = tokio::io::split(stream);
    let mut reader = BufReader::new(reader);
    let mut line = String::new();

    loop {
        line.clear();
        if reader.read_line(&mut line).await? == 0 { break; }

        let req: Request = match serde_json::from_str(line.trim()) {
            Ok(r) => r,
            Err(e) => {
                write_resp(&mut writer, &Response::Error {
                    message: format!("parse: {e}"),
                }).await?;
                continue;
            }
        };

        match req {
            Request::Ping => {
                write_resp(&mut writer, &Response::Pong).await?;
            }

            Request::Get { ns, key } => {
                match node.get(&ns, &key).await {
                    Ok(Some(data)) => {
                        write_resp(&mut writer, &Response::Hit {
                            size: data.len() as u64,
                        }).await?;
                        writer.write_all(&data).await?;
                        writer.flush().await?;
                    }
                    Ok(None) => write_resp(&mut writer, &Response::Miss).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::Probe { ns, key } => {
                match node.probe(&ns, &key).await {
                    Ok(Some(cost)) => {
                        write_resp(&mut writer, &Response::ProbeHit {
                            size: cost.result_bytes, ops: cost.ops,
                            mem_bytes: cost.mem_bytes, peak_mem: cost.peak_mem,
                        }).await?;
                    }
                    Ok(None) => write_resp(&mut writer, &Response::Miss).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::Check { ns, key } => {
                match node.check_local(&ns, &key).await {
                    Ok(Some(data)) => {
                        write_resp(&mut writer, &Response::Hit {
                            size: data.len() as u64,
                        }).await?;
                        writer.write_all(&data).await?;
                        writer.flush().await?;
                    }
                    Ok(None) => write_resp(&mut writer, &Response::Miss).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::Put { ns, key, size, meta } => {
                let mut buf = vec![0u8; size as usize];
                reader.read_exact(&mut buf).await?;
                let cost = crate::node::CostMeta {
                    ops: meta.ops, mem_bytes: meta.mem_bytes,
                    peak_mem: meta.peak_mem, result_bytes: 0,
                };
                match node.put(&ns, &key, &buf, meta.compute_ns, cost, &meta.tag).await {
                    Ok(()) => write_resp(&mut writer, &Response::Ok {
                        message: String::new(),
                    }).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::PutFile { ns, key, path, meta } => {
                let p = std::path::Path::new(&path);
                let cost = crate::node::CostMeta {
                    ops: meta.ops, mem_bytes: meta.mem_bytes,
                    peak_mem: meta.peak_mem, result_bytes: 0,
                };
                match node.put_file(&ns, &key, p, meta.compute_ns, cost, &meta.tag).await {
                    Ok((hash, size)) => {
                        write_resp(&mut writer, &Response::Stored { hash, size }).await?
                    }
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::GetFile { ns, hash, path } => {
                let p = std::path::Path::new(&path);
                match node.get_file(&ns, &hash, p).await {
                    Ok(Some(size)) => {
                        write_resp(&mut writer, &Response::Hit { size }).await?
                    }
                    Ok(None) => write_resp(&mut writer, &Response::Miss).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::Status { ns } => {
                write_resp(&mut writer, &Response::Info {
                    node_id: node.node_id(),
                    node_id_hex: node.node_id_hex(),
                    ns,
                    namespaces: node.ns_count().await,
                }).await?;
            }

            Request::Join { ns } => {
                match node.join(&ns).await {
                    Ok(()) => write_resp(&mut writer, &Response::Ok {
                        message: "joined".into(),
                    }).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }

            Request::Evict { ns, tag } => {
                match node.evict_tag(&ns, &tag).await {
                    Ok(keys) => write_resp(&mut writer, &Response::Ok {
                        message: format!("evicted {} entries", keys.len()),
                    }).await?,
                    Err(e) => write_resp(&mut writer, &Response::Error {
                        message: e.to_string(),
                    }).await?,
                }
            }
        }
    }
    Ok(())
}

async fn write_resp(w: &mut (impl AsyncWrite + Unpin), r: &Response) -> Result<()> {
    let mut json = serde_json::to_string(r)?;
    json.push('\n');
    w.write_all(json.as_bytes()).await?;
    w.flush().await?;
    Ok(())
}

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

    #[test]
    fn request_ping_deserializes() {
        let r: Request = serde_json::from_str(r#"{"op":"ping"}"#).unwrap();
        assert!(matches!(r, Request::Ping));
    }

    #[test]
    fn request_get_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"get","ns":"default","key":"abc123"}"#,
        ).unwrap();
        match r {
            Request::Get { ns, key } => {
                assert_eq!(ns, "default");
                assert_eq!(key, "abc123");
            }
            _ => panic!("expected Get"),
        }
    }

    #[test]
    fn request_probe_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"probe","ns":"default","key":"abc123"}"#,
        ).unwrap();
        assert!(matches!(r, Request::Probe { .. }));
    }

    #[test]
    fn request_check_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"check","ns":"default","key":"mod.func/abc123"}"#,
        ).unwrap();
        assert!(matches!(r, Request::Check { .. }));
    }

    #[test]
    fn request_put_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"put","ns":"test","key":"k","size":42,"meta":{"compute_ns":1000,"tag":"v1"}}"#,
        ).unwrap();
        match r {
            Request::Put { ns, key, size, meta } => {
                assert_eq!(ns, "test");
                assert_eq!(key, "k");
                assert_eq!(size, 42);
                assert_eq!(meta.compute_ns, 1000);
                assert_eq!(meta.tag, "v1");
            }
            _ => panic!("expected Put"),
        }
    }

    #[test]
    fn request_put_file_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"put_file","ns":"ns","key":"k","path":"/data/x.parquet","meta":{"tag":"v1"}}"#,
        ).unwrap();
        match r {
            Request::PutFile { path, meta, .. } => {
                assert_eq!(path, "/data/x.parquet");
                assert_eq!(meta.tag, "v1");
            }
            _ => panic!("expected PutFile"),
        }
    }

    #[test]
    fn request_get_file_deserializes() {
        let r: Request = serde_json::from_str(
            r#"{"op":"get_file","ns":"ns","hash":"abcd1234","path":"/tmp/out.csv"}"#,
        ).unwrap();
        match r {
            Request::GetFile { hash, path, .. } => {
                assert_eq!(hash, "abcd1234");
                assert_eq!(path, "/tmp/out.csv");
            }
            _ => panic!("expected GetFile"),
        }
    }

    #[test]
    fn response_stored_serializes() {
        let json = serde_json::to_string(&Response::Stored {
            hash: "abc".into(), size: 999,
        }).unwrap();
        assert!(json.contains("stored"));
        assert!(json.contains("999"));
    }

    #[test]
    fn response_pong_serializes() {
        let json = serde_json::to_string(&Response::Pong).unwrap();
        assert!(json.contains("pong"));
    }

    #[test]
    fn response_hit_serializes() {
        let json = serde_json::to_string(&Response::Hit { size: 1024 }).unwrap();
        assert!(json.contains("hit"));
        assert!(json.contains("1024"));
    }

    #[test]
    fn response_miss_serializes() {
        let json = serde_json::to_string(&Response::Miss).unwrap();
        assert!(json.contains("miss"));
    }

    #[test]
    fn response_error_serializes() {
        let json = serde_json::to_string(&Response::Error {
            message: "oops".to_string(),
        }).unwrap();
        assert!(json.contains("error"));
        assert!(json.contains("oops"));
    }

    #[tokio::test]
    async fn write_resp_newline_terminated() {
        let mut buf = Vec::new();
        write_resp(&mut buf, &Response::Pong).await.unwrap();
        let s = String::from_utf8(buf).unwrap();
        assert!(s.ends_with('\n'));
        let _: Response = serde_json::from_str(s.trim()).unwrap();
    }
}
