using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace Электронная_Фармация.Classes
{
    /// <summary>
    /// Downloads a Gitea release ZIP and applies it via an external batch updater
    /// so the running exe can be replaced. Preserves local databases and Logs.
    /// </summary>
    public static class AutoUpdater
    {
        public static async Task ApplyUpdateAsync(
            UpdateCheckResult update,
            IProgress<string> status = null)
        {
            if (update == null)
            {
                throw new ArgumentNullException(nameof(update));
            }

            var downloadUrl = !string.IsNullOrWhiteSpace(update.DownloadUrl)
                ? update.DownloadUrl
                : update.ReleaseUrl;
            if (string.IsNullOrWhiteSpace(downloadUrl) ||
                !downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
            {
                if (!string.IsNullOrWhiteSpace(downloadUrl) &&
                    !downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
                {
                    throw new InvalidOperationException(
                        "В релизе нет ZIP-сборки. Прикрепите ElectronicPharmacy-vX.Y.Z.zip к Release.");
                }

                throw new InvalidOperationException("Нет ссылки на ZIP обновления.");
            }

            var installDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(
                Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            var currentExe = Process.GetCurrentProcess().MainModule?.FileName;
            if (string.IsNullOrWhiteSpace(currentExe) || !File.Exists(currentExe))
            {
                currentExe = Directory.GetFiles(installDir, "*.exe")
                    .FirstOrDefault(p => !p.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase));
            }

            if (string.IsNullOrWhiteSpace(currentExe))
            {
                throw new InvalidOperationException("Не найден текущий exe.");
            }

            var workRoot = Path.Combine(Path.GetTempPath(), "elfisa-update-" + Guid.NewGuid().ToString("N"));
            var zipPath = Path.Combine(workRoot, "update.zip");
            var extractDir = Path.Combine(workRoot, "extract");
            Directory.CreateDirectory(extractDir);

            try
            {
                Report(status, "Скачивание обновления...");
                await DownloadAsync(downloadUrl, zipPath).ConfigureAwait(false);

                Report(status, "Распаковка...");
                ZipFile.ExtractToDirectory(zipPath, extractDir);

                var payloadDir = FindPayloadDirectory(extractDir);
                if (payloadDir == null)
                {
                    throw new InvalidOperationException("В архиве не найден exe приложения.");
                }

                // Переносим payload в путь без спецсимволов: имя корневой папки в архиве
                // бывает в чужой кодировке, и такой путь ломает аргумент SRC у robocopy
                // (особенно при запуске под elevated cmd с OEM-кодовой страницей).
                var asciiPayload = Path.Combine(workRoot, "payload");
                try
                {
                    if (Directory.Exists(asciiPayload))
                    {
                        Directory.Delete(asciiPayload, true);
                    }
                    Directory.Move(payloadDir, asciiPayload);
                    payloadDir = asciiPayload;
                }
                catch
                {
                    // Если перенос не удался — работаем с исходным путём.
                }

                var needsElevation = !IsDirectoryWritable(installDir);
                var updaterBat = Path.Combine(workRoot, "apply-update.cmd");
                WriteUpdaterScript(updaterBat);

                Report(status, needsElevation
                    ? "Нужны права администратора для установки в Program Files..."
                    : "Подготовка перезапуска...");

                var batchArgs = Quote(payloadDir) + " " + Quote(installDir) + " " +
                                Quote(Path.GetFileName(currentExe)) + " " +
                                Process.GetCurrentProcess().Id;

                var psi = new ProcessStartInfo
                {
                    UseShellExecute = true,
                    WorkingDirectory = workRoot,
                    WindowStyle = needsElevation ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
                    CreateNoWindow = !needsElevation
                };
                if (needsElevation)
                {
                    // ВАЖНО: повышаем права через cmd.exe (PE-исполняемый), а НЕ через
                    // .cmd-файл напрямую. ShellExecute с verb=runas на .cmd срабатывает не
                    // на всех системах (у batch может не быть runas-ассоциации), из-за чего
                    // elevated-процесс молча не запускается и обновление не применяется.
                    psi.FileName = "cmd.exe";
                    psi.Arguments = "/c \"\"" + updaterBat + "\" " + batchArgs + "\"";
                    psi.Verb = "runas";
                }
                else
                {
                    psi.FileName = updaterBat;
                    psi.Arguments = batchArgs;
                }

                try
                {
                    Process.Start(psi);
                }
                catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
                {
                    // ERROR_CANCELLED — пользователь отклонил UAC
                    throw new InvalidOperationException(
                        "Обновление отменено: нужны права администратора " +
                        "(программа установлена в Program Files).",
                        ex);
                }

                AppDebugLog.Info("AutoUpdater",
                    $"Updater started. elevate={needsElevation}, payload={payloadDir}, install={installDir}, ver={update.LatestVersion}");
            }
            catch
            {
                try
                {
                    if (Directory.Exists(workRoot))
                    {
                        Directory.Delete(workRoot, true);
                    }
                }
                catch
                {
                    // ignore cleanup errors
                }

                throw;
            }
        }

        private static bool IsDirectoryWritable(string directory)
        {
            try
            {
                var probe = Path.Combine(directory, ".elfisa-write-" + Guid.NewGuid().ToString("N"));
                File.WriteAllText(probe, "ok");
                File.Delete(probe);
                return true;
            }
            catch
            {
                return false;
            }
        }

        private static async Task DownloadAsync(string url, string zipPath)
        {
            using (var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) })
            {
                var token = AppConfig.UpdateApiToken;
                if (!string.IsNullOrWhiteSpace(token))
                {
                    client.DefaultRequestHeaders.Authorization =
                        new AuthenticationHeaderValue("token", token.Trim());
                }

                using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)
                           .ConfigureAwait(false))
                {
                    response.EnsureSuccessStatusCode();
                    using (var fs = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None))
                    {
                        await response.Content.CopyToAsync(fs).ConfigureAwait(false);
                    }
                }
            }
        }

        private static string FindPayloadDirectory(string extractRoot)
        {
            // ZIP may contain a single root folder (Compress-Archive of a directory).
            var exe = Directory.GetFiles(extractRoot, "*.exe", SearchOption.AllDirectories)
                .FirstOrDefault(p =>
                {
                    var name = Path.GetFileName(p) ?? string.Empty;
                    return !name.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase) &&
                           !name.Equals("UpdateCheckSmoke.exe", StringComparison.OrdinalIgnoreCase) &&
                           !name.StartsWith("unins", StringComparison.OrdinalIgnoreCase);
                });

            return exe == null ? null : Path.GetDirectoryName(exe);
        }

        private static void WriteUpdaterScript(string path)
        {
            // Waits for app exit, force-kills leftovers, renames locked exe,
            // copies with robocopy, verifies, restarts. Log: %TEMP%\elfisa-updater.log
            var sb = new StringBuilder();
            sb.AppendLine("@echo off");
            sb.AppendLine("setlocal EnableDelayedExpansion");
            sb.AppendLine("set \"SRC=%~1\"");
            sb.AppendLine("set \"DST=%~2\"");
            sb.AppendLine("set \"EXENAME=%~3\"");
            sb.AppendLine("set \"PID=%~4\"");
            sb.AppendLine("set \"LOG=%TEMP%\\elfisa-updater.log\"");
            sb.AppendLine("echo ===== %DATE% %TIME% ===== > \"%LOG%\"");
            sb.AppendLine("echo SRC=%SRC%>> \"%LOG%\"");
            sb.AppendLine("echo DST=%DST%>> \"%LOG%\"");
            sb.AppendLine("echo EXE=%EXENAME% PID=%PID%>> \"%LOG%\"");
            sb.AppendLine("set \"N=0\"");
            sb.AppendLine("echo Waiting for PID %PID% ... >> \"%LOG%\"");
            sb.AppendLine(":waitloop");
            sb.AppendLine("tasklist /FI \"PID eq %PID%\" 2>nul | findstr /C:\" %PID% \" >nul");
            sb.AppendLine("if errorlevel 1 goto donewait");
            sb.AppendLine("set /a N+=1");
            sb.AppendLine("if !N! GEQ 40 (");
            sb.AppendLine("  echo force kill PID %PID% >> \"%LOG%\"");
            sb.AppendLine("  taskkill /PID %PID% /F >nul 2>&1");
            sb.AppendLine("  goto donewait");
            sb.AppendLine(")");
            sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
            sb.AppendLine("goto waitloop");
            sb.AppendLine(":donewait");
            sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
            // Kill any leftover instances by image name (locks Program Files exe).
            sb.AppendLine("echo taskkill by name >> \"%LOG%\"");
            sb.AppendLine("taskkill /F /IM \"%EXENAME%\" >nul 2>&1");
            sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
            // Rename locked/old exe so copy can proceed even if handle lingered.
            sb.AppendLine("if exist \"%DST%\\%EXENAME%.old\" del /F /Q \"%DST%\\%EXENAME%.old\" >nul 2>&1");
            sb.AppendLine("if exist \"%DST%\\%EXENAME%\" (");
            sb.AppendLine("  move /Y \"%DST%\\%EXENAME%\" \"%DST%\\%EXENAME%.old\" >> \"%LOG%\" 2>&1");
            sb.AppendLine(")");
            sb.AppendLine("echo Copying update... >> \"%LOG%\"");
            // /IS /IT — force overwrite even if timestamps look the same
            sb.AppendLine("robocopy \"%SRC%\" \"%DST%\" /E /IS /IT /R:5 /W:1 /NFL /NDL /NJH /NJS /XF efClient.db *.db /XD Logs");
            sb.AppendLine("set \"RC=!ERRORLEVEL!\"");
            sb.AppendLine("echo robocopy=!RC! >> \"%LOG%\"");
            sb.AppendLine("if not exist \"%DST%\\%EXENAME%\" (");
            sb.AppendLine("  echo COPY FAILED — restoring old exe >> \"%LOG%\"");
            sb.AppendLine("  if exist \"%DST%\\%EXENAME%.old\" move /Y \"%DST%\\%EXENAME%.old\" \"%DST%\\%EXENAME%\" >nul 2>&1");
            sb.AppendLine("  echo Не удалось заменить файлы программы. См. %TEMP%\\elfisa-updater.log");
            sb.AppendLine("  pause");
            sb.AppendLine("  exit /b 1");
            sb.AppendLine(")");
            sb.AppendLine("if exist \"%DST%\\%EXENAME%.old\" del /F /Q \"%DST%\\%EXENAME%.old\" >nul 2>&1");
            sb.AppendLine("echo Starting \"%DST%\\%EXENAME%\" >> \"%LOG%\"");
            sb.AppendLine("start \"\" /D \"%DST%\" \"%DST%\\%EXENAME%\"");
            sb.AppendLine("exit /b 0");
            File.WriteAllText(path, sb.ToString(), Encoding.Default);
        }

        private static string Quote(string value)
        {
            return "\"" + (value ?? string.Empty).Replace("\"", string.Empty) + "\"";
        }

        private static void Report(IProgress<string> status, string message)
        {
            AppDebugLog.Info("AutoUpdater", message);
            status?.Report(message);
        }
    }
}
