/*
 * MIT License
 * 
 * Copyright (c) 2017 Michael VanOverbeek and ShiftOS devs
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.Hosting.Self;
using Nancy.ModelBinding;
using Nancy.Security;
using Nancy.TinyIoc;
using Newtonsoft.Json;
using ShiftOS.Objects;

namespace ShiftOS.Server.WebAdmin
{
    class Program
    {
        static void Main(string[] args)
        {
            var HostConf = new HostConfiguration();
            HostConf.UrlReservations.CreateAutomatically = true;
            HostConf.RewriteLocalhost = true;
            using(var nancy = new NancyHost(HostConf, new Uri("http://localhost:13371/mudadmin/")))
            {
                nancy.Start();
                Console.WriteLine($"[{DateTime.Now}] <AdminPanel/NancyInit> Initiating on localhost:13371...");
                Console.ReadLine();
            }
        }
    }

    public static class PageBuilder
    {
        public static string Build(string page, Dictionary<string, string> templateParams = null)
        {
            string templatehtml = Properties.Resources.HtmlTemplate;
            if (templateParams == null)
            {
                templateParams = new Dictionary<string, string>();
            }
            if (!templateParams.ContainsKey("{logout}"))
            {
                templateParams.Add("{logout}", "<li><a href=\"/mudadmin/logout\">Log out</a></li>");
            }
            if (SystemManager.MudIsRunning())
            {
                templateParams.Add("{mud_power}", "<li><a href='/mudadmin/poweroff'><span class='glyphicon glyphicon-power-off'></span> Power off</a></li>");
                templateParams.Add("{mud_restart}", "<li><a href='/mudadmin/restart'><span class='glyphicon glyphicon-refresh'></span> Restart</a></li>");
            }
            else
            {
                templateParams.Add("{mud_power}", "<li><a href='/mudadmin/poweron'><span class='glyphicon glyphicon-power-on'></span> Power on</a></li>");
                templateParams.Add("{mud_restart}", "");
            }

            if(templateParams["{logout}"] == "")
            {
                templateParams["{mud_power}"] = "";
                templateParams["{mud_restart}"] = "";

            }

            switch (page)
            {
                case "status":
                    templatehtml = templatehtml.Replace("{body}", Properties.Resources.Status);
                    break;
                case "login":
                    templatehtml = templatehtml.Replace("{body}", Properties.Resources.LoginView);
                    break;
                case "initialsetup":
                    templatehtml = templatehtml.Replace("{body}", Properties.Resources.SetupView);
                    break;
            }
            try
            {
                foreach (var param in templateParams)
                {
                    templatehtml = templatehtml.Replace(param.Key, param.Value);
                }
            }
            catch { }
            return templatehtml;
        }
    }

    public class MudUserIdentity : IUserIdentity
    {
        public MudUserIdentity(string username)
        {
            _username = username;
        }

        public IEnumerable<string> Claims
        {
            get
            {
                return SystemManager.GetClaims(_username);
            }
        }

        private string _username = "";

        public string UserName
        {
            get
            {
                return _username;
            }
        }
    }

    public static class SystemManager
    {
        public static bool MudIsRunning()
        {
            var processes = System.Diagnostics.Process.GetProcessesByName("ShiftOS.Server");
            return processes.Length > 0;
        }

        public static void KillMud()
        {
            var processes = System.Diagnostics.Process.GetProcessesByName("ShiftOS.Server");
            for(int i = 0; i < processes.Length; i++)
            {
                try
                {
                    processes[i].Kill();
                }
                catch
                {
                }
            }
        }

        public static List<string> GetClaims(string username)
        {
            foreach(var save in GetSaves())
            {
                if (save.IsMUDAdmin)
                {
                    return new List<string> { "User", "Admin" };
                }
            }
            return new List<string>(new[] { "User" });
        }

        public static Save[] GetSaves()
        {
            List<Save> saves = new List<Save>();
            if (Directory.Exists("saves"))
            {
                foreach(var saveFile in Directory.GetFiles("saves"))
                {
                    try
                    {
                        saves.Add(JsonConvert.DeserializeObject<Save>(Server.Program.ReadEncFile(saveFile)));
                    }
                    catch { }
                }
            }
            return saves.ToArray();
        }

        public static bool Login(string username, string password, out Guid id)
        {
            foreach (var user in GetSaves())
            {
                if (user.Username == username && user.Password == password)
                {
                    id = user.ID;
                    return true;
                }
            }
            id = new Guid();
            return false;
        }

        public static string BuildFormFromObject(object obj)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<form method='post' action=''><table class='table'>");
            foreach(var prop in obj.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
            {
                string name = "";
                string description = "No description.";
                foreach(var attrib in prop.GetCustomAttributes(false))
                {
                    if(attrib is FriendlyNameAttribute)
                    {
                        name = (attrib as FriendlyNameAttribute).Name;
                    }
                    if(attrib is FriendlyDescriptionAttribute)
                    {
                        description = (attrib as FriendlyDescriptionAttribute).Description;
                    }
                }
                if (name != "")
                {
                    sb.AppendLine("<tr>");

                    sb.AppendLine($@"<td width=""45%"">
    <p><strong>{name}</strong></p>
    <p>{description}</p>
</td>
<td>");
                    if (prop.PropertyType == typeof(bool))
                    {
                        string isChecked = ((bool)prop.GetValue(obj) == true) ? "checked" : "";
                        sb.AppendLine($"<input class='form-control' type='checkbox' name='{prop.Name}' {isChecked}/>");
                    }
                    else if (prop.PropertyType == typeof(string))
                    {
                        sb.AppendLine($"<input class='form-control' type='text' name='{prop.Name}' value='{prop.GetValue(obj)}'/>");
                    }

                    sb.AppendLine("</td></tr>");
                }
                else
                {
                    sb.AppendLine($"<input type='hidden' name='{prop.Name}' value='{prop.GetValue(obj)}'/>");
                }
            }
            sb.AppendLine("<tr><td></td><td><input class='btn btn-default' type='submit'/></td></tr>");
            sb.AppendLine("</table></form>");
            return sb.ToString();
        }

        public static Channel GetChat(string id)
        {
            if (File.Exists("chats.json"))
                foreach (var channel in JsonConvert.DeserializeObject<List<Channel>>(File.ReadAllText("chats.json")))
                {
                    if (channel.ID == id)
                        return channel;
                }
            return new Channel();
        }

        public static string BuildSaveListing(Save[] list)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<table class=\"table\">");

            sb.AppendLine(@"<tr>
    <td><strong>Username</strong></td>
    <td><strong>System Name</strong></td>
    <td><strong>Codepoints</strong></td>
    <td><strong>Shiftorium Upgrades</strong></td>
    <td><strong>Is MUD Admin</strong></td>
    <td><strong>Actions</strong></td>
</tr>");

            foreach(var save in list)
            {
                sb.AppendLine($@"<tr>
    <td>{save.Username}</td>
    <td>{save.SystemName}</td>
    <td>{save.Codepoints}</td>
    <td>{save.CountUpgrades()} installed, {save.Upgrades.Count} total</td>
    <td>{save.IsMUDAdmin}</td>
    <td>
        <a href=""/mudadmin/toggleadmin/{save.Username}"" class=""btn btn-danger"">Toggle admin</a>
        <a href=""/mudadmin/deletesave/{save.Username}"" class=""btn btn-danger"">Delete save</a>
    </td>
</tr>");
            }

            sb.AppendLine("</table>");
            return sb.ToString();
        }


        public static string GetAllChats()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<table class=\"table\">");
            sb.AppendLine($@"<tr><td><strong>ID</strong></td>
    <td><strong>Name</strong></td>
    <td><strong>Topic</strong></td>
    <td><strong>Is Discord Relay</strong></td>
    <td><strong>Discord channel ID</strong></td>
    <td><strong>Discord Bot Token</strong></td>
    <td><strong>Actions</strong></td></tr>");
            if (File.Exists("chats.json"))
            {
                foreach(var chat in JsonConvert.DeserializeObject<List<Channel>>(File.ReadAllText("chats.json")))
                {
                    sb.AppendLine($@"<tr>
    <td>{chat.ID}</td>
    <td>{chat.Name}</td>
    <td>{chat.Topic}</td>
    <td>{chat.IsDiscordProxy}</td>
    <td>{chat.DiscordChannelID}</td>
    <td>{chat.DiscordBotToken}</td>
    <td>
        <a href=""/mudadmin/editchat/{chat.ID}"" class=""btn btn-default""><span class=""glyphicon glyphicon-pencil""></span>Edit</a>
        <a href=""#"" class=""btn btn-default"" data-toggle=""modal"" data-target=""#modal_{chat.ID}""><span class=""glyphicon glyphicon-delete""></span> Delete</a>
    </td>
</tr>");
                    sb.AppendLine(CreateModal(chat.ID, "Delete " + chat.Name + "?", "Are you sure you want to delete this chat?", "/deletechat/" + chat.ID));
                }
            }
            sb.AppendLine("</table>");
            return sb.ToString();
        }

        public static string CreateModal(string id, string title, string msg, string callbackUrl)
        {
            return $@"<div id=""modal_{id}"" class=""modal fade"" role=""dialog"">
  <div class=""modal-dialog"">

    <!-- Modal content-->
    <div class=""modal-content"">
      <div class=""modal-header"">
        <button type=""button"" class=""close"" data-dismiss=""modal"">&times;</button>
        <h4 class=""modal-title"">{title}</h4>
      </div>
      <div class=""modal-body"">
        <p>{msg}</p>
      </div>
      <div class=""modal-footer"">
        <a href=""/mudadmin{callbackUrl}"" class=""btn btn-danger"">Yes</a>
        <button type=""button"" class=""btn btn-default"" data-dismiss=""modal"">No</button>
      </div>
    </div>

  </div>
</div>";
        }

        public static string GetCPWorth()
        {
            if (System.IO.Directory.Exists("saves"))
            {
                long cp = 0;

                foreach(var file in System.IO.Directory.GetFiles("saves"))
                {
                    if (file.EndsWith(".save"))
                    {
                        var save = JsonConvert.DeserializeObject<Save>(Server.Program.ReadEncFile(file));
                        cp += save.Codepoints;
                    }
                }
                return cp.ToString();
            }
            else
            {
                return "0";
            }
        }

        public static string GetUserCount()
        {
            if (System.IO.Directory.Exists("saves"))
            {
                return System.IO.Directory.GetFiles("saves").Length.ToString();
            }
            else
            {
                return "0";
            }
        }

        public static MudUserIdentity GetIdentity(Guid id)
        {
            foreach (var user in GetSaves())
            {
                if (user.ID == id)
                {
                    return new WebAdmin.MudUserIdentity(user.Username);
                }
            }
            return null;
        }

        internal static void MakeAdmin(string username)
        {
            Save sav = null;
            foreach(var save in GetSaves())
            {
                if (save.Username == username)
                    sav = save;
            }
            if(sav != null)
            {
                sav.IsMUDAdmin = true;
                Server.Program.WriteEncFile("saves/" + username + ".save", JsonConvert.SerializeObject(sav));
            }
        }

        internal static Save[] GetAdmins()
        {
            var saves = new List<Save>();
            foreach(var save in GetSaves())
            {
                if(save.IsMUDAdmin == true)
                {
                    saves.Add(save);
                }
            }
            return saves.ToArray();
        }
    }

    public class MudUser
    {
        [FriendlyName("Username")]
        [FriendlyDescription("The username you will appear as in-game.")]
        public string Username { get; set; }

        [FriendlyName("Password")]
        [FriendlyDescription("A password that you will use to log in to the admin panel and the game.")]
        public string Password { get; set; }

        [FriendlyName("System name")]
        [FriendlyDescription("An in-game hostname for your account. In ShiftOS, your user ID is always yourusername@yoursystemname. Be creative.")]
        public string SystemName { get; set; }

        public Guid ID { get; set; }
    }

    public class MudBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
        {
            var formsAuthConfiguration = new FormsAuthenticationConfiguration();
            formsAuthConfiguration.RedirectUrl = "~/login";
            formsAuthConfiguration.UserMapper = container.Resolve<IUserMapper>();
            FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
            base.ApplicationStartup(container, pipelines);
        }
    }


    public class MudUserMapper : IUserMapper
    {
        public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
        {
            return SystemManager.GetIdentity(identifier);
        }
    }

    public class LoginModule : NancyModule
    {
        public LoginModule()
        {
            Get["/login"] = parameters =>
            {
                if (SystemManager.GetSaves().Length > 0)
                {
                    if (SystemManager.GetAdmins().Length > 0)
                    {
                        return PageBuilder.Build("login", new Dictionary<string, string>
                    {
                        {"{logout}", "" }
                    });
                    }
                    else
                    {
                        return PageBuilder.Build("initialsetup", new Dictionary<string, string>
                    {
                        {"{logout}", "" },
                            {"{savelist}", BuildSaveList() }
                    });
                    }
                }
                else
                {
                    return PageBuilder.Build("bla", new Dictionary<string, string>
                    {
                        {"{body}", Properties.Resources.NoUsersFound },
                        {"{user_create_form}", SystemManager.BuildFormFromObject(new MudUser()) }
                    });
                }
            };

            Get["/logout"] = parameters =>
            {
                return this.Logout("~/");
            };

            Post["/login"] = parameters =>
            {
                if (SystemManager.GetSaves().Length > 0)
                {
                    if (SystemManager.GetAdmins().Length == 0)
                    {
                        var user = this.Bind<LoginRequest>();
                        SystemManager.MakeAdmin(user.username);
                        Guid id = new Guid();
                        if(SystemManager.Login(user.username, user.password, out id) == true)
                        {
                            return this.Login(id);
                        }
                        return new UserModule().Redirect("/login");
                    }
                    else
                    {
                        var user = this.Bind<LoginRequest>();
                        Guid id = new Guid();
                        if (SystemManager.Login(user.username, user.password, out id) == true)
                        {
                            return this.Login(id);
                        }
                        return new UserModule().Redirect("/login");
                    }
                }
                else
                {
                    var newUser = this.Bind<MudUser>();
                    var save = new Save();
                    save.Username = newUser.Username;
                    save.SystemName = newUser.SystemName;
                    save.Password = newUser.Password;
                    save.Codepoints = 0;
                    save.MyShop = "";
                    save.Upgrades = new Dictionary<string, bool>();
                    save.IsMUDAdmin = true;
                    save.StoryPosition = 1;

                    if (!Directory.Exists("saves"))
                        Directory.CreateDirectory("saves");
                    save.ID = Guid.NewGuid();

                    Server.Program.WriteEncFile("saves/" + save.Username + ".save", JsonConvert.SerializeObject(save));
                    return this.Login(save.ID);
                }
            };
        }

        private string BuildSaveList()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<table class='table'>");
            sb.AppendLine($@"<tr>
    <td><strong>Username</strong></td>
    <td><strong>System name</strong></td>
    <td><strong>Codepoints</strong></td>
    <td><strong>Actions</strong></td>
</tr>");

            foreach(var save in SystemManager.GetSaves())
            {
                sb.AppendLine($@"<tr>
    <td>{save.Username}</td>
    <td>{save.SystemName}</td>
    <td>{save.Codepoints}</td>
    <td><form method='post' action=''>
        <input type='hidden' name='username' value='{save.Username}'/><input type='hidden' name='password' value='{save.Password}'/>
        <input type='submit' value='Choose' class='btn btn-default'/>
    </form></td>
</tr>");
            }

            sb.AppendLine("</table>");
            return sb.ToString();
        }
    }



    public class UserModule : NancyModule
    {
        public string Redirect(string url)
        {
            return $@"<html>
    <head>
        <meta http-equiv=""refresh"" content=""0; url=/mudadmin{url}"" />
    </ head>
</html>";
        }

        public UserModule()
        {
            this.RequiresAuthentication();
            this.RequiresClaims("Admin");
            Get["/"] = _ =>
            {
                return Redirect("/status");
            };

            Get["/toggleadmin/{id}"] = parameters =>
            {
                string id = parameters.id;
                for (int i = 0; i < SystemManager.GetSaves().Length; i++)
                {
                    var save = SystemManager.GetSaves()[i];
                    if(save.Username.ToString() == id)
                    {
                        save.IsMUDAdmin = !save.IsMUDAdmin;
                        Server.Program.WriteEncFile("saves/" + save.Username + ".save", JsonConvert.SerializeObject(save));
                    }
                }
                return Redirect("/saves");

            };

            Get["/deletesave/{username}"] = parameters =>
            {


                string id = parameters.username;
                for (int i = 0; i < SystemManager.GetSaves().Length; i++)
                {
                    if (SystemManager.GetSaves()[i].Username.ToString() == id)
                    {
                        File.Delete("saves/" + SystemManager.GetSaves()[i].Username + ".save");
                    }
                }
                return Redirect("/saves");
            };


            Get["/saves"] = _ =>
            {
                return PageBuilder.Build("bla", new Dictionary<string, string>
                {
                    { "{body}", Properties.Resources.GenericTableList },
                    { "{listtitle}", "Test subjects"  },
                    { "{listdesc}", "Below is a list of test subjects (save files) on your multi-user domain. You can see their username, system name, Codepoints, amount of installed upgrades, and you can also perform basic actions on each save." },
                    { "{list}", SystemManager.BuildSaveListing(SystemManager.GetSaves()) }
                });
            };

            Get["/status"] = _ =>
            {
                return statusBuilder();
            };

            Get["/deletechat/{id}"] = parameters =>
            {
                string chatID = parameters.id;
                var chats = JsonConvert.DeserializeObject<List<Channel>>(File.ReadAllText("chats.json"));
                for(int i = 0; i < chats.Count; i++)
                {
                    try
                    {
                        if (chats[i].ID == chatID)
                            chats.RemoveAt(i);
                    }
                    catch { }
                }
                File.WriteAllText("chats.json", JsonConvert.SerializeObject(chats, Formatting.Indented));
                return Redirect("/chats");
            };

            Get["/chats"] = _ =>
            {
                return chatsListBuilder();
            };

            Get["/createchat"] = _ =>
            {
                return PageBuilder.Build("editchat", new Dictionary<string, string>
                {
                    {"{body}", Properties.Resources.ChatEditTemplate },
                    {"{form}", SystemManager.BuildFormFromObject(new Channel()) }
                });
            };

            Post["/createchat"] = parameters =>
            {
                var chat = this.Bind<Channel>();
                chat.ID = chat.Name.ToLower().Replace(" ", "_");
                List<Channel> chats = new List<Channel>();
                if (File.Exists("chats.json"))
                    chats = JsonConvert.DeserializeObject<List<Channel>>(File.ReadAllText("chats.json"));

                bool chatExists = false;

                for (int i = 0; i < chats.Count; i++)
                {
                    if (chats[i].ID == chat.ID)
                    {
                        chats[i] = chat;
                        chatExists = true;
                    }
                }

                if (!chatExists)
                {
                    chats.Add(chat);
                }

                File.WriteAllText("chats.json", JsonConvert.SerializeObject(chats, Formatting.Indented));

                return Redirect("/chats");
            };

            Get["/editchat/{id}"] = parameters =>
            {
                return PageBuilder.Build("editchat", new Dictionary<string, string>
                {
                    {"{body}", Properties.Resources.ChatEditTemplate },
                    {"{form}", SystemManager.BuildFormFromObject(SystemManager.GetChat(parameters.id)) }
                });
            };

            Post["/editchat/{id}"] = parameters =>
            {
                var chat = this.Bind<Channel>();
                chat.ID = chat.Name.ToLower().Replace(" ", "_");
                List<Channel> chats = new List<Channel>();
                if (File.Exists("chats.json"))
                    chats = JsonConvert.DeserializeObject<List<Channel>>(File.ReadAllText("chats.json"));

                bool chatExists = false;

                for (int i = 0; i < chats.Count; i++)
                {
                    if (chats[i].ID == chat.ID)
                    {
                        chats[i] = chat;
                        chatExists = true;
                    }
                }

                if (!chatExists)
                {
                    chats.Add(chat);
                }

                File.WriteAllText("chats.json", JsonConvert.SerializeObject(chats, Formatting.Indented));
                return Redirect("/chats");
            };

            Get["/poweron"] = _ =>
            {
                if (!SystemManager.MudIsRunning())
                {
                    System.Diagnostics.Process.Start("ShiftOS.Server.exe");
                }
                return Redirect("/");
            };

            Get["/poweroff"] = _ =>
            {
                if (SystemManager.MudIsRunning())
                {
                    SystemManager.KillMud();
                }
                return Redirect("/");
            };
            Get["/restart"] = _ =>
            {
                if (SystemManager.MudIsRunning())
                {
                    SystemManager.KillMud();
                }
                return Redirect("/poweron");
            };
        }

        private string statusBuilder()
        {
            return PageBuilder.Build("status", new Dictionary<string, string>{
                    { "{cp_worth}", SystemManager.GetCPWorth() },
                    { "{user_count}", SystemManager.GetUserCount() },
                    { "{system_time}", DateTime.Now.ToString() },
                });

        }

        private string chatsListBuilder()
        {
            return PageBuilder.Build("bla", new Dictionary<string, string>
            {
                { "{body}", Properties.Resources.ChatListView },
                { "{chat_table}", SystemManager.GetAllChats() }
            });
        }
    }

    public class LoginRequest
    {
        public string username { get; set; }
        public string password { get; set; }
    }
}