using System.Collections.Generic;
using GameList.Data;
using GameList.Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;

namespace GameList.Endpoints;

internal static class EndpointHelpers
{
    public static async Task<Player?> GetAuthenticatedPlayer(HttpContext ctx, AppDbContext db)
    {
        if (ctx?.User?.Identity?.IsAuthenticated != true) return null;

        if (ctx.Items.TryGetValue(nameof(Player), out var cached) && cached is Player cachedPlayer)
            return cachedPlayer;

        var idValue = ctx.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (string.IsNullOrWhiteSpace(idValue) || !Guid.TryParse(idValue, out var playerId))
        {
            // Auth cookie is present but malformed; clear and reject.
            await Infrastructure.PlayerIdentityExtensions.SignOutPlayerAsync(ctx);
            return null;
        }

        var existing = await db.Players.FindAsync(playerId);
        if (existing is null)
        {
            await Infrastructure.PlayerIdentityExtensions.SignOutPlayerAsync(ctx);
            return null;
        }

        ctx.Items[nameof(Player)] = existing;
        return existing;
    }

    public static async Task<Phase> GetPhase(AppDbContext db, Guid playerId)
    {
        var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
        if (player is null) return Phase.Suggest;

        var state = await db.AppState.FirstAsync();

        var changed = false;

        // Auto-upgrade any legacy Reveal phase to Vote to avoid blank screens
        if (player.CurrentPhase == Phase.Reveal)
        {
            player.CurrentPhase = Phase.Vote;
            changed = true;
        }

        // Keep phases aligned with results availability
        if (state.ResultsOpen && player.CurrentPhase != Phase.Results)
        {
            player.CurrentPhase = Phase.Results;
            changed = true;
        }
        else if (!state.ResultsOpen && player.CurrentPhase == Phase.Results)
        {
            player.CurrentPhase = Phase.Vote;
            player.VotesFinal = false;
            changed = true;
        }

        if (changed)
        {
            await db.SaveChangesAsync();
        }
        return player.CurrentPhase;
    }

    public static IResult PhaseMismatch(Phase required, Phase current) =>
        Results.BadRequest(new { error = $"This endpoint is available in the {required} phase. Your current phase is {current}." });

    public static string? TrimTo(string? input, int max) =>
        string.IsNullOrWhiteSpace(input)
            ? null
            : input.Trim() is var t && t.Length > 0
                ? t[..Math.Min(t.Length, max)]
                : null;

    public static bool IsValidImageUrl(string? url)
    {
        if (string.IsNullOrWhiteSpace(url)) return true; // empty is acceptable
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
        if (uri.Scheme is not ("http" or "https")) return false;
        var path = uri.AbsolutePath.ToLowerInvariant();
        return path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg")
               || path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif");
    }

