use std::time::Duration;
use uuid::Uuid;

use storage::ObjectStorage;

use crate::{ChannelError, ChannelResult};

const ATTACHMENT_KEY_PREFIX: &str = "attachments";
const DEFAULT_PRESIGNED_TTL: Duration = Duration::from_secs(3600);
const DEFAULT_MAX_FILE_SIZE: u64 = 50 * 1024 * 1024; // 50 MB

#[derive(Clone)]
pub struct CdnManager {
    storage: storage::AppStorage,
    presigned_ttl: Duration,
    max_file_size: u64,
}

impl CdnManager {
    pub fn new(storage: storage::AppStorage) -> Self {
        Self {
            storage,
            presigned_ttl: DEFAULT_PRESIGNED_TTL,
            max_file_size: DEFAULT_MAX_FILE_SIZE,
        }
    }

    pub fn with_config(
        storage: storage::AppStorage,
        presigned_ttl: Duration,
        max_file_size: u64,
    ) -> Self {
        Self {
            storage,
            presigned_ttl,
            max_file_size,
        }
    }

    pub fn max_file_size(&self) -> u64 {
        self.max_file_size
    }

    pub async fn upload_file(
        &self,
        room_id: Uuid,
        message_id: Uuid,
        file_data: &[u8],
        filename: &str,
        content_type: Option<String>,
    ) -> ChannelResult<CdnStoredFile> {
        if file_data.len() as u64 > self.max_file_size {
            return Err(ChannelError::Internal(
                "file exceeds max size".to_string(),
            ));
        }

        let filename = sanitize_filename(filename);
        let key = format!(
            "{}/{}/{}/{}",
            ATTACHMENT_KEY_PREFIX, room_id, message_id, filename
        );

        let options = storage::PutObjectOptions {
            content_type,
            content_length: Some(file_data.len() as i64),
            cache_control: Some("public, max-age=86400".to_string()),
        };

        let stored = self
            .storage
            .put_bytes(&key, file_data.to_vec(), options)
            .await?;

        Ok(CdnStoredFile {
            key: stored.key,
            url: stored.url,
            e_tag: stored.e_tag,
            size: file_data.len() as i64,
        })
    }

    pub async fn get_file(&self, key: &str) -> ChannelResult<CdnFileContent> {
        let object = self.storage.get_bytes(key).await?;
        Ok(CdnFileContent {
            bytes: object.bytes,
            content_type: object.content_type,
            content_length: object.content_length,
        })
    }

    pub async fn delete_file(&self, key: &str) -> ChannelResult<()> {
        self.storage.delete(key).await?;
        Ok(())
    }

    pub fn public_url(&self, key: &str) -> ChannelResult<Option<String>> {
        self.storage.public_url(key).map_err(Into::into)
    }

    pub async fn presigned_url(&self, key: &str) -> ChannelResult<String> {
        Ok(self
            .storage
            .presigned_get_url(key, self.presigned_ttl)
            .await?)
    }
}
fn sanitize_filename(name: &str) -> String {
    let cleaned: String = name
        .chars()
        .filter(|&c| c.is_ascii_graphic() && c != '/' && c != '\\' && c != '\0')
        .take(255)
        .collect::<String>()
        .trim()
        .to_owned();
    if cleaned.is_empty() {
        uuid::Uuid::new_v4().to_string()
    } else {
        cleaned
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CdnStoredFile {
    pub key: String,
    pub url: String,
    pub e_tag: Option<String>,
    pub size: i64,
}

#[derive(Debug)]
pub struct CdnFileContent {
    pub bytes: Vec<u8>,
    pub content_type: Option<String>,
    pub content_length: Option<i64>,
}
