use std::str::FromStr;

use gio::{ActionEntry, ListStore, prelude::*};
use gtk4::{FileDialog, FileFilter};
use news_flash::error::NewsFlashError;
use news_flash::models::{ArticleID, FeedID, Url};

use crate::app::App;
use crate::article_view::{ArticleBuilder, ArticleView};
use crate::content_page::{ArticleViewColumn, ContentPage};
use crate::gobject_models::GArticle;
use crate::i18n::{i18n, i18n_f};
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::util::{GtkUtil, JOIN_ERROR};

pub struct FileActions;

impl FileActions {
    pub fn setup() {
        // -------------------------
        // import opml
        // -------------------------
        let import_opml = ActionEntry::builder("import-opml")
            .activate(|_window, _action, _parameter| Self::import_opml())
            .build();

        // -------------------------
        // export opml
        // -------------------------
        let export_opml = ActionEntry::builder("export-opml")
            .activate(|_window, _action, _parameter| Self::export_opml())
            .build();

        // -------------------------
        // export article
        // -------------------------
        let export_article = ActionEntry::builder("export-article")
            .activate(|_window, _action, _parameter| Self::export_article())
            .build();

        // -------------------------
        // save image
        // -------------------------
        let save_image = ActionEntry::builder("save-webview-image")
            .parameter_type(Some(&String::static_variant_type()))
            .activate(|_window, _action, parameter| {
                let Some(image_uri) = parameter.and_then(|p| p.str()) else {
                    ContentPage::instance().simple_message(&i18n("save webview image: no parameter"));
                    return;
                };

                Self::save_image(image_uri);
            })
            .build();

        App::default().add_action_entries([import_opml, export_opml, save_image]);
        MainWindow::instance().add_action_entries([export_article]);
    }

