using LANCommander.PlaynitePlugin.Helpers;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Models;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.IO;
using System.Linq;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace LANCommander.PlaynitePlugin
{
    public class LANCommanderInstallController : InstallController
    {
        public static readonly ILogger Logger = LogManager.GetLogger();

        private LANCommanderLibraryPlugin Plugin;
        private PowerShellRuntime PowerShellRuntime;
        private Playnite.SDK.Models.Game PlayniteGame;

        public LANCommanderInstallController(LANCommanderLibraryPlugin plugin, Playnite.SDK.Models.Game game) : base(game)
        {
            Name = "Install using LANCommander";
            Plugin = plugin;
            PlayniteGame = game;
            PowerShellRuntime = new PowerShellRuntime();
        }

        public override void Install(InstallActionArgs args)
        {
            Logger.Trace("Game install triggered, checking connection...");

            while (!Plugin.ValidateConnection())
            {
                Logger.Trace("User not authenticated. Opening auth window...");

                Plugin.ShowAuthenticationWindow();
            }

            var gameId = Guid.Parse(Game.GameId);
            var game = Plugin.LANCommander.GetGame(gameId);

            Logger.Trace($"Installing game {game.Title} ({game.Id})...");

            var result = RetryHelper.RetryOnException<ExtractionResult>(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
            {
                Logger.Trace("Attempting to download and extract game...");
                return DownloadAndExtractGame(game);
            });

            if (!result.Success && !result.Canceled)
                throw new Exception("Could not extract the install archive. Retry the install or check your connection.");
            else if (result.Canceled)
                throw new Exception("Install was canceled");

            var installInfo = new GameInstallationData()
            {
                InstallDirectory = result.Directory
            };

            PlayniteGame.InstallDirectory = result.Directory;

            SDK.GameManifest manifest = null;

            var writeManifestSuccess = RetryHelper.RetryOnException(10, TimeSpan.FromSeconds(1), false, () =>
            {
                Logger.Trace("Attempting to get game manifest...");

                manifest = Plugin.LANCommander.GetGameManifest(gameId);

                WriteManifest(manifest, result.Directory);

                return true;
            });

            if (!writeManifestSuccess)
                throw new Exception("Could not get or write the manifest file. Retry the install or check your connection.");

            Logger.Trace("Saving scripts...");

            SaveScript(game, result.Directory, ScriptType.Install);
            SaveScript(game, result.Directory, ScriptType.Uninstall);
            SaveScript(game, result.Directory, ScriptType.NameChange);
            SaveScript(game, result.Directory, ScriptType.KeyChange);

            if (game.Redistributables != null && game.Redistributables.Count() > 0)
            {
                Logger.Trace("Installing required redistributables...");
                InstallRedistributables(game);
            }

            try
            {
                PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install);
                PowerShellRuntime.RunScript(PlayniteGame, ScriptType.NameChange, Plugin.Settings.PlayerName);

                var key = Plugin.LANCommander.GetAllocatedKey(game.Id);

                PowerShellRuntime.RunScript(PlayniteGame, ScriptType.KeyChange, $"\"{key}\"");
            }
            catch { }

            Plugin.UpdateGame(manifest, gameId);

            Plugin.DownloadCache.Remove(gameId);

            InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
        }

        private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game)
        {
            if (game == null)
            {
                Logger.Trace("Game failed to download! No game was specified!");

                throw new Exception("Game failed to download!");
            }

            var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());

            Logger.Trace($"Downloading and extracting \"{game.Title}\" to path {destination}");
            var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
            {
                try
                {
                    Directory.CreateDirectory(destination);
                    progress.ProgressMaxValue = 100;
                    progress.CurrentProgressValue = 0;

                    using (var gameStream = Plugin.LANCommander.StreamGame(game.Id))
                    using (var reader = ReaderFactory.Open(gameStream))
                    {
                        progress.ProgressMaxValue = gameStream.Length;

                        gameStream.OnProgress += (pos, len) =>
                        {
                            progress.CurrentProgressValue = pos;
                        };

                        reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
                        {
                            if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
                            {
                                reader.Cancel();
                                progress.IsIndeterminate = true;

                                reader.Dispose();
                                gameStream.Dispose();
                            }
                        };

                        reader.WriteAllToDirectory(destination, new ExtractionOptions()
                        {
                            ExtractFullPath = true,
                            Overwrite = true
                        });
                    }
                }
                catch (Exception ex)
                {
                    if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
                    {
                        Logger.Trace("User cancelled the download");

                        if (Directory.Exists(destination))
                        {
                            Logger.Trace("Cleaning up orphaned install files after cancelled install...");

                            Directory.Delete(destination, true);
                        }
                    }
                    else
                    {
                        Logger.Error(ex, $"Could not extract to path {destination}");

                        if (Directory.Exists(destination))
                        {
                            Logger.Trace("Cleaning up orphaned install files after bad install...");

                            Directory.Delete(destination, true);
                        }

                        throw new Exception("The game archive could not be extracted. Please try again or fix the archive!");
                    }
                }
            },
            new GlobalProgressOptions($"Downloading {game.Title}...")
            {
                IsIndeterminate = false,
                Cancelable = true,
            });

            var extractionResult = new ExtractionResult
            {
                Canceled = result.Canceled
            };

            if (!result.Canceled)
            {
                extractionResult.Success = true;
                extractionResult.Directory = destination;
                Logger.Trace($"Game successfully downloaded and extracted to {destination}");
            }

            return extractionResult;
        }

        private void InstallRedistributables(LANCommander.SDK.Models.Game game)
        {
            foreach (var redistributable in game.Redistributables)
            {
                string installScriptTempFile = null;
                string detectionScriptTempFile = null;
                string extractTempPath = null;

                try
                {
                    var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install);
                    installScriptTempFile = SaveTempScript(installScript);

                    var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall);
                    detectionScriptTempFile = SaveTempScript(detectionScript);

                    var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin);

                    // Redistributable is not installed
                    if (detectionResult == 0)
                    {
                        if (redistributable.Archives.Count() > 0)
                        {
                            var extractionResult = DownloadAndExtractRedistributable(redistributable);

                            if (extractionResult.Success)
                            {
                                extractTempPath = extractionResult.Directory;

                                PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
                            }
                        }
                        else
                        {
                            PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
                        }
                    }
                }
                catch (Exception ex)
                {
                    Logger.Error(ex, $"Redistributable {redistributable.Name} failed to install");
                }
                finally
                {
                    if (File.Exists(installScriptTempFile))
                        File.Delete(installScriptTempFile);

                    if (File.Exists(detectionScriptTempFile))
                        File.Delete(detectionScriptTempFile);

                    if (Directory.Exists(extractTempPath))
                        Directory.Delete(extractTempPath);
                }
            }
        }

        private ExtractionResult DownloadAndExtractRedistributable(LANCommander.SDK.Models.Redistributable redistributable)
        {
            if (redistributable == null)
            {
                Logger.Trace("Redistributable failed to download! No redistributable was specified!");

                throw new Exception("Redistributable failed to download!");
            }

            var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename());

            Logger.Trace($"Downloading and extracting \"{redistributable.Name}\" to path {destination}");
            var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
            {
                try
                {
                    Directory.CreateDirectory(destination);
                    progress.ProgressMaxValue = 100;
                    progress.CurrentProgressValue = 0;

                    using (var redistributableStream = Plugin.LANCommander.StreamRedistributable(redistributable.Id))
                    using (var reader = ReaderFactory.Open(redistributableStream))
                    {
                        progress.ProgressMaxValue = redistributableStream.Length;

                        redistributableStream.OnProgress += (pos, len) =>
                        {
                            progress.CurrentProgressValue = pos;
                        };

                        reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
                        {
                            if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
                            {
                                reader.Cancel();
                                progress.IsIndeterminate = true;

                                reader.Dispose();
                                redistributableStream.Dispose();
                            }
                        };

                        reader.WriteAllToDirectory(destination, new ExtractionOptions()
                        {
                            ExtractFullPath = true,
                            Overwrite = true
                        });
                    }
                }
                catch (Exception ex)
                {
                    if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
                    {
                        Logger.Trace("User cancelled the download");

                        if (Directory.Exists(destination))
                        {
                            Logger.Trace("Cleaning up orphaned install files after cancelled install...");

                            Directory.Delete(destination, true);
                        }
                    }
                    else
                    {
                        Logger.Error(ex, $"Could not extract to path {destination}");

                        if (Directory.Exists(destination))
                        {
                            Logger.Trace("Cleaning up orphaned install files after bad install...");

                            Directory.Delete(destination, true);
                        }

                        throw new Exception("The redistributable archive could not be extracted. Please try again or fix the archive!");
                    }
                }
            },
            new GlobalProgressOptions($"Downloading {redistributable.Name}...")
            {
                IsIndeterminate = false,
                Cancelable = true,
            });

            var extractionResult = new ExtractionResult
            {
                Canceled = result.Canceled
            };

            if (!result.Canceled)
            {
                extractionResult.Success = true;
                extractionResult.Directory = destination;
                Logger.Trace($"Redistributable successfully downloaded and extracted to {destination}");
            }

            return extractionResult;
        }

        private string Download(LANCommander.SDK.Models.Game game)
        {
            string tempFile = String.Empty;

            if (game != null)
            {
                Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
                {
                    progress.ProgressMaxValue = 100;
                    progress.CurrentProgressValue = 0;

                    var destination = Plugin.LANCommander.DownloadGame(game.Id, (changed) =>
                    {
                        progress.CurrentProgressValue = changed.ProgressPercentage;
                    }, (complete) =>
                    {
                        progress.CurrentProgressValue = 100;
                    });

                    // Lock the thread until download is done
                    while (progress.CurrentProgressValue != 100)
                    {

                    }

                    tempFile = destination;
                },
                new GlobalProgressOptions($"Downloading {game.Title}...")
                {
                    IsIndeterminate = false,
                    Cancelable = false,
                });

                return tempFile;
            }
            else
                throw new Exception("Game failed to download!");
        }

        private string Extract(LANCommander.SDK.Models.Game game, string archivePath)
        {
            var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());

            Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
            {
                Directory.CreateDirectory(destination);

                using (var fs = File.OpenRead(archivePath))
                using (var ts = new TrackableStream(fs))
                using (var reader = ReaderFactory.Open(ts))
                {
                    progress.ProgressMaxValue = ts.Length;
                    ts.OnProgress += (pos, len) =>
                    {
                        progress.CurrentProgressValue = pos;
                    };

                    reader.WriteAllToDirectory(destination, new ExtractionOptions()
                    {
                        ExtractFullPath = true,
                        Overwrite = true
                    });
                }
            },
            new GlobalProgressOptions($"Extracting {game.Title}...")
            {
                IsIndeterminate = false,
                Cancelable = false,
            });

            return destination;
        }

        private void WriteManifest(SDK.GameManifest manifest, string installDirectory)
        {
            var destination = Path.Combine(installDirectory, "_manifest.yml");

            Logger.Trace($"Attempting to write manifest to path {destination}");

            var serializer = new SerializerBuilder()
                .WithNamingConvention(new PascalCaseNamingConvention())
                .Build();

            Logger.Trace("Serializing manifest...");
            var yaml = serializer.Serialize(manifest);

            Logger.Trace("Writing manifest file...");
            File.WriteAllText(destination, yaml);
        }

        private string SaveTempScript(LANCommander.SDK.Models.Script script)
        {
            var tempPath = Path.GetTempFileName();

            File.Move(tempPath, tempPath + ".ps1");

            tempPath = tempPath + ".ps1";

            Logger.Trace($"Writing script {script.Name} to {tempPath}");

            File.WriteAllText(tempPath, script.Contents);

            return tempPath;
        }

        private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type)
        {
            var script = game.Scripts.FirstOrDefault(s => s.Type == type);

            if (script == null)
                return;

            if (script.RequiresAdmin)
                script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents;

            var filename = PowerShellRuntime.GetScriptFilePath(PlayniteGame, type);

            if (File.Exists(filename))
                File.Delete(filename);

            Logger.Trace($"Writing {type} script to {filename}");

            File.WriteAllText(filename, script.Contents);
        }
    }
}
