pub mod config;
pub mod metadata;

use self::config::AccountConfig;
use self::metadata::MinifluxMetadata;
use crate::feed_api::{FeedApi, FeedApiErrorKind, FeedApiResult, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, CategoryType, Enclosure, FatArticle, FavIcon, Feed, FeedID, FeedMapping, LoginData, PasswordLogin,
    PluginCapabilities, SyncResult, TagID, Url, NEWSFLASH_TOPLEVEL,
};
use crate::util;
use crate::util::favicon_cache::EXPIRES_AFTER_DAYS;
use crate::util::html2text::Html2Text;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use failure::ResultExt;
use log::{error, info, warn};
use miniflux_api::models::{Category as MinifluxCategory, Entry as MinifluxArticle, EntryStatus, Feed as MinifluxFeed};
use miniflux_api::models::{OrderBy, OrderDirection};
use miniflux_api::MinifluxApi;
use rayon::prelude::*;
use reqwest::Client;
use std::convert::{From, TryInto};

const DEFAULT_CATEGORY: &str = "New Category";

pub struct Miniflux {
    api: Option<MinifluxApi>,
    portal: Box<dyn Portal>,
    logged_in: bool,
    config: AccountConfig,
}

impl Miniflux {
    fn convert_category_vec(mut categories: Vec<MinifluxCategory>) -> Vec<Category> {
        categories
            .drain(..)
            .enumerate()
            .map(|(i, c)| Miniflux::convert_category(c, Some(i as i32)))
            .collect()
    }

    fn convert_category(category: MinifluxCategory, sort_index: Option<i32>) -> Category {
        let (id, _user_id, title) = category.decompose();
        Category {
            category_id: CategoryID::new(&id.to_string()),
            label: title,
            sort_index,
            parent_id: NEWSFLASH_TOPLEVEL.clone(),
            category_type: CategoryType::Default,
        }
    }

