mod config;
pub mod metadata;

use std::collections::HashSet;
use std::sync::Arc;

use self::config::AccountConfig;
use self::metadata::NextcloudMetadata;
use crate::feed_api::{FeedApi, FeedApiError, FeedApiResult, FeedHeaderMap, Portal};
use crate::models::{
    self, ArticleID, Category, CategoryID, CategoryMapping, DirectLogin, Direction, Enclosure, FatArticle, FavIcon, Feed, FeedID, FeedMapping,
    FeedUpdateResult, LoginData, Marked, NEWSFLASH_TOPLEVEL, PasswordLogin, PluginCapabilities, Read, StreamConversionResult, SyncResult, TagID, Url,
};
use crate::util;
use async_trait::async_trait;
use chrono::Utc;
use futures::future;
use nextcloud_news_api::models::{Feed as NcFeed, Folder, Item, ItemType};
use nextcloud_news_api::{ApiError as NextcloudError, NextcloudNewsApi};
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use semver::Version;
use tokio::sync::RwLock;

impl From<NextcloudError> for FeedApiError {
    fn from(error: NextcloudError) -> FeedApiError {
        match error {
            NextcloudError::Url(e) => FeedApiError::Url(e),
            NextcloudError::Json { source, json } => FeedApiError::Json { source, json },
            NextcloudError::Http(e) => FeedApiError::Network(e),
            NextcloudError::Parse => FeedApiError::Api {
                message: NextcloudError::Parse.to_string(),
            },
            NextcloudError::Input => FeedApiError::Api {
                message: NextcloudError::Input.to_string(),
            },
            NextcloudError::Unauthorized => FeedApiError::Auth,
            NextcloudError::Unknown => FeedApiError::Unknown,
        }
    }
}

pub struct Nextcloud {
    api: Option<NextcloudNewsApi>,
    portal: Arc<Box<dyn Portal>>,
    logged_in: bool,
    config: AccountConfig,
}

impl Nextcloud {
    fn ids_to_nc_ids<T: ToString>(ids: &[T]) -> Vec<i64> {
        ids.iter().filter_map(|id| id.to_string().parse::<i64>().ok()).collect()
    }

    fn id_to_nc_id<T: ToString>(id: &T) -> Option<i64> {
        id.to_string().parse::<i64>().ok()
    }

    fn convert_folder_vec(mut categories: Vec<Folder>) -> (Vec<Category>, Vec<CategoryMapping>) {
        categories
            .drain(..)
            .enumerate()
            .map(|(i, c)| Self::convert_folder(c, Some(i as i32)))
            .unzip()
    }

    fn convert_folder(folder: Folder, sort_index: Option<i32>) -> (Category, CategoryMapping) {
        let Folder { id, name } = folder;
        let category_id = CategoryID::new(&id.to_string());
        let category = Category {
            category_id: category_id.clone(),
            label: name,
        };
        let category_mapping = CategoryMapping {
            parent_id: NEWSFLASH_TOPLEVEL.clone(),
            category_id,
            sort_index,
        };
        (category, category_mapping)
    }

    fn convert_feed(feed: NcFeed) -> Feed {
        let NcFeed {
            id,
            url,
            title,
            favicon_link,
            added: _,
            folder_id: _,
            unread_count: _,
            ordering: _,
            link,
            pinned: _,
            update_error_count,
            last_update_error,
        } = feed;

        Feed {
            feed_id: FeedID::new(&id.to_string()),
            label: title,
            website: link.and_then(|link| Url::parse(&link).ok()),
            feed_url: Url::parse(&url).ok(),
            icon_url: favicon_link.and_then(|url| Url::parse(&url).ok()),
            error_count: update_error_count as i32,
            error_message: last_update_error,
        }
    }

