use std::{
    io::{BufWriter, Write},
    path::PathBuf,
};

use anyhow::Result;
use futures_util::StreamExt;
use kordophone::APIInterface;
use thiserror::Error;

use kordophone_db::database::Database;

use crate::daemon::events::Event as DaemonEvent;
use crate::daemon::events::Reply;
use crate::daemon::models::Attachment;
use crate::daemon::Daemon;

use std::sync::Arc;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::{Mutex, Semaphore};

use uuid::Uuid;

mod target {
    pub static ATTACHMENTS: &str = "attachments";
}

#[derive(Debug)]
pub enum AttachmentStoreEvent {
    // Get the attachment info for a given attachment guid.
    // Args: attachment guid, reply channel.
    GetAttachmentInfo(String, Reply<Attachment>),

    // Queue a download for a given attachment guid.
    // Args:
    // - attachment guid
    // - preview: whether to download the preview (true) or full attachment (false)
    QueueDownloadAttachment(String, bool),

    // Queue an upload for a given attachment file.
    // Args:
    // - path: the path to the attachment file
    // - reply: a reply channel to send the pending upload guid to
    QueueUploadAttachment(PathBuf, Reply<String>),
}

#[derive(Debug, Error)]
enum AttachmentStoreError {
    #[error("attachment has already been downloaded")]
    AttachmentAlreadyDownloaded,

    #[error("temporary file already exists, assuming download is in progress")]
    DownloadAlreadyInProgress,

    #[error("Client error: {0}")]
    APIClientError(String),
}

pub struct AttachmentStore {
    store_path: PathBuf,
    database: Arc<Mutex<Database>>,
    daemon_event_sink: Sender<DaemonEvent>,

    event_source: Receiver<AttachmentStoreEvent>,
    event_sink: Option<Sender<AttachmentStoreEvent>>,

    // Limits concurrent downloads to avoid overloading server and local I/O
    download_limit: Arc<Semaphore>,
}

impl AttachmentStore {
    pub fn get_default_store_path() -> PathBuf {
        let data_dir = Daemon::get_data_dir().expect("Unable to get data path");
        data_dir.join("attachments")
    }

    pub fn new(
        database: Arc<Mutex<Database>>,
        daemon_event_sink: Sender<DaemonEvent>,
    ) -> AttachmentStore {
        let store_path = Self::get_default_store_path();
        log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());

        // Create the attachment store if it doesn't exist
        std::fs::create_dir_all(&store_path)
            .expect("Wasn't able to create the attachment store path");

        let (event_sink, event_source) = tokio::sync::mpsc::channel(100);

        // Limit to at most 5 concurrent downloads by default
        let download_limit = Arc::new(Semaphore::new(5));

