// Fragments - connection_manager.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 std::cell::{Cell, RefCell};
use std::fs;
use std::path::Path;
use std::time::Duration;

use gio::prelude::*;
use gio::ListStore;
use glib::subclass::Signal;
use glib::{clone, Enum, KeyFile, Properties};
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use once_cell::sync::Lazy;
use transmission_gobject::{ClientError, TrClient};

use crate::app::FrgApplication;
use crate::backend::{Daemon, FrgConnection};
use crate::i18n::i18n;
use crate::settings::{settings_manager, Key};
use crate::{path, utils};

#[derive(Default, Display, Copy, Debug, Clone, EnumString, PartialEq, Enum, Eq)]
#[repr(u32)]
#[enum_type(name = "FrgDirectoryType")]
pub enum FrgDirectoryType {
    Incomplete,
    Download,
    #[default]
    None,
}

#[derive(Display, Copy, Debug, Clone, EnumString, PartialEq, Enum, Eq)]
#[repr(u32)]
#[enum_type(name = "FrgDaemonStopReason")]
pub enum FrgDaemonStopReason {
    Disconnected,
    Restart,
    Metered,
    VpnDisconnect,
}

#[derive(Default, Display, Copy, Debug, Clone, EnumString, PartialEq, Enum, Eq)]
#[repr(u32)]
#[enum_type(name = "FrgPollingMode")]
pub enum FrgPollingMode {
    #[default]
    Regular,
    InactiveWindow,
    SuspendedWindow,
}

mod imp {
    use super::*;

    #[derive(Properties)]
    #[properties(wrapper_type = super::FrgConnectionManager)]
    pub struct FrgConnectionManager {
        #[property(get)]
        pub client: TrClient,
        #[property(get, set=Self::set_polling_mode, builder(FrgPollingMode::default()))]
        pub polling_mode: RefCell<FrgPollingMode>,
        #[property(get)]
        connections: ListStore,
        #[property(get)]
        pub current_connection: RefCell<FrgConnection>,
        #[property(get, builder(FrgDirectoryType::default()))]
        pub invalid_dir: RefCell<FrgDirectoryType>,
        #[property(get, set)]
        allow_metered: Cell<bool>,

        daemon: Daemon,
    }

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