    fn convert_feed_vec(mut feeds: Vec<NcFeed>) -> (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: f
                        .folder_id
                        .map(|id| CategoryID::new(&id.to_string()))
                        .unwrap_or_else(|| NEWSFLASH_TOPLEVEL.clone()),
                    sort_index: Some(i as i32),
                });

                Self::convert_feed(f)
            })
            .collect();

        (feeds, mappings)
    }

    async fn convert_item_vec(items: Vec<Item>, feed_ids: &HashSet<FeedID>, portal: Arc<Box<dyn Portal>>) -> StreamConversionResult {
        let enclosures: Arc<RwLock<Vec<Enclosure>>> = Arc::new(RwLock::new(Vec::new()));
        let tasks = items
            .into_iter()
            .map(|i| {
                let feed_ids = feed_ids.clone();
                let portal = portal.clone();
                let enclosures = enclosures.clone();

                tokio::spawn(async move {
                    if feed_ids.contains(&FeedID::new(&i.feed_id.to_string())) || i.starred {
                        let (article, enclousre) = Self::convert_item(i, portal);
                        if let Some(enclosure) = enclousre {
                            enclosures.write().await.push(enclosure);
                        }
                        Some(article)
                    } else {
                        None
                    }
                })
            })
            .collect::<Vec<_>>();

        let articles = future::join_all(tasks).await.into_iter().filter_map(|res| res.ok().flatten()).collect();

        StreamConversionResult {
            articles,
            headlines: Vec::new(),
            taggings: Vec::new(),
            enclosures: Arc::into_inner(enclosures).map(|e| e.into_inner()).unwrap_or_default(),
        }
    }

    fn convert_item(item: Item, portal: Arc<Box<dyn Portal>>) -> (FatArticle, Option<Enclosure>) {
        let Item {
            id,
            guid: _,
            guid_hash: _,
            url,
            title,
            author,
            pub_date,
            body,
            enclosure_mime,
            enclosure_link,
            media_thumbnail,
            media_description,
            feed_id,
            unread,
            starred,
            rtl,
            last_modified: _,
            fingerprint: _,
        } = item;

        let article_id = ArticleID::new(&id.to_string());

        let article_exists_locally = portal.get_article_exists(&article_id).unwrap_or(false);

        let plain_text = if article_exists_locally {
            None
        } else {
            Some(util::html2text::html2text(&body))
        };

        let summary = plain_text.as_deref().map(util::html2text::text2summary);
        let thumbnail_url = if let Some(media_thumbnail) = &media_thumbnail {
            Some(media_thumbnail.clone())
        } else {
            crate::util::thumbnail::extract_thumbnail(&body)
        };

        let article = FatArticle {
            article_id: article_id.clone(),
            title,
            author,
            feed_id: FeedID::new(&feed_id.to_string()),
            url: url.and_then(|url| Url::parse(&url).ok()),
            date: util::timestamp_to_datetime(pub_date),
            synced: Utc::now(),
            updated: None,
            summary,
            html: Some(body),
            direction: Some(if rtl { Direction::RightToLeft } else { Direction::LeftToRight }),
            unread: if unread { Read::Unread } else { Read::Read },
            marked: if starred { Marked::Marked } else { Marked::Unmarked },
            scraped_content: None,
            plain_text,
            thumbnail_url,
        };
        let enclosure = enclosure_link.and_then(|enc_url| {
            Url::parse(&enc_url).ok().map(|url| Enclosure {
                article_id,
                url,
                mime_type: enclosure_mime,
                title: None,
                position: None,
                summary: media_description,
                thumbnail_url: media_thumbnail,
                filesize: None,
                width: None,
                height: None,
                duration: None,
                framerate: None,
                alternative: None,
                is_default: false,
            })
        });

        (article, enclosure)
    }
}

#[async_trait]
impl FeedApi for Nextcloud {
    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_reachable(&self, client: &Client) -> FeedApiResult<bool> {
        if let Some(api) = &self.api {
            let _version = api.get_version(client).await?;
            Ok(true)
        } else {
            Err(FeedApiError::Login)
        }
    }

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

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

    async fn get_login_data(&self) -> Option<LoginData> {
        if self.has_user_configured().unwrap_or(false) {
            let username = self.config.get_user_name();
            let password = self.config.get_password();

            if let (Some(username), Some(password)) = (username, password) {
                return Some(LoginData::Direct(DirectLogin::Password(PasswordLogin {
                    id: NextcloudMetadata::get_id(),
                    url: self.config.get_url(),
                    user: username,
                    password,
                    basic_auth: None,
                })));
            }
        }

        None
    }

