use std::{cmp::min, fmt};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Http error: {0}")]
    Http(#[from] http::Error),

    #[error("Network error: {0}")]
    Net(#[from] hyper::Error),

    #[error("Error serializing request payload: {0}")]
    Serialization(serde_json::Error),

    #[error("Error deserializing response: {error}\n{extract}")]
    Deserialization {
        error: serde_json::Error,
        extract: ErrorExtract,
    },

    #[error("Received a non 2xx status code: StatusCode: {received_status}, body: {body}")]
    Non2xxResponse {
        received_status: http::StatusCode,
        body: String,
    },
}

#[derive(Debug)]
pub enum ErrorExtract {
    Failed,
    Extracted { extract: String, pointer_idx: usize },
}

impl fmt::Display for ErrorExtract {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Extracted {
                extract,
                pointer_idx,
            } => {
                write!(f, "...")?;
                write!(f, "{}", extract)?;
                write!(f, "...\n")?;

                for _ in 0..pointer_idx + 3 {
                    write!(f, " ")?;
                }
                write!(f, "^")?;
            }

            _ => (),
        }

        Ok(())
    }
}

impl Error {
    pub fn deserialization(error: serde_json::Error, body: &[u8]) -> Self {
        // Try to take an extract from the body.
        let column = error.column();
        let extract = body
            .split(|b| b == &b'\n')
            .skip(error.line() - 1) // subtract 1 since indices start at 1.
            .next()
            .map_or_else(
                || ErrorExtract::Failed,
                |line| {
                    const GRASP: usize = 80;

                    let pointer_idx = min(column, GRASP);
                    let start = if GRASP < column {
                        column - GRASP
                    } else {
                        column
                    };
                    let snippet_bs = &line[start..min(column + GRASP, line.len())];
                    ErrorExtract::Extracted {
                        extract: String::from_utf8_lossy(snippet_bs).into_owned(),
                        pointer_idx,
                    }
                },
            );

        Self::Deserialization { error, extract }
    }

    pub fn non_2xx(received_status: http::StatusCode, body: &[u8]) -> Self {
        Self::Non2xxResponse {
            received_status,
            body: Self::sanitize_body(&body),
        }
    }

    fn sanitize_body(body: &[u8]) -> String {
        let mut res = body
            .chunks(128)
            .next()
            .map(|bs| String::from_utf8_lossy(&bs).to_string())
            .unwrap_or_else(|| "".to_owned());

        res.push_str("...");
        res
    }
}
