using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using PSLauncher.Models;

namespace PSLauncher.Core.Installations;

public sealed partial class InstallationRegistry : IInstallationRegistry
{
    /// <summary>
    /// Fichier metadata écrit à l'install par MainViewModel. Contient le nom de l'exe
    /// déclaré dans le manifest pour cette version. Permet à <see cref="Scan"/> de
    /// retrouver le bon .exe à lancer sans avoir le manifest en mémoire (cas du
    /// démarrage offline ou avant le 1er Check Updates).
    /// </summary>
    public const string MetadataFileName = ".proserve-meta.json";

    /// <summary>
    /// Pattern de fallback quand .proserve-meta.json est absent (vieux installs
    /// d'avant l'introduction du metadata). Couvre toutes les versions Unreal
    /// connues (UE_5_5, UE_5_7, etc.). Premier match alphabétique gagne.
    /// </summary>
    private const string FallbackExePattern = "PROSERVE_UE_*.exe";

    // IgnoreCase pour matcher à la fois les anciens dossiers "Proserve vX.Y.Z" et les
    // nouveaux "PROSERVE vX.Y.Z" (changement de casse de la marque).
    [GeneratedRegex(@"^PROSERVE v(?<v>\d+\.\d+\.\d+)$", RegexOptions.IgnoreCase)]
    private static partial Regex VersionFolderRegex();

    private readonly ILogger<InstallationRegistry> _logger;
    private readonly Func<string> _installRootProvider;

    public InstallationRegistry(ILogger<InstallationRegistry> logger, Func<string> installRootProvider)
    {
        _logger = logger;
        _installRootProvider = installRootProvider;
    }

    public IReadOnlyList<InstalledVersion> Scan()
    {
        var root = _installRootProvider();
        if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
        {
            _logger.LogWarning("Install root does not exist: {Root}", root);
            return Array.Empty<InstalledVersion>();
        }

        var results = new List<InstalledVersion>();
        foreach (var dir in Directory.EnumerateDirectories(root))
        {
            var name = Path.GetFileName(dir);
            var match = VersionFolderRegex().Match(name);
            if (!match.Success) continue;

            var exe = ResolveExecutable(dir);
            if (exe is null)
            {
                _logger.LogDebug("Skipped {Dir}: no executable found (no .proserve-meta.json + no fallback match)", dir);
                continue;
            }

            var info = new DirectoryInfo(dir);
            results.Add(new InstalledVersion(
                Version: match.Groups["v"].Value,
                FolderPath: dir,
                ExecutablePath: exe,
                InstalledAt: info.CreationTimeUtc,
                SizeBytes: SafeDirectorySize(dir)));
        }

        return results
            .OrderByDescending(v => SemVer.Parse(v.Version))
            .ToList();
    }

    /// <summary>
    /// Résout le .exe à lancer pour un dossier d'install donné. Stratégie :
    /// 1. Lit <c>.proserve-meta.json</c> (écrit par MainViewModel à l'install,
    ///    contient l'exe déclaré dans le manifest serveur — source canonique).
    /// 2. Fallback glob <c>PROSERVE_UE_*.exe</c> pour les vieux installs sans meta.
    /// 3. Null si aucun match (dossier corrompu ou pas un install PROSERVE).
    /// </summary>
    private string? ResolveExecutable(string installDir)
    {
        // Tentative 1 : metadata écrite par le launcher au moment de l'install
        var metaPath = Path.Combine(installDir, MetadataFileName);
        if (File.Exists(metaPath))
        {
            try
            {
                var raw = File.ReadAllText(metaPath);
                var meta = JsonSerializer.Deserialize<InstallMetadata>(raw);
                if (meta is { Executable: var exeName } && !string.IsNullOrWhiteSpace(exeName))
                {
                    var candidate = Path.Combine(installDir, exeName);
                    if (File.Exists(candidate)) return candidate;
                    _logger.LogWarning("Metadata declares {Exe} but file missing in {Dir} — falling back to glob", exeName, installDir);
                }
            }
            catch (Exception ex)
            {
                _logger.LogDebug(ex, "Failed to parse {Meta} (will fall back to glob)", metaPath);
            }
        }

        // Tentative 2 : glob pattern (couvre vieux installs sans metadata)
        try
        {
            var matches = Directory.GetFiles(installDir, FallbackExePattern, SearchOption.TopDirectoryOnly);
            if (matches.Length > 0)
            {
                Array.Sort(matches, StringComparer.OrdinalIgnoreCase);
                return matches[0];
            }
        }
        catch { /* ignore IO error */ }

        return null;
    }

