//! Unix socket protocol for daemon ↔ CLI.
//!
//! Simple line-oriented text protocol:
//!   PUT <ttl_secs> <content>\n
//!   PUTP <ttl_secs> <content>\n
//!   GET <key>\n
//!   DEL <key>\n
//!   PIN <key>\n
//!   UNPIN <key>\n
//!   STATUS\n
//!   SHUTDOWN\n
//!
//! Responses:
//!   OK <data>\n
//!   ERR <message>\n

/// A parsed request received from the CLI over the Unix socket.
pub enum Request {
    Put {
        ttl_secs: u64,
        content_b58: String,
        encrypt: bool,
    },
    Get {
        key: String,
    },
    Del {
        key: String,
    },
    Pin {
        key: String,
    },
    Unpin {
        key: String,
    },
    Status,
    Shutdown,
}

/// A response sent back to the CLI over the Unix socket.
pub enum Response {
    Ok(String),
    Err(String),
}

/// Parse a single protocol line into a [`Request`].
pub fn parse_request(line: &str) -> Result<Request, String> {
    let line = line.trim();
    if line.is_empty() {
        return Err("empty request".into());
    }

    let mut parts = line.splitn(3, ' ');
    let cmd = parts.next().unwrap();

    match cmd {
        "PUT" | "PUTP" => {
            let ttl_str =
                parts.next().ok_or("PUT requires: PUT <ttl> <data>")?;
            let content_b58 =
                parts.next().ok_or("PUT requires: PUT <ttl> <data>")?;
            let ttl_secs: u64 =
                ttl_str.parse().map_err(|_| "invalid TTL number")?;
            Ok(Request::Put {
                ttl_secs,
                content_b58: content_b58.to_string(),
                encrypt: cmd == "PUT",
            })
        }
        "GET" => {
            let key = parts.next().ok_or("GET requires: GET <key>")?;
            Ok(Request::Get {
                key: key.to_string(),
            })
        }
        "DEL" => {
            let key = parts.next().ok_or("DEL requires: DEL <key>")?;
            Ok(Request::Del {
                key: key.to_string(),
            })
        }
        "PIN" => {
            let key = parts.next().ok_or("PIN requires: PIN <key>")?;
            Ok(Request::Pin {
                key: key.to_string(),
            })
        }
        "UNPIN" => {
            let key = parts.next().ok_or("UNPIN requires: UNPIN <key>")?;
            Ok(Request::Unpin {
                key: key.to_string(),
            })
        }
        "STATUS" => Ok(Request::Status),
        "SHUTDOWN" => Ok(Request::Shutdown),
        _ => Err(format!("unknown command: {cmd}")),
    }
}

/// Serialize a [`Response`] into a protocol line (`OK ...\n` or `ERR ...\n`).
pub fn format_response(r: &Response) -> String {
    match r {
        Response::Ok(data) => format!("OK {data}\n"),
        Response::Err(msg) => format!("ERR {msg}\n"),
    }
}

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

    #[test]
    fn parse_put() {
        let r = parse_request("PUT 3600 deadbeef").unwrap();
        match r {
            Request::Put {
                ttl_secs,
                content_b58,
                encrypt,
            } => {
                assert_eq!(ttl_secs, 3600);
                assert_eq!(content_b58, "deadbeef");
                assert!(encrypt);
            }
            _ => panic!("expected Put"),
        }
    }

    #[test]
    fn parse_putp() {
        let r = parse_request("PUTP 60 abc").unwrap();
        match r {
            Request::Put { encrypt, .. } => assert!(!encrypt),
            _ => panic!("expected Put"),
        }
    }

    #[test]
    fn parse_get() {
        let r = parse_request("GET abc123#key456").unwrap();
        match r {
            Request::Get { key } => assert_eq!(key, "abc123#key456"),
            _ => panic!("expected Get"),
        }
    }

    #[test]
    fn parse_status() {
        assert!(matches!(parse_request("STATUS").unwrap(), Request::Status));
    }

    #[test]
    fn parse_empty_fails() {
        assert!(parse_request("").is_err());
    }

    #[test]
    fn parse_unknown_fails() {
        assert!(parse_request("FOOBAR").is_err());
    }

    #[test]
    fn format_ok() {
        let r = format_response(&Response::Ok("hello".into()));
        assert_eq!(r, "OK hello\n");
    }

    #[test]
    fn format_err() {
        let r = format_response(&Response::Err("oops".into()));
        assert_eq!(r, "ERR oops\n");
    }
}
