use crate::{
  endpoint::S3Endpoint,
  error::Error,
  list::{TreeItem, TreeItemKind, TreeItems, TreeList, strip_relative_path},
};
use async_trait::async_trait;
use aws_sdk_s3::types::{CommonPrefix, Object};
use std::{convert::TryFrom, time::SystemTime};

const PATH_SEPARATOR: char = '/';

#[async_trait]
impl TreeList for S3Endpoint {
  async fn list(&self, root_path: &str, prefix: Option<&str>) -> Result<TreeItems, Error> {
    let root_path = Self::get_root_path(root_path, PATH_SEPARATOR);
    let prefix = Self::get_request_prefix("", prefix);
    let delimiter = Self::get_delimiter(PATH_SEPARATOR);
    let bucket = self.bucket().to_string();

    let client = self.connection();

    let response = client
      .list_objects_v2()
      .bucket(bucket.clone())
      .delimiter(delimiter)
      .prefix(prefix)
      .send()
      .await;

    let list_result = response.map(|resp| {
      let mut items =
        Self::parse_response_common_prefixes(resp.common_prefixes, self.bucket(), &root_path);

      items.append(&mut Self::parse_response_objects(
        resp.contents,
        self.bucket(),
        &root_path,
      ));

      items
    });

    let items = list_result.unwrap_or_else(|error| {
      log::warn!("Cannot access {} content: {:?}", self.bucket(), error);
      TreeItems::default()
    });

    Ok(items)
  }
}

impl S3Endpoint {
  fn get_request_prefix(root_path: &str, prefix: Option<&str>) -> String {
    let absolute_prefix = Self::get_absolute_prefix(root_path, prefix, PATH_SEPARATOR);

    format!(
      "{}{}",
      absolute_prefix
        .trim_start_matches(PATH_SEPARATOR)
        .trim_end_matches(PATH_SEPARATOR),
      PATH_SEPARATOR
    )
  }

  fn parse_response_common_prefixes(
    common_prefixes: Option<Vec<CommonPrefix>>,
    bucket: &str,
    prefix: &str,
  ) -> TreeItems {
    common_prefixes
      .map(|common_prefixes| {
        common_prefixes
          .iter()
          .filter_map(move |common_prefix| {
            let item = TreeItem::try_from((prefix, common_prefix));

            if let Err(error) = &item {
              log::warn!("Cannot access entry in {bucket}: {error:?}");
            }

            item.ok()
          })
          .collect()
      })
      .unwrap_or_default()
  }

  fn parse_response_objects(objects: Option<Vec<Object>>, bucket: &str, prefix: &str) -> TreeItems {
    objects
      .map(|objects| {
        Box::new(objects.iter().filter_map(move |object| {
          let item = TreeItem::try_from((prefix, object));

          if let Err(error) = &item {
            log::warn!("Cannot access entry in {bucket}: {error:?}");
          }

          item.ok()
        }))
        .collect()
      })
      .unwrap_or_default()
  }
}

impl TryFrom<(&str, &CommonPrefix)> for TreeItem {
  type Error = Error;

  fn try_from((prefix, common_prefix): (&str, &CommonPrefix)) -> Result<Self, Self::Error> {
    let path = common_prefix
      .prefix
      .as_ref()
      .ok_or_else(|| Error::Other(format!("No prefix in common prefix: {common_prefix:?}")))?;

    let path = strip_relative_path(prefix, path, PATH_SEPARATOR);
    let prefix = if prefix == PATH_SEPARATOR.to_string() {
      prefix
    } else {
      prefix.trim_end_matches(PATH_SEPARATOR)
    };

    Ok(Self::new(
      TreeItemKind::Folder,
      &path,
      prefix,
      0,
      SystemTime::UNIX_EPOCH,
      None,
      PATH_SEPARATOR,
    ))
  }
}

impl TryFrom<(&str, &Object)> for TreeItem {
  type Error = Error;

  fn try_from((prefix, object): (&str, &Object)) -> Result<Self, Self::Error> {
    let path = object.key.as_ref().ok_or_else(|| {
      Error::Other(format!(
        "Cannot retrieve 'key' value from S3 object: {object:?}"
      ))
    })?;

    let path = strip_relative_path(prefix, path, PATH_SEPARATOR);
    let prefix = if prefix == PATH_SEPARATOR.to_string() {
      prefix
    } else {
      prefix.trim_end_matches(PATH_SEPARATOR)
    };

    let size = object
      .size
      .as_ref()
      .ok_or_else(|| {
        Error::Other(format!(
          "Cannot retrieve 'size' value from S3 object: {object:?}"
        ))
      })
      .map(|size| *size as u64)?;

    let last_modified = object.last_modified.as_ref().ok_or_else(|| {
      Error::Other(format!(
        "Cannot retrieve 'last_modified' value from S3 object: {object:?}"
      ))
    })?;
    let last_update = SystemTime::try_from(*last_modified)?;

    Ok(Self::new(
      TreeItemKind::File,
      &path,
      prefix,
      size,
      last_update,
      None,
      PATH_SEPARATOR,
    ))
  }
}

#[test]
pub fn test_get_root_path() {
  assert_eq!("/", &S3Endpoint::get_root_path("", PATH_SEPARATOR));
  assert_eq!("/", &S3Endpoint::get_root_path("/", PATH_SEPARATOR));
  assert_eq!("/root", &S3Endpoint::get_root_path("root", PATH_SEPARATOR));
  assert_eq!("/root", &S3Endpoint::get_root_path("root/", PATH_SEPARATOR));
  assert_eq!("/root", &S3Endpoint::get_root_path("/root", PATH_SEPARATOR));
  assert_eq!(
    "/root",
    &S3Endpoint::get_root_path("/root/", PATH_SEPARATOR)
  );
  assert_eq!(
    "/root/path",
    &S3Endpoint::get_root_path("root/path", PATH_SEPARATOR)
  );
  assert_eq!(
    "/root/path",
    &S3Endpoint::get_root_path("root/path/", PATH_SEPARATOR)
  );
  assert_eq!(
    "/root/path",
    &S3Endpoint::get_root_path("/root/path", PATH_SEPARATOR)
  );
  assert_eq!(
    "/root/path",
    &S3Endpoint::get_root_path("/root/path/", PATH_SEPARATOR)
  );
}

#[test]
pub fn test_list_request_prefix() {
  assert_eq!("/", &S3Endpoint::get_request_prefix("", None));
  assert_eq!("/", &S3Endpoint::get_request_prefix("/", None));
  assert_eq!("root/", &S3Endpoint::get_request_prefix("/root", None));
  assert_eq!(
    "root/path/",
    &S3Endpoint::get_request_prefix("/root/path", None)
  );

  assert_eq!(
    "prefix/",
    &S3Endpoint::get_request_prefix("", Some("prefix"))
  );
  assert_eq!(
    "prefix/",
    &S3Endpoint::get_request_prefix("/", Some("prefix"))
  );
  assert_eq!(
    "root/prefix/",
    &S3Endpoint::get_request_prefix("/root", Some("prefix"))
  );
  assert_eq!(
    "root/path/prefix/",
    &S3Endpoint::get_request_prefix("/root/path", Some("prefix"))
  );
}
