use crate::{messages::event::AsyncOperationError, prelude::*};
use std::{
    collections::HashMap, error::Error, hash::Hash, str::FromStr, sync::Arc, time::Duration,
};

use news_flash::{
    NewsFlash,
    error::NewsFlashError,
    models::{
        ArticleID, Category, CategoryID, CategoryMapping, Feed, FeedID, FeedMapping, Marked, Read,
        Tag, TagID, Url,
    },
};

use log::{debug, error, info};
use ratatui::{
    style::Color,
    text::{Line, Span},
};
use reqwest::{Client, ClientBuilder};
use tokio::sync::{Mutex, RwLock, mpsc::UnboundedSender};

#[derive(Clone)]
pub struct NewsFlashUtils {
    pub news_flash_lock: Arc<RwLock<NewsFlash>>,
    client_lock: Arc<RwLock<Client>>,
    config: Arc<Config>,
    command_sender: UnboundedSender<Message>,

    async_operation_mutex: Arc<Mutex<()>>,
}

// macro to wrap news flash async calls into spawns and send messages at the beginning and end
macro_rules! gen_async_call {
    {
        method_name: $method_name:ident,
        params: ($($param:ident: $param_type:ty),*),
        news_flash_var: $news_flash_var:ident,
        client_var: $client_var:ident,
        start_event: $start_event:expr,
        operation: $operation:stmt,
        success_event: $success_event:expr,
    } => {
        pub fn $method_name(&self, $($param: $param_type),*) {
            let news_flash_lock = self.news_flash_lock.clone();
            let client_lock = self.client_lock.clone();
            let command_sender = self.command_sender.clone();
            let async_operation_mutex = self.async_operation_mutex.clone();

            tokio::spawn(async move {
                let _lock = async_operation_mutex.lock().await;

                if let Err(e) = async {
                    command_sender.send(Message::Event($start_event)).map_err(|send_error|
                        color_eyre::eyre::eyre!(send_error))?;

                    let $news_flash_var = news_flash_lock.read().await;
                    let $client_var = client_lock.read().await;

                    $operation

                    command_sender.send(Message::Event($success_event)).map_err(|send_error|
                        color_eyre::eyre::eyre!(send_error))?;
                    Ok::<(), AsyncOperationError>(())
                }.await{
                    error!("Async call {} failed: {}", stringify!(&method_name), e,);
                    let _ = command_sender.send(Message::Event(Event::AsyncOperationFailed( e,
                                Box::new($start_event),)));
                }
            });
        }

    }


}

pub fn build_client(timeout: Duration) -> color_eyre::Result<Client> {
    let user_agent = format!(
        "eilmeldung/{} (RSS reader; +https://github.com/christo-auer/eilmeldung",
        env!("CARGO_PKG_VERSION")
    );
    let builder = ClientBuilder::new()
        .user_agent(user_agent.as_str())
        .hickory_dns(false)
        .gzip(true)
        .brotli(true)
        .timeout(timeout);

    Ok(builder.build()?)
}

#[rustfmt::skip]        
impl NewsFlashUtils {
    pub fn new(
        news_flash: NewsFlash,
        client: Client,
        config: Arc<Config>,
        command_sender: UnboundedSender<Message>,
    ) -> Self {
        debug!("Creating NewsFlashUtils");
        Self {
            news_flash_lock: Arc::new(RwLock::new(news_flash)),
            client_lock: Arc::new(RwLock::new(client)),
            config,
            command_sender,
            async_operation_mutex: Arc::new(Mutex::new(())),
        }
    }

    pub async fn rebuild_client(&self) -> color_eyre::Result<()>{
        info!("rebuilding reqwest client");
        let mut client = self.client_lock.write().await;
        *client = build_client(Duration::from_secs(self.config.network_timeout_seconds))?;
        Ok(())
    }