    /// <summary>
    /// Écrit le fichier metadata <c>.proserve-meta.json</c> dans le dossier d'install
    /// fraîchement extrait. Appelé par <see cref="MainViewModel"/> après le ZipInstaller,
    /// avec l'exe déclaré dans le manifest (<c>VersionManifest.Executable</c>).
    /// </summary>
    public Task WriteInstallMetadataAsync(string installDir, string executableName, CancellationToken ct)
    {
        var path = Path.Combine(installDir, MetadataFileName);
        var meta = new InstallMetadata { Executable = executableName };
        var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true });
        return File.WriteAllTextAsync(path, json, ct);
    }

    /// <summary>
    /// Marque dans la metadata qu'on a installé avec succès les redists Unreal
    /// (contenu de <c>_redist/</c> de la version). Évite de re-runner les UAC
    /// popups si l'utilisateur réinstalle la même version.
    /// </summary>
    public async Task MarkRedistInstalledAsync(string installDir, CancellationToken ct)
    {
        var path = Path.Combine(installDir, MetadataFileName);
        InstallMetadata? meta = null;
        if (File.Exists(path))
        {
            try { meta = JsonSerializer.Deserialize<InstallMetadata>(await File.ReadAllTextAsync(path, ct).ConfigureAwait(false)); }
            catch { meta = null; }
        }
        meta ??= new InstallMetadata();
        meta.RedistInstalledAt = DateTime.UtcNow;
        var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true });
        await File.WriteAllTextAsync(path, json, ct).ConfigureAwait(false);
    }

    /// <summary>
    /// Vrai si l'install dir a déjà eu son <c>_redist/</c> installé une fois (via
    /// metadata). Permet de skip le UAC popup chain à chaque réinstall de la même
    /// version.
    /// </summary>
    public bool IsRedistInstalled(string installDir)
    {
        var path = Path.Combine(installDir, MetadataFileName);
        if (!File.Exists(path)) return false;
        try
        {
            var meta = JsonSerializer.Deserialize<InstallMetadata>(File.ReadAllText(path));
            return meta?.RedistInstalledAt is not null;
        }
        catch { return false; }
    }

    private sealed class InstallMetadata
    {
        public string Executable { get; set; } = string.Empty;
        public DateTime? RedistInstalledAt { get; set; }
    }

    public InstalledVersion? Get(string version) =>
        Scan().FirstOrDefault(v => v.Version == version);

    public bool IsInstalled(string version) => Get(version) is not null;

    public Task DeleteAsync(string version, IProgress<double>? progress, CancellationToken ct)
    {
        var existing = Get(version) ?? throw new InvalidOperationException($"Version {version} not installed");
        return Task.Run(() =>
        {
            var trash = Path.Combine(_installRootProvider(), ".trash",
                $"{Path.GetFileName(existing.FolderPath)}-{DateTime.UtcNow:yyyyMMddHHmmss}");
            Directory.CreateDirectory(Path.GetDirectoryName(trash)!);
            Directory.Move(existing.FolderPath, trash);
            try
            {
                Directory.Delete(trash, recursive: true);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to delete trash {Path}", trash);
            }
            progress?.Report(1.0);
        }, ct);
    }

    private long SafeDirectorySize(string dir)
    {
        try
        {
            return new DirectoryInfo(dir)
                .EnumerateFiles("*", SearchOption.AllDirectories)
                .Sum(f => f.Length);
        }
        catch
        {
            return 0;
        }
    }
}
