using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using PSLauncher.Core.Lan;
using PSLauncher.Core.Security;
using PSLauncher.Models;

namespace PSLauncher.Core.Manifests;

public sealed class ManifestService : IManifestService
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    private readonly HttpClient _http;
    private readonly Func<string> _serverBaseUrlProvider;
    private readonly Func<string?> _channelProvider;
    private readonly IManifestCache _manifestCache;
    private readonly IPeerManifestFetcher _peerManifestFetcher;
    private readonly ISettingsLockService _settingsLock;
    private readonly ILogger<ManifestService> _logger;

    public ManifestSource LastSource { get; private set; } = ManifestSource.None;

    public ManifestService(
        HttpClient http,
        Func<string> serverBaseUrlProvider,
        Func<string?> channelProvider,
        IManifestCache manifestCache,
        IPeerManifestFetcher peerManifestFetcher,
        ISettingsLockService settingsLock,
        ILogger<ManifestService> logger)
    {
        _http = http;
        _serverBaseUrlProvider = serverBaseUrlProvider;
        _channelProvider = channelProvider;
        _manifestCache = manifestCache;
        _peerManifestFetcher = peerManifestFetcher;
        _settingsLock = settingsLock;
        _logger = logger;
    }

    /// <summary>
    /// No-op depuis v0.28 : le hash settings-lock vient maintenant de la
    /// <c>LicenseValidationResponse</c> (per-license). Voir
    /// <see cref="PSLauncher.Core.Licensing.LicenseService.ValidateAsync"/> et
    /// <see cref="PSLauncher.Core.Licensing.LicenseService.GetCached"/> pour la
    /// propagation effective vers <see cref="ISettingsLockService"/>.
    /// </summary>
    private void PropagateSettingsLock(RemoteManifest? manifest)
    {
        _ = manifest;
        _ = _settingsLock;
    }

    /// <summary>
    /// Timeout court sur la tentative OVH : si le serveur Internet ne répond pas
    /// rapidement, on bascule sur les fallbacks LAN/cache plutôt que d'attendre les
    /// 100s du HttpClient.Timeout par défaut. Avec la race OVH+peer, ce timeout
    /// devient juste une borne haute — en pratique le peer répond en &lt;200ms si
    /// dispo, OVH gagne la course en online normal.
    /// </summary>
    private const int OvhFetchTimeoutMs = 3_000;

    /// <summary>
    /// Stratégie : on probe la connectivité Internet d'abord (~10ms si online).
    /// - Online → OVH seul (source canonique, garantit de voir la version la plus
    ///   récente même si nos peers ont un manifest plus ancien). Fallback cache
    ///   disque si OVH plante.
    /// - Offline → peer LAN seul (pas la peine de payer le timeout OVH puisqu'on
    ///   sait que ça va échouer). Fallback cache disque si pas de peer.
    ///
    /// Le test de connectivité combine ICMP ping (rapide) + TCP connect au serveur
    /// (au cas où le firewall corporate bloque ICMP) pour minimiser les faux négatifs.
    /// </summary>
    public async Task<RemoteManifest> FetchAsync(CancellationToken ct)
    {
        var isOnline = await IsOnlineAsync(ct).ConfigureAwait(false);
        _logger.LogInformation("Fetching manifest (online={Online})", isOnline);

        if (isOnline)
        {
            // En ligne : OVH canonique, jamais de peer (qui pourrait avoir un manifest
            // plus ancien et masquer la dernière version publiée).
            try
            {
                var rawJson = await FetchFromOvhAsync(ct).ConfigureAwait(false);
                var manifest = JsonSerializer.Deserialize<RemoteManifest>(rawJson, JsonOptions)
                               ?? throw new InvalidOperationException("Empty manifest");
                await _manifestCache.SaveAsync(rawJson, ct).ConfigureAwait(false);
                LastSource = ManifestSource.Ovh;
                PropagateSettingsLock(manifest);
                return manifest;
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
            catch (Exception ex)
            {
                _logger.LogWarning("OVH manifest fetch failed despite online connectivity ({Reason}), falling back to disk cache", ex.Message);
                var cached = await _manifestCache.TryLoadAsync(ct).ConfigureAwait(false);
                if (cached is not null)
                {
                    LastSource = ManifestSource.DiskCache;
                    PropagateSettingsLock(cached);
                    return cached;
                }
                throw;
            }
        }

        // Offline : peer LAN direct, pas d'attente du timeout OVH.
        var peerJson = await _peerManifestFetcher.TryFetchRawAsync(ct).ConfigureAwait(false);
        if (peerJson is not null)
        {
            var manifest = JsonSerializer.Deserialize<RemoteManifest>(peerJson, JsonOptions)
                           ?? throw new InvalidOperationException("Empty peer manifest");
            _logger.LogInformation("Manifest récupéré via peer LAN (offline)");
            // Cache aussi le peer manifest pour servir aux autres clients du LAN.
            await _manifestCache.SaveAsync(peerJson, ct).ConfigureAwait(false);
            LastSource = ManifestSource.Peer;
            PropagateSettingsLock(manifest);
            return manifest;
        }

        // Aucun peer atteignable : on tente le cache disque local
        var localCache = await _manifestCache.TryLoadAsync(ct).ConfigureAwait(false);
        if (localCache is not null)
        {
            _logger.LogInformation("Manifest récupéré depuis le cache disque (offline + pas de peer)");
            LastSource = ManifestSource.DiskCache;
            PropagateSettingsLock(localCache);
            return localCache;
        }

        throw new InvalidOperationException("Manifest unavailable: offline, no LAN peer responded, no local cache.");
    }

    private async Task<string> FetchFromOvhAsync(CancellationToken ct)
    {
        // Channel = manifest filter par-license. NULL ou vide = on appelle
        // /manifest tout court (= versions.json default). Sinon /manifest?channel=X
        // (le serveur charge versions-{X}.json avec fallback sur default si fichier absent).
        // Whitelist côté client aussi pour éviter des chars exotiques qui casseraient l'URL.
        var channel = _channelProvider();
        var baseUrl = TrimSlash(_serverBaseUrlProvider()) + "/manifest";
        var url = !string.IsNullOrWhiteSpace(channel) && System.Text.RegularExpressions.Regex.IsMatch(channel!, "^[a-z0-9_-]{1,64}$")
            ? $"{baseUrl}?channel={Uri.EscapeDataString(channel!)}"
            : baseUrl;
        using var ovhCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        ovhCts.CancelAfter(OvhFetchTimeoutMs);
        using var req = new HttpRequestMessage(HttpMethod.Get, url);
        req.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
        {
            NoCache = true,
            NoStore = true,
        };
        req.Headers.Pragma.ParseAdd("no-cache");
        using var resp = await _http.SendAsync(req, ovhCts.Token).ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadAsStringAsync(ovhCts.Token).ConfigureAwait(false);
    }

    /// <summary>
    /// Détection rapide de connectivité Internet : 1) ping ICMP 8.8.8.8 (DNS Google,
    /// 99% reachable depuis n'importe quel LAN), 2) si ping ne marche pas (firewall
    /// corporate qui bloque ICMP), TCP connect direct sur le port HTTPS du serveur OVH.
    /// Online normal : ~10-30ms (premier ping suffit). Offline : ~1.5s pire cas
    /// (les deux probes timeout). Bon compromis vs un OVH HTTP fetch qui prend 3s.
    /// </summary>
    private async Task<bool> IsOnlineAsync(CancellationToken ct)
    {
        // Test 1 : ping ICMP (rapide ~10ms si online, fail-fast si offline)
        try
        {
            using var ping = new Ping();
            var reply = await ping.SendPingAsync(IPAddress.Parse("8.8.8.8"), TimeSpan.FromMilliseconds(800)).ConfigureAwait(false);
            if (reply.Status == IPStatus.Success)
            {
                _logger.LogDebug("Online: ping 8.8.8.8 OK ({Ms} ms)", reply.RoundtripTime);
                return true;
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Ping 8.8.8.8 raised exception (firewall blocks ICMP ?)");
        }

        // Test 2 : TCP connect direct sur le port HTTPS du serveur OVH. Cas du
        // firewall corporate qui bloque ICMP mais laisse passer HTTPS.
        try
        {
            if (Uri.TryCreate(_serverBaseUrlProvider(), UriKind.Absolute, out var serverUri))
            {
                using var tcp = new TcpClient();
                using var tcpCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                tcpCts.CancelAfter(TimeSpan.FromMilliseconds(800));
                await tcp.ConnectAsync(serverUri.Host, serverUri.Port == -1 ? 443 : serverUri.Port, tcpCts.Token).ConfigureAwait(false);
                _logger.LogDebug("Online: TCP connect to {Host}:443 OK", serverUri.Host);
                return true;
            }
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "TCP connect to server failed");
        }

        _logger.LogInformation("Offline detected (ping + TCP both failed)");
        return false;
    }

    public async Task<string> FetchReleaseNotesAsync(string url, CancellationToken ct)
    {
        _logger.LogInformation("Fetching release notes: {Url}", url);
        // Timeout court : les release notes sont du markdown léger, et si OVH ne
        // répond pas, on aime mieux ouvrir le dialog d'install rapidement avec
        // "release notes indisponibles" plutôt que de bloquer la UI 100s.
        using var rnCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        rnCts.CancelAfter(OvhFetchTimeoutMs);
        using var resp = await _http.GetAsync(url, rnCts.Token).ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadAsStringAsync(rnCts.Token).ConfigureAwait(false);
    }

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