        fn new() -> Self {
            let connections = ListStore::new::<FrgConnection>();

            Self {
                client: TrClient::new(),
                polling_mode: RefCell::default(),
                connections,
                current_connection: RefCell::default(),
                invalid_dir: RefCell::default(),
                allow_metered: Cell::default(),
                daemon: Daemon::default(),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for FrgConnectionManager {
        fn constructed(&self) {
            self.parent_constructed();

            let nm = gio::NetworkMonitor::default();
            nm.connect_network_metered_notify(clone!(@weak self as this => move |_| {
                this.check_for_stop_reason();
            }));

            self.read_connections();
            self.connections
                .connect_items_changed(clone!(@weak self as this => move |_, _, _, _|{
                    this.write_connections();
                }));
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("daemon-started").build(),
                    Signal::builder("daemon-stopped")
                        .param_types([FrgDaemonStopReason::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }

    impl FrgConnectionManager {
        /// Returns [FrgDaemonStopReason] if there is a reason why the daemon
        /// cannot be started
        pub async fn start_daemon(&self) -> anyhow::Result<Option<FrgDaemonStopReason>> {
            let stop_reason = self.stop_reason();

            if !self.daemon.is_running() {
                if let Some(reason) = &stop_reason {
                    // If there's a stop reason then don't start daemon at all
                    self.obj().emit_by_name::<()>("daemon-stopped", &[reason]);
                    return Ok(Some(*reason));
                } else {
                    // Start daemon
                    self.daemon.start().await?;

                    // Wait a bit to ensure that the daemon accepts rpc requests
                    glib::timeout_future(Duration::from_millis(500)).await;

                    // Connect client
                    let connection = self.obj().current_connection();
                    if !self.client.is_connected() && connection.is_fragments() {
                        self.update_polling_rate();
                        self.client.connect(connection.address()).await?;
                    }

                    self.obj().emit_by_name::<()>("daemon-started", &[]);
                }
            } else if self.daemon.is_running() {
                debug!("Daemon is already started.");

                if let Some(reason) = &stop_reason {
                    // Daemon is already running, but there's a stop reason -> stop it
                    self.stop_daemon(reason).await?;
                    return Ok(Some(*reason));
                }
            }

            Ok(None)
        }

        pub async fn stop_daemon(&self, stop_reason: &FrgDaemonStopReason) -> anyhow::Result<()> {
            if self.daemon.is_running() {
                if self.client.is_connected() && self.obj().current_connection().is_fragments() {
                    self.client.disconnect(true).await;
                }

                self.daemon.stop().await?;

                self.obj()
                    .emit_by_name::<()>("daemon-stopped", &[stop_reason]);
            }

            Ok(())
        }

        /// Returns `None` if there's no reason for stopping the daemon
        pub fn stop_reason(&self) -> Option<FrgDaemonStopReason> {
            // TODO: Also check for disconnecting from a VPN connection
            let nm = gio::NetworkMonitor::default();

            if nm.is_network_metered() && !self.obj().allow_metered() {
                return Some(FrgDaemonStopReason::Metered);
            }

            None
        }

        /// Check if there's a stop reason, and stop the daemon if necessary
        pub fn check_for_stop_reason(&self) {
            let stop_reason = self.stop_reason();
            let is_fragments = self.obj().current_connection().is_fragments();

            if let Some(reason) = stop_reason {
                if !self.daemon.is_running() {
                    return;
                }

                info!("Reason for stopping daemon: {:?}", reason);
                let fut = clone!(@weak self as this, @strong reason => async move {
                    let res = this.stop_daemon(&reason).await;
                    if let Err(e) = res {
                        utils::inapp_notification(&i18n("Unable to stop transmission-daemon"), gtk::Widget::NONE);
                        error!("Unable to stop transmission-daemon: {}", e.to_string());
                    }
                });
                glib::spawn_future_local(fut);
            } else if stop_reason.is_none() && is_fragments {
                let fut = clone!(@weak self as this => async move {
                    let res = this.start_daemon().await;
                    if let Err(e) = res {
                        utils::inapp_notification(&i18n("Unable to start transmission-daemon"), gtk::Widget::NONE);
                        error!("Unable to start transmission-daemon: {}", e.to_string());
                    }
                });
                glib::spawn_future_local(fut);
            }
        }

        pub fn set_polling_mode(&self, mode: FrgPollingMode) {
            let before = *self.polling_mode.borrow();

            if before == mode {
                return;
            }

            *self.polling_mode.borrow_mut() = mode;
            self.update_polling_rate();
        }

        pub fn update_polling_rate(&self) {
            let is_fragments = self.current_connection.borrow().is_fragments();
            let mode = self.obj().polling_mode();

            let rate = match mode {
                FrgPollingMode::Regular => {
                    if is_fragments {
                        1500
                    } else {
                        2500
                    }
                }
                FrgPollingMode::InactiveWindow => 5000,
                FrgPollingMode::SuspendedWindow => 30000,
            };

            debug!("Setting client polling rate to {}ms ({:?})", rate, mode);
            self.obj().client().set_polling_rate(rate);
        }

        fn read_connections(&self) {
            debug!("Read connection data");
            self.connections.remove_all();

            let path = Path::new(&*path::CONFIG).join("connections.conf");

            if !path.exists() {
                debug!("No connections file found, skip reading.");
                return;
            }

            let keyfile = KeyFile::new();
            keyfile
                .load_from_file(&path, glib::KeyFileFlags::NONE)
                .expect("Unable to load connections data");

            for group in keyfile.groups() {
                let group = group.as_str();
                let title = keyfile
                    .string(group, "title")
                    .expect("Unable to get connection title");
                let address = keyfile
                    .string(group, "address")
                    .expect("Unable to get address title");

                let connection = FrgConnection::new_with_uuid(&title, &address, group);
                self.connections.append(&connection);
            }
        }

        fn write_connections(&self) {
            debug!("Write connection data");

            let keyfile = KeyFile::new();

            for pos in 0..self.connections.n_items() {
                let connection = self
                    .connections
                    .item(pos)
                    .unwrap()
                    .downcast::<FrgConnection>()
                    .unwrap();

                // Don't save local Fragments connection
                if connection.is_fragments() {
                    continue;
                }

                let uuid = connection.uuid();

                keyfile.set_string(&uuid, "title", &connection.title());
                keyfile.set_string(&uuid, "address", &connection.address());
            }

            let path = Path::new(&*path::CONFIG).join("connections.conf");

            fs::write(path, keyfile.to_data().as_bytes())
                .expect("Unable to write connections data");
        }
    }
}

glib::wrapper! {
    pub struct FrgConnectionManager(ObjectSubclass<imp::FrgConnectionManager>);
}

impl FrgConnectionManager {
    pub fn new() -> Self {
        glib::Object::new()
    }

    pub async fn connect(
        &self,
        connection: &FrgConnection,
    ) -> Result<Option<FrgDaemonStopReason>, ClientError> {
        let imp = self.imp();

        if connection.uuid() == self.current_connection().uuid() && imp.client.is_connected() {
            debug!("Already connected with {}", connection.address());
            return Ok(None);
        }

        self.disconnect().await?;

        debug!("Connect to {}", &connection.address());
        *imp.current_connection.borrow_mut() = connection.clone();
        self.notify_current_connection();

        // If using Fragments local connenction ensure that daemon is started
        if connection.is_fragments() {
            let daemon_stop_reason = imp.start_daemon().await.map_err(|e| {
                utils::inapp_notification(
                    &i18n("Unable to start transmission-daemon"),
                    gtk::Widget::NONE,
                );
                error!("Unable to start transmission-daemon: {}", e.to_string());

                ClientError::TransmissionError("".into())
            })?;

            if let Some(reason) = daemon_stop_reason {
                info!(
                    "Not going to start daemon because of stop reason: {:?}",
                    reason
                );
                return Ok(daemon_stop_reason);
            }
        } else {
            imp.update_polling_rate();
            imp.client.connect(connection.address()).await?;
        }

        debug!("Connected with {}", &connection.address());

        // Save connection uuid to restore the connection on next Fragments startup
        settings_manager::set_string(Key::ClientLastConnection, connection.uuid());

        self.check_directories();
        Ok(None)
    }

    pub async fn disconnect(&self) -> Result<(), ClientError> {
        let imp = self.imp();

        if imp.client.address().is_empty() || !imp.client.is_connected() {
            // Nothing to disconnect
            return Ok(());
        }

        debug!("Disconnect from {}", &imp.client.address());
        if self.current_connection().is_fragments() {
            imp.stop_daemon(&FrgDaemonStopReason::Disconnected)
                .await
                .map_err(|e| {
                    utils::inapp_notification(
                        &i18n("Unable to stop transmission-daemon"),
                        gtk::Widget::NONE,
                    );
                    error!("Unable to stop transmission-daemon: {}", e.to_string());

                    ClientError::TransmissionError("".into())
                })?;
        } else {
            imp.client.disconnect(false).await;
        }

        Ok(())
    }

    /// Triggers a client reconnect. If the current connection is the local
    /// Fragments session it also restarts the Transmission daemon.
    pub async fn reconnect(&self) -> anyhow::Result<()> {
        debug!("Reconnect...");
        let imp = self.imp();

        if self.current_connection().is_fragments() {
            self.disconnect().await?;
            self.connect(&self.current_connection()).await?;
        } else {
            imp.stop_daemon(&FrgDaemonStopReason::Restart).await?;
            imp.start_daemon().await?;
        }

        Ok(())
    }

    /// `filename` can be a magnet url or a local file path
    pub async fn add_torrent_by_filename(&self, filename: String) -> anyhow::Result<()> {
        let imp = self.imp();
        debug!("Add new torrent by filename: {}", filename);
        imp.client.add_torrent_by_filename(filename).await?;
        Ok(())
    }

    /// `metainfo` is the base64 encoded content of a .torrent file
    pub async fn add_torrent_by_metainfo(&self, metainfo: String) -> anyhow::Result<()> {
        let imp = self.imp();
        debug!("Add new torrent by metainfo.");
        imp.client.add_torrent_by_metainfo(metainfo).await?;
        Ok(())
    }

    pub fn connection_by_uuid(&self, uuid: &str) -> Option<FrgConnection> {
        let connections = self.connections();

        for pos in 0..connections.n_items() {
            let connection = connections
                .item(pos)
                .unwrap()
                .downcast::<FrgConnection>()
                .unwrap();

            if connection.uuid() == uuid {
                return Some(connection);
            }
        }

        None
    }

    pub fn check_directories(&self) {
        let imp = self.imp();
        let mut dir_type = FrgDirectoryType::None;

        // We can only check for locally Fragments session, since we can't access remote
        // filesystems
        if self.current_connection().is_fragments() {
            let mut writable_download = false;
            let mut writable_incomplete = false;

            if let Some(download_dir) = self.client().session().download_dir() {
                writable_download = utils::check_writable(download_dir.path().unwrap());
            }

            // Only check incomplete-dir if enabled
            if self.client().session().incomplete_dir_enabled() {
                if let Some(incomplete_dir) = self.client().session().incomplete_dir() {
                    writable_incomplete = utils::check_writable(incomplete_dir.path().unwrap())
                }
            } else {
                writable_incomplete = true;
            };

            if !writable_incomplete {
                dir_type = FrgDirectoryType::Incomplete;
            } else if !writable_download {
                dir_type = FrgDirectoryType::Download;
            }
        }

        *imp.invalid_dir.borrow_mut() = dir_type;
        self.notify_invalid_dir();
    }
}

impl Default for FrgConnectionManager {
    fn default() -> Self {
        FrgApplication::default().connection_manager()
    }
}
