use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use sha2::{Digest, Sha256};
use tokio::sync::Mutex;

use crate::bus::MediaItem;
use crate::config::{FileTransferConfig, expand_path};
use crate::protocol::UploadDescriptor;

#[derive(Debug, Clone)]
pub struct PendingUpload {
    pub descriptor: UploadDescriptor,
    pub owner_chat_id: String,
    pub path: PathBuf,
    expires_at: Instant,
}

#[derive(Debug, thiserror::Error)]
pub enum UploadError {
    #[error("file transfer is disabled")]
    Disabled,
    #[error("too many files; maximum is {0}")]
    TooManyFiles(usize),
    #[error("duplicate upload id")]
    Duplicate,
    #[error("upload not found or expired")]
    NotFound,
    #[error("upload does not belong to this client")]
    WrongOwner,
    #[error("attachments exceed the per-message limit of {0} bytes")]
    MessageTooLarge(u64),
}

#[derive(Clone)]
pub struct UploadRegistry {
    config: FileTransferConfig,
    upload_dir: PathBuf,
    pending: Arc<Mutex<HashMap<String, PendingUpload>>>,
}

impl UploadRegistry {
    pub fn new(config: FileTransferConfig) -> Self {
        let upload_dir = expand_path(&config.upload_dir);
        Self {
            config,
            upload_dir,
            pending: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn disabled() -> Self {
        let config = FileTransferConfig {
            enabled: false,
            ..FileTransferConfig::default()
        };
        Self::new(config)
    }

    pub fn enabled(&self) -> bool {
        self.config.enabled
    }

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

    pub fn safe_file_name(name: &str) -> String {
        let base = Path::new(name)
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or("attachment");
        let cleaned = base
            .chars()
            .filter(|character| !character.is_control() && *character != '/' && *character != '\\')
            .collect::<String>();
        let cleaned = cleaned.trim().trim_matches('.');
        let mut result = if cleaned.is_empty() {
            "attachment".to_string()
        } else {
            cleaned.to_string()
        };
        while result.len() > 180 {
            result.pop();
        }
        result
    }

    pub async fn allocate_path(
        &self,
        chat_id: &str,
        file_name: &str,
    ) -> Result<(PathBuf, PathBuf, String), std::io::Error> {
        let scope_hash = hex_digest(chat_id.as_bytes());
        let upload_id = uuid::Uuid::new_v4().to_string();
        let directory = self.upload_dir.join(&scope_hash[..16]).join(&upload_id);
        tokio::fs::create_dir_all(&directory).await?;
        let safe_name = Self::safe_file_name(file_name);
        let final_path = directory.join(&safe_name);
        let temporary_path = directory.join(".upload.part");
        Ok((temporary_path, final_path, upload_id))
    }

    pub async fn register(
        &self,
        upload_id: String,
        chat_id: String,
        path: PathBuf,
        mut descriptor: UploadDescriptor,
    ) -> Result<UploadDescriptor, UploadError> {
        if !self.enabled() {
            return Err(UploadError::Disabled);
        }
        let ttl = Duration::from_secs(self.config.pending_ttl_seconds.max(1));
        let expires_at = Instant::now() + ttl;
        let expires_at_unix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
            .saturating_add(ttl.as_secs()) as i64;
        descriptor.upload_id = upload_id.clone();
        descriptor.expires_at = expires_at_unix;
        let mut pending = self.pending.lock().await;
        pending.insert(
            upload_id,
            PendingUpload {
                descriptor: descriptor.clone(),
                owner_chat_id: chat_id,
                path,
                expires_at,
            },
        );
        Ok(descriptor)
    }

    pub async fn take_many(
        &self,
        chat_id: &str,
        upload_ids: &[String],
    ) -> Result<Vec<PendingUpload>, UploadError> {
        if upload_ids.is_empty() {
            return Ok(Vec::new());
        }
        if !self.enabled() {
            return Err(UploadError::Disabled);
        }
        if upload_ids.len() > self.config.max_files_per_message {
            return Err(UploadError::TooManyFiles(self.config.max_files_per_message));
        }
        let unique = upload_ids.iter().collect::<HashSet<_>>();
        if unique.len() != upload_ids.len() {
            return Err(UploadError::Duplicate);
        }

        let now = Instant::now();
        let mut pending = self.pending.lock().await;
        let mut total = 0_u64;
        for id in upload_ids {
            let upload = pending.get(id).ok_or(UploadError::NotFound)?;
            if upload.expires_at <= now {
                return Err(UploadError::NotFound);
            }
            if upload.owner_chat_id != chat_id {
                return Err(UploadError::WrongOwner);
            }
            total = total.saturating_add(upload.descriptor.size);
        }
        if total > self.config.max_message_bytes {
            return Err(UploadError::MessageTooLarge(self.config.max_message_bytes));
        }

        Ok(upload_ids
            .iter()
            .filter_map(|id| pending.remove(id))
            .collect())
    }

    pub async fn restore(&self, uploads: Vec<PendingUpload>) {
        let mut pending = self.pending.lock().await;
        for upload in uploads {
            pending.insert(upload.descriptor.upload_id.clone(), upload);
        }
    }

    pub async fn cleanup_expired(&self) -> usize {
        let now = Instant::now();
        let expired = {
            let mut pending = self.pending.lock().await;
            let ids = pending
                .iter()
                .filter(|(_, upload)| upload.expires_at <= now)
                .map(|(id, _)| id.clone())
                .collect::<Vec<_>>();
            ids.into_iter()
                .filter_map(|id| pending.remove(&id))
                .map(|upload| upload.path)
                .collect::<Vec<_>>()
        };
        let count = expired.len();
        for path in expired {
            let _ = tokio::fs::remove_file(&path).await;
            if let Some(parent) = path.parent() {
                let _ = tokio::fs::remove_dir(parent).await;
            }
        }
        count
    }

    pub fn as_media(upload: &PendingUpload) -> MediaItem {
        MediaItem {
            path: upload.path.to_string_lossy().into_owned(),
            media_type: upload.descriptor.media_type.clone(),
            mime_type: Some(upload.descriptor.mime_type.clone()),
            original_key: None,
        }
    }
}

fn hex_digest(value: &[u8]) -> String {
    let digest = Sha256::digest(value);
    digest.iter().map(|byte| format!("{byte:02x}")).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sanitizes_file_names() {
        assert_eq!(
            UploadRegistry::safe_file_name("../../report.pdf"),
            "report.pdf"
        );
        assert_eq!(UploadRegistry::safe_file_name(".."), "attachment");
        assert_eq!(UploadRegistry::safe_file_name("a\n.txt"), "a.txt");
    }

    #[tokio::test]
    async fn upload_ids_are_scoped_and_consumed() {
        let registry = UploadRegistry::new(FileTransferConfig::default());
        registry
            .register(
                "upload-1".into(),
                "client-a".into(),
                PathBuf::from("/tmp/a"),
                UploadDescriptor {
                    upload_id: String::new(),
                    name: "a.txt".into(),
                    media_type: "file".into(),
                    mime_type: "text/plain".into(),
                    size: 10,
                    expires_at: 0,
                },
            )
            .await
            .unwrap();

        assert!(matches!(
            registry.take_many("client-b", &["upload-1".into()]).await,
            Err(UploadError::WrongOwner)
        ));
        assert_eq!(
            registry
                .take_many("client-a", &["upload-1".into()])
                .await
                .unwrap()
                .len(),
            1
        );
        assert!(matches!(
            registry.take_many("client-a", &["upload-1".into()]).await,
            Err(UploadError::NotFound)
        ));
    }

    #[tokio::test]
    async fn expired_pending_uploads_are_removed() {
        let directory = tempfile::tempdir().unwrap();
        let config = FileTransferConfig {
            upload_dir: directory.path().to_string_lossy().into_owned(),
            pending_ttl_seconds: 1,
            ..FileTransferConfig::default()
        };
        let registry = UploadRegistry::new(config);
        let path = directory.path().join("expired.txt");
        tokio::fs::write(&path, b"expired").await.unwrap();
        registry
            .register(
                "expired".into(),
                "client".into(),
                path.clone(),
                UploadDescriptor {
                    upload_id: String::new(),
                    name: "expired.txt".into(),
                    media_type: "file".into(),
                    mime_type: "text/plain".into(),
                    size: 7,
                    expires_at: 0,
                },
            )
            .await
            .unwrap();
        tokio::time::sleep(Duration::from_millis(1_050)).await;

        assert_eq!(registry.cleanup_expired().await, 1);
        assert!(!path.exists());
    }
}