    /// Attempt to re-login using stored credentials. Returns true if successful.
    pub async fn relogin(&self) -> bool {
        let news_flash = self.news_flash_lock.read().await;
        let client = self.client_lock.read().await;
        
        if let Some(login_data) = news_flash.get_login_data().await {
            info!("Attempting re-login to refresh session");
            match news_flash.login(login_data, &client).await {
                Ok(()) => {
                    info!("Re-login successful");
                    true
                }
                Err(e) => {
                    error!("Re-login failed: {}", e);
                    false
                }
            }
        } else {
            error!("No login data available for re-login");
            false
        }
    }

    // for polling
    pub fn is_async_operation_running(&self) -> bool {
        self.async_operation_mutex.try_lock().is_err()
    }

    gen_async_call! {
        method_name: set_offline,
        params: (offline: bool),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncSetOffline,
        operation: news_flash.set_offline(offline, &client).await?,
        success_event: Event::AsyncSetOfflineFinished(offline),
    }

    gen_async_call! {
        method_name: sync,
        params: (),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncSync,
        operation: let new_articles = news_flash.sync(&client, Default::default()).await?,
        success_event: Event::AsyncSyncFinished(new_articles),
    }

    gen_async_call! {
        method_name: fetch_thumbnail,
        params: (article_id: ArticleID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticleThumbnailFetch,
        operation: let thumbnail = news_flash.get_article_thumbnail(&article_id, &client).await?,
        success_event: Event::AsyncArticleThumbnailFetchFinished(thumbnail),
    }

    gen_async_call! {
        method_name: fetch_fat_article,
        params: (article_id: ArticleID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticleFatFetch,
        operation: let fat_article = {
            // Temporarily redirect stderr to suppress libxml xpath errors that would mess up the TUI
            let _stderr_redirect = crate::utils::prelude::StderrRedirect::new();
            
            news_flash
                .scrap_content_article(&article_id, &client)
                .await?
        },
        success_event: Event::AsyncArticleFatFetchFinished(fat_article),
    }

    gen_async_call! {
        method_name: set_article_status,
        params: (article_ids: Vec<ArticleID>, read: Read),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticlesSetRead,
        operation: news_flash.set_article_read(&article_ids, read, &client).await?,
        success_event: Event::AsyncArticlesSetReadFinished,
    }

    gen_async_call! {
        method_name: set_article_marked,
        params: (article_ids: Vec<ArticleID>, marked: Marked),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticlesSetRead,
        operation: news_flash.set_article_marked(&article_ids, marked, &client).await?,
        success_event: Event::AsyncArticlesMarkFinished,
    }

    gen_async_call! {
        method_name: tag_articles,
        params: (article_ids: Vec<ArticleID>, tag_id: TagID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticleTag,
        operation:
            for article_id in article_ids {
                    news_flash.tag_article(&article_id, &tag_id, &client).await?;
            },
        success_event: Event::AsyncArticleTagFinished,
    }

    gen_async_call! {
        method_name: untag_articles,
        params: (article_ids: Vec<ArticleID>, tag_id: TagID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncArticleUntag,
        operation:
            for article_id in article_ids {
                news_flash.untag_article(&article_id, &tag_id, &client).await?;
            },
        success_event: Event::AsyncArticleUntagFinished,
    }

    gen_async_call! {
        method_name: add_tag,
        params: (tag_title: String, color: Option<Color>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncTagAdd,
        operation: let tag = news_flash.add_tag( tag_title.as_str(), color.map(|color| color.to_string()), &client).await?,
        success_event: Event::AsyncTagAddFinished(tag),
    }

    gen_async_call! {
        method_name: remove_tag,
        params: (tag_id: TagID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncTagRemove,
        operation: news_flash.remove_tag(&tag_id, &client).await?,
        success_event: Event::AsyncTagRemoveFinished,
    }

    gen_async_call! {
        method_name: edit_tag,
        params: (tag_id: TagID, new_tag_title: String, color: Option<Color>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncTagEdit,
        operation:
            let tag = news_flash.edit_tag( &tag_id, new_tag_title.as_str(), &color.map(|color| color.to_string()), &client).await?,
        success_event: Event::AsyncTagEditFinished(tag),
    }

    gen_async_call! {
        method_name: set_all_read,
        params: (),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedSetRead,
        operation: news_flash.set_all_read(&client).await?,
        success_event: Event::AsyncSetAllReadFinished,
    }

    gen_async_call! {
        method_name: set_feed_read,
        params: (feed_ids: Vec<FeedID>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedSetRead,
        operation: news_flash.set_feed_read(&feed_ids, &client).await?,
        success_event: Event::AsyncFeedSetReadFinished,
    }

    gen_async_call! {
        method_name: set_category_read,
        params: (category_ids: Vec<CategoryID>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncCategorySetRead,
        operation: news_flash.set_category_read(&category_ids, &client).await?,
        success_event: Event::AsyncCategorySetReadFinished,
    }

    gen_async_call! {
        method_name: set_tag_read,
        params: (tag_ids: Vec<TagID>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncTagSetRead,
        operation: news_flash.set_tag_read(&tag_ids, &client).await?,
        success_event: Event::AsyncTagSetReadFinished,
    }

    gen_async_call! {
        method_name: add_feed,
        params: (url: Url, title: Option<String>, category_id: Option<CategoryID>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedAdd,
        operation: let (feed, .. ) = news_flash.add_feed(&url, title, category_id, &client).await?,
        success_event: Event::AsyncFeedAddFinished(feed),
    }

    gen_async_call! {
        method_name: fetch_feed,
        params: (feed_id: FeedID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedFetch,
        operation: let fetched = news_flash.fetch_feed(&feed_id, &client, Default::default()).await?,
        success_event: Event::AsyncFeedFetchFinished(feed_id, fetched),
    }

    gen_async_call! {
        method_name: add_category,
        params: (title: String, parent : Option<CategoryID>),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncCategoryAdd,
        operation: let (category, .. ) = news_flash.add_category(&title, parent.as_ref(), &client).await?,
        success_event: Event::AsyncCategoryAddFinished(category),
    }

    gen_async_call! {
        method_name: rename_feed,
        params: (feed_id: FeedID, title: String),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedRename,
        operation: let feed = news_flash.rename_feed(&feed_id, title.as_str(), &client).await?,
        success_event: Event::AsyncRenameFeedFinished(feed),
    }

    gen_async_call! {
        method_name: rename_category,
        params: (category_id: CategoryID, title: String),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncCategoryRename,
        operation: let category = news_flash.rename_category(&category_id, title.as_str(), &client).await?,
        success_event: Event::AsyncCategoryRenameFinished(category),
    }

    gen_async_call! {
        method_name: remove_category,
        params: (category_id: CategoryID, remove_children: bool),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncCategoryRemove,
        operation: news_flash.remove_category(&category_id, remove_children, &client).await?,
        success_event: Event::AsyncCategoryRemoveFinished,
    }

    gen_async_call! {
        method_name: remove_feed,
        params: (feed_id: FeedID),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedRemove,
        operation: news_flash.remove_feed(&feed_id, &client).await?,
        success_event: Event::AsyncFeedRemoveFinished,
    }

    gen_async_call! {
        method_name: edit_feed_url,
        params: (feed_id: FeedID, new_url: String),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedUrlChange,
        operation: news_flash.edit_feed_url(&feed_id, &new_url, &client).await?,
        success_event: Event::AsyncFeedUrlChangeFinished,
    }

    gen_async_call! {
        method_name: move_feed,
        params: (from_feed_mapping: FeedMapping, to_feed_mapping: FeedMapping),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncFeedMove,
        operation: news_flash.move_feed(&from_feed_mapping, &to_feed_mapping, &client).await?,
        success_event: Event::AsyncFeedMoveFinished,
    }

    gen_async_call! {
        method_name: move_category,
        params: (category_mapping: CategoryMapping),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncCategoryMove,
        operation: news_flash.move_category(&category_mapping, &client).await?,
        success_event: Event::AsyncCategoryMoveFinished,
    }

    gen_async_call! {
        method_name: import_opml,
        params: (opml: String, parse_all_feeds: bool),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncImportOpml,
        operation: news_flash.import_opml(&opml, parse_all_feeds, &client).await?,
        success_event: Event::AsyncImportOpmlFinished,
    }

    gen_async_call! {
        method_name: logout,
        params: (),
        news_flash_var: news_flash,
        client_var: client,
        start_event: Event::AsyncLogout,
        operation: news_flash.logout(&client).await?,
        success_event: Event::AsyncLogoutFinished,
    }

    pub fn generate_id_map<V, I: Hash + Eq + Clone>(
        items: &[V],
        id_extractor: impl Fn(&V) -> I,
    ) -> HashMap<I, V>
    where
        V: Clone,
    {
        items
            .iter()
            .map(|item| (id_extractor(item), item.clone()))
            .collect()
    }

    pub fn generate_one_to_many<E, I: Hash + Eq + Clone, V>(
        mappings: &[E],
        id_extractor: impl Fn(&E) -> I,
        value_extractor: impl Fn(&E) -> V,
    ) -> HashMap<I, Vec<V>>
    where
        V: Clone,
    {
        mappings.iter().fold(HashMap::new(), |mut acc, mapping| {
            acc.entry(id_extractor(mapping).clone())
                .or_default()
                .push(value_extractor(mapping).clone());
            acc
        })
    }

    pub fn tag_color(tag: &Tag) -> Option<Color> {
        if let Some(color_str) = tag.color.clone()
            && let Ok(tag_color) = Color::from_str(color_str.as_str())
        {
            return Some(tag_color);
        }

        None
    }

    pub fn tag_to_line<'a>(tag: &Tag, config: &Config, override_color: Option<Color>) -> Line<'a> {
        let color = override_color
            .or(Self::tag_color(tag))
            .or(config.theme.tag().fg)
            .unwrap_or_default();
        let style = config.theme.tag().fg(color);
        to_bubble(Span::styled(tag.label.to_owned(), style))
    }

    fn get_root_cause_message(error: &dyn Error) -> String {
        let mut current_error = error;
        while let Some(source) = current_error.source() {
            current_error = source;
        }
        current_error.to_string()
    }

    pub fn error_to_message(news_flash_error: &NewsFlashError) -> String {
        match news_flash_error {
            NewsFlashError::Database(database_error) => {
                format!("Database error ({}).", Self::get_root_cause_message(&database_error))
            }
            
            NewsFlashError::API(feed_api_error) => {
                format!("API error ({})", Self::get_root_cause_message(&feed_api_error))
            }
            
            NewsFlashError::IO(error) => {
                format!("IO error ({})", Self::get_root_cause_message(&error))
            }
            
            NewsFlashError::LoadBackend => {
                "Failed to load NewsFlash backend.".to_string()
            }
            
            NewsFlashError::Icon(fav_icon_error) => {
                format!("Favicon error: {}.", fav_icon_error)
            }
            
            NewsFlashError::Url(parse_error) => {
                format!("Invalid URL format: {}", parse_error)
            }
            
            NewsFlashError::NotLoggedIn => {
                "You need be logged in to perform this action. Please log in first.".to_string()
            }
            
            NewsFlashError::Thumbnail => {
                "Failed to load or generate thumbnail image for the article.".to_string()
            }
            
            NewsFlashError::OPML(error) => {
                format!("OPML file processing failed: {}. The file may be corrupted or invalid.", error)
            }
            
            NewsFlashError::ImageDownload(image_download_error) => {
                format!("Failed to download images for article: {}", image_download_error)
            }
            
            NewsFlashError::GrabContent => {
                "Failed to download full article content.".to_string()
            }
            
            NewsFlashError::Semaphore(acquire_error) => {
                format!("Unable to start concurrent operation: {}", acquire_error)
            }
            
            NewsFlashError::Syncing => {
                "Cannot perform this operation while syncing feeds. Please wait for sync to complete.".to_string()
            }
            
            NewsFlashError::Offline => {
                "Cannot perform this operation while offline.".to_string()
            }
            
            NewsFlashError::Unknown => {
                "An unknown error occurred.".to_string()
            }
        }
    }
}

#[allow(clippy::type_complexity)]
pub fn get_feeds_and_categories(
    news_flash: &NewsFlash,
) -> Result<
    (
        Vec<Feed>,
        std::collections::HashMap<news_flash::models::FeedID, Feed>,
        std::collections::HashMap<news_flash::models::FeedID, news_flash::models::FeedMapping>,
        Vec<Category>,
        std::collections::HashMap<CategoryID, Category>,
        std::collections::HashMap<CategoryID, news_flash::models::CategoryMapping>,
    ),
    color_eyre::eyre::Error,
> {
    let (feeds, feed_mapping) = news_flash.get_feeds()?;
    let feed_for_feed_id = NewsFlashUtils::generate_id_map(&feeds, |feed| feed.feed_id.to_owned());
    let feed_mapping_for_feed_id =
        NewsFlashUtils::generate_id_map(&feed_mapping, |mapping| mapping.feed_id.to_owned());
    let (categories, category_mapping) = news_flash.get_categories()?;
    let category_for_category_id =
        NewsFlashUtils::generate_id_map(&categories, |category| category.category_id.to_owned());
    let category_mapping_for_category_id =
        NewsFlashUtils::generate_id_map(&category_mapping, |category_mapping| {
            category_mapping.category_id.to_owned()
        });
    Ok((
        feeds,
        feed_for_feed_id,
        feed_mapping_for_feed_id,
        categories,
        category_for_category_id,
        category_mapping_for_category_id,
    ))
}

pub fn sort_feeds_and_categories(
    feeds: &mut [Feed],
    categories: &mut [Category],
    feed_mapping_for_feed_id: &std::collections::HashMap<
        news_flash::models::FeedID,
        news_flash::models::FeedMapping,
    >,
    category_mapping_for_category_id: &std::collections::HashMap<
        news_flash::models::CategoryID,
        news_flash::models::CategoryMapping,
    >,
) {
    let category_cmp = |c1: Option<&CategoryID>, c2: Option<&CategoryID>| {
        let sort_index_for_c1 = c1.and_then(|c_id| {
            category_mapping_for_category_id
                .get(c_id)
                .map(|mapping| &mapping.sort_index)
        });
        let sort_index_for_c2 = c2.and_then(|c_id| {
            category_mapping_for_category_id
                .get(c_id)
                .map(|mapping| &mapping.sort_index)
        });

        sort_index_for_c1.cmp(&sort_index_for_c2)
    };

    categories.sort_by(|c1, c2| category_cmp(Some(&c1.category_id), Some(&c2.category_id)));

    feeds.sort_by(|f1, f2| {
        let feed_mapping_for_f1 = feed_mapping_for_feed_id.get(&f1.feed_id);
        let feed_mapping_for_f2 = feed_mapping_for_feed_id.get(&f2.feed_id);

        category_cmp(
            feed_mapping_for_f1.map(|mapping| &mapping.category_id),
            feed_mapping_for_f2.map(|mapping| &mapping.category_id),
        )
        .then(
            feed_mapping_for_f1
                .map(|feed_mapping| feed_mapping.sort_index)
                .cmp(&feed_mapping_for_f2.map(|feed_mapping| feed_mapping.sort_index)),
        )
    });
}
