use std::path::PathBuf;

use http_body_util::{BodyExt as _, Full};
use hyper::{Method, Request, StatusCode, Uri, body::Bytes, client::conn};
use hyper_util::rt::TokioIo;
use tokio::net::UnixStream;

#[derive(Debug, thiserror::Error)]
pub enum GetError {
    #[error("failed to connect to unix socket")]
    SocketConnect(#[source] std::io::Error),
    #[error("failed to complete http handshake")]
    HttpHandshake(#[source] hyper::Error),
    #[error("given key-value key was invalid")]
    InvalidKey(#[source] serde_urlencoded::ser::Error),
    #[error("failed to create request url")]
    InvalidUrl(#[source] hyper::http::Error),
    #[error("failed to build http request")]
    BuildRequest(#[source] hyper::http::Error),
    #[error("failed to send http request")]
    SendRequest(#[source] hyper::Error),
    #[error("http request returned with a failed response: {0}")]
    FailedResponse(StatusCode),
    #[error("failed to collect response body")]
    CollectBody(#[source] hyper::Error),
}

#[derive(Debug, thiserror::Error)]
pub enum SetError {
    #[error("failed to connect to unix socket")]
    SocketConnect(#[source] std::io::Error),
    #[error("failed to complete http handshake")]
    HttpHandshake(#[source] hyper::Error),
    #[error("given key-value key was invalid")]
    InvalidKey(#[source] serde_urlencoded::ser::Error),
    #[error("failed to create request url")]
    InvalidUrl(#[source] hyper::http::Error),
    #[error("failed to build http request")]
    BuildRequest(#[source] hyper::http::Error),
    #[error("failed to send http request")]
    SendRequest(#[source] hyper::Error),
    #[error("http request returned with a failed response: {0}")]
    FailedResponse(StatusCode),
}

pub struct Client {
    path: PathBuf,
}

impl Client {
    pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, GetError> {
        let stream = UnixStream::connect(&self.path)
            .await
            .map_err(GetError::SocketConnect)?;
        let io = TokioIo::new(stream);

        let (mut sender, conn) = conn::http1::handshake(io)
            .await
            .map_err(GetError::HttpHandshake)?;

        tokio::task::spawn(async move {
            if let Err(err) = conn.await {
                tracing::error!("Connection failed: {:?}", err);
            }
        });

        let query = write_query(key).map_err(GetError::InvalidKey)?;

        let uri = Uri::builder()
            .scheme("http")
            .authority("localhost.invalid-tld")
            .path_and_query(query)
            .build()
            .map_err(GetError::InvalidUrl)?;

        let req = Request::builder()
            .method(Method::GET)
            .uri(uri)
            .body(Full::new(Bytes::from_static(&[])))
            .map_err(GetError::BuildRequest)?;

        let res = sender
            .send_request(req)
            .await
            .map_err(GetError::SendRequest)?;

        if matches!(res.status(), StatusCode::BAD_REQUEST) {
            return Err(GetError::FailedResponse(res.status()));
        }
        if matches!(res.status(), StatusCode::NOT_FOUND) {
            return Ok(None);
        }

        let body = res
            .into_body()
            .collect()
            .await
            .map_err(GetError::CollectBody)?
            .to_bytes();

        Ok(Some(body.to_vec()))
    }

    pub async fn set(&self, key: &str, value: &[u8]) -> Result<(), SetError> {
        let stream = UnixStream::connect(&self.path)
            .await
            .map_err(SetError::SocketConnect)?;
        let io = TokioIo::new(stream);

        let (mut sender, conn) = conn::http1::handshake(io)
            .await
            .map_err(SetError::HttpHandshake)?;

        tokio::task::spawn(async move {
            if let Err(err) = conn.await {
                tracing::error!("Connection failed: {:?}", err);
            }
        });

        let query = write_query(key).map_err(SetError::InvalidKey)?;

        let uri = Uri::builder()
            .scheme("http")
            .authority("localhost.invalid-tld")
            .path_and_query(query)
            .build()
            .map_err(SetError::BuildRequest)?;

        let body = Full::new(Bytes::from(value.to_vec()));

        let req = Request::builder()
            .method(Method::POST)
            .uri(uri)
            .body(body)
            .map_err(SetError::BuildRequest)?;

        let res = sender
            .send_request(req)
            .await
            .map_err(SetError::SendRequest)?;

        if matches!(res.status(), StatusCode::BAD_REQUEST) {
            return Err(SetError::FailedResponse(res.status()));
        }

        Ok(())
    }
}

impl Default for Client {
    fn default() -> Self {
        Self {
            path: PathBuf::from("/tmp/last-sky.sock"),
        }
    }
}

#[inline]
fn write_query(key: &str) -> Result<String, serde_urlencoded::ser::Error> {
    #[derive(serde::Serialize)]
    struct Key<'k> {
        key: &'k str,
    }

    let mut output = "/?".to_string();

    let mut encoder = form_urlencoded::Serializer::new(&mut output);
    let serializer = serde_urlencoded::Serializer::new(&mut encoder);

    <Key<'_> as serde::Serialize>::serialize(&Key { key }, serializer)?;

    Ok(output)
}
