using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace BrassAndSigil.Server.Services;

/// <summary>
/// On-demand snapshot of online players + positions, formatted for BlueMap's
/// live player overlay. Pull-based: the BlueMap web UI polls a JSON file at
/// <c>/map/maps/overworld/live/players.json</c> roughly every 2 s, and the
/// daemon intercepts that path and calls <see cref="SnapshotAsync"/> per
/// request. Closed tab = no requests = no RCON calls -- same model as
/// <c>/api/players</c>, no server-side timer to manage.
/// </summary>
public static class BlueMapPlayers
{
    public static async Task<List<object>> SnapshotAsync(RconManager rcon, string serverDir, CancellationToken ct)
    {
        var listResp = await rcon.SendCommandAsync("list", ct);
        // Format: "There are N of a max of M players online: name1, name2, ..."
        var colon = listResp.IndexOf(':');
        var names = colon < 0
            ? Array.Empty<string>()
            : listResp.Substring(colon + 1)
                      .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

        var result = new List<object>();
        foreach (var name in names)
        {
            ct.ThrowIfCancellationRequested();
            try
            {
                var posResp = await rcon.SendCommandAsync($"data get entity {name} Pos", ct);
                var pos = ParseDoubleTriple(posResp);
                if (pos is null) continue;
                var rotResp = await rcon.SendCommandAsync($"data get entity {name} Rotation", ct);
                var rot = ParseFloatPair(rotResp);

                result.Add(new
                {
                    uuid     = ResolveUuid(serverDir, name),
                    name,
                    foreign  = false,
                    position = new { x = pos.Value.x, y = pos.Value.y, z = pos.Value.z },
                    rotation = new { pitch = rot?.pitch ?? 0, yaw = rot?.yaw ?? 0, roll = 0.0 },
                });
            }
            catch
            {
                // One bad player shouldn't drop the rest of the list.
            }
        }
        return result;
    }

    // ── UUID resolution ──────────────────────────────────────────────────────
    // BlueMap uses the UUID for two things: marker identity across polls (so a
    // changing UUID makes the marker flash on/off as it thinks the player keeps
    // leaving and rejoining) and skin lookup against Mojang's profile API. We
    // need the *real* Mojang UUID to satisfy both -- MC writes name→uuid pairs
    // into <serverDir>/usercache.json after each successful auth. We cache that
    // file's contents in memory and reload on mtime change, since usercache
    // updates rarely (player join, periodic refresh).
    private sealed class UserCacheEntry
    {
        public string? Name { get; set; }
        public string? Uuid { get; set; }
    }

    private static readonly object _cacheLock = new();
    private static Dictionary<string, string>? _cache;     // case-insensitive name → uuid
    private static DateTime _cacheLoadedAt = DateTime.MinValue;
    private static string? _cachePath;

    private static string ResolveUuid(string serverDir, string name)
    {
        var path = Path.Combine(Path.GetFullPath(serverDir), "usercache.json");
        lock (_cacheLock)
        {
            if (File.Exists(path))
            {
                var mtime = File.GetLastWriteTimeUtc(path);
                if (_cache is null || _cachePath != path || mtime > _cacheLoadedAt)
                {
                    try
                    {
                        var json = File.ReadAllText(path);
                        var entries = JsonSerializer.Deserialize<UserCacheEntry[]>(json, JsonOpts.CaseInsensitive);
                        var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                        if (entries is not null)
                            foreach (var e in entries)
                                if (!string.IsNullOrEmpty(e.Name) && !string.IsNullOrEmpty(e.Uuid))
                                    dict[e.Name] = e.Uuid;
                        _cache = dict;
                        _cachePath = path;
                        _cacheLoadedAt = mtime;
                    }
                    catch
                    {
                        // Corrupt usercache shouldn't take down the marker -- fall through.
                    }
                }
            }
            if (_cache is not null && _cache.TryGetValue(name, out var uuid)) return uuid;
        }

        // Fallback when usercache hasn't been written yet (very early after
        // first auth) or has been wiped: deterministic UUID derived from the
        // name. Stable across polls so the marker doesn't flash; skin lookup
        // will 404 against Mojang and BlueMap will show a default head.
        return DeriveStableUuid(name).ToString();
    }

    private static Guid DeriveStableUuid(string name)
    {
        var bytes = SHA1.HashData(Encoding.UTF8.GetBytes("brass-sigil:" + name.ToLowerInvariant()));
        var guidBytes = new byte[16];
        Array.Copy(bytes, guidBytes, 16);
        return new Guid(guidBytes);
    }

    // Parse a NBT-style double triple like "[123.4d, 64.0d, -56.7d]" out of an
    // RCON `data get` response.
    private static (double x, double y, double z)? ParseDoubleTriple(string resp)
    {
        var m = Regex.Match(resp, @"\[([\-\d\.]+)d?,\s*([\-\d\.]+)d?,\s*([\-\d\.]+)d?\]");
        if (!m.Success) return null;
        if (!double.TryParse(m.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var x)) return null;
        if (!double.TryParse(m.Groups[2].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var y)) return null;
        if (!double.TryParse(m.Groups[3].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var z)) return null;
        return (x, y, z);
    }

    // Rotation comes as `[yaw, pitch]` in floats.
    private static (double pitch, double yaw)? ParseFloatPair(string resp)
    {
        var m = Regex.Match(resp, @"\[([\-\d\.]+)f?,\s*([\-\d\.]+)f?\]");
        if (!m.Success) return null;
        if (!double.TryParse(m.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var yaw))   return null;
        if (!double.TryParse(m.Groups[2].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var pitch)) return null;
        return (pitch, yaw);
    }
}
