use std::fmt::Debug;

pub use error::*;
use serde::{Deserialize, Serialize};

mod error;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum IpfsConnectionType {
    HttpGateway,
    IpfsNode,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub struct IpfsLoaderConfig {
    connection_type: IpfsConnectionType,
    base_url: String,
}

impl IpfsLoaderConfig {
    pub fn http_gateway(url: &str) -> IpfsLoaderConfig {
        IpfsLoaderConfig {
            connection_type: IpfsConnectionType::HttpGateway,
            base_url: url.to_string(),
        }
    }

    pub fn ipfs_node(url: &str) -> IpfsLoaderConfig {
        IpfsLoaderConfig {
            connection_type: IpfsConnectionType::IpfsNode,
            base_url: url.to_string(),
        }
    }
}

impl Default for IpfsLoaderConfig {
    fn default() -> Self {
        Self {
            connection_type: IpfsConnectionType::HttpGateway,
            base_url: "https://cloudflare-ipfs.com/ipfs/".to_string(),
        }
    }
}

#[derive(Clone)]
pub struct IpfsLoader {
    connection_type: IpfsConnectionType,
    base_url: String,
    client: reqwest::Client,
}

impl IpfsLoader {
    pub fn new(config: &IpfsLoaderConfig) -> Result<Self, IpfsLoaderError> {
        Ok(Self {
            connection_type: config.connection_type.clone(),
            base_url: config.base_url.clone(),
            client: reqwest::Client::builder().build()?,
        })
    }

    #[allow(clippy::should_implement_trait)]
    pub fn default() -> Result<Self, IpfsLoaderError> {
        Self::new(&IpfsLoaderConfig::default())
    }

    pub async fn load(&self, path: &str) -> Result<Vec<u8>, IpfsLoaderError> {
        let response = match self.connection_type {
            IpfsConnectionType::HttpGateway => {
                let full_url = format!("{}/{}", self.base_url, path);
                self.client.get(full_url).send().await?
            }
            IpfsConnectionType::IpfsNode => {
                let full_url = format!("{}/api/v0/cat?arg={}", self.base_url, path);
                self.client.post(full_url).send().await?
            }
        };
        let status = response.status();
        if status.is_success() {
            let bytes = response.bytes().await?.to_vec();
            Ok(bytes)
        } else {
            const MAX_MESSAGE_SIZE: usize = 200;
            let body = String::from_utf8_lossy(&response.bytes().await?).to_string();
            let message = if body.len() > MAX_MESSAGE_SIZE {
                format!("{}...", &body[0..MAX_MESSAGE_SIZE - 3])
            } else {
                body.clone()
            };

            Err(IpfsLoaderError::IpfsLoadObjectFailed {
                path: path.to_string(),
                status,
                message,
            })
        }
    }

    pub async fn load_utf8_lossy(&self, path: &str) -> Result<String, IpfsLoaderError> {
        let bytes = self.load(path).await?;
        let str = String::from_utf8_lossy(&bytes).to_string();
        Ok(str)
    }
}

#[cfg(test)]
mod tests {
    use crate::meta::{IpfsConnectionType, IpfsLoaderConfig};

    static CONFIG_JSON: &str = r#"
    {
      "connection_type": "http_gateway",
      "base_url": "http://example.com/"
    }
    "#;

    #[test]
    fn test_config_deserialization() -> anyhow::Result<()> {
        let config: IpfsLoaderConfig = serde_json::from_str(CONFIG_JSON)?;
        assert_eq!(config.connection_type, IpfsConnectionType::HttpGateway);
        assert_eq!(config.base_url, "http://example.com/");
        Ok(())
    }
}
