using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using NitroxModel.Discovery.Models;
using NitroxModel.Helper;
using NitroxModel.Platforms.OS.Shared;
using NitroxModel.Platforms.OS.Windows;
using NitroxModel.Platforms.Store.Exceptions;
using NitroxModel.Platforms.Store.Interfaces;

namespace NitroxModel.Platforms.Store;

public sealed class Steam : IGamePlatform
{
    public string Name => nameof(Steam);
    public Platform Platform => Platform.STEAM;

    private string SteamProcessName => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "steam_osx" : "steam";

    public bool OwnsGame(string gameRootPath) =>
        gameRootPath switch
        {
            not null when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => Directory.Exists(Path.Combine(gameRootPath, "Plugins", "steam_api.bundle")),
            not null when File.Exists(Path.Combine(gameRootPath, GameInfo.Subnautica.DataFolder, "Plugins", "x86_64", "steam_api64.dll")) => true,
            not null when File.Exists(Path.Combine(gameRootPath, GameInfo.Subnautica.DataFolder, "Plugins", "steam_api64.dll")) => true,
            _ => false
        };

    public async Task<ProcessEx> StartPlatformAsync()
    {
        // If steam is already running, do not start it.
        ProcessEx steam = ProcessEx.GetFirstProcess(SteamProcessName);
        if (steam is not null)
        {
            return steam;
        }

        // Steam is not running, start it.
        string exe = GetExeFile();
        if (exe is null)
        {
            return null;
        }

        string launchExe = exe;
        string launchArgs = "-silent"; // Don't show Steam window
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            launchExe = "/bin/sh";
            launchArgs = $@"-c ""nohup '{exe}' {launchArgs}"" &";
        }
        Stopwatch steamReadyStopwatch = Stopwatch.StartNew();
        Process process = Process.Start(new ProcessStartInfo
        {
            WorkingDirectory = Path.GetDirectoryName(exe) ?? Directory.GetCurrentDirectory(),
            FileName = launchExe,
            WindowStyle = ProcessWindowStyle.Minimized,
            UseShellExecute = true,
            Arguments = launchArgs
        });

        if (process is not { HasExited: false })
        {
            return null;
        }

