use gettextrs::*;
use adw::prelude::*;
use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};

pub mod components {
    #[cfg(feature = "smart-summary")]
    pub(crate) mod smart_summary;
    #[cfg(feature = "smart-summary")]
    pub(crate) use smart_summary::{
        SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput
    };

    pub(crate) mod post_editor;
    pub(crate) use post_editor::{
        PostEditor, Input as PostEditorInput
    };

    pub(crate) mod tag_pill;
    // pub(crate) use tag_pill::{TagPill, TagPillDelete}

    pub mod signin;
    pub use signin::{SignIn, Output as SignInOutput};

    pub mod preferences;
    pub use preferences::Preferences;
}

use components::{post_editor::Post, PostEditorInput};

pub mod secrets;
pub mod micropub;
pub mod util;
pub const APPLICATION_ID: &str = "xyz.vikanezrimaya.kittybox.Bowl";
pub const CLIENT_ID_STR: &str = "https://kittybox.fireburn.ru/bowl/";

pub const VISIBILITY: [&str; 2] = ["public", "private"];

#[derive(Debug)]
pub struct App {
    secret_schema: libsecret::Schema,
    http: soup::Session,
    submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,

    // TODO: make this support multiple users
    micropub: Option<micropub::Client>,

    signin: AsyncController<components::SignIn>,
    post_editor: Controller<components::PostEditor<micropub::Error>>,
}

impl App {
    async fn get_login_state(schema: &libsecret::Schema, http: soup::Session) -> Result<Option<micropub::Client>, glib::Error> {
        let mut retrievables = libsecret::password_search_future(Some(schema), {
            let mut attrs = std::collections::HashMap::default();
            attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::ACCESS_TOKEN);
            attrs
        }, libsecret::SearchFlags::ALL).await
            // SAFETY: the representation of `glib::Error` is the same, since we're
            // building against the same C library, so while this is a safety crime, it's
            // probably a minor one.
            //
            // Additionally I'm ensuring the original wrapper is forgotten in case it has
            // a drop handler that decrements a reference counter or something.
            //
            // This is only a workaround for libsecret not being updated in time.
            .map_err(|e| unsafe {
                let ptr = e.as_ptr();
                std::mem::forget(e);
                glib::translate::from_glib_full::<_, glib::Error>(
                    // We can't name the original type here.
                    #[allow(clippy::missing_transmute_annotations)]
                    std::mem::transmute::<_, *mut glib::ffi::GError>(ptr)
                )
            })?;

        if retrievables.is_empty() {
            Ok(None)
        } else {
            retrievables.sort_by_key(|s| s.created());
            let iterable = retrievables.iter().rev();
            for retrievable in iterable {
                let attrs = retrievable.attributes();
                let attrs_ref = attrs
                    .iter()
                    .map(|(k, v)| (k.as_ref(), v.as_ref()))
                    .collect();
                let micropub_uri = match attrs
                    .get(crate::secrets::MICROPUB)
                    .and_then(|v| glib::Uri::parse(
                        v, glib::UriFlags::NONE
                    ).ok()) {
                        Some(uri) => uri,
                        None => {
                            let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
                            continue
                        },
                    };

                let micropub = crate::micropub::Client::new(
                    http.clone(),
                    micropub_uri,
                    retrievable.retrieve_secret_future().await
                        // SAFETY: see above
                        .map_err(|e| unsafe {
                            let ptr = e.as_ptr();
                            std::mem::forget(e);
                            glib::translate::from_glib_full::<_, glib::Error>(
                                // We can't name the original type here.
                                #[allow(clippy::missing_transmute_annotations)]
                                std::mem::transmute::<_, *mut glib::ffi::GError>(ptr)
                            )
                        })?
                        .unwrap()
                        .text()
                        .unwrap()
                        .to_string()
                );

                // Skip the token if we can't access ?q=config
                if let Err(micropub::Error::Micropub(err)) = micropub.config().await {
                    if err.error == kittybox_util::micropub::ErrorKind::NotAuthorized {
                        continue;
                    }
                }

                return Ok(Some(micropub))
            }

            Ok(None)
        }
    }

    fn about() -> adw::AboutDialog {
        adw::AboutDialog::builder()
            .application_name(gettext("Bowl for Kittybox"))
            .developer_name("Vika Shleina")
            .version(env!("CARGO_PKG_VERSION"))
            .website("https://kittybox.fireburn.ru/bowl/")
            .developers(vec!["Vika https://fireburn.ru"])
            .license_type(gtk::License::Agpl30Only)
            .copyright("© 2024 Vika Shleina")
            .build()
    }
}