    async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
        if let LoginData::Direct(DirectLogin::Password(data)) = data
            && let Some(url_string) = data.url.clone()
        {
            let url = Url::parse(&url_string)?;
            let api = NextcloudNewsApi::new(&url, data.user.clone(), data.password.clone())?;

            let nextcloud_news_api::models::Version { version: version_string } = api.get_version(client).await?;
            let semver = Version::parse(&version_string).map_err(|_| {
                tracing::error!(%version_string,"Failed to parse version string: {version_string}");
                FeedApiError::Login
            })?;
            let min_version = Version::new(18, 1, 1);
            if semver < min_version {
                tracing::error!("Nextcloud News app is version {semver}. Minimal required version is {min_version}.");
                return Err(FeedApiError::UnsupportedVersion {
                    min_supported: min_version,
                    found: Some(semver),
                });
            }

            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(FeedApiError::Login)
    }

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

    async fn initial_sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let folders = api.get_folders(client);
            let feeds = api.get_feeds(client);

            let unread_items = api.get_items(client, -1, None, None, None, Some(false), None);
            let starred_items = api.get_items(client, -1, None, Some(ItemType::Starred), None, None, None);

            let (folders, feeds, unread_items, starred_items) = futures::try_join!(folders, feeds, unread_items, starred_items)?;

            let (categories, category_mappings) = Nextcloud::convert_folder_vec(folders);
            let (feeds, feed_mappings) = Nextcloud::convert_feed_vec(feeds);

            let feed_id_set: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

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

            let mut unread = Self::convert_item_vec(unread_items, &feed_id_set, self.portal.clone()).await;
            articles.append(&mut unread.articles);
            enclosures.append(&mut unread.enclosures);

