using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using PSLauncher.Core.Downloads;
using PSLauncher.Core.Installations;

namespace PSLauncher.Core.Lan;

public sealed class ZipCacheStore : IZipCacheStore
{
    // Validation stricte du nom de version pour éviter path traversal.
    // Couvre semver (1.2.3) et variantes (1.2.3-rc1).
    private static readonly Regex VersionRegex = new(@"^[0-9A-Za-z._-]+$", RegexOptions.Compiled);

    private const string ZipPrefix = "proserve-";
    private const string ZipSuffix = ".zip";
    private const string ShaSuffix = ".zip.sha256";

    private readonly IDownloadStateStore _downloadStore;
    private readonly IInstallationRegistry _registry;
    private readonly ILogger<ZipCacheStore> _logger;

    public ZipCacheStore(
        IDownloadStateStore downloadStore,
        IInstallationRegistry registry,
        ILogger<ZipCacheStore> logger)
    {
        _downloadStore = downloadStore;
        _registry = registry;
        _logger = logger;
    }

    public string GetCachedZipPath(string version)
    {
        if (!VersionRegex.IsMatch(version))
            throw new ArgumentException($"Invalid version: {version}", nameof(version));
        return Path.Combine(_downloadStore.GetDownloadsDirectory(), $"{ZipPrefix}{version}{ZipSuffix}");
    }

    private string GetShaPath(string version) =>
        Path.Combine(_downloadStore.GetDownloadsDirectory(), $"{ZipPrefix}{version}{ShaSuffix}");

    public bool TryGetCachedEntry(string version, out CachedZip entry)
    {
        entry = null!;
        if (!VersionRegex.IsMatch(version)) return false;
        var zipPath = GetCachedZipPath(version);
        var shaPath = GetShaPath(version);
        if (!File.Exists(zipPath) || !File.Exists(shaPath)) return false;
        try
        {
            var sha = File.ReadAllText(shaPath, Encoding.ASCII).Trim();
            if (sha.Length != 64) return false; // SHA-256 hex = 64 chars
            var fi = new FileInfo(zipPath);
            // Force la lecture des metadata maintenant — si le fichier disparaît
            // ici (race avec File.Move overwrite=true du DownloadManager), on attrape
            // FileNotFoundException ci-dessous silencieusement. Sans ce Refresh(),
            // FileInfo.Length aurait fait l'I/O lazy et throw plus tard.
            fi.Refresh();
            entry = new CachedZip(version, zipPath, fi.Length, sha, fi.LastWriteTimeUtc);
            return true;
        }
        catch (FileNotFoundException)
        {
            // Race connue et bénigne : un peer client probe /v1/has/{ver} pile au
            // moment où DownloadManager.VerifyAndFinalizeAsync fait File.Move
            // (delete-then-rename sur NTFS). Le client retentera ou tombera sur OVH.
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Cache lookup failed for {Version}", version);
            return false;
        }
    }

    public async Task PromoteAsync(string srcPath, string version, string sha256, CancellationToken ct)
    {
        if (!VersionRegex.IsMatch(version))
            throw new ArgumentException($"Invalid version: {version}", nameof(version));
        if (sha256.Length != 64)
            throw new ArgumentException("SHA-256 hex must be 64 chars", nameof(sha256));

        var dstPath = GetCachedZipPath(version);
        var shaPath = GetShaPath(version);

        await Task.Run(() =>
        {
            ct.ThrowIfCancellationRequested();
            // Si le ZIP est déjà au bon endroit (cas standard post-DownloadManager :
            // VerifyAndFinalizeAsync a déjà fait File.Move(partial, final)), pas besoin
            // de bouger. On écrit juste le sidecar SHA.
            if (!string.Equals(Path.GetFullPath(srcPath), Path.GetFullPath(dstPath), StringComparison.OrdinalIgnoreCase))
            {
                if (File.Exists(dstPath)) File.Delete(dstPath);
                File.Move(srcPath, dstPath);
            }
            File.WriteAllText(shaPath, sha256.ToLowerInvariant(), Encoding.ASCII);
        }, ct).ConfigureAwait(false);

        _logger.LogInformation("Promoted v{Version} to LAN cache (size={Size} bytes)", version, new FileInfo(dstPath).Length);
    }

