using System.Collections.Concurrent;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PSLauncher.Core.Configuration;
using PSLauncher.Models;

namespace PSLauncher.Core.Lan;

public sealed class LanDiscoveryService : BackgroundService, ILanDiscoveryService
{
    private const int BeaconIntervalMs = 15_000;
    private const int PeerTtlSeconds = 60;
    /// <summary>TTL des peers persistés sur disque entre deux runs du launcher.</summary>
    private const int KnownPeerStaleHours = 168; // 1 semaine
    private const string KnownPeersFileName = "lan-peers.json";

    private readonly Func<LanCacheConfig> _configProvider;
    private readonly IZipCacheStore _cache;
    private readonly IConfigStore _configStore;
    private readonly HttpClient _http;
    private readonly ILogger<LanDiscoveryService> _logger;

    private readonly ConcurrentDictionary<string, DiscoveredPeer> _peers = new(StringComparer.OrdinalIgnoreCase);
    /// <summary>Peers pré-chargés depuis le disque au démarrage. Pas de TTL court — ils
    /// servent de "seed" pour ne pas attendre les 15s du premier beacon UDP.
    /// PeerSourceResolver / PeerManifestFetcher les utilisent en plus de ListDiscovered().</summary>
    private List<DiscoveredPeer> _knownPeers = new();
    private string? _ourLanIp;
    private DateTime _lastDiskFlush = DateTime.MinValue;

    public LanDiscoveryService(
        Func<LanCacheConfig> configProvider,
        IZipCacheStore cache,
        IConfigStore configStore,
        HttpClient http,
        ILogger<LanDiscoveryService> logger)
    {
        _configProvider = configProvider;
        _cache = cache;
        _configStore = configStore;
        _http = http;
        _logger = logger;
    }

    public IReadOnlyList<DiscoveredPeer> ListDiscovered()
    {
        var cutoff = DateTime.UtcNow.AddSeconds(-PeerTtlSeconds);
        return _peers.Values.Where(p => p.LastSeenUtc >= cutoff).ToList();
    }

    /// <summary>
    /// Peers connus de runs précédents (persistés sur disque, max 1 semaine).
    /// Sert de seed pour démarrer rapidement avant que le premier beacon arrive.
    /// Probés en HTTP par PeerSourceResolver — si le peer est offline on aura juste
    /// un timeout puis on essaiera le suivant.
    /// </summary>
    public IReadOnlyList<DiscoveredPeer> ListKnown() => _knownPeers;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var cfg = _configProvider();
        if (!cfg.ServerEnabled && !cfg.ClientEnabled)
        {
            _logger.LogInformation("LAN discovery disabled (both ServerEnabled and ClientEnabled are false)");
            return;
        }

        _ourLanIp = GetLocalLanIp();

        // Charge les peers connus de la session précédente. Les probes pourront
        // ainsi essayer ces IPs immédiatement, sans attendre les 15s du premier
        // beacon UDP. Si un peer est offline, on aura juste un timeout HTTP rapide.
        _knownPeers = LoadKnownPeers();
        if (_knownPeers.Count > 0)
            _logger.LogInformation("Loaded {Count} known peers from disk cache", _knownPeers.Count);

        // BOOTSTRAP ACTIF : kick off un probe HTTP /v1/info en parallèle sur tous les
        // peers connus (cache disque) + manuels (config). Découverte instantanée au
        // lieu d'attendre les 15 s du prochain beacon UDP. Fire-and-forget : le reste
        // du service démarre normalement pendant que le probe tourne en background.
        _ = Task.Run(() => BootstrapProbeKnownPeersAsync(cfg, stoppingToken), stoppingToken);

        // Listener (toujours actif si Client OU Server activé)
        UdpClient? listener = null;
        try
        {
            listener = new UdpClient(AddressFamily.InterNetwork);
            listener.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            listener.Client.Bind(new IPEndPoint(IPAddress.Any, cfg.DiscoveryPort));
            listener.EnableBroadcast = true;
            _logger.LogInformation("LAN discovery listening on UDP :{Port}", cfg.DiscoveryPort);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to bind UDP discovery socket on port {Port}", cfg.DiscoveryPort);
            listener?.Dispose();
            return;
        }

        var listenTask = ListenLoopAsync(listener, stoppingToken);

        // Broadcaster (uniquement si Server activé)
        Task? broadcastTask = null;
        if (cfg.ServerEnabled)
        {
            broadcastTask = BroadcastLoopAsync(cfg.DiscoveryPort, cfg.ServerPort, stoppingToken);
        }

