using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Foxchat.Core.Database;
using Foxchat.Core.Utils;
using NodaTime;
using NodaTime.Text;

namespace Foxchat.Core.Federation;

public partial class RequestSigningService(ILogger logger, IClock clock, IDatabaseContext db, CoreConfig config)
{
    private readonly ILogger _logger = logger.ForContext<RequestSigningService>();
    private readonly IClock _clock = clock;
    private readonly CoreConfig _config = config;
    private readonly RSA _rsa = db.GetInstanceKeysAsync().GetAwaiter().GetResult();
    private readonly HttpClient _httpClient = new();

    public string GenerateSignature(SignatureData data)
    {
        var plaintext = GeneratePlaintext(data);
        var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
        var hash = SHA256.HashData(plaintextBytes);

        var formatter = new RSAPKCS1SignatureFormatter(_rsa);
        formatter.SetHashAlgorithm(nameof(SHA256));
        var signature = formatter.CreateSignature(hash);

        _logger.Debug("Generated signature for {Host} {RequestPath}", data.Host, data.RequestPath);
        return Convert.ToBase64String(signature);
    }

    public bool VerifySignature(
        string publicKey, string encodedSignature, string dateHeader, string host, string requestPath, int? contentLength, string? userId)
    {
        var rsa = RSA.Create();
        rsa.ImportFromPem(publicKey);

        var now = _clock.GetCurrentInstant();
        var time = ParseTime(dateHeader);
        if ((now + Duration.FromMinutes(1)) < time)
        {
            throw new ApiError.IncomingFederationError("Request was made in the future");
        }
        else if ((now - Duration.FromMinutes(1)) > time)
        {
            throw new ApiError.IncomingFederationError("Request was made too long ago");
        }

        var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId));
        var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
        var hash = SHA256.HashData(plaintextBytes);

        if (!CryptoUtils.TryFromBase64String(encodedSignature, out var signature))
        {
            throw new ApiError.IncomingFederationError("Invalid base64 signature");
        }

        var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
        deformatter.SetHashAlgorithm(nameof(SHA256));

        return deformatter.VerifySignature(hash, signature);
    }

    private static string GeneratePlaintext(SignatureData data)
    {
        var time = FormatTime(data.Time);
        var contentLength = data.ContentLength != null ? data.ContentLength.ToString() : "";
        var userId = data.UserId ?? "";

        return $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}";
    }

    private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
    private static string FormatTime(Instant time) => _pattern.Format(time);
    private static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
}