use std::{borrow::Borrow, sync::Arc};

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

pub mod components {
    pub(crate) mod 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};
}

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 {
    state: AuthState,

    secret_schema: libsecret::Schema,
    http: soup::Session,

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

#[derive(Debug)]
enum AuthState {
    LoggedOut,
    LoggedIn {
        submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
        micropub: micropub::Client
    }
}

impl App {
    async fn get_login_state(schema: &libsecret::Schema) -> Result<AuthState, 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(AuthState::LoggedOut)
        } 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
                        },
                    };

                return Ok(AuthState::LoggedIn {
                    micropub: crate::micropub::Client::new(
                        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()
                    ),
                    submit_busy_guard: None
                })
            }

            Ok(AuthState::LoggedOut)
        }
    }
}

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

#[derive(Default, Debug)]
pub struct AppRootWidgets {
    root: adw::ApplicationWindow,
    toolbar_view: adw::ToolbarView,
    top_bar: adw::HeaderBar,
    top_bar_btn: gtk::Button,
}

//#[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 = ();

    type Widgets = AppRootWidgets;

    type Root = adw::ApplicationWindow;

    fn init_root() -> Self::Root {
        let window = Self::Root::default();
        window.set_size_request(360, 294);
        window.set_default_size(360, 640);
        #[cfg(debug_assertions)]
        window.add_css_class("devel");

        window
    }

    fn init_loading_widgets(root: Self::Root) -> Option<relm4::loading_widgets::LoadingWidgets> {
        let spinner = gtk::Spinner::builder()
            .spinning(true)
            .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 schema = crate::secrets::get_schema();
        let state = App::get_login_state(&schema).await.unwrap();
        let http = soup::Session::builder()
            .user_agent(concat!(env!("CARGO_PKG_NAME"),"/",env!("CARGO_PKG_VERSION")," "))
            .build();
        let model = App {
            state,

            http: http.clone(),
            secret_schema: crate::secrets::get_schema(),

            post_editor: components::PostEditor::builder()
                .launch(None)
                .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 mut widgets = Self::Widgets {
            root: window,
            ..Self::Widgets::default()
        };

        widgets.toolbar_view.add_top_bar(&widgets.top_bar);

        widgets.top_bar.pack_end(&widgets.top_bar_btn);

        widgets.top_bar_btn.set_icon_name("document-send-symbolic");
        widgets.top_bar_btn.set_tooltip("Send post");
        widgets.top_bar_btn.connect_clicked(glib::clone!(
            #[strong] sender,
            move |_button| sender.input(Self::Input::SubmitButtonPressed)
        ));

        widgets.root.set_content(Some(&widgets.toolbar_view));

        // Separate component choosing logic from initialization. We
        // already have all the parts here, might as well use them.
        model.update_view(&mut widgets, sender);

        AsyncComponentParts { model, widgets }
    }


    fn update_view(&self, widgets: &mut Self::Widgets, _sender: AsyncComponentSender<Self>) {
        // Bind the child component, if any, here.
        match &self.state {
            AuthState::LoggedOut => {
                widgets.root.set_title(Some("Sign in with your website"));
                widgets.toolbar_view.set_content(Some(self.signin.widget()));
                widgets.top_bar_btn.set_visible(false);
            },
            AuthState::LoggedIn {
                submit_busy_guard,
                ..
            } => {
                widgets.root.set_title(Some("Create post"));
                widgets.toolbar_view.set_content(Some(self.post_editor.widget()));
                widgets.top_bar_btn.set_sensitive(submit_busy_guard.is_none());
                widgets.top_bar_btn.set_visible(true);
            }
        }        
    }


    async fn update(
        &mut self,
        message: Self::Input,
        _sender: AsyncComponentSender<Self>,
        _root: &Self::Root
    ) {
        match message {
            Input::Authorize(data) => {
                let schema = crate::secrets::get_schema();
                let mut attributes = std::collections::HashMap::new();
                let _me = data.me.to_string();
                let _micropub = data.micropub.to_string();
                attributes.insert(secrets::ME, _me.as_str());
                attributes.insert(secrets::TOKEN_KIND, secrets::ACCESS_TOKEN);
                attributes.insert(secrets::MICROPUB, _micropub.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(&schema),
                    attributes.clone(),
                    Some(libsecret::COLLECTION_DEFAULT),
                    data.me.to_str().as_str(),
                    &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(&schema),
                        attributes,
                        Some(libsecret::COLLECTION_DEFAULT),
                        data.me.to_str().as_str(),
                        refresh_token
                    ).await {
                        Ok(()) => {},
                        Err(err) => log::error!("Failed to store refresh token to the secret store: {}", err),
                    }
                }

                self.state = AuthState::LoggedIn {
                    micropub: crate::micropub::Client::new(
                        data.micropub.clone(), data.access_token.clone()
                    ),
                    submit_busy_guard: None
                };
            },
            Input::SubmitButtonPressed => {
                if let AuthState::LoggedIn {
                    ref mut submit_busy_guard,
                    ..
                } = &mut self.state {
                    *submit_busy_guard = Some(relm4::main_adw_application().mark_busy());
                    self.post_editor.emit(PostEditorInput::Submit);
                };
            },
            Input::PostEditor(None) => {
                if let AuthState::LoggedIn {
                    ref mut submit_busy_guard,
                    ..
                } = &mut self.state {
                    *submit_busy_guard = None;
                }
            }
            Input::PostEditor(Some(post)) => {
                if let AuthState::LoggedIn {
                    ref mut submit_busy_guard,
                    ref micropub
                } = &mut self.state {
                    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));
                        }
                    }
                    *submit_busy_guard = None;
                }
            },
        }
    }
}