using Gtk;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;
using System.Reflection;

namespace SSHVPNApplication {
    class SSHVPNGUI {
        static System.Diagnostics.Process DaemonProcess = null;
        static System.Net.Sockets.Socket ipc_cnxn = null;
        static bool check_daemon_health = true;

        protected static void Main(string[] args) {
            var xdg_config_home = System.Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
            if (xdg_config_home == null) {
                xdg_config_home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.config";
            }

            //Console.WriteLine(xdg_config_home);

            // normalise xdg_config_home
            if (xdg_config_home[xdg_config_home.Length-1] != '/') {
                xdg_config_home += "/";
            }

            string config_file_directory = xdg_config_home + "com.bangsparks.sshvpnapplication";
            string profiles_directory = config_file_directory + "/profiles";

            System.IO.Directory.CreateDirectory(config_file_directory);
            System.IO.Directory.CreateDirectory(profiles_directory);

            string config_docs_name = config_file_directory + "/CONFIG_README.txt";
            System.IO.File.WriteAllText(config_docs_name, "Configuration files\n\nConfigurations for this application are stored in in the 'profiles' directory.  Each profile is stored as a file with the name of the profile (extensions are interpreted as part of the name).  If no profile is specified on launch, 'default' will be assumed.\nThe contents of the file should be a single line, in the format 'user@host:port' or 'user@host' (port assumed to be 22).  To specify custom ssh keys, put them in the same folder as the config file and use the same name as the profile they are for, but append '.id_rsa', '.id_dsa', '.id_ecdsa' or '.id_ed25519'.  Such custom keys will be used alongside your normal SSH keys, and multiple of these can be present.\n\nNote that ssh key auth must be used.");

            /*foreach (var arg in args) {
                Console.WriteLine(arg);
            }*/

            string profile_name = "";

            try {
                profile_name = args[0];
            } catch (IndexOutOfRangeException) {
                profile_name = "default";
            }

            //Console.WriteLine(profile_name);

            string profile_file = profiles_directory + "/" + profile_name;
            string ssh_connection_details = "";
            try {
                ssh_connection_details = System.IO.File.ReadAllText(profile_file);

                System.Collections.Generic.List<string> sshKeyFileNames = new System.Collections.Generic.List<string>() {"id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", profile_file + ".id_rsa", profile_file + ".id_dsa", profile_file + ".id_ecdsa", profile_file + ".id_ed25519"};

                // construct ssh command so users ssh keys respected
                // TODO: determine and respect gender and pronouns of user

                //prefixed with a single quote so that the command is treated as one argument and we don't need to concatenate a list or anything
                string sshCommand = "'ssh";
                // loop through ssh keys of user
                foreach (var sshKeyFileName in sshKeyFileNames) {
                    sshCommand += " -i ";
                    sshCommand += Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
                    sshCommand += "/.ssh/";
                    sshCommand += sshKeyFileName;
                }
                // terminate this argument
                sshCommand += "'";

                System.IO.Directory.CreateDirectory("/tmp/com.bangsparks.sshvpnapplication");

                // explicit namespace used to prevent conflict with Gtk.Socket();
                var sock = new System.Net.Sockets.Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
                var endPoint = new UnixDomainSocketEndPoint("/tmp/com.bangsparks.sshvpnapplication/ipc_socket");

                DaemonProcess = System.Diagnostics.Process.Start("pkexec", "mono " + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "/csharp_sshuttle_wrap.exe" + " " + sshCommand + " " + ssh_connection_details);

                Thread DaemonHealthCheckWorkerThread = new Thread(new ThreadStart(daemon_health_check_worker));
                DaemonHealthCheckWorkerThread.Start();

                sock.Bind(endPoint);
                sock.Listen(1);
                ipc_cnxn = sock.Accept();

                var ipc_buf = new byte[1024];
                var ipc_received = ipc_cnxn.Receive(ipc_buf, SocketFlags.None);
                var ipc_resp = Encoding.UTF8.GetString(ipc_buf, 0, ipc_received);

                if(ipc_resp == "DAEMON_READY") {
                    Console.WriteLine("Root daemon is connected");
                    check_daemon_health = false;
                } else {
                    Console.WriteLine("Unexpected response from daemon: " + ipc_resp);
                }

                Application.Init();

                Window window = new Window ("Sshuttle GUI [" + profile_name + "]");

                Button StartStopButton = new Button("Start");
                StartStopButton.Clicked += i_dont_give_a_fuck_about_your_firewall;
                StartStopButton.Show();

                //StartStopButton.Label = "test";

                window.Add(StartStopButton);

                window.Show();

                // calls delete_event when window closed
                window.DeleteEvent += delete_event;

                // make a border for the window
                window.BorderWidth = 10;

                Application.Run();
            } catch (FileNotFoundException) {
                Application.Init();

                Window error_window = new Window("Sshuttle GUI [" + profile_name + "] - Configuration Error");
                Label label = new Label("Please specify a configuration as documented by the text file in '" + config_file_directory + "'.");
                label.Show();

                error_window.Add(label);
                error_window.Show();

                // calls delete_event when window closed
                error_window.DeleteEvent += delete_event_error_window;

                // make a border for the window
                error_window.BorderWidth = 10;

                Application.Run();
            }
        }

