use super::Options;
use crate::models;
use crate::storage::{Error, object_store::ObjectStore};
use async_trait::async_trait;
use pbbson::Model;
use pbbson::bson::spec::BinarySubtype;
use pbbson::bson::{self, Binary, Bson, DateTime};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tempfile::NamedTempFile;
use tokio::fs;
use url::Url;

pub struct LocalFileSystemObjectStore {
    #[allow(unused)]
    url: Url,
}

impl LocalFileSystemObjectStore {
    pub async fn new(url: &Url, _options: Options) -> Result<Self, Error> {
        Ok(Self { url: url.clone() })
    }
}

#[async_trait]
impl ObjectStore for LocalFileSystemObjectStore {
    async fn create(&self, metadata: Model, path: &Path) -> Result<Model, Error> {
        // Validate
        let mut metadata = metadata.clone();
        if !metadata.contains_field("filename") {
            return Err(Error::invalid_argument("Metadata requires a filename"));
        }

        // Get or assign id
        let id = match metadata.id() {
            Ok(id) => id,
            _ => {
                let id = xid::new().to_string();
                metadata.insert("id", id.clone());
                id
            }
        };

        // Copy file into place
        let to_path = PathBuf::from(self.url.path()).join(id);
        let bytes_written = tokio::fs::copy(path, &to_path)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;

        // Update metadata when needed
        if !metadata.contains_field("createdAt") {
            let now = DateTime::from_system_time(SystemTime::now());
            metadata.set_datetime("createdAt", now);
        }
        if !metadata.contains_field("length") {
            metadata.insert("length", Bson::Int64(bytes_written as i64));
        }
        if !metadata.contains_field("md5") {
            let mut file = File::open(path).map_err(|e| Error::internal(e.to_string()))?;
            if let Ok(file_metadata) = file.metadata() {
                let mut buf = vec![0; file_metadata.len() as usize];
                file.read(&mut buf).map_err(|e| Error::internal(e.to_string()))?;
                metadata.insert(
                    "md5",
                    Bson::Binary(Binary {
                        subtype: BinarySubtype::Md5,
                        bytes: md5::compute(&buf).to_vec(),
                    }),
                );
            }
        }

        // Store metadata
        let metadata_path = format!("{}.json", to_path.display());
        let metadata_as_json = models::to_json(&metadata, false)?;
        let data = serde_json::to_vec(&metadata_as_json).map_err(|e| Error::internal(e.to_string()))?;
        tokio::fs::write(&metadata_path, &data)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;

        Ok(metadata)
    }

    async fn delete(&self, id: &str, by_account_id: Option<String>) -> Result<(), Error> {
        let mut metadata = self.find(id).await?;
        let now = DateTime::from_system_time(SystemTime::now());
        metadata.insert("deletedAt", now);
        if let Some(by_account_id) = by_account_id {
            metadata.insert("deletedBy", Bson::String(by_account_id));
        }
        let _ = self.update(metadata).await?;
        Ok(())
    }

    async fn download(&self, id: &str) -> Result<NamedTempFile, Error> {
        let path = PathBuf::from(self.url.path()).join(id);
        let file = NamedTempFile::new().map_err(|e| Error::internal(e.to_string()))?;
        fs::copy(path, file.path())
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        Ok(file)
    }

    async fn find(&self, id: &str) -> Result<Model, Error> {
        let path = PathBuf::from(self.url.path()).join(format!("{id}.json"));
        if !path.exists() {
            return Err(Error::NotFound("No such file".to_string()));
        }
        let data = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        let json_data: serde_json::Value = serde_json::from_str(&data).map_err(|e| Error::internal(e.to_string()))?;
        let mut doc = bson::serialize_to_document(&json_data).map_err(|e| Error::internal(e.to_string()))?;
        doc.insert("id", Bson::String(id.to_string()));
        Ok(Model::from(doc))
    }

    async fn find_many(&self, _filter: Model, _ordering: Option<Model>) -> Result<Vec<Model>, Error> {
        let path = PathBuf::from(self.url.path());
        let mut metadatas = Vec::new();
        let mut reader = fs::read_dir(path).await.map_err(|e| Error::internal(e.to_string()))?;
        while let Ok(Some(entry)) = reader.next_entry().await {
            let filename = match entry.file_name().to_str() {
                None => continue,
                Some(filename) => filename.to_string(),
            };
            if !filename.ends_with(".json") {
                continue;
            }
            let data = tokio::fs::read_to_string(entry.path())
                .await
                .map_err(|e| Error::internal(e.to_string()))?;
            let json_data: serde_json::Value =
                serde_json::from_str(&data).map_err(|e| Error::internal(e.to_string()))?;
            let mut doc = bson::serialize_to_document(&json_data).map_err(|e| Error::internal(e.to_string()))?;
            let id = match filename.strip_suffix(".json") {
                None => continue,
                Some(id) => id.to_string(),
            };
            doc.insert("id", Bson::String(id));
            metadatas.push(Model::from(doc));
        }
        Ok(metadatas)
    }

    async fn update(&self, metadata: Model) -> Result<Model, Error> {
        let id = metadata.id()?;
        let to_path = PathBuf::from(self.url.path()).join(id);
        let metadata_path = format!("{}.json", to_path.display());
        let metadata_as_json = models::to_json(&metadata, false)?;
        let data = serde_json::to_vec(&metadata_as_json).map_err(|e| Error::internal(e.to_string()))?;
        tokio::fs::write(&metadata_path, &data)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        Ok(metadata)
    }
}