#[derive(Debug)]
#[doc(hidden)]
pub enum Input {
    SubmitButtonPressed,
    Authorize(Box<components::SignInOutput>),
    SignOut,
    PostEditor(Option<Post>),
}

relm4::new_action_group!(AppActionGroup, "app");
relm4::new_stateless_action!(AboutAction, AppActionGroup, "about");
relm4::new_stateless_action!(SignOutAction, AppActionGroup, "sign-out");
relm4::new_stateless_action!(PreferencesAction, AppActionGroup, "preferences");

#[relm4::component(pub async)]
impl AsyncComponent for App {
    /// The type of the messages that this component can receive.
    type Input = Input;
    /// The type of the messages that this component can send.
    type Output = ();
    /// The type of data with which this component will be initialized.
    type Init = ();
    /// The type of the command outputs that this component can receive.
    type CommandOutput = ();

    menu! {
        main_menu: {
            &gettext("Sign out") => SignOutAction,
            &gettext("Preferences") => PreferencesAction,
            &gettext("About") => AboutAction,
        }
    }

    view! {
        #[root]
        root = adw::ApplicationWindow {
            #[iterate]
            add_css_class: [
                #[cfg(debug_assertions)] "devel"
            ],

            #[watch]
            set_title: if model.micropub.is_none() {
                Some(gettext("Bowl - Sign in with your website"))
            } else {
                Some(gettext("Bowl"))
            }.as_deref(),

            adw::ToolbarView {
                add_top_bar = &adw::HeaderBar {
                    pack_end = &gtk::MenuButton {
                        #[wrap(Some)]
                        set_popover = &gtk::PopoverMenu::from_model(Some(&main_menu)) {},
                        #[watch]
                        set_visible: model.micropub.is_some(),
                        set_icon_name: relm4_icons::icon_names::MENU,
                    },
                    pack_end = &gtk::Button {
                        set_icon_name: "document-send-symbolic",
                        set_tooltip: &gettext("Publish"),
                        #[watch]
                        set_visible: model.micropub.is_some(),
                        #[watch]
                        set_sensitive: model.submit_busy_guard.is_none(),
                        connect_clicked => Self::Input::SubmitButtonPressed,
                    },
                },

                #[transition = "Crossfade"]
                match model.micropub.as_ref() {
                    Some(_) => model.post_editor.widget().clone(),
                    None => model.signin.widget().clone(),
                }
            },
        },
    }

    fn init_loading_widgets(root: Self::Root) -> Option<relm4::loading_widgets::LoadingWidgets> {
        root.set_size_request(360, 294);
        root.set_default_size(360, 640);

        let spinner = gtk::Spinner::builder()
            .spinning(true)
            .height_request(32)
            .width_request(32)
            .halign(gtk::Align::Center)
            .valign(gtk::Align::Center)
            .build();

        root.set_content(Some(&spinner));
        Some(LoadingWidgets::new(root, spinner))
    }

    /// Initialize the UI and model.
    async fn init(
        _init: Self::Init,
        window: Self::Root,
        sender: AsyncComponentSender<Self>,
    ) -> AsyncComponentParts<Self> {
        let secret_schema = crate::secrets::get_schema();
        let http = soup::Session::builder()
            .user_agent(concat!(env!("CARGO_PKG_NAME"),"/",env!("CARGO_PKG_VERSION")," "))
            .build();

        let state = App::get_login_state(
            &secret_schema, http.clone()
        ).await.unwrap();

        let model = App {
            submit_busy_guard: None,

            http: http.clone(),
            micropub: state,
            secret_schema,

            post_editor: {
                #[cfg(feature = "smart-summary")]
                let init = (http.clone(), None);
                #[cfg(not(feature = "smart-summary"))]
                let init = None;

                components::PostEditor::builder()
                    .launch(init)
                    .forward(sender.input_sender(), Self::Input::PostEditor)
            },
            signin: components::SignIn::builder()
                .launch((glib::Uri::parse(
                    CLIENT_ID_STR, glib::UriFlags::NONE
                ).unwrap(), http))
                .forward(
                    sender.input_sender(),
                    |o| Self::Input::Authorize(Box::new(o))
                )
        };

        let widgets = view_output!();

        let input_sender = sender.input_sender().clone();
        let weak_window = window.downgrade();
        let about_action: RelmAction<AboutAction> = RelmAction::new_stateless(move |_| {
            App::about().present(weak_window.upgrade().as_ref());
        });
        let weak_window = window.downgrade();
        let preferences_action: RelmAction<PreferencesAction> = RelmAction::new_stateless(move |_| {
            // This could be built as an action that sends an input to open preferences.
            //
            // But I find this an acceptable alternative.
            let mut prefs = components::Preferences::builder()
                .launch(())
                .detach();

            prefs.emit(weak_window.upgrade().map(|w| w.upcast()));
            prefs.detach_runtime();
        });
        let sign_out_action: RelmAction<SignOutAction> = RelmAction::new_stateless(move |_| {
            input_sender.emit(Input::SignOut)
        });
        let mut action_group: RelmActionGroup<AppActionGroup> = RelmActionGroup::new();
        action_group.add_action(about_action);
        action_group.add_action(preferences_action);
        action_group.add_action(sign_out_action);
        action_group.register_for_widget(&window);

        AsyncComponentParts { model, widgets }
    }