        try
        {
            await Task.WhenAll(broadcastTask is null ? new[] { listenTask } : new[] { listenTask, broadcastTask })
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException) { /* expected on shutdown */ }
        finally
        {
            try { listener.Close(); } catch { }
            listener.Dispose();
        }
    }

    private async Task ListenLoopAsync(UdpClient listener, CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var result = await listener.ReceiveAsync(ct).ConfigureAwait(false);
                var senderHost = result.RemoteEndPoint.Address.ToString();

                // Ignore nos propres beacons
                if (_ourLanIp is not null && string.Equals(senderHost, _ourLanIp, StringComparison.OrdinalIgnoreCase))
                    continue;

                var json = Encoding.UTF8.GetString(result.Buffer);
                var beacon = JsonSerializer.Deserialize<BeaconPayload>(json);
                if (beacon is null || beacon.V != 1) continue;
                if (string.IsNullOrEmpty(beacon.Host) || beacon.Port <= 0 || beacon.Port > 65535) continue;

                var peer = new DiscoveredPeer(
                    Host: beacon.Host,
                    Port: beacon.Port,
                    Versions: (IReadOnlyList<string>?)beacon.Versions ?? Array.Empty<string>(),
                    LastSeenUtc: DateTime.UtcNow);
                _peers[beacon.Host] = peer;

                // Persist to disk au plus toutes les 30s pour ne pas spammer le SSD
                // (un beacon arrive toutes les 15s par peer × N peers = beaucoup d'écritures).
                if ((DateTime.UtcNow - _lastDiskFlush).TotalSeconds >= 30)
                {
                    _lastDiskFlush = DateTime.UtcNow;
                    SaveKnownPeers(_peers.Values);
                }
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                _logger.LogDebug(ex, "Discovery listen iteration failed (will retry)");
                try { await Task.Delay(500, ct); } catch { break; }
            }
        }
    }

    private async Task BroadcastLoopAsync(int discoveryPort, int httpPort, CancellationToken ct)
    {
        using var sender = new UdpClient(AddressFamily.InterNetwork) { EnableBroadcast = true };
        var endpoint = new IPEndPoint(IPAddress.Broadcast, discoveryPort);

        while (!ct.IsCancellationRequested)
        {
            try
            {
                var versions = _cache.ListCached().Select(c => c.Version).ToList();
                var host = _ourLanIp ?? GetLocalLanIp() ?? "0.0.0.0";
                var payload = new BeaconPayload
                {
                    V = 1,
                    Host = host,
                    Port = httpPort,
                    Versions = versions,
                };
                var json = JsonSerializer.Serialize(payload);
                var bytes = Encoding.UTF8.GetBytes(json);
                await sender.SendAsync(bytes, bytes.Length, endpoint).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.LogDebug(ex, "Discovery broadcast failed (will retry)");
            }
            try { await Task.Delay(BeaconIntervalMs, ct); } catch { break; }
        }
    }

    /// <summary>
    /// Renvoie l'IPv4 LAN du PC : interface UP non-loopback avec une default
    /// gateway IPv4. Filtre Hyper-V virtual switch, VPN sans gateway, etc.
    /// Same logic that MainViewModel.LocalIpAddress uses for the sidebar info.
    /// </summary>
    public static string? GetLocalLanIp()
    {
        try
        {
            return NetworkInterface.GetAllNetworkInterfaces()
                .Where(n => n.OperationalStatus == OperationalStatus.Up
                         && n.NetworkInterfaceType != NetworkInterfaceType.Loopback
                         && n.GetIPProperties().GatewayAddresses
                              .Any(g => g.Address.AddressFamily == AddressFamily.InterNetwork))
                .SelectMany(n => n.GetIPProperties().UnicastAddresses)
                .Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork)
                .Select(a => a.Address.ToString())
                .FirstOrDefault();
        }
        catch { return null; }
    }

    private sealed class BeaconPayload
    {
        [JsonPropertyName("v")] public int V { get; set; }
        [JsonPropertyName("host")] public string Host { get; set; } = "";
        [JsonPropertyName("port")] public int Port { get; set; }
        [JsonPropertyName("versions")] public List<string>? Versions { get; set; }
    }

    private sealed class StoredPeer
    {
        [JsonPropertyName("host")] public string Host { get; set; } = "";
        [JsonPropertyName("port")] public int Port { get; set; }
        [JsonPropertyName("versions")] public List<string> Versions { get; set; } = new();
        [JsonPropertyName("lastSeenUtc")] public DateTime LastSeenUtc { get; set; }
    }

    private string GetKnownPeersPath() =>
        Path.Combine(_configStore.ConfigDirectory, KnownPeersFileName);

    private List<DiscoveredPeer> LoadKnownPeers()
    {
        var path = GetKnownPeersPath();
        if (!File.Exists(path)) return new List<DiscoveredPeer>();
        try
        {
            var raw = File.ReadAllText(path);
            var stored = JsonSerializer.Deserialize<List<StoredPeer>>(raw);
            if (stored is null) return new List<DiscoveredPeer>();
            var staleAfter = DateTime.UtcNow.AddHours(-KnownPeerStaleHours);
            return stored
                .Where(s => s.LastSeenUtc >= staleAfter
                         && !string.IsNullOrEmpty(s.Host)
                         && s.Port > 0 && s.Port < 65536)
                .Select(s => new DiscoveredPeer(s.Host, s.Port, s.Versions, s.LastSeenUtc))
                .ToList();
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Failed to load known peers cache (non-fatal)");
            return new List<DiscoveredPeer>();
        }
    }

    /// <summary>
    /// Probe HTTP actif en parallèle de tous les peers connus (cache disque) et
    /// manuels (config). Pour chaque peer qui répond à /v1/info, on l'injecte dans
    /// _peers avec lastSeen=now → il apparaît immédiatement dans ListDiscovered()
    /// et donc dans la sidebar + PeerSourceResolver. Aucune attente d'un beacon UDP.
    /// Timeout court (1 s) par peer, parallel pour ne pas additionner.
    /// </summary>
    private async Task BootstrapProbeKnownPeersAsync(LanCacheConfig cfg, CancellationToken ct)
    {
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var urls = new List<string>();
        foreach (var p in _knownPeers)
        {
            var u = $"http://{p.Host}:{p.Port}";
            if (seen.Add(u)) urls.Add(u);
        }
        foreach (var raw in cfg.ManualPeerUrls ?? new List<string>())
        {
            var t = raw.Trim().TrimEnd('/');
            if (string.IsNullOrEmpty(t)) continue;
            if (seen.Add(t)) urls.Add(t);
        }

        if (urls.Count == 0)
        {
            _logger.LogDebug("Bootstrap probe: no known/manual peers to probe");
            return;
        }

        _logger.LogInformation("Bootstrap probe: attempting {Count} known/manual peers in parallel", urls.Count);
        var tasks = urls.Select(u => ProbeOnePeerAsync(u, ct)).ToArray();
        await Task.WhenAll(tasks).ConfigureAwait(false);

        var alive = _peers.Count;
        _logger.LogInformation("Bootstrap probe done: {Alive} peer(s) responding", alive);
        if (alive > 0)
        {
            // Persist immediately — la session précédente avait peut-être des peers stale,
            // on remet à jour avec ceux qui répondent vraiment maintenant.
            SaveKnownPeers(_peers.Values);
            _lastDiskFlush = DateTime.UtcNow;
        }
    }

    private async Task ProbeOnePeerAsync(string baseUrl, CancellationToken ct)
    {
        try
        {
            using var probeCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            probeCts.CancelAfter(TimeSpan.FromSeconds(1));
            using var resp = await _http.GetAsync($"{baseUrl}/v1/info", probeCts.Token).ConfigureAwait(false);
            if (!resp.IsSuccessStatusCode) return;
            var json = await resp.Content.ReadAsStringAsync(probeCts.Token).ConfigureAwait(false);
            var beacon = JsonSerializer.Deserialize<BeaconPayload>(json);
            if (beacon is null || string.IsNullOrEmpty(beacon.Host) || beacon.Port <= 0) return;

            // Ignore notre propre IP (au cas où une URL manuelle pointerait sur nous-mêmes)
            if (_ourLanIp is not null && string.Equals(beacon.Host, _ourLanIp, StringComparison.OrdinalIgnoreCase))
                return;

            var peer = new DiscoveredPeer(
                Host: beacon.Host,
                Port: beacon.Port,
                Versions: (IReadOnlyList<string>?)beacon.Versions ?? Array.Empty<string>(),
                LastSeenUtc: DateTime.UtcNow);
            _peers[beacon.Host] = peer;
            _logger.LogInformation("Bootstrap probe: peer alive at {Url} (versions: {V})", baseUrl,
                beacon.Versions is { Count: > 0 } ? string.Join(",", beacon.Versions) : "—");
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested) { /* shutdown */ }
        catch (Exception ex)
        {
            _logger.LogDebug("Bootstrap probe of {Url} failed: {Reason}", baseUrl, ex.Message);
        }
    }

    private void SaveKnownPeers(IEnumerable<DiscoveredPeer> peers)
    {
        var path = GetKnownPeersPath();
        try
        {
            var stored = peers
                .Where(p => !string.IsNullOrEmpty(p.Host))
                .Select(p => new StoredPeer
                {
                    Host = p.Host,
                    Port = p.Port,
                    Versions = p.Versions.ToList(),
                    LastSeenUtc = p.LastSeenUtc,
                })
                .ToList();
            var json = JsonSerializer.Serialize(stored);
            // Atomic write : .tmp puis rename pour ne pas corrompre si crash mi-écriture.
            var tmp = path + ".tmp";
            File.WriteAllText(tmp, json);
            File.Move(tmp, path, overwrite: true);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Failed to save known peers cache (non-fatal)");
        }
    }
}
