using System.Diagnostics;

namespace PSLauncher.Updater;

/// <summary>
/// Mini exe qui remplace le launcher principal (qui ne peut pas s'auto-écraser pendant qu'il tourne).
///
/// Args :
///   --target=&lt;chemin_complet_de_PSLauncher.exe&gt;
///   --source=&lt;chemin_du_nouveau_PSLauncher.exe&gt;
///   [--launch] : relance PSLauncher.exe une fois la copie faite
///   [--pid=&lt;PID&gt;] : attend que ce process se termine avant de copier
///
/// Logs : %LocalAppData%/PSLauncher/logs/updater.log (rotated, last 5 runs).
/// </summary>
internal static class Program
{
    private static string _logPath = "";

    [STAThread]
    private static int Main(string[] args)
    {
        InitLog();
        try
        {
            Log("==== PSLauncher.Updater started ====");
            Log("args: " + string.Join(' ', args));

            var opts = ParseArgs(args);
            if (string.IsNullOrEmpty(opts.Target) || string.IsNullOrEmpty(opts.Source))
            {
                Log("ERROR missing --target/--source");
                return 2;
            }
            Log($"target = {opts.Target}");
            Log($"source = {opts.Source}");
            Log($"pid    = {opts.Pid}");
            Log($"launch = {opts.Launch}");

            // 1. Wait for the main launcher to exit
            if (opts.Pid is int pid)
            {
                try
                {
                    var proc = Process.GetProcessById(pid);
                    Log($"Waiting for PID {pid} to exit...");
                    proc.WaitForExit(60_000);
                    Log("PID exited.");
                }
                catch (Exception ex) { Log($"PID lookup: {ex.Message} (already dead, continuing)"); }
            }

            // 2. Wait for target file to be unlocked
            var deadline = DateTime.UtcNow.AddSeconds(60);
            while (DateTime.UtcNow < deadline)
            {
                if (!File.Exists(opts.Target)) break;
                try
                {
                    using var fs = File.Open(opts.Target, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                    break;
                }
                catch (IOException) { Thread.Sleep(250); }
            }
            Log("target unlocked.");

            // 3. Backup
            var backup = opts.Target + ".bak";
            try { if (File.Exists(backup)) File.Delete(backup); } catch (Exception ex) { Log($"backup pre-clean: {ex.Message}"); }
            try
            {
                if (File.Exists(opts.Target))
                {
                    File.Move(opts.Target, backup);
                    Log($"backup OK -> {backup}");
                }
            }
            catch (Exception ex)
            {
                Log($"BACKUP FAILED: {ex.Message}");
                return 3;
            }

            // 4. Copy new file
            try
            {
                File.Copy(opts.Source, opts.Target, overwrite: true);
                Log("copy OK.");
            }
            catch (Exception ex)
            {
                Log($"COPY FAILED: {ex.Message}");
                try { if (File.Exists(backup)) File.Move(backup, opts.Target, overwrite: true); } catch { }
                return 4;
            }

            // 4b. Strip Mark-of-the-Web (zone identifier) qui peut faire silencieusement bloquer
            // un exe téléchargé d'internet quand on essaie de le lancer via explorer.exe.
            try
            {
                File.Delete(opts.Target + ":Zone.Identifier");
                Log("zone identifier stripped (or absent).");
            }
            catch (Exception ex) { Log($"zone strip: {ex.Message}"); }

            // 5. Relance — on essaie d'abord explorer.exe pour dé-élever, fallback sur
            // Process.Start direct (qui peut hériter de l'admin si on était en runas).
            if (opts.Launch)
            {
                if (!TryRelaunchViaExplorer(opts.Target))
                {
                    Log("explorer trick failed — fallback to direct Process.Start");
                    try
                    {
                        Process.Start(new ProcessStartInfo
                        {
                            FileName = opts.Target,
                            UseShellExecute = true,
                            WorkingDirectory = Path.GetDirectoryName(opts.Target)!,
                        });
                        Log("direct relaunch OK.");
                    }
                    catch (Exception ex) { Log($"DIRECT RELAUNCH FAILED: {ex.Message}"); }
                }
            }

            // 6. Cleanup
            try { File.Delete(opts.Source); Log("cleaned up source .new"); } catch { }
            Log("==== Updater done ====");
            return 0;
        }
        catch (Exception ex)
        {
            Log("FATAL: " + ex);
            return 1;
        }
    }

    /// <summary>
    /// Lance l'exe via explorer.exe pour dé-élever : explorer tourne dans la session
    /// utilisateur normale, donc le child est non-admin. Retourne false si on n'a pas
    /// pu spawn explorer.exe du tout.
    /// </summary>
    private static bool TryRelaunchViaExplorer(string targetExe)
    {
        try
        {
            var psi = new ProcessStartInfo
            {
                FileName = "explorer.exe",
                Arguments = "\"" + targetExe + "\"",
                UseShellExecute = false,
                CreateNoWindow = true,
            };
            var p = Process.Start(psi);
            Log($"explorer.exe spawned (PID {p?.Id})");
            return p is not null;
        }
        catch (Exception ex)
        {
            Log($"explorer.exe spawn failed: {ex.Message}");
            return false;
        }
    }

    private static void InitLog()
    {
        try
        {
            var dir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "PSLauncher", "logs");
            Directory.CreateDirectory(dir);
            _logPath = Path.Combine(dir, "updater.log");
            // simple rotation : si > 256 KiB on archive
            try
            {
                var fi = new FileInfo(_logPath);
                if (fi.Exists && fi.Length > 256 * 1024)
                {
                    var archive = _logPath + "." + DateTime.UtcNow.ToString("yyyyMMddHHmmss") + ".log";
                    File.Move(_logPath, archive);
                }
            }
            catch { }
        }
        catch { _logPath = ""; }
    }

    private static void Log(string line)
    {
        if (string.IsNullOrEmpty(_logPath)) return;
        try
        {
            File.AppendAllText(_logPath,
                DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff") + "  " + line + Environment.NewLine);
        }
        catch { }
    }

    private sealed class Options
    {
        public string? Target { get; set; }
        public string? Source { get; set; }
        public bool Launch { get; set; }
        public int? Pid { get; set; }
    }

    private static Options ParseArgs(string[] args)
    {
        var o = new Options();
        foreach (var a in args)
        {
            // Strip surrounding quotes that come from the launcher's quoted arguments
            var arg = a;
            if (arg.StartsWith("--target=", StringComparison.OrdinalIgnoreCase))
                o.Target = StripQuotes(arg["--target=".Length..]);
            else if (arg.StartsWith("--source=", StringComparison.OrdinalIgnoreCase))
                o.Source = StripQuotes(arg["--source=".Length..]);
            else if (arg.StartsWith("--pid=", StringComparison.OrdinalIgnoreCase))
                o.Pid = int.TryParse(arg["--pid=".Length..], out var p) ? p : null;
            else if (arg.Equals("--launch", StringComparison.OrdinalIgnoreCase))
                o.Launch = true;
        }
        return o;
    }

    private static string StripQuotes(string s)
    {
        if (s.Length >= 2 && s[0] == '"' && s[^1] == '"')
            return s.Substring(1, s.Length - 2);
        return s;
    }
}
