#![allow(clippy::result_large_err)]

use std::{path::Path, time::UNIX_EPOCH};

use reqwest::{blocking::Client, header::IF_MODIFIED_SINCE, StatusCode};
use serde::{Deserialize, Serialize};
use time::{macros::format_description, OffsetDateTime};

use crate::{
    action::{ActionError, Context, Pair},
    action_impl::ActionImpl,
    util::write_file,
};

/// Download a file into dependencies.
///
/// Only download if the file is missing from dependencies, or has changed
/// on the server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpGet {
    items: Vec<Pair>,
}

impl HttpGet {
    pub(crate) fn new(items: Vec<Pair>) -> Self {
        Self { items }
    }

    pub(crate) fn items(&self) -> &[Pair] {
        &self.items
    }
}

impl ActionImpl for HttpGet {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let deps = context.deps_dir();
        for pair in self.items.iter() {
            let filename = deps.join(pair.filename());
            http_get_to_file(pair.url(), &filename)?;
        }
        Ok(())
    }
}

/// Download a file over HTTP.
fn http_get_to_file(url: &str, filename: &Path) -> Result<(), HttpGetError> {
    let timestamp = if let Ok(meta) = filename.metadata() {
        meta.modified().unwrap_or(UNIX_EPOCH)
    } else {
        UNIX_EPOCH
    };

    let fmt = format_description!(
        "[weekday repr:short], [day padding:zero] [month repr:short] [year] [hour]:[minute]:[second] GMT"
    );
    let ts = OffsetDateTime::from(timestamp)
        .format(fmt)
        .map_err(HttpGetError::TimeFormat)?;

    let client = Client::builder()
        .build()
        .map_err(HttpGetError::ClientBuild)?;
    let req = client
        .get(url)
        .header(IF_MODIFIED_SINCE, ts)
        .build()
        .map_err(HttpGetError::Client)?;

    let resp = client
        .execute(req)
        .map_err(|err| HttpGetError::Get(url.into(), err))?;

    match resp.status() {
        StatusCode::NOT_MODIFIED => {}
        StatusCode::OK => {
            let body = resp
                .bytes()
                .map_err(|err| HttpGetError::GetBody(url.into(), err))?;
            write_file(filename, &body)?;
        }
        x => {
            return Err(HttpGetError::UnwantedStatus(x));
        }
    }

    Ok(())
}

/// Errors from `http_get` action.
#[derive(Debug, thiserror::Error)]
pub enum HttpGetError {
    /// Forwarded from `util` module.
    #[error(transparent)]
    Util(#[from] crate::util::UtilError),

    /// Can't format time as string.
    #[error("failed to format time stamp")]
    TimeFormat(#[source] time::error::Format),

    /// Can't build an HTTP client.
    #[error("failed to create HTTP client")]
    ClientBuild(#[source] reqwest::Error),

    /// Can't create an HTTP client.
    #[error("failed to build a reqwest client")]
    Client(#[source] reqwest::Error),

    /// Can't build an HTTP request.
    #[error("failed to build a reqwest request")]
    BuildRequest(#[source] reqwest::Error),

    /// Can't get file with GET.
    #[error("failed to GET URL {0:?}")]
    Get(String, reqwest::Error),

    /// Can't get GET response body.
    #[error("failed to get body of response from {0:?}")]
    GetBody(String, reqwest::Error),

    /// HTTP GET returned weird status code.
    #[error("failure getting file with HTTP GET: status code {0}")]
    UnwantedStatus(StatusCode),
}

impl From<HttpGetError> for ActionError {
    fn from(value: HttpGetError) -> Self {
        Self::HttpGet(value)
    }
}
