#nullable enable
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Baya.Application.Contracts.Payments;
using Microsoft.Extensions.Options;

namespace Baya.Infrastructure.CrossCutting.Seams.Real;

/// <summary>
/// Real <see cref="IWebhookVerifier"/> (refinement-phase-8, 6.1). Verifies an inbound PSP/BNPL callback's
/// authenticity by an <b>HMAC-SHA256 over the raw body</b> against the per-provider signing secret
/// (<c>Seams:Payments:WebhookSigningSecrets[{provider}]</c>), read from the <c>Seams:Payments:SignatureHeader</c>
/// header. Selected by <c>Seams:Payments:Provider</c> together with the PSP + settlement adapters. An invalid
/// signature yields <c>SignatureValid = false</c> so the handler stores the event ignored and <b>no money moves</b>.
///
/// <para><b>Signatureless fallback.</b> When no signing secret is configured for a provider, the signature can't
/// be checked here — the guard is the handler's mandatory server-side <c>verify</c> re-check on every success
/// event (<c>IPaymentProvider.VerifyAsync</c>), exactly as the contract prescribes. The event-id/type/reference
/// extraction is unchanged from the mock, so <c>HandlePaymentWebhook</c>'s upsert-first/no-op-on-duplicate
/// ordering is untouched.</para>
/// </summary>
public sealed class HmacWebhookVerifier(IOptions<SeamOptions> options) : IWebhookVerifier
{
    private readonly PaymentsOptions _options = options.Value.Payments;

    public WebhookVerification Verify(string provider, IReadOnlyDictionary<string, string> headers, string rawBody)
    {
        var signatureValid = VerifySignature(provider, headers, rawBody);

        string externalEventId = string.Empty;
        string eventType = string.Empty;
        string? gatewayReferenceCode = null;

        try
        {
            using var doc = JsonDocument.Parse(rawBody);
            var root = doc.RootElement;
            if (root.TryGetProperty("external_event_id", out var id))
                externalEventId = id.GetString() ?? string.Empty;
            if (root.TryGetProperty("event_type", out var type))
                eventType = type.GetString() ?? string.Empty;
            if (root.TryGetProperty("gateway_reference_code", out var reference))
                gatewayReferenceCode = reference.GetString();
        }
        catch (JsonException)
        {
            // Unparseable body → empty id/type; the handler stores it and no-ops (nothing to confirm).
        }

        var isSuccessEvent = eventType.Contains("succeed", StringComparison.OrdinalIgnoreCase);
        return new WebhookVerification(signatureValid, externalEventId, eventType, gatewayReferenceCode, isSuccessEvent);
    }

    private bool VerifySignature(string provider, IReadOnlyDictionary<string, string> headers, string rawBody)
    {
        if (!_options.WebhookSigningSecrets.TryGetValue(provider, out var secret) || string.IsNullOrEmpty(secret))
            return true; // no secret configured → the server-side verify re-check is the guard (contract fallback).

        if (!headers.TryGetValue(_options.SignatureHeader, out var provided) || string.IsNullOrEmpty(provided))
            return false; // a secret is configured but the callback carried no signature → reject.

        var computed = Convert.ToHexStringLower(
            HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(rawBody)));

        // Constant-time compare; tolerate a provider that prefixes the scheme (e.g. "sha256=...").
        var candidate = provided.Contains('=') ? provided[(provided.IndexOf('=') + 1)..] : provided;
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(computed), Encoding.UTF8.GetBytes(candidate.ToLowerInvariant()));
    }
}
