using System.Diagnostics;
using System.Text.Json;

namespace SirayaPrinter.Sync;

public sealed class SyncEngine
{
    private const string DefaultManifestBase =
        "https://pos.siraya.food/downloads/printer/channels";

    private readonly HttpClient _http = new() { Timeout = TimeSpan.FromMinutes(10) };

    public string GetInstalledVersion()
    {
        var ptr = Path.Combine(PathMapper.InstallRoot, "current-version.txt");
        if (File.Exists(ptr))
            return File.ReadAllText(ptr).Trim();

        var pkg = Path.Combine(PathMapper.InstallRoot, "package.json");
        if (File.Exists(pkg))
        {
            try
            {
                using var doc = JsonDocument.Parse(File.ReadAllText(pkg));
                if (doc.RootElement.TryGetProperty("version", out var v))
                    return v.GetString() ?? "unknown";
            }
            catch { }
        }
        return "unknown";
    }

    public string ManifestUrl(string channel) =>
        $"{DefaultManifestBase}/{channel}/manifest.json";

    public async Task<SyncManifest> FetchManifestAsync(string channel, CancellationToken ct) =>
        await ManifestLoader.FetchAsync(_http, ManifestUrl(channel), ct);

    public List<ManifestFile> GetChangedFiles(SyncManifest manifest)
    {
        var changed = new List<ManifestFile>();
        foreach (var file in manifest.Files)
        {
            var local = PathMapper.ToInstallPath(file.Path);
            if (!HashUtil.Matches(local, file.Sha256))
                changed.Add(file);
        }
        return changed;
    }

    public async Task CheckAsync(string channel, CancellationToken ct)
    {
        if (!Directory.Exists(PathMapper.InstallRoot))
            throw new InvalidOperationException(
                $"Not installed at {PathMapper.InstallRoot}. Run Install.cmd first.");

        var installed = GetInstalledVersion();
        var manifest = await FetchManifestAsync(channel, ct);
        var changed = GetChangedFiles(manifest);

        Console.WriteLine($"Installed:  {installed}");
        Console.WriteLine($"Remote:     {manifest.Version} ({manifest.Channel})");
        Console.WriteLine($"Released:   {manifest.ReleasedAt}");
        Console.WriteLine($"Changes:    {changed.Count} file(s) of {manifest.Files.Count}");

        if (changed.Count == 0)
        {
            Console.WriteLine("Up to date.");
            return;
        }

        Console.WriteLine("Files to sync:");
        foreach (var f in changed.Take(20))
            Console.WriteLine($"  {f.Path} ({f.Size} bytes)");
        if (changed.Count > 20)
            Console.WriteLine($"  ... and {changed.Count - 20} more");
    }

    public async Task ApplyAsync(string channel, bool openBrowser, CancellationToken ct)
    {
        if (!Directory.Exists(PathMapper.InstallRoot))
            throw new InvalidOperationException(
                $"Not installed at {PathMapper.InstallRoot}. Run Install.cmd first.");

        var installed = GetInstalledVersion();
        var manifest = await FetchManifestAsync(channel, ct);
        var changed = GetChangedFiles(manifest);

        if (changed.Count == 0)
        {
            Console.WriteLine("Already up to date.");
            if (openBrowser) await OpenSetupUiAsync(ct);
            return;
        }

        Console.WriteLine($"Syncing {changed.Count} file(s) → v{manifest.Version} ({channel})");

        var snapshotDir = SnapshotCurrentInstall(installed);
        Console.WriteLine($"Snapshot:   {snapshotDir}");

        if (Directory.Exists(PathMapper.StageDir))
            Directory.Delete(PathMapper.StageDir, true);
        Directory.CreateDirectory(PathMapper.StageDir);

        try
        {
            foreach (var file in changed)
            {
                var url = $"{manifest.BaseUrl.TrimEnd('/')}/{file.Path.Replace('\\', '/')}";
                var stagePath = PathMapper.ToStagePath(file.Path);
                Directory.CreateDirectory(Path.DirectoryName(stagePath)!);
                Console.WriteLine($"Download  {file.Path}");
                await DownloadFileAsync(url, stagePath, ct);
                if (!HashUtil.Matches(stagePath, file.Sha256))
                    throw new InvalidOperationException($"Hash mismatch after download: {file.Path}");
            }

            ServiceControl.Stop();

            foreach (var file in changed)
            {
                var stagePath = PathMapper.ToStagePath(file.Path);
                var installPath = PathMapper.ToInstallPath(file.Path);
                Directory.CreateDirectory(Path.DirectoryName(installPath)!);
                File.Copy(stagePath, installPath, overwrite: true);
            }

            if (changed.Any(f => f.Path.Replace('\\', '/').EndsWith("package-lock.json", StringComparison.OrdinalIgnoreCase)))
            {
                Console.WriteLine("package-lock.json changed — running npm ci...");
                RunNpmCi();
            }

            File.WriteAllText(
                Path.Combine(PathMapper.InstallRoot, "current-version.txt"),
                manifest.Version);

            ServiceControl.Start();

            var healthy = await HealthChecker.WaitForHealthAsync(90, ct);
            if (!healthy)
                throw new InvalidOperationException(
                    "Service started but health check failed on ports 5000-5010. Rolled back.");

            Console.WriteLine($"Updated {installed} → {manifest.Version}");
            if (openBrowser) await OpenSetupUiAsync(ct);
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Update failed: {ex.Message}");
            Console.WriteLine("Rolling back...");
            RestoreSnapshot(snapshotDir);
            try { ServiceControl.Start(); } catch { }
            throw;
        }
        finally
        {
            if (Directory.Exists(PathMapper.StageDir))
                Directory.Delete(PathMapper.StageDir, true);
        }
    }