            let mut starred = Self::convert_item_vec(starred_items, &feed_id_set, self.portal.clone()).await;
            articles.append(&mut starred.articles);
            enclosures.append(&mut starred.enclosures);

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                feed_mappings: util::vec_to_option(feed_mappings),
                category_mappings: util::vec_to_option(category_mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(articles),
                enclosures: util::vec_to_option(enclosures),
            });
        }

        Err(FeedApiError::Login)
    }

    async fn sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
        if let Some(api) = &self.api {
            let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp() as u64;

            let folders = api.get_folders(client);
            let feeds = api.get_feeds(client);
            let updated_items = api.get_updated_items(client, last_sync, None, None);

            let (folders, feeds, updated_items) = futures::try_join!(folders, feeds, updated_items)?;

            let (categories, category_mappings) = Nextcloud::convert_folder_vec(folders);
            let (feeds, mappings) = Nextcloud::convert_feed_vec(feeds);
            let feed_id_set: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();

            let conversion_result = Self::convert_item_vec(updated_items, &feed_id_set, self.portal.clone()).await;

            return Ok(SyncResult {
                feeds: util::vec_to_option(feeds),
                categories: util::vec_to_option(categories),
                feed_mappings: util::vec_to_option(mappings),
                category_mappings: util::vec_to_option(category_mappings),
                tags: None,
                taggings: None,
                headlines: None,
                articles: util::vec_to_option(conversion_result.articles),
                enclosures: util::vec_to_option(conversion_result.enclosures),
            });
        }

        Err(FeedApiError::Login)
    }

    async fn fetch_feed(&self, feed_id: &FeedID, client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FeedUpdateResult> {
        if let Some(api) = &self.api {
            let feeds = api.get_feeds(client).await?;
            let (feeds, _mappings) = Nextcloud::convert_feed_vec(feeds);

            let feed = feeds.iter().find(|feed| &feed.feed_id == feed_id).cloned();

            let mut feed_id_set: HashSet<FeedID> = HashSet::new();
            feed_id_set.insert(feed_id.clone());

            let nextcloud_feed_id = feed_id.as_str().parse::<u64>().map_err(|_| FeedApiError::Api {
                message: format!("Failed to parse id {feed_id}"),
            })?;

            let items = api.get_items(client, -1, None, None, Some(nextcloud_feed_id), None, None).await?;
            let conversion_result = Self::convert_item_vec(items, &feed_id_set, self.portal.clone()).await;

            Ok(FeedUpdateResult {
                feed,
                taggings: None,
                articles: util::vec_to_option(conversion_result.articles),
                enclosures: util::vec_to_option(conversion_result.enclosures),
            })
        } else {
            Err(FeedApiError::Login)
        }
    }

    async fn set_article_read(&self, articles: &[ArticleID], read: models::Read, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let nc_ids = Self::ids_to_nc_ids(articles);

            match read {
                Read::Read => api.mark_items_read(client, nc_ids).await?,
                Read::Unread => api.mark_items_unread(client, nc_ids).await?,
            }

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_article_marked(&self, articles: &[ArticleID], marked: models::Marked, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let nc_ids = Self::ids_to_nc_ids(articles);

            match marked {
                Marked::Marked => api.mark_items_starred(client, nc_ids).await?,
                Marked::Unmarked => api.mark_items_unstarred(client, nc_ids).await?,
            }

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_feed_read(&self, feeds: &[FeedID], articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let nc_ids = Self::ids_to_nc_ids(feeds);
            let newest_unread_article_id = articles.iter().filter_map(Self::id_to_nc_id).max().unwrap_or(i64::MAX);

            let mut futures = Vec::new();
            for feed_id in nc_ids {
                futures.push(api.mark_feed(client, feed_id, newest_unread_article_id));
            }
            let results = futures::future::join_all(futures).await;
            let result: Result<Vec<()>, FeedApiError> = results.into_iter().map(|res| res.map_err(FeedApiError::from)).collect();
            let _ = result?;

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_category_read(&self, categories: &[CategoryID], articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let nc_ids = Self::ids_to_nc_ids(categories);
            let newest_unread_article_id = articles.iter().filter_map(Self::id_to_nc_id).max().unwrap_or(i64::MAX);

            let mut futures = Vec::new();
            for folder_id in nc_ids {
                futures.push(api.mark_folder(client, folder_id, newest_unread_article_id));
            }
            let results = futures::future::join_all(futures).await;
            let result: Result<Vec<()>, FeedApiError> = results.into_iter().map(|res| res.map_err(FeedApiError::from)).collect();
            let _ = result?;

            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn set_tag_read(&self, _tags: &[TagID], _articles: &[ArticleID], _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiError::Unsupported)
    }

    async fn set_all_read(&self, articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            let newest_unread_article_id = articles.iter().filter_map(Self::id_to_nc_id).max().unwrap_or(i64::MAX);
            let _ = api.mark_all_items_read(client, newest_unread_article_id).await?;
            return Ok(());
        }
        Err(FeedApiError::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 folder_id = category_id.and_then(|id| Self::id_to_nc_id(&id));

            let feed = api.create_feed(client, url.as_str(), folder_id).await?;

            if let Some(title) = title {
                api.rename_feed(client, feed.id, &title).await?;
            }

            let category = api
                .get_folders(client)
                .await?
                .iter()
                .find(|f| Some(f.id) == folder_id)
                .map(|f| Self::convert_folder(f.clone(), None))
                .map(|(c, _m)| c);

            return Ok((Self::convert_feed(feed), category));
        }
        Err(FeedApiError::Login)
    }

    async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.delete_feed(client, Self::id_to_nc_id(id).unwrap()).await?;
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn move_feed(&self, feed_id: &FeedID, _from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.move_feed(client, Self::id_to_nc_id(feed_id).unwrap(), Some(Self::id_to_nc_id(to).unwrap()))
                .await?;
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult<FeedID> {
        if let Some(api) = &self.api {
            api.rename_feed(client, Self::id_to_nc_id(feed_id).unwrap(), new_title).await?;
            return Ok(feed_id.clone());
        }
        Err(FeedApiError::Login)
    }

    async fn edit_feed_url(&self, _feed_id: &FeedID, _new_url: &str, _client: &Client) -> FeedApiResult<()> {
        Err(FeedApiError::Unsupported)
    }

    async fn add_category<'a>(&self, title: &str, _parent: Option<&'a CategoryID>, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            let folder = api.create_folder(client, title).await?;
            return Ok(CategoryID::new(&folder.id.to_string()));
        }
        Err(FeedApiError::Login)
    }

    async fn remove_category(&self, id: &CategoryID, _remove_children: bool, client: &Client) -> FeedApiResult<()> {
        if let Some(api) = &self.api {
            api.delete_folder(client, Self::id_to_nc_id(id).unwrap()).await?;
            return Ok(());
        }
        Err(FeedApiError::Login)
    }

    async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
        if let Some(api) = &self.api {
            api.rename_folder(client, Self::id_to_nc_id(id).unwrap(), new_title).await?;
            return Ok(id.clone());
        }
        Err(FeedApiError::Login)
    }

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

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

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

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

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

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

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

    async fn get_favicon(&self, _feed_id: &FeedID, _client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FavIcon> {
        Err(FeedApiError::Unsupported)
    }
}