    fn convert_feed(feed: MinifluxFeed, sort_index: Option<i32>) -> Feed {
        let (
            id,
            _user_id,
            title,
            site_url,
            feed_url,
            _rewrite_rules,
            _scraper_rules,
            _crawler,
            _checked_at,
            _etag_header,
            _last_modified_header,
            _parsing_error_count,
            _parsing_error_message,
            _category,
            _icon,
        ) = feed.decompose();

        Feed {
            feed_id: FeedID::new(&id.to_string()),
            label: title,
            website: match Url::parse(&site_url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            feed_url: match Url::parse(&feed_url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            icon_url: None,
            sort_index,
        }
    }

    fn convert_feed_vec(mut feeds: Vec<MinifluxFeed>) -> (Vec<Feed>, Vec<FeedMapping>) {
        let mut mappings: Vec<FeedMapping> = Vec::new();
        let feeds = feeds
            .drain(..)
            .enumerate()
            .map(|(i, f)| {
                mappings.push(FeedMapping {
                    feed_id: FeedID::new(&f.id.to_string()),
                    category_id: CategoryID::new(&f.category.id.to_string()),
                });

                Miniflux::convert_feed(f, Some(i as i32))
            })
            .collect();

        (feeds, mappings)
    }

    fn convert_entry(entry: MinifluxArticle, portal: &Box<dyn Portal>) -> FatArticle {
        let (id, _user_id, feed_id, title, url, _comments_url, author, content, _hash, published_at, status, starred, _feed) = entry.decompose();

        let article_id = ArticleID::new(&id.to_string());
        let article_exists_locally = match portal.get_article_exists(&article_id) {
            Ok(exists) => exists,
            Err(_) => false,
        };
        let plain_text = if article_exists_locally { None } else { Html2Text::process(&content) };
        let summary = if let Some(plain_text) = &plain_text {
            Some(Html2Text::to_summary(plain_text))
        } else {
            None
        };

        FatArticle {
            article_id,
            title: Some(title),
            author: if &author == "" { None } else { Some(author) },
            feed_id: FeedID::new(&feed_id.to_string()),
            url: match Url::parse(&url) {
                Ok(url) => Some(url),
                Err(_) => None,
            },
            date: match DateTime::parse_from_str(&published_at, "%Y-%m-%dT%H:%M:%S%.f%:z") {
                Ok(date) => date.with_timezone(&Utc),
                Err(_) => Utc::now(),
            }
            .naive_utc(),
            summary,
            html: Some(content),
            direction: None,
            unread: match status.as_str().try_into() {
                Ok(status) => match status {
                    EntryStatus::Read => models::Read::Read,
                    _ => models::Read::Unread,
                },
                Err(_) => models::Read::Unread,
            },
            marked: if starred { models::Marked::Marked } else { models::Marked::Unmarked },
            scraped_content: None,
            plain_text,
        }
    }

    fn convert_entry_vec(entries: Vec<MinifluxArticle>, portal: &Box<dyn Portal>) -> (Vec<FatArticle>, Vec<Enclosure>) {
        let enclosures: Vec<Enclosure> = Vec::new();
        let articles = entries.into_par_iter().map(|e| Miniflux::convert_entry(e, portal)).collect();

        (articles, enclosures)
    }

    pub async fn get_articles(
        &self,
        status: Option<EntryStatus>,
        before: Option<i64>,
        after: Option<i64>,
        before_entry_id: Option<i64>,
        after_entry_id: Option<i64>,
        starred: Option<bool>,
        client: &Client,
    ) -> FeedApiResult<Vec<FatArticle>> {
        if let Some(api) = &self.api {
            let batch_size: i64 = 100;
            let mut offset: Option<i64> = None;
            let mut articles: Vec<FatArticle> = Vec::new();

            loop {
                let entries = api
                    .get_entries(
                        status,                     // status
                        offset,                     // offset
                        Some(batch_size),           // limit
                        Some(OrderBy::PublishedAt), // order
                        Some(OrderDirection::Desc), // direction
                        before,                     // before
                        after,                      // after
                        before_entry_id,            // before_entry_id
                        after_entry_id,             // after_entry_id
                        starred,                    // starred
                        client,
                    )
                    .await
                    .context(FeedApiErrorKind::Api)?;

                let entry_count = entries.len();
                let (mut converted_articles, _) = Miniflux::convert_entry_vec(entries, &self.portal);
                articles.append(&mut converted_articles);

                if entry_count < batch_size as usize {
                    break;
                }

                offset = match offset {
                    Some(offset) => Some(offset + batch_size),
                    None => Some(batch_size),
                };
            }
            return Ok(articles);
        }
        Err(FeedApiErrorKind::Login)?
    }

    fn string_ids_to_miniflux_ids(ids: &[ArticleID]) -> Vec<i64> {
        ids.iter().filter_map(|article_id| article_id.to_string().parse::<i64>().ok()).collect()
    }
}

#[async_trait]
impl FeedApi for Miniflux {
    fn features(&self) -> FeedApiResult<PluginCapabilities> {
        Ok(PluginCapabilities::ADD_REMOVE_FEEDS | PluginCapabilities::SUPPORT_CATEGORIES | PluginCapabilities::MODIFY_CATEGORIES)
    }

    fn has_user_configured(&self) -> FeedApiResult<bool> {
        Ok(self.api.is_some())
    }

    async fn is_logged_in(&self, _client: &Client) -> FeedApiResult<bool> {
        Ok(self.logged_in)
    }

    fn user_name(&self) -> Option<String> {
        self.config.get_user_name()
    }

    fn get_login_data(&self) -> Option<LoginData> {
        if let Ok(true) = self.has_user_configured() {
            if let Some(username) = self.config.get_user_name() {
                if let Some(password) = self.config.get_password() {
                    return Some(LoginData::Password(PasswordLogin {
                        id: MinifluxMetadata::get_id(),
                        url: self.config.get_url(),
                        user: username,
                        password: password,
                        http_user: None, // miniflux authentication already uses basic auth
                        http_password: None,
                    }));
                }
            }
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let LoginData::Password(data) = data {
            let url_string = data.url.clone().ok_or(FeedApiErrorKind::Url)?;
            let url = Url::parse(&url_string).context(FeedApiErrorKind::Url)?;
            let api = MinifluxApi::new(&url.get(), data.user.clone(), data.password.clone());
            let user = api.get_current_user(client).await.context(FeedApiErrorKind::Api)?;
            println!("{:?}", user);
            self.config.set_url(&url_string);
            self.config.set_password(&data.password);
            self.config.set_user_name(&data.user);
            self.config.write()?;
            self.api = Some(api);
            self.logged_in = true;
            return Ok(());
        }

        self.logged_in = false;
        self.api = None;
        Err(FeedApiErrorKind::Login)?
    }

    async fn logout(&mut self, _client: &Client) -> FeedApiResult<()> {
        self.config.delete()?;
        Ok(())
    }

    async fn initial_sync(&self, client: &Client) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_categories(client).await.context(FeedApiErrorKind::Api)?;
            let categories = Miniflux::convert_category_vec(categories);

            let feeds = api.get_feeds(client).await.context(FeedApiErrorKind::Api)?;
            let (feeds, mappings) = Miniflux::convert_feed_vec(feeds);

            let mut articles: Vec<FatArticle> = Vec::new();

            // starred articles
            let mut starred = self
                .get_articles(
                    None,       // status
                    None,       // before
                    None,       // after
                    None,       // before_entry_id
                    None,       // after_entry_id
                    Some(true), // starred
                    client,
                )
                .await?;
            articles.append(&mut starred);

            // unread articles
            let mut unread = self
                .get_articles(
                    Some(EntryStatus::Unread), // status
                    None,                      // before
                    None,                      // after
                    None,                      // before_entry_id
                    None,                      // after_entry_id
                    Some(false),               // starred
                    client,
                )
                .await?;
            articles.append(&mut unread);

            // latest read articles
            let entries = api
                .get_entries(
                    Some(EntryStatus::Read),    // status
                    None,                       // offset
                    Some(100),                  // limit
                    Some(OrderBy::PublishedAt), // order
                    Some(OrderDirection::Desc), // direction
                    None,                       // before
                    None,                       // after
                    None,                       // before_entry_id
                    None,                       // after_entry_id
                    None,                       // starred
                    client,
                )
                .await
                .context(FeedApiErrorKind::Api)?;
            let (mut read_articles, _) = Miniflux::convert_entry_vec(entries, &self.portal);
            articles.append(&mut read_articles);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                mappings: util::vec_to_option(mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn sync(&self, max_count: u32, _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let categories = api.get_categories(client).await.context(FeedApiErrorKind::Api)?;
            let categories = Miniflux::convert_category_vec(categories);

            let feeds = api.get_feeds(client).await.context(FeedApiErrorKind::Api)?;
            let (feeds, mappings) = Miniflux::convert_feed_vec(feeds);

            let mut articles: Vec<FatArticle> = Vec::new();

            // unread articles
            let mut unread = self
                .get_articles(
                    Some(EntryStatus::Unread), // status
                    None,                      // before
                    None,                      // after
                    None,                      // before_entry_id
                    None,                      // after_entry_id
                    None,                      // starred
                    client,
                )
                .await?;
            articles.append(&mut unread);

            let entries = api
                .get_entries(
                    Some(EntryStatus::Read),    // status
                    None,                       // offset
                    Some(i64::from(max_count)), // limit
                    Some(OrderBy::PublishedAt), // order
                    Some(OrderDirection::Desc), // direction
                    None,                       // before
                    None,                       // after
                    None,                       // before_entry_id
                    None,                       // after_entry_id
                    None,                       // starred
                    client,
                )
                .await
                .context(FeedApiErrorKind::Api)?;
            let (mut read_articles, _) = Miniflux::convert_entry_vec(entries, &self.portal);
            articles.append(&mut read_articles);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                mappings: util::vec_to_option(mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: None,
            });
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_article_read(&self, articles: &[ArticleID], read: models::Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let entries = Miniflux::string_ids_to_miniflux_ids(articles);
            let status = match read {
                models::Read::Read => EntryStatus::Read,
                models::Read::Unread => EntryStatus::Unread,
            };
            api.update_entries_status(entries, status, client).await.context(FeedApiErrorKind::Api)?;

            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_article_marked(&self, articles: &[ArticleID], marked: models::Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let articles = self.portal.get_articles(articles).context(FeedApiErrorKind::Portal)?;

            for article in articles {
                if article.marked != marked {
                    if let Ok(entry_id) = article.article_id.to_string().parse::<i64>() {
                        api.toggle_bookmark(entry_id, client).await.context(FeedApiErrorKind::Api)?;
                    }
                }
            }

            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_feed_read(&self, feeds: &[FeedID], _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let mut unread_ids: Vec<i64> = Vec::new();
            for feed in feeds {
                let unread_feed_ids = self.portal.get_article_ids_unread_feed(feed).context(FeedApiErrorKind::Portal)?;
                let mut unread_feed_ids = Miniflux::string_ids_to_miniflux_ids(&unread_feed_ids);
                unread_ids.append(&mut unread_feed_ids);
            }
            if !unread_ids.is_empty() {
                api.update_entries_status(unread_ids, EntryStatus::Read, client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_category_read(&self, categories: &[CategoryID], _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let mut unread_ids: Vec<i64> = Vec::new();
            for category in categories {
                let unread_feed_ids = self.portal.get_article_ids_unread_category(category).context(FeedApiErrorKind::Portal)?;
                let mut unread_feed_ids = Miniflux::string_ids_to_miniflux_ids(&unread_feed_ids);
                unread_ids.append(&mut unread_feed_ids);
            }
            if !unread_ids.is_empty() {
                api.update_entries_status(unread_ids, EntryStatus::Read, client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn set_tag_read(&self, _tags: &[TagID], _last_sync: DateTime<Utc>, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn set_all_read(&self, _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let unread_ids = self.portal.get_article_ids_unread_all().context(FeedApiErrorKind::Portal)?;
            if !unread_ids.is_empty() {
                let unread_ids = Miniflux::string_ids_to_miniflux_ids(&unread_ids);
                api.update_entries_status(unread_ids, EntryStatus::Read, client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_feed(
        &self,
        url: &Url,
        title: Option<String>,
        category_id: Option<CategoryID>,
        client: &Client,
    ) -> FeedApiResult<(Feed, Option<Category>)> {
        if let Some(api) = &self.api {
            let category_id = match category_id {
                Some(category_id) => category_id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?,
                None => {
                    info!("Creating empty category for feed");
                    match api.create_category(DEFAULT_CATEGORY, client).await {
                        Ok(category) => category.id,
                        Err(_) => {
                            warn!("Creating empty category failed");
                            info!("Checking if 'New Category' already exists");

                            let categories = api.get_categories(client).await.context(FeedApiErrorKind::Api)?;

                            match categories.iter().find(|c| c.title == DEFAULT_CATEGORY) {
                                Some(new_category) => new_category.id,
                                None => match categories.first() {
                                    Some(first_category) => first_category.id,
                                    None => {
                                        error!("Was not able to create or find cateogry to add feed into");
                                        return Err(FeedApiErrorKind::Api)?;
                                    }
                                },
                            }
                        }
                    }
                }
            };

            let feed_id = api.create_feed(&url.get(), category_id, client).await.context(FeedApiErrorKind::Api)?;

            if let Some(title) = title {
                api.update_feed(feed_id, Some(&title), None, None, None, None, None, None, client)
                    .await
                    .context(FeedApiErrorKind::Api)?;
            }

            let feed = api.get_feed(feed_id, client).await.context(FeedApiErrorKind::Api)?;
            let category = api
                .get_categories(client)
                .await
                .context(FeedApiErrorKind::Api)?
                .iter()
                .find(|c| c.id == category_id)
                .map(|c| Miniflux::convert_category(c.clone(), None));

            return Ok((Miniflux::convert_feed(feed, None), category));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let feed_id = id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;
            api.delete_feed(feed_id, client).await.context(FeedApiErrorKind::Api)?;
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn move_feed(&self, feed_id: &FeedID, _from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let category_id = to.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;

            let miniflux_feed_id = feed_id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;

            api.update_feed(miniflux_feed_id, None, Some(category_id), None, None, None, None, None, client)
                .await
                .context(FeedApiErrorKind::Api)?;
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult<FeedID> {
        if let Some(api) = &self.api {
            let miniflux_feed_id = feed_id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;

            api.update_feed(miniflux_feed_id, Some(new_title), None, None, None, None, None, None, client)
                .await
                .context(FeedApiErrorKind::Api)?;

            return Ok(feed_id.clone());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_category(&self, title: &str, _parent: Option<&CategoryID>, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            let category = api.create_category(title, client).await.context(FeedApiErrorKind::Api)?;
            return Ok(CategoryID::new(&category.id.to_string()));
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn remove_category(&self, id: &CategoryID, _remove_children: bool, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            // FIXME: figure out how api behaves regarding deleting child feeds
            let miniflux_id = id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;
            api.delete_category(miniflux_id, client).await.context(FeedApiErrorKind::Api)?;
            return Ok(());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            let miniflux_id = id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;
            api.update_category(miniflux_id, new_title, client).await.context(FeedApiErrorKind::Api)?;
            return Ok(id.clone());
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn move_category(&self, _id: &CategoryID, _parent: &CategoryID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn import_opml(&self, opml: &str, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.import_opml(opml, client).await.context(FeedApiErrorKind::Api)?;
        }
        Err(FeedApiErrorKind::Login)?
    }

    async fn add_tag(&self, _title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn remove_tag(&self, _id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn rename_tag(&self, _id: &TagID, _new_title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn tag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn untag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn get_favicon(&self, feed_id: &FeedID, client: &Client) -> FeedApiResult<FavIcon> {
        if let Some(api) = &self.api {
            let miniflux_feed_id = feed_id.to_string().parse::<i64>().context(FeedApiErrorKind::Unknown)?;

            let favicon = api.get_feed_icon(miniflux_feed_id, client).await.context(FeedApiErrorKind::Api)?;

            if let Some(start) = favicon.data.find(',') {
                let data = base64::decode(&favicon.data[start + 1..]).context(FeedApiErrorKind::Encryption)?;

                let favicon = FavIcon {
                    feed_id: feed_id.clone(),
                    expires: Utc::now().naive_utc() + Duration::days(EXPIRES_AFTER_DAYS),
                    format: Some(favicon.mime_type),
                    etag: None,
                    source_url: None,
                    data: Some(data),
                };

                return Ok(favicon);
            }
        }
        Err(FeedApiErrorKind::Login)?
    }
}