    private async Task DownloadFileAsync(string url, string dest, CancellationToken ct)
    {
        using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
        response.EnsureSuccessStatusCode();
        await using var stream = await response.Content.ReadAsStreamAsync(ct);
        await using var file = File.Create(dest);
        await stream.CopyToAsync(file, ct);
    }

    private static string SnapshotCurrentInstall(string version)
    {
        var safeVersion = string.IsNullOrWhiteSpace(version) ? "unknown" : version;
        var dest = Path.Combine(PathMapper.VersionsDir, safeVersion);
        if (Directory.Exists(dest))
            Directory.Delete(dest, true);
        Directory.CreateDirectory(dest);

        CopyDirectory(PathMapper.InstallRoot, dest, skipNames: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "versions", "daemon", "node_modules",
        });
        return dest;
    }

    private static void RestoreSnapshot(string snapshotDir)
    {
        if (!Directory.Exists(snapshotDir))
            throw new InvalidOperationException($"Snapshot missing: {snapshotDir}");

        foreach (var entry in Directory.GetFileSystemEntries(PathMapper.InstallRoot))
        {
            var name = Path.GetFileName(entry);
            if (name.Equals("versions", StringComparison.OrdinalIgnoreCase))
                continue;
            if (Directory.Exists(entry))
                Directory.Delete(entry, true);
            else
                File.Delete(entry);
        }

        CopyDirectory(snapshotDir, PathMapper.InstallRoot, skipNames: new HashSet<string>(StringComparer.OrdinalIgnoreCase));
    }

    private static void CopyDirectory(string src, string dest, HashSet<string>? skipNames = null)
    {
        Directory.CreateDirectory(dest);
        foreach (var entry in Directory.GetFileSystemEntries(src))
        {
            var name = Path.GetFileName(entry);
            if (skipNames != null && skipNames.Contains(name))
                continue;
            var target = Path.Combine(dest, name);
            if (Directory.Exists(entry))
                CopyDirectory(entry, target, skipNames);
            else
                File.Copy(entry, target, overwrite: true);
        }
    }

    private static void RunNpmCi()
    {
        var npm = Path.Combine(PathMapper.InstallRoot, "runtime", "npm.cmd");
        if (!File.Exists(npm))
            throw new InvalidOperationException($"npm.cmd not found at {npm}");

        var psi = new ProcessStartInfo
        {
            FileName = npm,
            Arguments = "ci --omit=dev",
            WorkingDirectory = PathMapper.InstallRoot,
            UseShellExecute = false,
            CreateNoWindow = true,
        };
        using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Could not start npm");
        proc.WaitForExit();
        if (proc.ExitCode != 0)
            throw new InvalidOperationException($"npm ci failed with exit code {proc.ExitCode}");
    }

    private static async Task OpenSetupUiAsync(CancellationToken ct)
    {
        var port = await HealthChecker.FindHealthyPortAsync(ct);
        if (port == null) return;
        var url = $"http://127.0.0.1:{port}/";
        Console.WriteLine($"Opening {url}");
        Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
    }
}