        steam = new ProcessEx(process);
        // Wait for steam to write to its log file, which indicates it's ready to start games.
        using CancellationTokenSource steamReadyCts = new(TimeSpan.FromSeconds(30));
        try
        {
            DateTime consoleLogFileLastWrite = GetSteamConsoleLogLastWrite(exe);
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                await RegistryEx.CompareWaitAsync<int>(@"SOFTWARE\Valve\Steam\ActiveProcess\ActiveUser",
                                                       v => v > 0,
                                                       steamReadyCts.Token);
            }
            Log.Debug("Waiting for Steam to get ready...");
            while (consoleLogFileLastWrite == GetSteamConsoleLogLastWrite(exe) && !steamReadyCts.IsCancellationRequested)
            {
                await Task.Delay(250, steamReadyCts.Token);
            }
        }
        catch (OperationCanceledException)
        {
            // ignored
        }
        catch (Exception ex)
        {
            Log.Error(ex);
        }
        Log.Debug($"Steam wait result: {(steamReadyCts.IsCancellationRequested ? "timed out" : $"startup successful and took about {steamReadyStopwatch.Elapsed.TotalSeconds}s")}");

        return steam;
    }

    public string GetExeFile()
    {
        string steamExecutable = "";

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            steamExecutable = Path.Combine(RegistryEx.Read(@"SOFTWARE\Valve\Steam\SteamPath", steamExecutable), "steam.exe");
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            steamExecutable = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Steam", "Steam.AppBundle", "Steam", "Contents", "MacOS", "steam_osx");
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            string userHomePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
            if (!Directory.Exists(userHomePath))
            {
                return null;
            }

            string steamPath = Path.Combine(userHomePath, ".steam", "steam");
            // support flatpak
            if (!Directory.Exists(steamPath))
            {
                steamPath = Path.Combine(userHomePath, ".var", "app", "com.valvesoftware.Steam", "data", "Steam");
            }

            steamExecutable = Path.Combine(steamPath, "steam.sh");
        }

        return File.Exists(steamExecutable) ? Path.GetFullPath(steamExecutable) : null;
    }

    public async Task<ProcessEx> StartGameAsync(string pathToGameExe, string launchArguments, int steamAppId)
    {
        try
        {
            using ProcessEx steam = await StartPlatformAsync();
            if (steam == null)
            {
                throw new GamePlatformException(this, "Platform is not running and could not be found.");
            }
        }
        catch (OperationCanceledException ex)
        {
            throw new GamePlatformException(this, "Timeout reached while waiting for platform to start. Try again once platform has finished loading.", ex);
        }

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
#if DEBUG // Needed to start multiple SN instances, but Steam Overlay doesn't work this way so only active for devs
            return ProcessEx.Start(
                pathToGameExe,
                [("SteamGameId", steamAppId.ToString()), ("SteamAppID", steamAppId.ToString()), (NitroxUser.LAUNCHER_PATH_ENV_KEY, NitroxUser.LauncherPath)],
                Path.GetDirectoryName(pathToGameExe),
                launchArguments
            );
#else
            return new ProcessEx(Process.Start(new ProcessStartInfo
            {
                FileName = GetExeFile(),
                Arguments = $"""-applaunch {steamAppId} --nitrox "{NitroxUser.LauncherPath}" {launchArguments}"""
            }));
#endif
        }

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            string steamPath = Path.GetDirectoryName(GetExeFile());
            return StartGameWithProton(steamPath, pathToGameExe, steamAppId, launchArguments);
        }

        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            return ProcessEx.Start(
                pathToGameExe,
                [("SteamGameId", steamAppId.ToString()), ("SteamAppID", steamAppId.ToString()), (NitroxUser.LAUNCHER_PATH_ENV_KEY, NitroxUser.LauncherPath)],
                Path.GetDirectoryName(pathToGameExe),
                launchArguments
            );
        }

        throw new NotSupportedException("Your operating system is not supported by Nitrox");
    }

    private static ProcessEx StartGameWithProton(string steamPath, string pathToGameExe, int steamAppId, string launchArguments)
    {
        // function to get library path for given game id
        static string GetLibraryPath(string steamPath, string gameId)
        {
            string libraryFoldersPath = Path.Combine(steamPath, "config", "libraryfolders.vdf");
            string content = File.ReadAllText(libraryFoldersPath);

            // Regex to match library folder entries
            Regex folderRegex = new(@"""(\d+)""\s*\{[^}]*""path""\s*""([^""]+)""[^}]*""apps""\s*\{([^}]+)\}", RegexOptions.Singleline);
            MatchCollection matches = folderRegex.Matches(content);

            foreach (Match match in matches)
            {
                string path = match.Groups[2].Value;
                string apps = match.Groups[3].Value;

                // Check if the gameId exists in the apps section
                if (Regex.IsMatch(apps, $@"""{gameId}""\s*""[^""]+"""))
                {
                    return path;
                }
            }

            return ""; // Return empty string if not found
        }

        static List<string> GetAllLibraryPaths(string steamPath)
        {
            string libraryFoldersPath = Path.Combine(steamPath, "config", "libraryfolders.vdf");
            string content = File.ReadAllText(libraryFoldersPath);

            // Regex to match library folder entries
            Regex folderRegex = new(@"""(\d+)""\s*\{[^}]*""path""\s*""([^""]+)""", RegexOptions.Singleline);
            MatchCollection matches = folderRegex.Matches(content);

            List<string> libraryPaths = [];
            foreach (Match match in matches)
            {
                string path = match.Groups[2].Value.Replace("\\\\", "\\");
                if (Directory.Exists(Path.Combine(path, "steamapps", "common")))
                {
                    libraryPaths.Add(path);
                }
            }
            // Add the default Steam library path
            string defaultLibraryPath = Path.Combine(steamPath);
            if (!libraryPaths.Contains(defaultLibraryPath))
            {
                libraryPaths.Add(defaultLibraryPath);
            }

            return libraryPaths;
        }

        static string GetProtonVersionFromConfigVdf(string configVdfFile, string appId)
        {
            try
            {
                string fileContent = File.ReadAllText(configVdfFile);
                Match compatToolMatch = Regex.Match(fileContent, @"""CompatToolMapping""\s*{((?:\s*""\d+""[^{]+[^}]+})*)\s*}");

                if (compatToolMatch.Success)
                {
                    string compatToolMapping = compatToolMatch.Groups[1].Value;
                    string appIdPattern = $@"""{appId}""[^{{]*\{{[^}}]*""name""\s*""([^""]+)""";
                    Match appIdMatch = Regex.Match(compatToolMapping, appIdPattern);

                    if (appIdMatch.Success)
                    {
                        return appIdMatch.Groups[1].Value;
                    }
                }

                return null;
            }
            catch (Exception ex)
            {
                Log.Debug(ex);
                return null;
            }
        }

        string compatdataPath = "";
        if (!string.IsNullOrEmpty(pathToGameExe))
        {
            string[] pathComponents = pathToGameExe.Split(Path.DirectorySeparatorChar);
            int steamAppsIndex = pathComponents.GetIndex("steamapps");
            if (steamAppsIndex != -1)
            {
                string steamAppsPath = string.Join(Path.DirectorySeparatorChar.ToString(), pathComponents, 0, steamAppsIndex + 1);
                compatdataPath = Path.Combine(steamAppsPath, "compatdata", steamAppId.ToString());
            }
        }

        string sniperappid = "1628350";
        string sniperruntimepath = Path.Combine(GetLibraryPath(steamPath, sniperappid), "steamapps", "common", "SteamLinuxRuntime_sniper");

        string protonPath = null;
        string protonRoot = Path.Combine(steamPath, "compatibilitytools.d");
        string protonVersion = GetProtonVersionFromConfigVdf(Path.Combine(steamPath, "config", "config.vdf"), steamAppId.ToString()) ?? "proton_9";
        bool isValveProton = protonVersion.StartsWith("proton_", StringComparison.OrdinalIgnoreCase);
        if (isValveProton)
        {
            int index = protonVersion.IndexOf("proton_", StringComparison.OrdinalIgnoreCase);
            if (index != -1)
            {
                protonVersion = protonVersion[(index + "proton_".Length)..];
            }
            if (protonVersion == "experimental")
            {
                protonVersion = "-";
            }

            foreach (string path in GetAllLibraryPaths(steamPath))
            {
                foreach (string dir in Directory.EnumerateDirectories(Path.Combine(path, "steamapps", "common")))
                {
                    if (dir.Contains($"Proton {protonVersion}"))
                    {
                        protonPath = dir;
                        break;
                    }
                }
                if (protonPath != null)
                {
                    break;
                }
            }
        }
        else
        {
            protonPath = Path.Combine(protonRoot, protonVersion);
        }
        if (protonPath == null)
        {
            throw new Exception("Game is not using Proton. Please change game properties in Steam to use the Proton compatibility layer.");
        }

        ProcessStartInfo startInfo = new()
        {
            FileName = Path.Combine(sniperruntimepath, "_v2-entry-point"),
            Arguments = $" --verb=run -- \"{Path.Combine(protonPath, "proton")}\" run \"{pathToGameExe}\" {launchArguments}",
            WorkingDirectory = Path.GetDirectoryName(pathToGameExe) ?? "",
            UseShellExecute = false,
            Environment =
            {
                [NitroxUser.LAUNCHER_PATH_ENV_KEY] = NitroxUser.LauncherPath,
                ["SteamGameId"] = steamAppId.ToString(),
                ["SteamAppID"] = steamAppId.ToString(),
                ["STEAM_COMPAT_APP_ID"] = steamAppId.ToString(),
                ["WINEPREFIX"] = compatdataPath,
                ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = steamPath,
                ["STEAM_COMPAT_DATA_PATH"] = compatdataPath,
            }
        };
        return new ProcessEx(Process.Start(startInfo));
    }

    private static DateTime GetSteamConsoleLogLastWrite(string steamExePath)
    {
        string steamLogsPath = steamExePath switch
        {
            not null when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Steam", "logs"),
            not null when Path.GetDirectoryName(steamExePath) is { } steamPath => Path.Combine(steamPath, "logs"),
            _ => ""
        };
        return File.GetLastWriteTime(Path.Combine(steamLogsPath, "console_log.txt"));
    }
}
