use std::collections::HashSet;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::Path;

use async_trait::async_trait;
use chrono::Datelike;

use crate::constants::ELASTIC_FLICKR_PHOTOS_INDEX;
use crate::download::downloader::Downloader;
use crate::errors::*;
use crate::helpers::es_helper::ElasticHelper;
use crate::models::flickr_photo::FlickrPhoto;
use crate::processors::report::Report;
use crate::services::es_client::ElasticClient;
use crate::services::flickr_client::FlickrClient;

#[derive(Debug, Clone)]
pub(crate) struct FlickrDownloader {
    es_client: ElasticClient,
    es_helper: ElasticHelper,
    flick_client: FlickrClient,
    // TODO replace string path with Path or PathBuf
    path: String,
}

#[derive(Debug, Clone)]
pub(crate) enum DownloadStatus {
    OK,
    ABORTED,
    ERROR,
}

impl FlickrDownloader {
    pub fn new(path: String) -> Self {
        FlickrDownloader {
            es_client: ElasticClient::new(ELASTIC_FLICKR_PHOTOS_INDEX),
            es_helper: ElasticHelper::new(ELASTIC_FLICKR_PHOTOS_INDEX),
            flick_client: FlickrClient::new(),
            path,
        }
    }

    async fn get_photo_taken_date(&self, photo: &FlickrPhoto) -> (String, String, String) {
        let taken_date = self.flick_client.get_flickr_photo_taken_date(photo).await.expect("flickr taken date error");
        let year = taken_date.year().to_string();
        let month = FlickrDownloader::add_0_to_short_numbers(taken_date.month().to_string());
        let day = FlickrDownloader::add_0_to_short_numbers(taken_date.day().to_string());
        (year, month, day)
    }

    fn check_dir_exists_or_create(&self, year: String, month: String, day: String) -> Option<String> {
        let path = [&self.path, year.as_str(), month.as_str(), day.as_str()].join("/");

        if Path::new(path.as_str()).exists() {
            Some(path)
        } else {
            if Path::new(&self.path.as_str()).exists() {
                let result = create_dir_all(&path.as_str());
                if result.is_ok() {
                    return Some(path)
                }
            }
            None
        }
    }

    pub(crate) async fn download_photo(&self, photo: &FlickrPhoto, path: String) -> DownloadStatus {
        let dir = Path::new(path.as_str());
        let response = reqwest::get(self.get_target_url(&photo).as_str()).await;
        if response.is_err() {
            return DownloadStatus::ERROR;
        }
        let mut response = response.unwrap();

        let filename = dir.join(format!("{}.{}", &photo.title, &photo.originalformat));
        let dest = File::create(filename);

        if dest.is_ok() {
            let mut dest = dest.unwrap();
            while let Some(chunk) = response.chunk().await.unwrap() {
                let result = dest.write_all(&chunk);
                if result.is_err() {
                    return DownloadStatus::ABORTED;
                }
            }
            DownloadStatus::OK
        } else {
            DownloadStatus::ERROR
        }
    }

    fn get_target_url(&self, photo: &FlickrPhoto) -> String {
        format!("https://farm{}.staticflickr.com/{}/{}_{}_o.{}",
                photo.farm, photo.server, photo.id,
                photo.originalsecret, photo.originalformat)
    }

    fn add_0_to_short_numbers(number: String) -> String {
        if number.len() < 2 {
            "0".to_owned() + &number
        } else {
            number
        }
    }
}

#[async_trait]
impl Downloader<FlickrPhoto> for FlickrDownloader {
    async fn download_photos(&self, photos: HashSet<FlickrPhoto>) -> Result<Report> {
        let mut report = Report::new();
        let dir = Path::new(&self.path);
        if !dir.exists() || !dir.is_dir() {
            return Err("path does not exists or is not a directory".into());
        }
        report.files_to_process = photos.len() as u32;

        for mut photo in photos {
            let (year, month, day): (String, String, String) = self.get_photo_taken_date(&photo).await;
            let path = self.check_dir_exists_or_create(year, month, day)
                .expect("directory creation encounters an error");
            println!("photo expected target : {:?}", path);
            match self.download_photo(&photo, path).await {
                DownloadStatus::OK => {
                    report.files_processed += 1;
                    photo.downloaded = true;
                    let _result = self.es_client.index_flickr_photo(&photo).await;
                },
                DownloadStatus::ABORTED => report.files_aborted += 1,
                DownloadStatus::ERROR => report.errors += 1
            }
        };
        Ok(report)
    }
}
