using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using PSLauncher.Models;

namespace PSLauncher.Core.Lan;

public sealed class PeerSourceResolver : IPeerSourceResolver
{
    private const int MaxPeersToProbe = 5;

    private readonly Func<LanCacheConfig> _configProvider;
    private readonly ILanDiscoveryService _discovery;
    private readonly HttpClient _http;
    private readonly ILogger<PeerSourceResolver> _logger;

    public PeerSourceResolver(
        Func<LanCacheConfig> configProvider,
        ILanDiscoveryService discovery,
        HttpClient http,
        ILogger<PeerSourceResolver> logger)
    {
        _configProvider = configProvider;
        _discovery = discovery;
        _http = http;
        _logger = logger;
    }

    public async Task<PeerSource?> TryResolveAsync(
        string version,
        long expectedSize,
        string expectedSha256,
        CancellationToken ct)
    {
        var cfg = _configProvider();
        if (!cfg.ClientEnabled)
        {
            return null;
        }

        // Construit la liste fusionnée : actifs (beacons récents) + connus (cache
        // disque) + manuels. PeerCandidates dédoublonne et préserve l'ordre.
        var candidates = PeerCandidates.Build(_discovery, cfg.ManualPeerUrls);

        if (candidates.Count == 0)
        {
            _logger.LogDebug("No peers configured/discovered, skipping LAN probe");
            return null;
        }

        var probeMs = cfg.ProbeTimeoutMs > 0 ? cfg.ProbeTimeoutMs : 1000;
        foreach (var baseUrl in candidates.Take(MaxPeersToProbe))
        {
            ct.ThrowIfCancellationRequested();
            try
            {
                using var probeCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                probeCts.CancelAfter(probeMs);

                var hasUrl = $"{baseUrl}/v1/has/{version}";
                using var resp = await _http.GetAsync(hasUrl, probeCts.Token).ConfigureAwait(false);
                if (!resp.IsSuccessStatusCode)
                {
                    _logger.LogDebug("Peer {Url} returned {Status} for v{Version}", baseUrl, (int)resp.StatusCode, version);
                    continue;
                }
                var info = await resp.Content.ReadFromJsonAsync<HasResponse>(probeCts.Token).ConfigureAwait(false);
                if (info is null) continue;

                // Vérif size + sha256 contre le manifest signé Ed25519.
                // Si un peer ment, son ZIP sera rejeté à la vérif SHA finale par
                // DownloadManager — mais on peut filtrer dès maintenant pour ne
                // pas perdre 14 Go de DL inutile.
                if (info.Size != expectedSize)
                {
                    _logger.LogWarning("Peer {Url} reports size {Size} for v{Version}, expected {Expected} — skipping",
                        baseUrl, info.Size, version, expectedSize);
                    continue;
                }
                if (!string.Equals(info.Sha256, expectedSha256, StringComparison.OrdinalIgnoreCase))
                {
                    _logger.LogWarning("Peer {Url} reports SHA mismatch for v{Version} — skipping", baseUrl, version);
                    continue;
                }

                var zipUrl = new Uri($"{baseUrl}/v1/zip/{version}");
                var host = new Uri(baseUrl).Host;
                _logger.LogInformation("Peer match for v{Version} : {Url}", version, baseUrl);
                return new PeerSource(zipUrl, host);
            }
            catch (OperationCanceledException) when (!ct.IsCancellationRequested)
            {
                _logger.LogDebug("Peer {Url} probe timed out for v{Version}", baseUrl, version);
            }
            catch (Exception ex)
            {
                _logger.LogDebug(ex, "Peer {Url} probe failed for v{Version}", baseUrl, version);
            }
        }

        _logger.LogInformation("No peer has v{Version}, will fall back to OVH", version);
        return null;
    }

    private sealed class HasResponse
    {
        [JsonPropertyName("sha256")] public string Sha256 { get; set; } = "";
        [JsonPropertyName("size")] public long Size { get; set; }
        [JsonPropertyName("mtime")] public DateTime Mtime { get; set; }
    }
}