    public static async Task<bool> IsReachableImageAsync(string? url, IHttpClientFactory httpFactory, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(url)) return true;
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
        if (uri.Scheme is not ("http" or "https")) return false;
        if (!await IsSafePublicHostAsync(uri, httpFactory, ct)) return false;

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(3));

        var handler = new HttpClientHandler
        {
            AllowAutoRedirect = false
        };
        var client = new HttpClient(handler);

        try
        {
            using var head = new HttpRequestMessage(HttpMethod.Head, uri);
            var headResp = await client.SendAsync(head, HttpCompletionOption.ResponseHeadersRead, cts.Token);
            if (headResp.IsSuccessStatusCode && headResp.StatusCode is not System.Net.HttpStatusCode.Redirect)
            {
                var ctHeader = headResp.Content.Headers.ContentType?.MediaType;
                if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
                    return true;
                if (headResp.Content.Headers.ContentLength is long len && len > MaxImageBytes) return false;
            }
        }
        catch { /* fallback */ }

        try
        {
            using var get = new HttpRequestMessage(HttpMethod.Get, uri);
            get.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 1023);
            var resp = await client.SendAsync(get, HttpCompletionOption.ResponseHeadersRead, cts.Token);
            if (!resp.IsSuccessStatusCode) return false;
            if (resp.StatusCode is System.Net.HttpStatusCode.Redirect) return false;
            if (resp.Content.Headers.ContentLength is long len && len > MaxImageBytes) return false;

            var ctHeader = resp.Content.Headers.ContentType?.MediaType;
            if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
                return true;

            await using var stream = await resp.Content.ReadAsStreamAsync(cts.Token);
            var rented = new byte[12];
            var read = await stream.ReadAsync(rented, 0, rented.Length, cts.Token);
            var sig = new ReadOnlySpan<byte>(rented, 0, read);

            if (IsMagic(sig, "PNG")) return true;
            if (IsMagic(sig, new byte[] { 0xFF, 0xD8 })) return true; // JPEG
            if (IsMagic(sig, "GIF8")) return true;
            if (IsRiffWithTag(sig, "WEBP")) return true;
            if (ContainsFtyp(sig, "avif")) return true;

            return false;
        }
        catch
        {
            return false;
        }
    }

    private const long MaxImageBytes = 5 * 1024 * 1024; // 5 MB guard

    private static async Task<bool> IsSafePublicHostAsync(Uri uri, IHttpClientFactory httpFactory, CancellationToken ct)
    {
        try
        {
            var host = uri.Host;
            if (Uri.CheckHostName(host) == UriHostNameType.Dns || Uri.CheckHostName(host) == UriHostNameType.IPv4 || Uri.CheckHostName(host) == UriHostNameType.IPv6)
            {
                var addresses = await System.Net.Dns.GetHostAddressesAsync(host, ct);
                foreach (var ip in addresses)
                {
                    if (System.Net.IPAddress.IsLoopback(ip)) return false;
                    if (IsPrivate(ip)) return false;
                }
            }
            else
            {
                return false;
            }

            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsPrivate(System.Net.IPAddress ip)
    {
        if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
        {
            var bytes = ip.GetAddressBytes();
            return bytes[0] switch
            {
                10 => true,
                172 when bytes[1] >= 16 && bytes[1] <= 31 => true,
                192 when bytes[1] == 168 => true,
                127 => true,
                _ => false
            };
        }

        if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)
        {
            return ip.IsIPv6LinkLocal || ip.IsIPv6SiteLocal || ip.IsIPv6Multicast || System.Net.IPAddress.IsLoopback(ip);
        }

        return false;
    }

    private static bool IsMagic(ReadOnlySpan<byte> data, string ascii)
    {
        var bytes = System.Text.Encoding.ASCII.GetBytes(ascii);
        return data.StartsWith(bytes);
    }

    private static bool IsMagic(ReadOnlySpan<byte> data, ReadOnlySpan<byte> magic) => data.StartsWith(magic);

    private static bool IsRiffWithTag(ReadOnlySpan<byte> data, string tag)
    {
        if (data.Length < 12) return false;
        var riff = System.Text.Encoding.ASCII.GetBytes("RIFF");
        if (!data.StartsWith(riff)) return false;
        var tagBytes = System.Text.Encoding.ASCII.GetBytes(tag);
        return data[8..].StartsWith(tagBytes);
    }

    private static bool ContainsFtyp(ReadOnlySpan<byte> data, string brand)
    {
        if (data.Length < 12) return false;
        var ftyp = System.Text.Encoding.ASCII.GetBytes("ftyp");
        if (!data[4..].StartsWith(ftyp)) return false;
        var brandBytes = System.Text.Encoding.ASCII.GetBytes(brand);
        return data[8..].StartsWith(brandBytes);
    }

    public static bool IsValidHttpUrl(string? url)
    {
        if (string.IsNullOrWhiteSpace(url)) return true; // empty is allowed
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
        return uri.Scheme is "http" or "https";
    }

    public static async Task<bool> IsAdmin(HttpContext ctx, AppDbContext db)
    {
        var player = await GetAuthenticatedPlayer(ctx, db);
        return player?.IsAdmin == true;
    }

    public static AppState NewAppState() => new()
    {
        Id = 1,
        ResultsOpen = false,
        UpdatedAt = DateTimeOffset.UnixEpoch
    };

    public static Dictionary<int, int> BuildLinkRoots(IEnumerable<(int Id, int? ParentId)> items)
    {
        var parentMap = items.ToDictionary(x => x.Id, x => x.ParentId);
        var roots = new Dictionary<int, int>();
        foreach (var id in parentMap.Keys)
        {
            roots[id] = FindRootId(id, parentMap);
        }
        return roots;
    }

    public static int FindRootId(int suggestionId, IReadOnlyDictionary<int, int?> parentMap)
    {
        var current = suggestionId;
        var visited = new HashSet<int>();

        while (parentMap.TryGetValue(current, out var parent) && parent is int p && !visited.Contains(p))
        {
            visited.Add(current);
            current = p;
        }

        return current;
    }

    public static List<int> LinkedIdsFor(int suggestionId, IReadOnlyDictionary<int, int> rootIndex)
    {
        if (!rootIndex.TryGetValue(suggestionId, out var root)) return new();
        return rootIndex.Where(kv => kv.Value == root).Select(kv => kv.Key).ToList();
    }
}
