// Fragments - connection_box.rs
// Copyright (C) 2022-2024  Felix Häcker <haeckerfelix@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use async_recursion::async_recursion;
use glib::{clone, subclass};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gdk, glib, CompositeTemplate};
use transmission_gobject::{ClientError, TrAuthentication, TrClient};

use crate::app::FrgApplication;
use crate::backend::{
    secret_store, FrgConnection, FrgConnectionManager, FrgDaemonStopReason, FrgDirectoryType,
};
use crate::i18n::{i18n, i18n_f};
use crate::ui::{FrgDragOverlay, FrgTorrentPage};
use crate::{config, utils};

pub enum View {
    Authentication,
    Loading,
    Ready,
    Torrents,
    Metered,
    Failure(String),
}

mod imp {
    use super::*;

    #[derive(Default, Debug, CompositeTemplate)]
    #[template(resource = "/de/haeckerfelix/Fragments/gtk/connection_box.ui")]
    pub struct FrgConnectionBox {
        #[template_child]
        stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub torrent_page: TemplateChild<FrgTorrentPage>,
        #[template_child]
        spinner: TemplateChild<gtk::Spinner>,
        #[template_child]
        welcome_status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        failure_status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        username_row: TemplateChild<adw::EntryRow>,
        #[template_child]
        password_row: TemplateChild<adw::PasswordEntryRow>,
        #[template_child]
        authenticate_button: TemplateChild<gtk::Button>,
        #[template_child]
        invalid_dir_banner: TemplateChild<adw::Banner>,
        #[template_child]
        drag_overlay: TemplateChild<FrgDragOverlay>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for FrgConnectionBox {
        const NAME: &'static str = "FrgConnectionBox";
        type ParentType = gtk::Box;
        type Type = super::FrgConnectionBox;

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();
        }