        static void i_dont_give_a_fuck_about_your_firewall(object obj, EventArgs args) {
            Button StartStopButton = (Button)obj; // explicit cast so we can get the label without the compiler throwing a shit fit

            // yes, I am really using the current label to determine whether the process should be running or not
            if (StartStopButton.Label == "Start") {
                StartStopButton.Label = "Stop";
                // start sshuttle
                ipc_cnxn.Send(Encoding.UTF8.GetBytes("START_SSHUTTLE"));
                var ipc_buf = new byte[1024];
                var ipc_received = ipc_cnxn.Receive(ipc_buf, SocketFlags.None);
                var ipc_resp = Encoding.UTF8.GetString(ipc_buf, 0, ipc_received);

                if(ipc_resp == "SSHUTTLE_STARTED") {
                    Console.WriteLine("Root daemon started sshuttle");
                } else if(ipc_resp == "SSHUTTLE_ALREADY_RUNNING") {
                    Console.WriteLine("Root daemon reports sshuttle is already running!");
                } else {
                    Console.WriteLine("Unexpected response from daemon: " + ipc_resp);
                }
            } else {
                StartStopButton.Label = "Start";
                // stop sshuttle
                ipc_cnxn.Send(Encoding.UTF8.GetBytes("STOP_SSHUTTLE"));
            }
        }

        static void delete_event(object obj, DeleteEventArgs args) {
            // callback executed when the close button is pressed (or equivalent, depending on desktop environment/window manager/wayland compositor)
            ipc_cnxn.Send(Encoding.UTF8.GetBytes("APPLICATION_TERMINATE"));
            
            var ipc_buf = new byte[1024];
            var ipc_received = ipc_cnxn.Receive(ipc_buf, SocketFlags.None);
            var ipc_resp = Encoding.UTF8.GetString(ipc_buf, 0, ipc_received);

            if (ipc_resp == "DAEMON_OK_TERMINATE") {
                Console.WriteLine("Daemon reports that we can terminate");
            } else if (ipc_resp == "DAEMON_OK_TERMINATE_AFTER_DELAY") {
                // give sshuttle time to exit so console hopefully does not get output after application apparently terminates
                Console.WriteLine("Daemon reports that we can terminate after a short delay");
                System.Threading.Thread.Sleep(500);
            } else {
                Console.WriteLine("Unexpected response from daemon: " + ipc_resp);
            }

            // cleanly exit the application, so it doesnt remain running in the background
            Application.Quit();
        }

        static void delete_event_error_window(object obj, DeleteEventArgs args) {
            Application.Quit();
        }

        static void daemon_health_check_worker() {
            // thread used to check if daemon is still alive while we are waiting for it to connect to the IPC socket, so we don't hang indefinitely on an authentication failure
            while (check_daemon_health) {
                DaemonProcess.Refresh();
                if (DaemonProcess.HasExited) {
                    Console.WriteLine("Daemon dead!");

                    // clean up ipc socket (since we cannot be sure if the daemon did this)
                    System.IO.File.Delete("/tmp/com.bangsparks.sshvpnapplication/ipc_socket");

                    // close application
                    Environment.Exit(1);
                }
            }
        }
    }
}