    public async Task PruneAsync(int keepLatest, CancellationToken ct)
    {
        if (keepLatest < 0) keepLatest = 0;

        await Task.Run(() =>
        {
            try
            {
                // Versions encore installées : à protéger inconditionnellement.
                var installedVersions = new HashSet<string>(
                    _registry.Scan().Select(i => i.Version),
                    StringComparer.OrdinalIgnoreCase);

                var all = ListCached()
                    .OrderByDescending(c => c.ModifiedUtc)
                    .ToList();

                int kept = 0;
                foreach (var entry in all)
                {
                    ct.ThrowIfCancellationRequested();
                    bool isInstalled = installedVersions.Contains(entry.Version);
                    if (kept < keepLatest || isInstalled)
                    {
                        kept++;
                        continue;
                    }

                    try
                    {
                        File.Delete(entry.Path);
                        var shaPath = GetShaPath(entry.Version);
                        if (File.Exists(shaPath)) File.Delete(shaPath);
                        _logger.LogInformation("Pruned cached ZIP v{Version} ({Size} bytes)", entry.Version, entry.SizeBytes);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "Could not prune cached ZIP {Path} (will retry next time)", entry.Path);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Cache pruning failed (non-fatal)");
            }
        }, ct).ConfigureAwait(false);
    }

    public async Task InvalidateAsync(string version, CancellationToken ct)
    {
        if (!VersionRegex.IsMatch(version))
        {
            _logger.LogWarning("InvalidateAsync called with invalid version: {Version}", version);
            return;
        }

        await Task.Run(() =>
        {
            ct.ThrowIfCancellationRequested();
            var zipPath = GetCachedZipPath(version);
            var shaPath = GetShaPath(version);
            bool zipDeleted = false, shaDeleted = false;
            try
            {
                if (File.Exists(zipPath)) { File.Delete(zipPath); zipDeleted = true; }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not delete cached ZIP for v{Version} at {Path} (file lock?)",
                    version, zipPath);
            }
            try
            {
                if (File.Exists(shaPath)) { File.Delete(shaPath); shaDeleted = true; }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not delete sidecar SHA for v{Version} at {Path}",
                    version, shaPath);
            }
            if (zipDeleted || shaDeleted)
            {
                _logger.LogInformation(
                    "Invalidated LAN cache for v{Version} (zip={ZipGone}, sidecar={ShaGone})",
                    version, zipDeleted, shaDeleted);
            }
        }, ct).ConfigureAwait(false);
    }

    public IReadOnlyList<CachedZip> ListCached()
    {
        var dir = _downloadStore.GetDownloadsDirectory();
        if (!Directory.Exists(dir)) return Array.Empty<CachedZip>();

        var result = new List<CachedZip>();
        foreach (var zipPath in Directory.EnumerateFiles(dir, $"{ZipPrefix}*{ZipSuffix}", SearchOption.TopDirectoryOnly))
        {
            // Skip les .partial (ne matchent pas le pattern de toute façon)
            var name = Path.GetFileName(zipPath);
            if (!name.StartsWith(ZipPrefix, StringComparison.Ordinal)) continue;
            if (!name.EndsWith(ZipSuffix, StringComparison.Ordinal)) continue;
            var version = name.Substring(ZipPrefix.Length, name.Length - ZipPrefix.Length - ZipSuffix.Length);
            if (!VersionRegex.IsMatch(version)) continue;
            if (TryGetCachedEntry(version, out var entry))
                result.Add(entry);
        }
        return result;
    }
}
