using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.WebUtilities;

namespace GmRelay.Web.Services;

public sealed class TelegramAuthService(IConfiguration configuration)
{
    public bool Verify(IQueryCollection query, out long telegramId, out string name)
    {
        telegramId = 0;
        name = string.Empty;

        if (!query.TryGetValue("hash", out var hash))
            return false;

        var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"];
        if (string.IsNullOrEmpty(token))
            return false;

        // 1. Sort and join
        var dataCheckList = query
            .Where(x => x.Key != "hash")
            .OrderBy(x => x.Key)
            .Select(x => $"{x.Key}={x.Value}")
            .ToList();

        var dataCheckString = string.Join("\n", dataCheckList);

        // 2. Compute Secret Key (static method — no IDisposable needed)
        var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(token));

        // 3. Compute Hash (static method — no IDisposable needed)
        var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString));

        // 4. Timing-safe comparison to prevent timing attacks
        var hashBytes = Convert.FromHexString(hash.ToString());
        if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes))
            return false;

        // 5. Check expiration (auth_date)
        if (query.TryGetValue("auth_date", out var authDateStr) && long.TryParse(authDateStr, out var authDate))
        {
            var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (now - authDate > 86400) // 24 hours
                return false;
        }

        if (query.TryGetValue("id", out var idStr) && long.TryParse(idStr, out telegramId))
        {
            var firstName = query["first_name"].ToString();
            var lastName = query["last_name"].ToString();
            name = string.IsNullOrWhiteSpace(lastName) ? firstName : $"{firstName} {lastName}";
            return true;
        }

        return false;
    }

    public bool VerifyWebAppInitData(string initData, out long telegramId, out string name)
    {
        telegramId = 0;
        name = string.Empty;

        if (string.IsNullOrWhiteSpace(initData))
            return false;

        var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"];
        if (string.IsNullOrEmpty(token))
            return false;

        var values = QueryHelpers.ParseQuery(initData);
        if (!values.TryGetValue("hash", out var hash) || string.IsNullOrWhiteSpace(hash))
            return false;

        var dataCheckString = string.Join(
            "\n",
            values
                .Where(pair => pair.Key != "hash")
                .OrderBy(pair => pair.Key, StringComparer.Ordinal)
                .Select(pair => $"{pair.Key}={pair.Value}"));

        var secretKey = HMACSHA256.HashData(Encoding.UTF8.GetBytes("WebAppData"), Encoding.UTF8.GetBytes(token));
        var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString));

        byte[] hashBytes;
        try
        {
            hashBytes = Convert.FromHexString(hash.ToString());
        }
        catch (FormatException)
        {
            return false;
        }

        if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes))
            return false;

        if (!values.TryGetValue("auth_date", out var authDateStr) ||
            !long.TryParse(authDateStr, out var authDate))
        {
            return false;
        }

        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (now - authDate > 86400)
            return false;

        if (!values.TryGetValue("user", out var userJson) || string.IsNullOrWhiteSpace(userJson))
            return false;

        return TryReadWebAppUser(userJson.ToString(), out telegramId, out name);
    }

    public bool VerifyLoginPayload(TelegramLoginPayload payload, out long telegramId, out string name)
    {
        telegramId = 0;
        name = string.Empty;

        if (payload.Id <= 0 ||
            string.IsNullOrWhiteSpace(payload.FirstName) ||
            payload.AuthDate <= 0 ||
            string.IsNullOrWhiteSpace(payload.Hash))
        {
            return false;
        }

        var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"];
        if (string.IsNullOrEmpty(token))
            return false;

        var values = new SortedDictionary<string, string>(StringComparer.Ordinal)
        {
            ["auth_date"] = payload.AuthDate.ToString(System.Globalization.CultureInfo.InvariantCulture),
            ["first_name"] = payload.FirstName,
            ["id"] = payload.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)
        };

        if (!string.IsNullOrWhiteSpace(payload.LastName))
            values["last_name"] = payload.LastName;
        if (!string.IsNullOrWhiteSpace(payload.PhotoUrl))
            values["photo_url"] = payload.PhotoUrl;
        if (!string.IsNullOrWhiteSpace(payload.Username))
            values["username"] = payload.Username;

        var dataCheckString = string.Join("\n", values.Select(pair => $"{pair.Key}={pair.Value}"));
        var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(token));
        var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString));

        byte[] hashBytes;
        try
        {
            hashBytes = Convert.FromHexString(payload.Hash);
        }
        catch (FormatException)
        {
            return false;
        }
        catch (ArgumentException)
        {
            return false;
        }

        if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes))
            return false;

        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (now - payload.AuthDate > 86400)
            return false;

        telegramId = payload.Id;
        name = string.IsNullOrWhiteSpace(payload.LastName)
            ? payload.FirstName
            : $"{payload.FirstName} {payload.LastName}";
        return true;
    }

    private static bool TryReadWebAppUser(string userJson, out long telegramId, out string name)
    {
        telegramId = 0;
        name = string.Empty;

        try
        {
            using var document = JsonDocument.Parse(userJson);
            var root = document.RootElement;

            if (!root.TryGetProperty("id", out var idElement) || !idElement.TryGetInt64(out telegramId))
                return false;

            var firstName = root.TryGetProperty("first_name", out var firstNameElement)
                ? firstNameElement.GetString() ?? string.Empty
                : string.Empty;
            var lastName = root.TryGetProperty("last_name", out var lastNameElement)
                ? lastNameElement.GetString() ?? string.Empty
                : string.Empty;
            var username = root.TryGetProperty("username", out var usernameElement)
                ? usernameElement.GetString()
                : null;

            name = (firstName, lastName) switch
            {
                ({ Length: > 0 }, { Length: > 0 }) => $"{firstName} {lastName}",
                ({ Length: > 0 }, _) => firstName,
                _ when !string.IsNullOrWhiteSpace(username) => "@" + username,
                _ => $"Telegram {telegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)}"
            };

            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }
}

public sealed record TelegramLoginPayload(
    [property: JsonPropertyName("id")] long Id,
    [property: JsonPropertyName("first_name")] string FirstName,
    [property: JsonPropertyName("last_name")] string? LastName,
    [property: JsonPropertyName("username")] string? Username,
    [property: JsonPropertyName("photo_url")] string? PhotoUrl,
    [property: JsonPropertyName("auth_date")] long AuthDate,
    [property: JsonPropertyName("hash")] string Hash);
