using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;

namespace Prospect.Client.Config;

// Cross-platform (Linux/Proton + Windows) helper to point a The Cycle: Frontier client at a
// server. The Prospect agent reads the target PlayFab URL from `backend.txt` next to the game
// executable, so switching servers = writing that one file. This tool also imports the server
// certificate and launches the game (natively on Windows, guided on Linux/Proton).
internal static class Program
{
    private const string AppId = "868270";

    // Presets use the public domains so the same target works for everyone (LAN or internet):
    // DNS resolves to the public IP for friends, and the unified TLS cert covers both hostnames.
    // On the same LAN, add a hosts entry (domain -> 192.168.1.136) to avoid hairpin NAT.
    private static readonly (string Key, string Label, string Url)[] Presets =
    {
        ("prod",    "PROD",    "https://tc.nfteam.ovh:8443"),
        ("preprod", "PREPROD", "https://rd-tc.nfteam.ovh:8444"),
    };

    private static string SettingsPath => Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
        "ProspectSwitcher", "settings.json");

    private static Settings _settings = new();

    private static int Main(string[] args)
    {
        try { Console.OutputEncoding = Encoding.UTF8; } catch { /* some terminals reject it */ }
        LoadSettings();
        return args.Length > 0 ? RunCli(args) : MenuLoop();
    }

    // ---- Scriptable CLI ----------------------------------------------------
    private static int RunCli(string[] args)
    {
        string? folder = null, target = null;
        for (var i = 0; i < args.Length; i++)
        {
            switch (args[i])
            {
                case "--folder" when i + 1 < args.Length: folder = args[++i]; break;
                case "--set" when i + 1 < args.Length: target = args[++i]; break;
                case "-h" or "--help": PrintCliHelp(); return 0;
            }
        }
        if (folder != null) { _settings.GameFolder = folder; SaveSettings(); }
        if (target == null) { PrintCliHelp(); return folder != null ? 0 : 1; }
        var url = ResolveTarget(target);
        if (url == null) { Console.Error.WriteLine("Cible inconnue : " + target); return 1; }
        return WriteBackend(url) ? 0 : 1;
    }

    private static void PrintCliHelp()
    {
        Console.WriteLine("ProspectServerSwitcher — sélecteur de serveur The Cycle (multi-OS)");
        Console.WriteLine();
        Console.WriteLine("  (sans argument)            menu interactif");
        Console.WriteLine("  --folder <chemin Win64>    définit le dossier du jeu (mémorisé)");
        Console.WriteLine("  --set <cible>              écrit backend.txt");
        Console.WriteLine();
        Console.WriteLine("Cibles : " + string.Join(", ", Presets.Select(p => p.Key)) + ", ou une URL https://…");
        Console.WriteLine("Ex : ProspectServerSwitcher --folder \"…/Prospect/Binaries/Win64\" --set preprod-lan");
    }

    private static string? ResolveTarget(string t)
    {
        foreach (var p in Presets)
            if (string.Equals(p.Key, t, StringComparison.OrdinalIgnoreCase)) return p.Url;
        if (t.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return t;
        return null;
    }

    // ---- Interactive menu --------------------------------------------------
    private static int MenuLoop()
    {
        while (true)
        {
            Console.WriteLine();
            Console.WriteLine("=== The Cycle — Sélecteur de serveur ===");
            Console.WriteLine("Dossier du jeu : " + (string.IsNullOrEmpty(_settings.GameFolder) ? "(non défini)" : _settings.GameFolder));
            Console.WriteLine("backend.txt    : " + ReadCurrentBackend());
            Console.WriteLine();
            Console.WriteLine("  1) Définir le dossier du jeu (Win64)");
            for (var i = 0; i < Presets.Length; i++)
                Console.WriteLine($"  {i + 2}) {Presets[i].Label,-24} {Presets[i].Url}");
            var n = Presets.Length + 2;
            Console.WriteLine($"  {n}) Serveur personnalisé…");
            Console.WriteLine($"  {n + 1}) Importer un certificat…");
            Console.WriteLine($"  {n + 2}) Lancer le jeu");
            Console.WriteLine("  0) Quitter");
            Console.Write("> ");

            var raw = Console.ReadLine()?.Trim();
            if (raw is null or "0") return 0;
            if (!int.TryParse(raw, out var choice)) { Console.WriteLine("Choix invalide."); continue; }

            if (choice == 1) SetFolder();
            else if (choice >= 2 && choice < n) WriteBackend(Presets[choice - 2].Url);
            else if (choice == n) SetCustom();
            else if (choice == n + 1) ImportCert();
            else if (choice == n + 2) LaunchGame();
            else Console.WriteLine("Choix invalide.");
        }
    }

    private static void SetFolder()
    {
        Console.Write("Chemin du dossier Win64 : ");
        var p = Console.ReadLine()?.Trim().Trim('"');
        if (string.IsNullOrEmpty(p)) return;
        if (!Directory.Exists(p)) { Console.WriteLine("⚠ Dossier introuvable."); return; }
        _settings.GameFolder = p;
        SaveSettings();
        Console.WriteLine("✔ Dossier mémorisé.");
    }

    private static void SetCustom()
    {
        Console.Write("URL du serveur (ex : https://mon-serveur:8443) : ");
        var u = Console.ReadLine()?.Trim();
        if (!string.IsNullOrWhiteSpace(u)) WriteBackend(u);
    }

    // ---- backend.txt -------------------------------------------------------
    private static bool WriteBackend(string url)
    {
        if (!EnsureFolder(out var dir)) return false;
        try
        {
            // No trailing newline: the agent reads the raw file and requires it to start with https://.
            File.WriteAllText(Path.Combine(dir, "backend.txt"), url, new UTF8Encoding(false));
            Console.WriteLine($"✔ backend.txt → {url}");
            Console.WriteLine("  (relance le jeu : backend.txt n'est lu qu'au démarrage du loader.)");
            return true;
        }
        catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return false; }
    }

    private static string ReadCurrentBackend()
    {
        if (string.IsNullOrEmpty(_settings.GameFolder)) return "(dossier non défini)";
        var f = Path.Combine(_settings.GameFolder, "backend.txt");
        try
        {
            if (!File.Exists(f)) return "(absent)";
            var v = File.ReadAllText(f).Trim();
            var match = Presets.FirstOrDefault(p => p.Url == v);
            return match.Key != null ? $"{v}  [{match.Label.Trim()}]" : v;
        }
        catch { return "(illisible)"; }
    }

    // ---- Certificate -------------------------------------------------------
    private static void ImportCert()
    {
        // Prefer fetching the cert live from the server the client points at (backend.txt), so
        // it's always the current one; fall back to a .crt file.
        X509Certificate2? cert = null;
        var target = ReadBackendUrl();
        if (target != null && TryFetchServerCert(target, out cert))
            Console.WriteLine($"Certificat récupéré depuis {target}");
        else
        {
            Console.Write("Chemin d'un certificat (.crt) : ");
            var p = Console.ReadLine()?.Trim().Trim('"');
            if (string.IsNullOrEmpty(p) || !File.Exists(p)) { Console.WriteLine("⚠ Annulé."); return; }
            try { cert = new X509Certificate2(p); } catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return; }
        }
        if (cert == null) { Console.WriteLine("⚠ Aucun certificat."); return; }
        Console.WriteLine($"  empreinte : {cert.Thumbprint}");

        if (OperatingSystem.IsWindows()) ImportCertWindows(cert);
        else ImportCertProton(cert);
    }

    private static string? ReadBackendUrl()
    {
        if (string.IsNullOrEmpty(_settings.GameFolder)) return null;
        var f = Path.Combine(_settings.GameFolder, "backend.txt");
        try { return File.Exists(f) ? File.ReadAllText(f).Trim() : null; } catch { return null; }
    }

    private static bool TryFetchServerCert(string url, out X509Certificate2? cert)
    {
        cert = null;
        try
        {
            var uri = new Uri(url);
            using var tcp = new System.Net.Sockets.TcpClient();
            tcp.Connect(uri.Host, uri.Port <= 0 ? 443 : uri.Port);
            X509Certificate2? captured = null;
            using var ssl = new System.Net.Security.SslStream(tcp.GetStream(), false,
                (_, c, _, _) => { if (c != null) captured = new X509Certificate2(c.Export(X509ContentType.Cert)); return true; });
            ssl.AuthenticateAsClient(uri.Host);
            cert = captured;
            return cert != null;
        }
        catch { return false; }
    }

    [System.Runtime.Versioning.SupportedOSPlatform("windows")]
    private static void ImportCertWindows(X509Certificate2 cert)
    {
        try
        {
            using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
            store.Open(OpenFlags.ReadWrite);
            store.Add(cert);
            Console.WriteLine("✔ Certificat importé (Autorités racines de confiance, utilisateur).");
        }
        catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); }
    }

    // Trust the cert inside the game's Proton/Wine prefix by writing the serialized cert blob
    // into the prefix Root store via `wine reg import` (the wine `certutil` tool is unreliable
    // under Proton). The prefix is chosen explicitly, so it works even though the client runs
    // as a non-Steam shortcut (whose appid isn't the game's 868270).
    private static void ImportCertProton(X509Certificate2 cert)
    {
        var wine = FindProtonWine();
        if (wine == null) { Console.WriteLine("⚠ Aucun wine Proton trouvé (Steam/Proton installé ?)."); return; }
        var prefix = SelectPrefix();
        if (prefix == null) return;

        // Wine serialized cert blob: SHA1-hash property (id 3) + encoded cert property (id 0x20).
        var der = cert.RawData;
        var sha1 = cert.GetCertHash();
        byte[] blob;
        using (var ms = new MemoryStream())
        {
            void Prop(uint id, byte[] data)
            {
                ms.Write(BitConverter.GetBytes(id));
                ms.Write(BitConverter.GetBytes(1u));
                ms.Write(BitConverter.GetBytes((uint)data.Length));
                ms.Write(data);
            }
            Prop(3, sha1);
            Prop(0x20, der);
            blob = ms.ToArray();
        }
        var thumb = Convert.ToHexString(sha1);
        var hex = string.Join(",", blob.Select(b => b.ToString("x2")));
        var regFile = Path.Combine(Path.GetTempPath(), "prospect-cert.reg");
        File.WriteAllText(regFile,
            "REGEDIT4\r\n\r\n" +
            $"[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates\\{thumb}]\r\n" +
            $"\"Blob\"=hex:{hex}\r\n");

        Console.WriteLine("⚠ Ferme complètement le jeu avant de continuer (sinon le registre ne sera pas pris en compte).");
        Console.Write("Appuie sur Entrée pour importer… ");
        Console.ReadLine();

        var wineserver = Path.Combine(Path.GetDirectoryName(wine)!, "wineserver");
        var regWinePath = "Z:" + regFile.Replace('/', '\\'); // Z: maps to / inside the prefix
        RunWine(wineserver, "-k", prefix);            // stop any running wineserver (flush)
        RunWine(wine, $"reg import \"{regWinePath}\"", prefix);
        RunWine(wineserver, "-k", prefix);            // flush the change to disk

        var sysreg = Path.Combine(prefix, "system.reg");
        var ok = File.Exists(sysreg) && File.ReadAllText(sysreg).Contains(thumb, StringComparison.OrdinalIgnoreCase);
        Console.WriteLine(ok
            ? $"✔ Certificat importé dans le préfixe : {prefix}\n  Relance le jeu."
            : "✖ Import non confirmé — assure-toi que le jeu est fermé et réessaie.");
    }

    private static void RunWine(string exe, string args, string prefix)
    {
        try
        {
            var psi = new ProcessStartInfo(exe, args)
            {
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            };
            psi.Environment["WINEPREFIX"] = prefix;
            psi.Environment["WINEDEBUG"] = "-all";
            using var p = Process.Start(psi);
            p?.WaitForExit(20000);
        }
        catch (Exception ex) { Console.WriteLine("  (wine) " + ex.Message); }
    }

    private static string? FindProtonWine()
    {
        foreach (var root in SteamRoots())
        {
            var common = Path.Combine(root, "steamapps", "common");
            if (!Directory.Exists(common)) continue;
            var candidates = Directory.GetDirectories(common, "Proton*")
                .Select(d => Path.Combine(d, "files", "bin", "wine"))
                .Where(File.Exists)
                .OrderByDescending(w => w.Contains("Experimental"))
                .ThenByDescending(File.GetLastWriteTimeUtc)
                .ToList();
            if (candidates.Count > 0) return candidates[0];
        }
        return null;
    }

    private static string? SelectPrefix()
    {
        if (!string.IsNullOrEmpty(_settings.WinePrefix) && Directory.Exists(_settings.WinePrefix))
            return _settings.WinePrefix;

        var prefixes = new List<string>();
        foreach (var root in SteamRoots())
        {
            var cd = Path.Combine(root, "steamapps", "compatdata");
            if (!Directory.Exists(cd)) continue;
            foreach (var d in Directory.GetDirectories(cd))
            {
                var pfx = Path.Combine(d, "pfx");
                if (File.Exists(Path.Combine(pfx, "system.reg"))) prefixes.Add(Path.GetFullPath(pfx));
            }
        }
        prefixes = prefixes.Distinct()
            .OrderByDescending(p => File.GetLastWriteTimeUtc(Path.Combine(p, "system.reg")))
            .ToList();
        if (prefixes.Count == 0) { Console.WriteLine("⚠ Aucun préfixe Proton trouvé."); return null; }

        Console.WriteLine("Préfixes Proton détectés (le plus récemment utilisé en premier = sûrement ton jeu) :");
        for (var i = 0; i < prefixes.Count; i++)
            Console.WriteLine($"  {i + 1}) {prefixes[i]}");
        Console.Write("Numéro du préfixe (Entrée = 1) : ");
        var raw = Console.ReadLine()?.Trim();
        var idx = string.IsNullOrEmpty(raw) ? 0 : (int.TryParse(raw, out var n) ? n - 1 : -1);
        if (idx < 0 || idx >= prefixes.Count) { Console.WriteLine("⚠ Choix invalide."); return null; }
        _settings.WinePrefix = prefixes[idx];
        SaveSettings();
        return _settings.WinePrefix;
    }

    private static IEnumerable<string> SteamRoots()
    {
        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        yield return Path.Combine(home, ".steam", "steam");
        yield return Path.Combine(home, ".local", "share", "Steam");
    }

    // ---- Launch ------------------------------------------------------------
    private static void LaunchGame()
    {
        if (OperatingSystem.IsWindows())
        {
            if (!EnsureFolder(out var dir)) return;
            var loader = Path.Combine(dir, "Prospect.Client.Loader.exe");
            if (!File.Exists(loader)) { Console.WriteLine("⚠ Prospect.Client.Loader.exe introuvable dans le dossier."); return; }
            try
            {
                Process.Start(new ProcessStartInfo(loader) { WorkingDirectory = dir, UseShellExecute = true });
                Console.WriteLine("▶ Jeu lancé.");
            }
            catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); }
            return;
        }
        Console.WriteLine("Sous Linux/Proton, lance via Steam (le loader doit tourner dans le préfixe Proton) :");
        Console.WriteLine("  steam steam://rungameid/" + AppId);
    }

    // ---- Helpers -----------------------------------------------------------
    private static bool EnsureFolder(out string dir)
    {
        dir = _settings.GameFolder;
        if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
        {
            Console.WriteLine("⚠ Définis d'abord le dossier du jeu (option 1).");
            return false;
        }
        return true;
    }

    private static void LoadSettings()
    {
        try
        {
            if (File.Exists(SettingsPath))
                _settings = JsonSerializer.Deserialize<Settings>(File.ReadAllText(SettingsPath)) ?? new Settings();
        }
        catch { _settings = new Settings(); }
    }

    private static void SaveSettings()
    {
        try
        {
            Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
            File.WriteAllText(SettingsPath, JsonSerializer.Serialize(_settings));
        }
        catch { /* best-effort persistence */ }
    }

    private sealed class Settings
    {
        public string GameFolder { get; set; } = "";
        public string WinePrefix { get; set; } = "";
    }
}