        fn instance_init(obj: &subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    impl ObjectImpl for FrgConnectionBox {
        fn constructed(&self) {
            self.parent_constructed();

            let cm = FrgConnectionManager::default();
            let client = cm.client();

            // Set welcome icon
            if config::PROFILE == "development" {
                let icon = Some("de.haeckerfelix.Fragments.Devel");
                self.welcome_status_page.set_icon_name(icon);
            }

            // Initial view -> Loading
            self.set_view(View::Loading);

            // Show a message when the currently configured directory is not accessible
            cm.connect_invalid_dir_notify(clone!(@weak self as this => move |cm|{
                let res: Option<String> = match cm.invalid_dir(){
                    FrgDirectoryType::Download => Some(
                        i18n("The configured download directory cannot be accessed."),
                    ),
                    FrgDirectoryType::Incomplete => Some(
                        i18n("The configured incomplete directory cannot be accessed."),
                    ),
                    _ => None,
                };

                if let Some(res) = res{
                    this.invalid_dir_banner.set_revealed(true);
                    this.invalid_dir_banner.set_title(&res);
                }else{
                    this.invalid_dir_banner.set_revealed(false);
                }
            }));

            cm.connect_local(
                "daemon-started",
                false,
                clone!(@weak self as this => @default-panic, move |_| {
                    this.update_torrents_view();
                    None
                }),
            );

            cm.connect_local(
                "daemon-stopped",
                false,
                clone!(@weak self as this => @default-panic, move |val| {
                    let reason = val[1].get::<FrgDaemonStopReason>().unwrap();
                    this.show_stop_reason(reason);
                    None
                }),
            );

            client.torrents().connect_local(
                "items-changed",
                false,
                clone!(@weak self as this => @default-panic, move |_| {
                    this.update_torrents_view();
                    None
                }),
            );

            client.connect_local("connection-failure", false, move |_| {
                let app = FrgApplication::default();
                let cm = FrgConnectionManager::default();
                let connection = cm.current_connection();

                utils::inapp_notification(
                    &i18n_f("Connection with {} was lost.", &[&connection.title()]),
                    gtk::Widget::NONE,
                );

                // Try to reconnect
                app.activate_action("set-connection", Some(&connection.uuid().to_variant()));
                None
            });

            // Drag and drop support
            let drop_target = self.drag_overlay.drop_target();
            drop_target.set_types(&[gdk::FileList::static_type()]);

            drop_target.connect_accept(clone!(@weak client => @default-return false, move |_, _| {
                // Only accept drops when we're connected to a Transmission session
                client.is_connected()
            }));

            drop_target.connect_drop(
                clone!(@weak cm => @default-return false, move |_, value, _, _| {
                    let files = match value.get::<gdk::FileList>() {
                        Ok(list) => list.files(),
                        Err(err) => {
                            error!("Issue with drop value: {err}");
                            return false;
                        }
                    };

                    if !files.is_empty() {
                        let app = FrgApplication::default();
                        app.add_torrents_from_files(&files);
                        return true;
                    } else {
                        error!("Dropped FileList was empty");
                        return false;
                    }
                }),
            );

            self.stack.add_controller(drop_target.clone());
        }
    }
    impl WidgetImpl for FrgConnectionBox {}

    impl BoxImpl for FrgConnectionBox {}

    #[gtk::template_callbacks]
    impl FrgConnectionBox {
        pub fn update_torrents_view(&self) {
            let client = FrgConnectionManager::default().client();
            let model = client.torrents();

            if !client.is_connected() {
                return;
            }

            if model.n_items() == 0 {
                self.set_view(View::Ready);
            } else {
                self.set_view(View::Torrents);
            }
        }

        pub fn show_authentication(&self, failure: bool) {
            if failure {
                self.username_row.add_css_class("error");
                self.password_row.add_css_class("error");
            } else {
                self.username_row.remove_css_class("error");
                self.password_row.remove_css_class("error");
            }

            self.set_view(View::Authentication);
        }

        pub fn show_stop_reason(&self, reason: FrgDaemonStopReason) {
            match reason {
                FrgDaemonStopReason::Metered => self.set_view(View::Metered),
                FrgDaemonStopReason::VpnDisconnect => unimplemented!(),
                _ => (),
            }
        }

        pub fn set_view(&self, view: View) {
            self.spinner.set_spinning(false);

            let name = match view {
                View::Authentication => {
                    // Unset previous entry text
                    self.username_row.set_text("");
                    self.password_row.set_text("");
                    "authentication"
                }
                View::Loading => {
                    self.spinner.set_spinning(true);
                    "loading"
                }
                View::Ready => "ready",
                View::Torrents => "torrents",
                View::Metered => "metered",
                View::Failure(err) => {
                    self.failure_status_page.set_description(Some(&err));
                    "failure"
                }
            };

            self.stack.set_visible_child_name(name);
        }

        #[template_callback]
        async fn authenticate_button_clicked(&self) {
            let cm = FrgConnectionManager::default();

            let auth = TrAuthentication::new(&self.username_row.text(), &self.password_row.text());
            cm.client().set_authentication(auth.clone());

            let res = secret_store::store(&cm.current_connection(), &auth).await;
            if let Err(err) = res {
                warn!("Credentials could not be saved: {}", err.to_string());
                utils::inapp_notification(
                    &i18n("Credentials could not be saved"),
                    gtk::Widget::NONE,
                );
            }

            FrgApplication::default().activate_action("reconnect", None);
        }

        #[template_callback]
        fn reconnect_button_clicked(&self) {
            FrgApplication::default().activate_action("reconnect", None);
        }

        #[template_callback]
        fn allow_metered_clicked(&self) {
            debug!("Allow metered connections...");

            let cm = FrgConnectionManager::default();
            cm.set_allow_metered(true);

            FrgApplication::default().activate_action("reconnect", None);
        }
    }
}

glib::wrapper! {
    pub struct FrgConnectionBox(
        ObjectSubclass<imp::FrgConnectionBox>)
        @extends gtk::Widget, gtk::Box;
}

impl FrgConnectionBox {
    #[async_recursion(?Send)]
    pub async fn set_connection(&self, connection_uuid: String) {
        let cm = FrgConnectionManager::default();

        // Try to retrieve a `FrgConnection` by using the connection uuid
        let connection = if let Some(connection) = cm.connection_by_uuid(&connection_uuid) {
            connection
        } else {
            let msg = i18n_f("No connection found with UUID \"{}\"", &[&connection_uuid]);
            utils::inapp_notification(&msg, gtk::Widget::NONE);

            // Fallback to local Fragments connection
            let default_uuid = FrgConnection::default().uuid();
            if connection_uuid != default_uuid {
                self.set_connection(default_uuid).await;
            }

            return;
        };

        self.imp().set_view(View::Loading);

        // Establish connection
        match cm.connect(&connection).await {
            Ok(stop_reason) => {
                if let Some(reason) = stop_reason {
                    self.imp().show_stop_reason(reason);
                } else {
                    self.imp().update_torrents_view();
                }
            }
            Err(err) => {
                if matches!(err, ClientError::TransmissionUnauthorized) {
                    if let Ok(Some(auth)) = secret_store::get(&connection).await {
                        let addr = connection.address();

                        // Test if the saved credentials are valid
                        if let Err(err) =
                            TrClient::test_connectivity(addr, Some(auth.clone())).await
                        {
                            if matches!(err, ClientError::TransmissionUnauthorized) {
                                // Nope -> Show authentication page again
                                self.imp().show_authentication(true);
                            }
                        } else {
                            cm.client().set_authentication(auth);
                            FrgApplication::default().activate_action("reconnect", None);
                        }
                    } else {
                        self.imp().show_authentication(false);
                    }
                } else {
                    let msg = i18n_f(
                        "Failed to establish a connection with the Transmission service (“{}”).",
                        &[&err.to_string()],
                    );

                    warn!("{msg}");
                    self.imp().set_view(View::Failure(msg));
                }
            }
        }
    }
}