    fn import_opml() {
        let filter = FileFilter::new();
        filter.add_pattern("*.OPML");
        filter.add_pattern("*.opml");
        filter.add_mime_type("application/xml");
        filter.add_mime_type("text/xml");
        filter.add_mime_type("text/x-opml");
        filter.set_name(Some("OPML"));

        let filter_list = ListStore::new::<FileFilter>();
        filter_list.append(&filter);

        let dialog = FileDialog::builder()
            .accept_label(i18n("_Open"))
            .title(i18n("Import OPML"))
            .modal(true)
            .initial_folder(&gio::File::for_path(glib::home_dir()))
            .filters(&filter_list)
            .build();

        glib::spawn_future_local(async move {
            let Ok(file) = dialog.open_future(Some(&MainWindow::instance())).await else {
                // FIXME: error
                return;
            };

            let buffer = match GtkUtil::read_bytes_from_file(&file) {
                Ok(buffer) => buffer,
                Err(error) => {
                    ContentPage::instance().simple_message(&error.to_string());
                    return;
                }
            };

            let opml_content = match String::from_utf8(buffer) {
                Ok(string) => string,
                Err(error) => {
                    ContentPage::instance()
                        .simple_message(&i18n_f("Failed read OPML string: {}", &[&error.to_string()]));
                    return;
                }
            };

            App::default().set_is_syncing(false);

            let future = TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let news_flash_guad = news_flash.read().await;
                let news_flash = news_flash_guad.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
                let client = App::client().await;
                news_flash.import_opml(&opml_content, false, &client).await
            });

            if let Err(error) = future.await.expect(JOIN_ERROR) {
                ContentPage::instance().newsflash_error(&i18n("Failed to import OPML"), error);
            } else {
                ContentPage::instance().update_sidebar();
            }
        });
    }

    fn export_opml() {
        let filter = FileFilter::new();
        filter.add_pattern("*.OPML");
        filter.add_pattern("*.opml");
        filter.add_mime_type("application/xml");
        filter.add_mime_type("text/xml");
        filter.add_mime_type("text/x-opml");
        filter.set_name(Some("OPML"));

        let filter_list = ListStore::new::<FileFilter>();
        filter_list.append(&filter);

        let dialog = FileDialog::builder()
            .accept_label(i18n("_Save"))
            .title(i18n("Export OPML"))
            .modal(true)
            .initial_folder(&gio::File::for_path(glib::home_dir()))
            .initial_name("Newsflash.OPML")
            .filters(&filter_list)
            .build();

        glib::spawn_future_local(async move {
            let Ok(file) = dialog.save_future(Some(&MainWindow::instance())).await else {
                // FIXME: error
                return;
            };

            let news_flash = App::news_flash();
            let news_flash_guard = news_flash.read().await;
            let Some(news_flash) = news_flash_guard.as_ref() else {
                // FIXME: error
                return;
            };
            let opml = news_flash.export_opml().await;
            let opml = match opml {
                Ok(opml) => opml,
                Err(error) => {
                    ContentPage::instance().newsflash_error(&i18n("Failed to get OPML data"), error);
                    return;
                }
            };

            // Format XML
            let Ok(doc) = xmlem::Document::from_str(&opml) else {
                ContentPage::instance().simple_message(&i18n("Failed to parse OPML data for formatting"));
                return;
            };
            let opml = doc.to_string_pretty();

            if let Err(error) = GtkUtil::write_bytes_to_file(opml, &file).await {
                ContentPage::instance().simple_message(&error.to_string());
            }
        });
    }

    fn export_article() {
        let article = match ArticleViewColumn::instance().article() {
            Some(article) => article,
            _ => return,
        };
        let title = article.title();
        let is_offline = App::default().is_offline();
        let article_id: ArticleID = article.article_id().into();
        let feed_id: FeedID = article.feed_id().into();

        let filter = FileFilter::new();
        filter.add_pattern("*.html");
        filter.add_mime_type("text/html");
        filter.set_name(Some("HTML"));

        let filter_list = ListStore::new::<FileFilter>();
        filter_list.append(&filter);

        let dialog = FileDialog::builder()
            .accept_label(i18n("_Save"))
            .title(i18n("Export Article"))
            .modal(true)
            .initial_folder(&gio::File::for_path(glib::home_dir()))
            .filters(&filter_list)
            .build();

        if let Some(title) = &title {
            dialog.set_initial_name(Some(&format!("{}.html", title.replace('/', "_"))));
        } else {
            dialog.set_initial_name(Some("Article.html"));
        }

        glib::spawn_future_local(async move {
            let Ok(file) = dialog.save_future(Some(&MainWindow::instance())).await else {
                // FIXME: error
                return;
            };

            App::default().set_is_exporting_article(true);

            let prefer_scraped_content = ArticleView::instance().prefer_scraped_content();

            let future = TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let news_flash_guad = news_flash.read().await;
                let news_flash = news_flash_guad.as_ref()?;

                if is_offline {
                    return None;
                }

                let fat_article = news_flash.get_fat_article(&article_id).ok()?;
                let (feeds, _mappings) = news_flash.get_feeds().ok()?;
                let feed = feeds.into_iter().find(|f| f.feed_id == feed_id)?;

                Some((fat_article, feed))
            });

            let article = match future.await.expect(JOIN_ERROR) {
                Some((fat_article, feed)) => {
                    let updated_article = GArticle::from_fat_article(fat_article, Some(&feed), vec![], vec![]);
                    ArticleViewColumn::instance().set_article(Some(&updated_article));
                    updated_article
                }
                None => article,
            };

            let html = ArticleBuilder::from_garticle(&article, prefer_scraped_content, true);
            if let Err(error) = GtkUtil::write_bytes_to_file(html, &file).await {
                ContentPage::instance().simple_message(&error.to_string());
            }

            App::default().set_is_exporting_article(false);
        });
    }

    fn save_image(image_uri_str: &str) {
        let Ok(image_url) = Url::parse(image_uri_str) else {
            return;
        };

        let Some(mut segments) = image_url.path_segments() else {
            return;
        };

        let file_name: String = match segments.next_back() {
            Some(last_segment) => last_segment.into(),
            None => "image.jpeg".into(),
        };

        let filter = FileFilter::new();
        filter.add_pattern("*.jpg");
        filter.add_pattern("*.jpeg");
        filter.add_pattern("*.png");
        filter.add_mime_type("image/jpeg");
        filter.add_mime_type("image/png");
        filter.set_name(Some("Image"));

        let filter_list = ListStore::new::<FileFilter>();
        filter_list.append(&filter);

        let dialog = FileDialog::builder()
            .accept_label(i18n("_Save"))
            .title(i18n("Save Image"))
            .modal(true)
            .initial_folder(&gio::File::for_path(glib::home_dir()))
            .initial_name(&file_name)
            .filters(&filter_list)
            .build();

        let image_uri_str = image_uri_str.to_owned();

        glib::spawn_future_local(async move {
            let Ok(file) = dialog.save_future(Some(&MainWindow::instance())).await else {
                // FIXME: error
                return;
            };

            let uri_clone = image_uri_str.clone();

            let future = TokioRuntime::spawn(async move {
                let client = App::client().await;
                client.get(&image_uri_str).send().await?.bytes().await
            });

            let Ok(image_bytes) = future.await.expect(JOIN_ERROR) else {
                ContentPage::instance().simple_message(&i18n_f("Failed to download image {}", &[&uri_clone]));
                return;
            };

            if let Err(error) = GtkUtil::write_bytes_to_file(image_bytes, &file).await {
                ContentPage::instance().simple_message(&error.to_string());
            }
        });
    }
}
