using System.Net.Http.Json;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using NSec.Cryptography;
using PSLauncher.Core.Configuration;
using PSLauncher.Models;

namespace PSLauncher.Core.Licensing;

public sealed class LicenseService : ILicenseService
{
    private const int CachedValidityDays = 7;

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    private readonly HttpClient _http;
    private readonly Func<string> _serverBaseUrlProvider;
    private readonly IConfigStore _configStore;
    private readonly LocalConfig _config;
    private readonly ILogger<LicenseService> _logger;
    private readonly string? _serverPublicKeyHex;

    public LicenseService(
        HttpClient http,
        Func<string> serverBaseUrlProvider,
        IConfigStore configStore,
        LocalConfig config,
        ILogger<LicenseService> logger)
    {
        _http = http;
        _serverBaseUrlProvider = serverBaseUrlProvider;
        _configStore = configStore;
        _config = config;
        _logger = logger;
        _serverPublicKeyHex = TryReadEmbeddedPublicKey();
    }

    public bool HasLicense() => !string.IsNullOrEmpty(_config.License.EncryptedKey);

    public string GetMachineId()
    {
        // Combine MachineGuid + nom utilisateur, hash SHA-256 → ID stable, sans leak du GUID brut
        string raw;
        try
        {
            using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
            raw = (key?.GetValue("MachineGuid") as string) ?? Environment.MachineName;
        }
        catch
        {
            raw = Environment.MachineName;
        }
        var input = $"{raw}|{Environment.UserName}";
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    public async Task<LicenseValidationResponse> ValidateAsync(string licenseKey, CancellationToken ct)
    {
        var url = TrimSlash(_serverBaseUrlProvider()) + "/license/validate";
        var req = new LicenseValidationRequest
        {
            LicenseKey = licenseKey,
            MachineId = GetMachineId(),
            MachineLabel = $"{Environment.MachineName} / {Environment.UserName}",
            LauncherVersion = GetLauncherVersion(),
        };

        _logger.LogInformation("Validating license at {Url} (key {KeyHint}…)", url, licenseKey.Length >= 8 ? licenseKey[..8] : licenseKey);

        using var httpReq = new HttpRequestMessage(HttpMethod.Post, url);
        httpReq.Content = JsonContent.Create(req);
        using var resp = await _http.SendAsync(httpReq, ct).ConfigureAwait(false);

        var bodyText = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
        LicenseValidationResponse? parsed;
        try
        {
            parsed = JsonSerializer.Deserialize<LicenseValidationResponse>(bodyText, JsonOptions);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Réponse serveur invalide : {ex.Message}\n{bodyText}", ex);
        }
        if (parsed is null)
            throw new InvalidOperationException("Réponse serveur vide");

        // Statuts d'erreur applicatifs renvoyés en 4xx avec un body décrivant l'erreur
        if (!resp.IsSuccessStatusCode && string.IsNullOrEmpty(parsed.Status))
        {
            parsed.Status = "invalid";
            parsed.Message ??= $"HTTP {(int)resp.StatusCode}";
        }

        // Vérification de la signature serveur (si on a la clé publique embarquée et une signature)
        if (!string.IsNullOrEmpty(_serverPublicKeyHex) && !string.IsNullOrEmpty(parsed.Signature))
        {
            if (!VerifySignature(parsed, parsed.Signature, _serverPublicKeyHex!))
            {
                _logger.LogError("License response signature INVALID — possible MITM");
                throw new InvalidOperationException("Réponse serveur non authentifiée (signature invalide)");
            }
            _logger.LogDebug("License response signature OK");
        }

        return parsed;
    }

    public LicenseValidationResponse? GetCached()
    {
        if (string.IsNullOrEmpty(_config.License.EncryptedKey)) return null;
        if (_config.License.LastValidationAt is null) return null;

        // Cache offline valide 7 jours après la dernière validation réussie
        var age = DateTime.UtcNow - _config.License.LastValidationAt.Value;
        if (age.TotalDays > CachedValidityDays) return null;

        return new LicenseValidationResponse
        {
            Status = _config.License.CachedEntitlementUntil >= DateTime.UtcNow ? "valid" : "expired",
            OwnerName = _config.License.CachedOwnerName,
            DownloadEntitlementUntil = _config.License.CachedEntitlementUntil,
            ServerTime = _config.License.LastValidationAt,
        };
    }

    public void SaveCached(string licenseKey, LicenseValidationResponse response)
    {
        var encrypted = ProtectKey(licenseKey);
        _config.License.EncryptedKey = encrypted;
        _config.License.LastValidationAt = DateTime.UtcNow;
        _config.License.CachedOwnerName = response.OwnerName;
        _config.License.CachedEntitlementUntil = response.DownloadEntitlementUntil;
        _configStore.Save(_config);
    }

    public void Clear()
    {
        _config.License = new LicenseConfig();
        _configStore.Save(_config);
    }

    public bool CanDownloadVersion(LicenseValidationResponse? license, VersionManifest version)
    {
        if (license is null) return false;
        return license.CanDownload(version);
    }

    public async Task<string?> GetSignedDownloadUrlAsync(string version, CancellationToken ct)
    {
        var key = GetDecryptedKey();
        if (string.IsNullOrEmpty(key)) return null;

        var url = TrimSlash(_serverBaseUrlProvider()) + "/download-url/" + version;
        try
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, url);
            req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key);
            using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
            if (!resp.IsSuccessStatusCode)
            {
                _logger.LogInformation("Signed URL endpoint returned {Status} — falling back to public URL", (int)resp.StatusCode);
                return null;
            }
            var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
            using var doc = JsonDocument.Parse(body);
            return doc.RootElement.TryGetProperty("url", out var u) ? u.GetString() : null;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Signed URL fetch failed, falling back to public URL");
            return null;
        }
    }

    public string? GetDecryptedKey()
    {
        if (string.IsNullOrEmpty(_config.License.EncryptedKey)) return null;
        try { return UnprotectKey(_config.License.EncryptedKey); }
        catch (Exception ex) { _logger.LogWarning(ex, "Failed to decrypt cached license key"); return null; }
    }

    // ----- DPAPI -----

    private static string ProtectKey(string clearKey)
    {
        var data = Encoding.UTF8.GetBytes(clearKey);
        var protectedBytes = ProtectedData.Protect(data, optionalEntropy: null, scope: DataProtectionScope.CurrentUser);
        return Convert.ToBase64String(protectedBytes);
    }

    private static string UnprotectKey(string base64)
    {
        var protectedBytes = Convert.FromBase64String(base64);
        var data = ProtectedData.Unprotect(protectedBytes, optionalEntropy: null, scope: DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(data);
    }

    // ----- Signature -----

    /// <summary>
    /// Encode canonical du payload en JSON sans le champ 'signature', exactement comme côté PHP
    /// (Crypto::canonicalJson : JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).
    /// </summary>
    public static byte[] CanonicalBytesFor(LicenseValidationResponse response)
    {
        // On reconstruit un dictionnaire ordonné comme côté PHP, sans signature
        var dict = new Dictionary<string, object?>
        {
            ["status"]                     = response.Status,
            ["licenseId"]                  = response.LicenseId,
            ["ownerName"]                  = response.OwnerName,
            ["issuedAt"]                   = FormatDateAtom(response.IssuedAt),
            ["downloadEntitlementUntil"]   = FormatDateAtom(response.DownloadEntitlementUntil),
            ["maxMachines"]                = response.MaxMachines,
            ["serverTime"]                 = FormatDateAtom(response.ServerTime),
        };
        var opts = new JsonSerializerOptions
        {
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            DefaultIgnoreCondition = JsonIgnoreCondition.Never,
        };
        return JsonSerializer.SerializeToUtf8Bytes(dict, opts);
    }

    /// <summary>
    /// Format figé "yyyy-MM-ddTHH:mm:ssZ" en UTC, identique à ce que produit côté serveur
    /// PHP `(new DateTimeImmutable($s, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z')`.
    /// </summary>
    private static string? FormatDateAtom(DateTime? d)
    {
        if (d is null) return null;
        return d.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss") + "Z";
    }

    public static bool VerifySignature(LicenseValidationResponse response, string base64Signature, string publicKeyHex)
    {
        try
        {
            var alg = SignatureAlgorithm.Ed25519;
            var pkBytes = Convert.FromHexString(publicKeyHex);
            var pk = PublicKey.Import(alg, pkBytes, KeyBlobFormat.RawPublicKey);
            var sig = Convert.FromBase64String(base64Signature);
            var payload = CanonicalBytesFor(response);
            return alg.Verify(pk, payload, sig);
        }
        catch
        {
            return false;
        }
    }

    // ----- Public key embarquée -----

    private static string? TryReadEmbeddedPublicKey()
    {
        var asm = Assembly.GetExecutingAssembly();
        foreach (var name in asm.GetManifestResourceNames())
        {
            if (name.EndsWith("server-pubkey.txt", StringComparison.OrdinalIgnoreCase))
            {
                using var s = asm.GetManifestResourceStream(name);
                if (s is null) continue;
                using var sr = new StreamReader(s);
                var hex = sr.ReadToEnd().Trim();
                if (hex.StartsWith("#") || string.IsNullOrEmpty(hex)) return null; // placeholder/ commenté
                return hex;
            }
        }
        return null;
    }

    private static string GetLauncherVersion()
    {
        var asm = Assembly.GetExecutingAssembly();
        return asm.GetName().Version?.ToString() ?? "0.0.0";
    }

    private static string TrimSlash(string s) => s.TrimEnd('/');
}
