use std::path::Path;

use nanoserde::{DeJson, DeJsonErr};
use reqwest::Url;
use reqwest::header::HeaderValue;

use crate::ext::Extendable;
use crate::{Client, Ezs3Err};

fn err<S: Into<String>>(s: S) -> Ezs3Err {
    Ezs3Err::ConfigErr(s.into())
}

fn bail<T, S: Into<String>>(s: S) -> Result<T, Ezs3Err> {
    let msg = s.into();
    Err(Ezs3Err::ConfigErr(msg))
}

impl Extendable for std::io::Error {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Ezs3Err {
        let msg = format!("{}: I/O error: {}", s.as_ref(), &self);
        Ezs3Err::ConfigErr(msg)
    }
}

impl Extendable for DeJsonErr {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Ezs3Err {
        let msg = format!(
            "{}: at {}:{}: {:?}",
            s.as_ref(),
            &self.line,
            &self.col,
            &self.msg
        );
        Ezs3Err::ConfigErr(msg)
    }
}

impl Extendable for url::ParseError {
    type Output = Ezs3Err;

    fn extend<S: AsRef<str>>(self, s: S) -> Ezs3Err {
        let msg = format!("{}: {}", s.as_ref(), &self);
        Ezs3Err::ConfigErr(msg)
    }
}

#[derive(Clone, Debug, DeJson, Default)]
struct CfgFile {
    aws_access_key_id: Option<String>,
    aws_secret_access_key: Option<String>,
    aws_region: Option<String>,
    aws_endpoint_url_s3: Option<String>,
    bucket_name: Option<String>,
}

impl CfgFile {
    pub(crate) fn load<P: AsRef<Path>>(cfg_file_path: P) -> Result<CfgFile, Ezs3Err> {
        let path = cfg_file_path.as_ref();
        let text = std::fs::read_to_string(path).extend("reading configuration file")?;

        CfgFile::deserialize_json(&text).extend("deserializing configuration file")
    }
}

fn missing<T>(cfg_val: &str) -> Result<T, Ezs3Err> {
    let env_val = cfg_val.to_ascii_uppercase();
    let msg = format!(
        "no {} in config file or {} environment variable set",
        cfg_val, &env_val,
    );
    bail(msg)
}

impl Client {
    /// Instantiate and configure a new S3 client.
    ///
    /// The `Client` requires these pieces of information to function:
    /// * `aws_access_key_id`
    /// * `aws_secret_access_key`
    /// * `aws_region`
    /// * `aws_endpoint_url_s3`
    /// * `bucket_name`
    ///
    /// These can be supplied by passing a JSON configuration file or
    /// through environment variables, or some combination of the two. For
    /// example, you may wish to supply the less-sensitive values from a
    /// file:
    ///
    /// `config.json`:
    /// ```json
    /// {
    ///   "aws_region": "auto",
    ///   "aws_endpoint_url_s3": "https://t3.storage.dev",
    ///   "bucket_name": "my-app-bucket"
    /// }
    /// ```
    ///
    /// and the secret stuff via the environment:
    ///
    /// ```text
    /// AWS_ACCESS_KEY_ID="tqx4..."
    /// AWS_SECRET_ACCESS_KEY="0x9_4..."
    /// ```
    ///
    /// ## Note:
    ///
    /// * Keys in the JSON configuration file are expected to be in
    ///   lower-case; environment variable names are expected to be in
    ///   upper-case.
    /// * If a given value is supplied in _both_ the config file _and_ as
    ///   an environment variable, the environment variable will take
    ///   precedence.
    ///
    /// ```
    /// use ezs3::{Client, Ezs3Err};
    ///
    /// # fn run() -> Result<(), Ezs3Err> {
    /// let cfg = Client::configure(Some("config.json"))?;
    /// # Result::<(), Ezs3Err>::Ok(())
    /// # }
    /// ```
    pub fn configure<P: AsRef<Path>>(cfg_file_path: Option<P>) -> Result<Client, Ezs3Err> {
        let cfgf = match cfg_file_path.as_ref() {
            None => CfgFile::default(),
            Some(path) => CfgFile::load(path)?,
        };

        let key_id = match std::env::var("AWS_ACCESS_KEY_ID") {
            Ok(key_id) => key_id,
            Err(_) => match cfgf.aws_access_key_id {
                None => missing("aws_access_key_id")?,
                Some(key_id) => key_id,
            },
        };
        let key = match std::env::var("AWS_SECRET_ACCESS_KEY") {
            Ok(key) => key,
            Err(_) => match cfgf.aws_secret_access_key {
                None => missing("aws_secret_access_key")?,
                Some(key) => key,
            },
        };
        let endpoint = match std::env::var("AWS_ENDPOINT_URL_S3") {
            Ok(epstr) => Url::parse(&epstr).extend("parsing AWS_ENDPOINT_URL_S3 value")?,
            Err(_) => match cfgf.aws_endpoint_url_s3 {
                None => missing("aws_endpoint_url_s3")?,
                Some(epstr) => Url::parse(&epstr).extend("parsing aws_endpoint_url_s3 value")?,
            },
        };
        let region = match std::env::var("AWS_REGION") {
            Ok(region) => region,
            Err(_) => match cfgf.aws_region {
                None => missing("aws_region")?,
                Some(region) => region,
            },
        };
        let bucket = match std::env::var("BUCKET_NAME") {
            Ok(bucket) => bucket,
            Err(_) => match cfgf.bucket_name {
                None => missing("bucket_name")?,
                Some(bucket) => bucket,
            },
        };

        let host = endpoint.host_str().ok_or_else(|| {
            err(format!(
                "configured endpoint URL ({}) has no host portion",
                &endpoint
            ))
        })?;

        let new_host = format!("{}.{}", &bucket, &host);
        let endpoint = {
            let mut endpoint = endpoint;
            endpoint.set_host(Some(&new_host)).map_err(|e| {
                err(format!(
                    "invalid bucket-specific host ({}): {}",
                    &new_host, &e
                ))
            })?;
            endpoint
        };
        let host = HeaderValue::from_str(&new_host).extend("creating Host header archetype")?;

        let client = Client {
            reqwester: reqwest::Client::new(),
            key_id,
            key,
            endpoint,
            region,
            bucket,
            host,
        };

        Ok(client)
    }
}
