use futures::{SinkExt, StreamExt};
use std::error::Error;
use std::io::Error as IoError;
use tokio::net::UnixStream;
// use tokio::runtime::Builder;
use crate::json_codec::JsonCodec;
use crate::unix_proto::{ClientRequest, ClientResponse};
use tokio::time::{self, Duration};
use tokio_util::codec::Framed;

type ClientCodec = JsonCodec<ClientResponse, ClientRequest>;

pub struct DaemonClient {
    req_stream: Framed<UnixStream, ClientCodec>,
    default_timeout: u64,
}

impl DaemonClient {
    pub async fn new(path: &str, default_timeout: u64) -> Result<Self, Box<dyn Error>> {
        trace!(?path);
        let stream = UnixStream::connect(path).await.inspect_err(|e| {
            error!(
                "Unix socket stream setup error while connecting to {} -> {:?}",
                path, e
            );
        })?;

        let req_stream = Framed::new(stream, ClientCodec::default());

        trace!("connected");

        Ok(DaemonClient {
            req_stream,
            default_timeout,
        })
    }

    async fn call_inner(&mut self, req: ClientRequest) -> Result<ClientResponse, Box<dyn Error>> {
        self.req_stream.send(req).await?;
        self.req_stream.flush().await?;
        trace!("flushed, waiting ...");
        match self.req_stream.next().await {
            Some(Ok(res)) => {
                debug!("Response -> {:?}", res);
                Ok(res)
            }
            _ => {
                error!("Error making request to kanidm_unixd");
                Err(Box::new(IoError::other("oh no!")))
            }
        }
    }

    pub async fn call(
        &mut self,
        req: ClientRequest,
        timeout: Option<u64>,
    ) -> Result<ClientResponse, Box<dyn Error>> {
        let sleep = time::sleep(Duration::from_secs(timeout.unwrap_or(self.default_timeout)));
        tokio::pin!(sleep);

        tokio::select! {
            _ = &mut sleep => {
                error!(?timeout, "Timed out making request to kanidm_unixd");
                Err(Box::new(IoError::other("timeout")))
            }
            res = self.call_inner(req) => {
                res
            }
        }
    }
}