        AttachmentStore {
            store_path: store_path,
            database: database,
            daemon_event_sink: daemon_event_sink,
            event_source: event_source,
            event_sink: Some(event_sink),
            download_limit,
        }
    }

    pub fn get_event_sink(&mut self) -> Sender<AttachmentStoreEvent> {
        self.event_sink.take().unwrap()
    }

    fn get_attachment(&self, guid: &String) -> Attachment {
        Self::get_attachment_impl(&self.store_path, guid)
    }

    pub fn get_attachment_impl(store_path: &PathBuf, guid: &String) -> Attachment {
        let base_path = store_path.join(guid);
        let mut attachment = Attachment {
            guid: guid.to_owned(),
            base_path: base_path,
            metadata: None,
            mime_type: None,
            cached_full_path: None,
            cached_preview_path: None,
        };

        // Best-effort: if files already exist, cache their exact paths and infer MIME type.
        let stem = attachment
            .base_path
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default();

        let legacy_full = attachment.base_path.with_extension("full");
        if legacy_full.exists() {
            attachment.cached_full_path = Some(legacy_full);
        }

        let legacy_preview = attachment.base_path.with_extension("preview");
        if legacy_preview.exists() {
            attachment.cached_preview_path = Some(legacy_preview);
        }

        if attachment.cached_full_path.is_none() || attachment.cached_preview_path.is_none() {
            let full_prefix = format!("{}.full.", stem);
            let preview_prefix = format!("{}.preview.", stem);
            let parent = attachment
                .base_path
                .parent()
                .unwrap_or_else(|| std::path::Path::new("."));

            if let Ok(entries) = std::fs::read_dir(parent) {
                for entry in entries.flatten() {
                    let name = entry.file_name().to_string_lossy().to_string();

                    if !name.ends_with(".download") {
                        if attachment.cached_full_path.is_none() && name.starts_with(&full_prefix) {
                            attachment.cached_full_path = Some(entry.path());
                            continue;
                        }

                        if attachment.cached_preview_path.is_none()
                            && name.starts_with(&preview_prefix)
                        {
                            attachment.cached_preview_path = Some(entry.path());
                        }
                    }
                }
            }
        }

        if let Some(existing_full) = &attachment.cached_full_path {
            if let Some(m) = mime_guess::from_path(existing_full).first_raw() {
                attachment.mime_type = Some(m.to_string());
            }
        }

        attachment
    }

    async fn download_attachment_impl(
        store_path: &PathBuf,
        database: &mut Arc<Mutex<Database>>,
        daemon_event_sink: &Sender<DaemonEvent>,
        guid: &String,
        preview: bool,
    ) -> Result<()> {
        let attachment = Self::get_attachment_impl(store_path, guid);

        if attachment.is_downloaded(preview) {
            log::debug!(target: target::ATTACHMENTS, "Attachment already downloaded: {}", attachment.guid);
            return Err(AttachmentStoreError::AttachmentAlreadyDownloaded.into());
        }

        // Check if any in-progress temporary file exists already for this attachment
        if let Some(in_progress) = Self::find_in_progress_download(&attachment, preview) {
            log::warn!(target: target::ATTACHMENTS, "Temporary file already exists: {}, assuming download is in progress", in_progress.display());
            // Treat as a non-fatal condition so we don't spam errors.
            return Err(AttachmentStoreError::DownloadAlreadyInProgress.into());
        }

        log::debug!(target: target::ATTACHMENTS, "Starting download for attachment: {}", attachment.guid);

        let mut client = Daemon::get_client_impl(database).await?;
        let (mut stream, content_type) = client
            .fetch_attachment_with_metadata(&attachment.guid, preview)
            .await
            .map_err(|e| AttachmentStoreError::APIClientError(format!("{:?}", e)))?;

        let kind = if preview { "preview" } else { "full" };
        let normalized_mime = content_type
            .as_deref()
            .map(|s| s.split(';').next().unwrap_or(s).trim().to_string());

        let guessed_ext = normalized_mime
            .as_deref()
            .and_then(|m| Attachment::preferred_extension_for_mime(m))
            .unwrap_or("bin");

        let final_path = attachment
            .base_path
            .with_extension(format!("{}.{}", kind, guessed_ext));
        let temporary_path = final_path.with_extension(format!(
            "{}.download",
            final_path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("")
        ));

        let file = std::fs::File::create(&temporary_path)?;
        let mut writer = BufWriter::new(&file);

        log::trace!(target: target::ATTACHMENTS, "Writing attachment {:?} data to temporary file {:?}", &attachment.guid, &temporary_path);
        while let Some(Ok(data)) = stream.next().await {
            writer.write(data.as_ref())?;
        }

        // Flush and sync the temporary file before moving
        writer.flush()?;
        file.sync_all()?;

        // Atomically move the temporary file to the final location
        std::fs::rename(&temporary_path, &final_path)?;

        log::debug!(target: target::ATTACHMENTS, "Completed download for attachment: {}", attachment.guid);

        // Send a signal to the daemon that the attachment has been downloaded.
        let event = DaemonEvent::AttachmentDownloaded(attachment.guid.clone());
        daemon_event_sink.send(event).await.unwrap();

        Ok(())
    }

    fn find_in_progress_download(attachment: &Attachment, preview: bool) -> Option<PathBuf> {
        let kind = if preview { "preview" } else { "full" };

        // Legacy temp path: guid.<kind>.download
        let legacy = attachment
            .base_path
            .with_extension(format!("{}.download", kind));
        if legacy.exists() {
            return Some(legacy);
        }

        // Scan for any guid.<kind>.<ext>.download
        let parent = attachment
            .base_path
            .parent()
            .unwrap_or_else(|| std::path::Path::new("."));
        let stem = attachment
            .base_path
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default();
        let prefix = format!("{}.{}.", stem, kind);

        if let Ok(entries) = std::fs::read_dir(parent) {
            for entry in entries.flatten() {
                let name = entry.file_name().to_string_lossy().to_string();
                if name.starts_with(&prefix) && name.ends_with(".download") {
                    return Some(entry.path());
                }
            }
        }

        None
    }

    async fn upload_attachment_impl(
        store_path: &PathBuf,
        incoming_path: &PathBuf,
        upload_guid: &String,
        database: &mut Arc<Mutex<Database>>,
        daemon_event_sink: &Sender<DaemonEvent>,
    ) -> Result<String> {
        use tokio::fs::File;
        use tokio::io::BufReader;

        // Create uploads directory if it doesn't exist.
        let uploads_path = store_path.join("uploads");
        std::fs::create_dir_all(&uploads_path).unwrap();

        // First, copy the file to the store path, under /uploads/.
        log::trace!(target: target::ATTACHMENTS, "Copying attachment to uploads directory: {}", uploads_path.display());
        let temporary_path = uploads_path.join(incoming_path.file_name().unwrap());
        std::fs::copy(incoming_path, &temporary_path).unwrap();

        // Open file handle to the temporary file,
        log::trace!(target: target::ATTACHMENTS, "Opening stream to temporary file: {}", temporary_path.display());
        let file = File::open(&temporary_path).await?;
        let reader: BufReader<File> = BufReader::new(file);

        // Upload the file to the server.
        let filename = incoming_path.file_name().unwrap().to_str().unwrap();
        log::trace!(target: target::ATTACHMENTS, "Uploading attachment to server: {}", &filename);
        let mut client = Daemon::get_client_impl(database).await?;

        let metadata = std::fs::metadata(&temporary_path)?;
        let size = metadata.len();
        let guid = client.upload_attachment(reader, filename, size).await?;

        // Delete the temporary file.
        log::debug!(target: target::ATTACHMENTS, "Upload completed with guid {}, deleting temporary file: {}", guid, temporary_path.display());
        std::fs::remove_file(&temporary_path).unwrap();

        // Send a signal to the daemon that the attachment has been uploaded.
        let event = DaemonEvent::AttachmentUploaded(upload_guid.clone(), guid.clone());
        daemon_event_sink.send(event).await.unwrap();

        Ok(guid)
    }

    pub async fn run(&mut self) {
        loop {
            tokio::select! {
                Some(event) = self.event_source.recv() => {
                    log::debug!(target: target::ATTACHMENTS, "Received attachment store event: {:?}", event);

                    match event {
                        AttachmentStoreEvent::QueueDownloadAttachment(guid, preview) => {
                            let attachment = self.get_attachment(&guid);
                            if !attachment.is_downloaded(preview) {
                                let store_path = self.store_path.clone();
                                let mut database = self.database.clone();
                                let daemon_event_sink = self.daemon_event_sink.clone();
                                let _guid = guid.clone();
                                let limiter = self.download_limit.clone();

                                // Spawn a new task here so we don't block incoming queue events.
                                tokio::spawn(async move {
                                    // Acquire a slot in the concurrent download limiter.
                                    let _permit = limiter.acquire_owned().await.expect("Semaphore closed");
                                    let result = Self::download_attachment_impl(
                                        &store_path,
                                        &mut database,
                                        &daemon_event_sink,
                                        &_guid,
                                        preview,
                                    ).await;

                                    if let Err(e) = result {
                                        // Downgrade noise for expected cases
                                        if let Some(kind) = e.downcast_ref::<AttachmentStoreError>() {
                                            match kind {
                                                AttachmentStoreError::AttachmentAlreadyDownloaded => {
                                                    log::debug!(target: target::ATTACHMENTS, "Attachment already downloaded: {}", &_guid);
                                                    let _ = daemon_event_sink
                                                        .send(DaemonEvent::AttachmentDownloaded(_guid.clone()))
                                                        .await;
                                                }
                                                AttachmentStoreError::DownloadAlreadyInProgress => {
                                                    // Already logged a warning where detected
                                                    log::debug!(target: target::ATTACHMENTS, "Download already in progress: {}", &_guid);
                                                }
                                                _ => {
                                                    log::error!(target: target::ATTACHMENTS, "Error downloading attachment {}: {}", &_guid, kind);
                                                }
                                            }
                                        } else {
                                            log::error!(target: target::ATTACHMENTS, "Error downloading attachment {}: {}", &_guid, e);
                                        }
                                    }
                                });

                                log::debug!(target: target::ATTACHMENTS, "Queued download for attachment: {}", &guid);
                            } else {
                                log::debug!(target: target::ATTACHMENTS, "Attachment already downloaded: {}", guid);
                                let _ = self
                                    .daemon_event_sink
                                    .send(DaemonEvent::AttachmentDownloaded(guid))
                                    .await;
                            }
                        }

                        AttachmentStoreEvent::GetAttachmentInfo(guid, reply) => {
                            let attachment = self.get_attachment(&guid);
                            reply.send(attachment).unwrap();
                        }

                        AttachmentStoreEvent::QueueUploadAttachment(path, reply) => {
                            let upload_guid = Uuid::new_v4().to_string();
                            let store_path = self.store_path.clone();
                            let mut database = self.database.clone();
                            let daemon_event_sink = self.daemon_event_sink.clone();

                            let _upload_guid = upload_guid.clone();
                            tokio::spawn(async move {
                                let result = Self::upload_attachment_impl(
                                    &store_path,
                                    &path,
                                    &_upload_guid,
                                    &mut database,
                                    &daemon_event_sink,
                                ).await;

                                if let Err(e) = result {
                                    log::error!(target: target::ATTACHMENTS, "Error uploading attachment {}: {}", &_upload_guid, e);
                                }
                            });

                            reply.send(upload_guid).unwrap();
                        }
                    }
                }
            }
        }
    }
}
