using System.Security.Cryptography;
using System.Text;
using CashLineERP.Application.Identity;

namespace CashLineERP.Api.Infrastructure;

internal sealed class SimpleAccessTokenService : IAccessTokenService
{
    private const int TokenLifetimeHours = 1;
    private readonly byte[] _signingKey;

    public SimpleAccessTokenService(IConfiguration configuration)
    {
        var rawKey = configuration["Authentication:SigningKey"];
        if (string.IsNullOrWhiteSpace(rawKey))
        {
            rawKey = "cashline-erp-development-signing-key-2026";
        }

        _signingKey = Encoding.UTF8.GetBytes(rawKey);
    }

    public string IssueToken(ApplicationUserIdentityView user)
    {
        var expiresAtUtc = DateTimeOffset.UtcNow.AddHours(TokenLifetimeHours).ToUnixTimeSeconds();
        var roleCodes = string.Join(',', user.RoleCodes
            .Where(roleCode => !string.IsNullOrWhiteSpace(roleCode))
            .Select(roleCode => roleCode.Trim().ToUpperInvariant())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(roleCode => roleCode, StringComparer.OrdinalIgnoreCase));
        var functionCodes = string.Join(',', user.FunctionCodes
            .Where(functionCode => !string.IsNullOrWhiteSpace(functionCode))
            .Select(functionCode => functionCode.Trim().ToUpperInvariant())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(functionCode => functionCode, StringComparer.OrdinalIgnoreCase));
        var payload = string.Join('|',
            user.UserId.ToString(),
            Escape(user.LoginName),
            Escape(user.DisplayName),
            Escape(user.UserTypeCode),
            user.PartyId?.ToString() ?? string.Empty,
            Escape(roleCodes),
            Escape(functionCodes),
            expiresAtUtc.ToString());

        var payloadBytes = Encoding.UTF8.GetBytes(payload);
        var signatureBytes = ComputeSignature(payloadBytes);

        return $"{Encode(payloadBytes)}.{Encode(signatureBytes)}";
    }

    public AuthenticatedUserTokenView? ValidateToken(string accessToken)
    {
        if (string.IsNullOrWhiteSpace(accessToken))
        {
            return null;
        }

        var segments = accessToken.Split('.', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 2)
        {
            return null;
        }

        var payloadBytes = Decode(segments[0]);
        var providedSignatureBytes = Decode(segments[1]);
        if (payloadBytes is null || providedSignatureBytes is null)
        {
            return null;
        }

        var expectedSignatureBytes = ComputeSignature(payloadBytes);
        if (!CryptographicOperations.FixedTimeEquals(providedSignatureBytes, expectedSignatureBytes))
        {
            return null;
        }

        var payloadText = Encoding.UTF8.GetString(payloadBytes);
        var parts = payloadText.Split('|');
        if ((parts.Length != 7 && parts.Length != 8)
            || !long.TryParse(parts[0], out var userId)
            || !long.TryParse(parts[^1], out var expiresUnixSeconds))
        {
            return null;
        }

        long? partyId = null;
        if (!string.IsNullOrWhiteSpace(parts[4]))
        {
            if (!long.TryParse(parts[4], out var parsedPartyId))
            {
                return null;
            }

            partyId = parsedPartyId;
        }

        var expiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(expiresUnixSeconds);
        if (expiresAtUtc <= DateTimeOffset.UtcNow)
        {
            return null;
        }

        return new AuthenticatedUserTokenView(
            userId,
            Unescape(parts[1]),
            Unescape(parts[2]),
            Unescape(parts[3]),
            partyId,
            Unescape(parts[5])
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray(),
            parts.Length == 8
                ? Unescape(parts[6])
                    .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray()
                : Array.Empty<string>(),
            expiresAtUtc);
    }

    private byte[] ComputeSignature(byte[] payloadBytes)
    {
        using var hmac = new HMACSHA256(_signingKey);
        return hmac.ComputeHash(payloadBytes);
    }

    private static string Encode(byte[] data)
    {
        return Convert.ToBase64String(data)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    private static byte[]? Decode(string value)
    {
        try
        {
            var normalized = value.Replace('-', '+').Replace('_', '/');
            var padding = normalized.Length % 4;
            if (padding > 0)
            {
                normalized = normalized.PadRight(normalized.Length + (4 - padding), '=');
            }

            return Convert.FromBase64String(normalized);
        }
        catch (FormatException)
        {
            return null;
        }
    }

    private static string Escape(string value) => Convert.ToBase64String(Encoding.UTF8.GetBytes(value));

    private static string Unescape(string value) => Encoding.UTF8.GetString(Convert.FromBase64String(value));
}
