#nullable enable
using System.Net;
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Baya.Infrastructure.CrossCutting.Seams.Real;

/// <summary>
/// Real <see cref="ISmsSender"/> over the <b>Kavenegar</b> Iranian SMS gateway (refinement-phase-8, 5.1 —
/// launch-critical). OTP delivery uses Kavenegar's <c>verify/lookup</c> pattern API (a pre-approved template,
/// no marketing pre-clearance needed for transactional OTPs); free-form transactional messages use
/// <c>sms/send</c> from the registered sender line. Selected by <c>Seams:Sms:Provider = kavenegar</c>; the seam
/// is swapped by a registration change only, so no handler is touched.
///
/// <para><b>The OTP is never logged.</b> Once this ships the Development OTP-in-logs/echo bridge is disabled
/// (the code only leaves the process over the SMS wire) — only the phone tail + gateway status are logged.</para>
/// </summary>
public sealed class KavenegarSmsSender(
    HttpClient httpClient,
    IOptions<SeamOptions> options,
    ILogger<KavenegarSmsSender> logger) : ISmsSender
{
    private readonly SmsOptions _options = options.Value.Sms;

    public async Task SendOtpAsync(string phone, string code, CancellationToken cancellationToken = default)
    {
        // verify/lookup: the code is delivered through the approved OTP template — never a free-form message,
        // which is what keeps transactional OTPs deliverable without marketing pre-clearance.
        var query = new Dictionary<string, string?>
        {
            ["receptor"] = phone,
            ["token"] = code,
            ["template"] = _options.OtpTemplate,
        };

        await SendAsync($"v1/{_options.ApiKey}/verify/lookup.json", query, phone, cancellationToken);
    }

    public async Task SendAsync(string phone, string message, CancellationToken cancellationToken = default)
    {
        var query = new Dictionary<string, string?>
        {
            ["receptor"] = phone,
            ["sender"] = _options.SenderLine,
            ["message"] = message,
        };

        await SendAsync($"v1/{_options.ApiKey}/sms/send.json", query, phone, cancellationToken);
    }

    private async Task SendAsync(
        string path, IReadOnlyDictionary<string, string?> query, string phone, CancellationToken cancellationToken)
    {
        var url = QueryHelpers(path, query);

        using var response = await httpClient.GetAsync(url, cancellationToken);
        var body = await response.Content.ReadAsStringAsync(cancellationToken);

        // Kavenegar always returns 200 for a well-formed request and carries the real outcome in `return.status`
        // (200 = accepted). A non-accepted status (e.g. 411 invalid receptor, 418 credit) is a delivery failure —
        // surfaced as an exception so the OTP command reports the send failure rather than silently "succeeding".
        var status = TryReadReturnStatus(body);
        if (response.StatusCode != HttpStatusCode.OK || status is not 200)
        {
            logger.LogWarning(
                "Kavenegar SMS delivery failed for phone ending {PhoneTail} — http {Http}, return status {ReturnStatus}",
                Tail(phone), (int)response.StatusCode, status);
            throw new InvalidOperationException($"Kavenegar SMS delivery failed (return status {status}).");
        }

        logger.LogInformation("Kavenegar SMS accepted for phone ending {PhoneTail}", Tail(phone));
    }

    private static int? TryReadReturnStatus(string body)
    {
        try
        {
            using var doc = JsonDocument.Parse(body);
            return doc.RootElement.TryGetProperty("return", out var ret)
                   && ret.TryGetProperty("status", out var status)
                ? status.GetInt32()
                : null;
        }
        catch (JsonException)
        {
            return null;
        }
    }

    private static string QueryHelpers(string path, IReadOnlyDictionary<string, string?> query)
    {
        var pairs = query
            .Where(kvp => kvp.Value is not null)
            .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}");
        return $"{path}?{string.Join('&', pairs)}";
    }

    private static string Tail(string phone) =>
        string.IsNullOrEmpty(phone) ? "????" : phone[^Math.Min(4, phone.Length)..];
}