    async fn update(
        &mut self,
        message: Self::Input,
        _sender: AsyncComponentSender<Self>,
        _root: &Self::Root
    ) {
        match message {
            Input::SignOut => {
                if self.micropub.take().is_some() {
                    let _ = libsecret::password_clear_future(
                        Some(&self.secret_schema),
                        Default::default(),
                    ).await;
                    self.micropub = None;
                }
            },
            Input::Authorize(data) => {
                let mut attributes = std::collections::HashMap::new();
                let _me = data.me.to_string();
                let _micropub = data.micropub.to_string();
                let scope = data.scope.to_string();
                attributes.insert(secrets::ME, _me.as_str());
                attributes.insert(secrets::TOKEN_KIND, secrets::ACCESS_TOKEN);
                attributes.insert(secrets::MICROPUB, _micropub.as_str());
                attributes.insert(secrets::SCOPE, scope.as_str());
                let exp = data.expires_in
                    .as_ref()
                    .map(std::time::Duration::as_secs)
                    .as_ref()
                    .map(u64::to_string);
                if let Some(expires_in) = exp.as_deref() {
                    attributes.insert(secrets::EXPIRES_IN, expires_in);
                }

                match libsecret::password_store_future(
                    Some(&self.secret_schema),
                    attributes.clone(),
                    Some(libsecret::COLLECTION_DEFAULT),
                    &gettext!("Micropub access token for {}", &data.me),
                    &data.access_token
                ).await {
                    Ok(()) => {},
                    Err(err) => log::error!("Failed to store access token to the secret store: {}", err),
                }
                if let Some(refresh_token) = data.refresh_token.as_deref() {
                    attributes.insert(secrets::TOKEN_KIND, secrets::REFRESH_TOKEN);
                    attributes.remove(secrets::EXPIRES_IN);
                    match libsecret::password_store_future(
                        Some(&self.secret_schema),
                        attributes,
                        Some(libsecret::COLLECTION_DEFAULT),
                        &format!("Micropub refresh token for {}", &data.me),
                        refresh_token
                    ).await {
                        Ok(()) => {},
                        Err(err) => log::error!("Failed to store refresh token to the secret store: {}", err),
                    }
                }

                self.micropub = Some(crate::micropub::Client::new(
                    self.http.clone(), data.micropub.clone(), data.access_token.clone()
                ));
            },
            Input::SubmitButtonPressed => {
                if self.micropub.is_some() {
                    self.submit_busy_guard = Some(relm4::main_adw_application().mark_busy());
                    self.post_editor.emit(PostEditorInput::Submit);
                };
            },
            Input::PostEditor(None) => {
                self.submit_busy_guard = None;
            }
            Input::PostEditor(Some(post)) => {
                if let Some(micropub) = self.micropub.as_ref() {
                    let mf2 = post.into();
                    log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap());
                    match micropub.send_post(mf2).await {
                        Ok(uri) => {
                            self.post_editor.emit(PostEditorInput::SubmitDone(uri));
                        },
                        Err(err) => {
                            log::warn!("Error sending post: {}", err);
                            self.post_editor.emit(PostEditorInput::SubmitError(err));
                        }
                    }
                }
                self.submit_busy_guard = None;
            },
        }
    }
}