pub mod metadata;

use self::metadata::LocalMetadata;
use crate::feed_api::{FeedApi, FeedApiErrorKind, FeedApiResult, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, FatArticle, FavIcon, Feed, FeedID, LoginData, Marked, PluginCapabilities, Read, SyncResult, TagID, Url,
};
use crate::util::html2text::Html2Text;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use failure::ResultExt;
use feed_rs::parser;
use log::{error, warn};
use rayon::prelude::*;
use reqwest::Client;
use std::str;

pub struct LocalRSS {
    portal: Box<dyn Portal>,
}

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

    fn has_user_configured(&self) -> FeedApiResult<bool> {
        Ok(true)
    }

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

    fn user_name(&self) -> Option<String> {
        None
    }

    fn get_login_data(&self) -> Option<LoginData> {
        Some(LoginData::None(LocalMetadata::get_id()))
    }

    async fn login(&mut self, _data: LoginData, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn logout(&mut self, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn initial_sync(&self, client: &Client) -> FeedApiResult<SyncResult> {
        let feeds = self.portal.get_feeds().context(FeedApiErrorKind::Portal)?;
        let mut future_store = Vec::new();

        for feed in feeds {
            let client = client.clone();
            future_store.push(async move {
                let mut articles: Vec<FatArticle> = Vec::new();

                if let Some(url) = &feed.feed_url {
                    let feed_request = match client.get(url.get()).send().await {
                        Ok(response) => response,
                        Err(_) => {
                            error!("Downloading feed failed: {}", url);
                            return articles;
                        }
                    };

                    if !feed_request.status().is_success() {
                        error!("Downloading feed failed: {}", url);
                        return articles;
                    }

                    let result_string = match feed_request.text().await {
                        Ok(result_string) => result_string,
                        Err(_) => {
                            error!("Reading response as string failed: {}", url);
                            return articles;
                        }
                    };

                    if let Ok(parsed_feed) = parser::parse(result_string.as_bytes()) {
                        let mut feed_articles = parsed_feed
                            .entries
                            .into_par_iter()
                            .filter_map(move |entry| {
                                let article_id = ArticleID::new(&entry.id);

                                match self.portal.get_article_exists(&article_id) {
                                    Ok(false) => {}
                                    Ok(true) | Err(_) => return None,
                                }

                                let html = entry.content.map(|c| c.body).flatten(); // FIXME: handle content-type
                                let plain_text = match &entry.summary {
                                    Some(summary) => match Html2Text::process(&summary.content) {
                                        Some(summary) => Some(summary),
                                        None => {
                                            let summary = match escaper::decode_html(&summary.content) {
                                                Ok(summary) => summary,
                                                Err(e) => {
                                                    warn!("Error {:?} at character {}", e.kind, e.position);
                                                    summary.content.clone()
                                                }
                                            };
                                            let summary = str::replace(&summary, "\n", " ");
                                            let summary = str::replace(&summary, "\r", " ");
                                            let summary = str::replace(&summary, "_", " ");
                                            Some(summary)
                                        }
                                    },
                                    None => match &html {
                                        Some(html) => Html2Text::process(html),
                                        None => None,
                                    },
                                };
                                let summary = plain_text.as_deref().map(|t| Html2Text::to_summary(t));

                                let article = FatArticle {
                                    article_id,
                                    feed_id: feed.feed_id.clone(),
                                    title: entry
                                        .title
                                        .map(|t| match escaper::decode_html(&t.content) {
                                            Ok(title) => Some(title),
                                            Err(error) => {
                                                warn!("Error {:?} at character {}", error.kind, error.position);
                                                Some(t.content)
                                            }
                                        })
                                        .flatten(),
                                    url: entry.links.get(0).map(|l| Url::parse(&l.href).ok()).flatten(),
                                    author: entry.authors.get(0).map(|person| person.name.clone()),
                                    date: match entry.published {
                                        Some(published) => published,
                                        None => match entry.updated {
                                            Some(updated) => updated,
                                            None => Utc::now(),
                                        },
                                    }
                                    .naive_utc(),
                                    direction: None,
                                    marked: Marked::Unmarked,
                                    unread: Read::Unread,
                                    html: match html {
                                        Some(html) => Some(html),
                                        None => match &entry.summary {
                                            Some(original_summary) => Some(original_summary.content.clone()),
                                            None => plain_text.clone(),
                                        },
                                    },
                                    scraped_content: None,
                                    summary,
                                    plain_text,
                                };

                                Some(article)
                            })
                            .collect();
                        articles.append(&mut feed_articles);
                    } else {
                        error!("Couldn't parse feed content");
                    }
                } else {
                    warn!("No feed url for feed: '{}'", feed.feed_id);
                }

                return articles;
            });
        }

        let result_vec = futures::future::join_all(future_store).await;
        let articles = result_vec.into_iter().flatten().collect::<Vec<FatArticle>>();

        Ok(SyncResult {
            feeds: None,
            categories: None,
            mappings: None,
            tags: None,
            headlines: None,
            articles: if articles.is_empty() { None } else { Some(articles) },
            enclosures: None,
            taggings: None,
        })
    }

    async fn sync(&self, _max_count: u32, _last_sync: DateTime<Utc>, client: &Client) -> FeedApiResult<SyncResult> {
        self.initial_sync(client).await
    }

    async fn set_article_read(&self, _articles: &[ArticleID], _read: models::Read, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn set_article_marked(&self, _articles: &[ArticleID], _marked: models::Marked, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn set_feed_read(&self, _feeds: &[FeedID], _last_sync: DateTime<Utc>, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn set_category_read(&self, _categories: &[CategoryID], _last_sync: DateTime<Utc>, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

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

    async fn set_all_read(&self, _last_sync: DateTime<Utc>, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn add_feed(
        &self,
        url: &Url,
        title: Option<String>,
        category_id: Option<CategoryID>,
        client: &Client,
    ) -> FeedApiResult<(Feed, Option<Category>)> {
        let feed_request = client.get(url.get()).send().await.context(FeedApiErrorKind::Url)?;

        if !feed_request.status().is_success() {
            error!("Downloading feed failed: {}", url);
            return Err(FeedApiErrorKind::Url)?;
        }

        let result_string = feed_request
            .text()
            .await
            .map_err(|err| {
                error!("Reading response as string failed: {}", url);
                err
            })
            .context(FeedApiErrorKind::IO)?;

        if let Ok(feed) = parser::parse(result_string.as_bytes()) {
            let title = match title {
                Some(title) => title,
                None => match feed.title {
                    Some(title) => title.content.clone(),
                    None => "Unknown Feed".to_owned(),
                },
            };

            let mut website: Option<Url> = None;
            if !feed.links.is_empty() {
                // see if there is a link with rel='alternate' -> this is probably what we want
                if let Some(link) = feed.links.iter().find(|link| link.rel == Some("alternate".to_owned())) {
                    if let Ok(url) = Url::parse(&link.href) {
                        website = Some(url);
                    }
                }
                // otherwise just take the first link
                else if let Ok(url) = Url::parse(&feed.links[0].href) {
                    website = Some(url);
                }
            }

            let icon_url = match feed.icon {
                Some(image) => match Url::parse(&image.uri) {
                    Ok(url) => Some(url),
                    Err(_) => None,
                },
                None => None,
            };

            let feed = Feed {
                feed_id: FeedID::new(&feed.id),
                label: title,
                website,
                feed_url: Some(url.clone()),
                icon_url,
                sort_index: None,
            };

            let categories = self.portal.get_categories().context(FeedApiErrorKind::Portal)?;
            let category = categories.iter().find(|c| Some(&c.category_id) == category_id.as_ref()).cloned();

            return Ok((feed, category));
        }

        Err(FeedApiErrorKind::Unsupported)?
    }

    async fn remove_feed(&self, _id: &FeedID, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn move_feed(&self, _feed_id: &FeedID, _from: &CategoryID, _to: &CategoryID, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn rename_feed(&self, feed_id: &FeedID, _new_title: &str, _client: &Client) -> FeedApiResult<FeedID> {
        Ok(feed_id.clone())
    }

    async fn add_category(&self, title: &str, _parent: Option<&CategoryID>, _client: &Client) -> FeedApiResult<CategoryID> {
        Ok(CategoryID::new(title))
    }

    async fn remove_category(&self, _id: &CategoryID, _remove_children: bool, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn rename_category(&self, id: &CategoryID, _new_title: &str, _client: &Client) -> FeedApiResult<CategoryID> {
        Ok(id.clone())
    }

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

    async fn import_opml(&self, _opml: &str, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn add_tag(&self, title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Ok(TagID::new(title))
    }

    async fn remove_tag(&self, _id: &TagID, _client: &Client) -> FeedApiResult<()> {
        Ok(())
    }

    async fn rename_tag(&self, id: &TagID, _new_title: &str, _client: &Client) -> FeedApiResult<TagID> {
        Ok(id.clone())
    }

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

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

    async fn get_favicon(&self, _feed_id: &FeedID, _client: &Client) -> FeedApiResult<FavIcon> {
        Err(FeedApiErrorKind::Unsupported)?
    }
}
