using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FlatRender.IdentitySvc.Application.Services.Interfaces;
using FlatRender.IdentitySvc.Domain.Entities;
using FlatRender.IdentitySvc.Domain.Enums;
using FlatRender.IdentitySvc.Infrastructure.Data;
using FlatRender.IdentitySvc.Models.Responses;
using Microsoft.EntityFrameworkCore;

namespace FlatRender.IdentitySvc.Application.Services;

public class PaymentService(
    IdentityDbContext db,
    IHttpClientFactory httpClientFactory,
    IConfiguration config) : IPaymentService
{
    // ── Config ────────────────────────────────────────────────────────────────────
    private string ZarinPalMerchantId    => config["ZarinPal:MerchantId"] ?? "";
    private string ZarinPalCallbackUrl   => config["ZarinPal:CallbackUrl"] ?? "http://localhost:8080/v1/payments/callback/zarinpal";
    private bool   ZarinPalSandbox       => config["ZarinPal:Sandbox"] == "true";

    private string SnapPayClientId       => config["SnapPay:ClientId"] ?? "";
    private string SnapPayClientSecret   => config["SnapPay:ClientSecret"] ?? "";
    private string SnapPayBaseUrl        => config["SnapPay:BaseUrl"] ?? "https://api.snappay.ir";
    private string SnapPayCallbackUrl    => config["SnapPay:CallbackUrl"] ?? "http://localhost:8080/v1/payments/callback/snappay";

    // Tara payment gateway (tara.ir) — API key auth
    // Docs: https://www.tara.ir/documents/payment-gateway
    private string TaraApiKey            => config["Tara:ApiKey"] ?? "";
    private string TaraBaseUrl           => config["Tara:BaseUrl"] ?? "https://api.tara.ir";
    private string TaraCallbackUrl       => config["Tara:CallbackUrl"] ?? "http://localhost:8080/v1/payments/callback/tara";

    private string StripeSecretKey       => config["Stripe:SecretKey"] ?? "";
    private string StripeWebhookSecret   => config["Stripe:WebhookSecret"] ?? "";
    private const long IrrToToman = 10;  // 1 Toman = 10 Rials

    // ── Queries ───────────────────────────────────────────────────────────────────

    public async Task<PagedResponse<PaymentResponse>> GetUserPaymentsAsync(Guid userId, int page, int pageSize)
    {
        var total = await db.Payments.CountAsync(p => p.UserId == userId);
        var payments = await db.Payments
            .Where(p => p.UserId == userId)
            .OrderByDescending(p => p.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        return new PagedResponse<PaymentResponse>(
            payments.Select(MapPaymentResponse).ToList(),
            new PaginationMeta(page, pageSize, total, (long)(page * pageSize) < total)
        );
    }

    public async Task<PaymentResponse> GetByIdAsync(Guid paymentId, Guid userId)
    {
        var payment = await db.Payments
            .FirstOrDefaultAsync(p => p.Id == paymentId && p.UserId == userId)
            ?? throw new KeyNotFoundException("Payment not found");
        return MapPaymentResponse(payment);
    }

    // ── ZarinPal initiation ───────────────────────────────────────────────────────

    public async Task<string> InitiateZarinPalAsync(Guid paymentId, Guid userId)
    {
        var payment = await db.Payments.FirstOrDefaultAsync(
            p => p.Id == paymentId && p.UserId == userId && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment not found or already processed");

        if (string.IsNullOrEmpty(ZarinPalMerchantId))
            throw new InvalidOperationException("ZarinPal:MerchantId is not configured");

        var amountToman = payment.AmountMinor / IrrToToman;
        var baseUrl = ZarinPalSandbox
            ? "https://sandbox.zarinpal.com/pg/v4/payment"
            : "https://api.zarinpal.com/pg/v4/payment";

        var http = httpClientFactory.CreateClient("zarinpal");
        var reqBody = new
        {
            merchant_id  = ZarinPalMerchantId,
            amount       = amountToman,
            callback_url = ZarinPalCallbackUrl,
            description  = payment.Title ?? "پرداخت فلت‌رندر",
            metadata     = new { order_id = paymentId.ToString() },
        };

        var resp = await http.PostAsJsonAsync($"{baseUrl}/request.json", reqBody);
        resp.EnsureSuccessStatusCode();

        var json  = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root  = json!.RootElement;
        var code  = root.GetProperty("data").GetProperty("code").GetInt32();

        if (code != 100)
        {
            var errDetail = root.TryGetProperty("errors", out var errEl) ? errEl.ToString() : "no details";
            throw new InvalidOperationException($"ZarinPal request failed (code={code}): {errDetail}");
        }

        var authority = root.GetProperty("data").GetProperty("authority").GetString()!;
        payment.GatewayToken = authority;
        payment.UpdatedAt    = DateTime.UtcNow;
        await db.SaveChangesAsync();

        var startPage = ZarinPalSandbox
            ? "https://sandbox.zarinpal.com/pg/StartPay/"
            : "https://www.zarinpal.com/pg/StartPay/";

        return $"{startPage}{authority}";
    }

    // ── ZarinPal callback (browser returns after payment) ─────────────────────────

    public async Task<string> HandleZarinPalCallbackAsync(string authority, string status)
    {
        if (status != "OK")
            return "/payment/result?status=failed&gateway=zarinpal";

        var payment = await db.Payments.Include(p => p.User)
            .FirstOrDefaultAsync(p => p.GatewayToken == authority && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment record not found for this authority");

        var amountToman = payment.AmountMinor / IrrToToman;
        var baseUrl = ZarinPalSandbox
            ? "https://sandbox.zarinpal.com/pg/v4/payment"
            : "https://api.zarinpal.com/pg/v4/payment";

        var http = httpClientFactory.CreateClient("zarinpal");
        var verifyBody = new
        {
            merchant_id = ZarinPalMerchantId,
            amount      = amountToman,
            authority,
        };

        var resp = await http.PostAsJsonAsync($"{baseUrl}/verify.json", verifyBody);
        resp.EnsureSuccessStatusCode();

        var json = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root = json!.RootElement;
        var code = root.GetProperty("data").GetProperty("code").GetInt32();

        if (code is 100 or 101) // 101 = already verified (idempotent)
        {
            var refId = root.GetProperty("data").GetProperty("ref_id").GetInt64().ToString();
            payment.Status       = PaymentStatus.Succeeded;
            payment.GatewayTrackId = refId;
            payment.ConfirmedAt  = DateTime.UtcNow;
            payment.UpdatedAt    = DateTime.UtcNow;

            if (payment.PlanId.HasValue)
                await ActivatePlanAsync(payment);

            await db.SaveChangesAsync();
            return $"/payment/result?status=success&ref={refId}";
        }

        payment.Status        = PaymentStatus.Failed;
        payment.FailedAt      = DateTime.UtcNow;
        payment.FailureReason = $"ZarinPal verify code: {code}";
        payment.UpdatedAt     = DateTime.UtcNow;
        await db.SaveChangesAsync();
        return "/payment/result?status=failed&gateway=zarinpal";
    }

    // ── SnapPay initiation ────────────────────────────────────────────────────────
    // API ref: https://developer.snappay.ir/
    // Flow: POST /api/v1/payment/token → get paymentToken → redirect to snappay.ir/payment/{token}
    // Callback query params: paymentToken, merchantOrderId, shapSnapStatus (DONE|FAIL|CANCEL)
    // Verify: POST /api/v1/payment/verify

    public async Task<string> InitiateSnapPayAsync(Guid paymentId, Guid userId)
    {
        var payment = await db.Payments.FirstOrDefaultAsync(
            p => p.Id == paymentId && p.UserId == userId && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment not found or already processed");

        if (string.IsNullOrEmpty(SnapPayClientId) || string.IsNullOrEmpty(SnapPayClientSecret))
            throw new InvalidOperationException("SnapPay:ClientId / SnapPay:ClientSecret are not configured");

        var amountToman = payment.AmountMinor / IrrToToman;
        var http = httpClientFactory.CreateClient("snappay");

        var reqBody = new
        {
            clientId       = SnapPayClientId,
            clientSecret   = SnapPayClientSecret,
            amount         = amountToman,
            currency       = "TOMAN",
            callbackUrl    = SnapPayCallbackUrl,
            description    = payment.Title ?? "پرداخت فلت‌رندر",
            merchantOrderId = paymentId.ToString(),
        };

        var resp = await http.PostAsJsonAsync($"{SnapPayBaseUrl}/api/v1/payment/token", reqBody);
        resp.EnsureSuccessStatusCode();

        var json = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root = json!.RootElement;

        if (!root.TryGetProperty("status", out var statusEl) || !statusEl.GetBoolean())
        {
            var error = root.TryGetProperty("error", out var e) ? e.ToString() : "SnapPay error";
            throw new InvalidOperationException($"SnapPay payment request failed: {error}");
        }

        var paymentToken = root.GetProperty("response").GetProperty("paymentToken").GetString()!;
        payment.GatewayToken = paymentToken;
        payment.UpdatedAt    = DateTime.UtcNow;
        await db.SaveChangesAsync();

        return $"https://snappay.ir/payment/{paymentToken}";
    }

    public async Task<string> HandleSnapPayCallbackAsync(string paymentToken, string shapStatus)
    {
        // shapSnapStatus values: DONE = success, FAIL / CANCEL = failure
        if (!shapStatus.Equals("DONE", StringComparison.OrdinalIgnoreCase))
            return "/payment/result?status=failed&gateway=snappay";

        var payment = await db.Payments.Include(p => p.User)
            .FirstOrDefaultAsync(p => p.GatewayToken == paymentToken && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment record not found for this token");

        var http = httpClientFactory.CreateClient("snappay");
        var verifyBody = new
        {
            clientId     = SnapPayClientId,
            clientSecret = SnapPayClientSecret,
            paymentToken,
        };

        var resp = await http.PostAsJsonAsync($"{SnapPayBaseUrl}/api/v1/payment/verify", verifyBody);
        resp.EnsureSuccessStatusCode();

        var json = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root = json!.RootElement;

        if (root.TryGetProperty("status", out var okEl) && okEl.GetBoolean())
        {
            var responseObj  = root.GetProperty("response");
            var transactionId = responseObj.TryGetProperty("transactionId", out var tid)
                ? tid.ToString() : paymentToken;

            payment.Status        = PaymentStatus.Succeeded;
            payment.GatewayTrackId = transactionId;
            payment.ConfirmedAt   = DateTime.UtcNow;
            payment.UpdatedAt     = DateTime.UtcNow;

            if (payment.PlanId.HasValue)
                await ActivatePlanAsync(payment);

            await db.SaveChangesAsync();
            return $"/payment/result?status=success&ref={transactionId}";
        }

        payment.Status        = PaymentStatus.Failed;
        payment.FailedAt      = DateTime.UtcNow;
        payment.FailureReason = root.TryGetProperty("error", out var err)
            ? err.ToString() : "SnapPay verification failed";
        payment.UpdatedAt     = DateTime.UtcNow;
        await db.SaveChangesAsync();
        return "/payment/result?status=failed&gateway=snappay";
    }

    // ── Tara initiation ────────────────────────────────────────────────────────────
    // API ref: https://www.tara.ir/documents/payment-gateway
    // Auth: X-Api-Key request header
    // Flow: POST /v1/payment/request → { token, redirectUrl }
    //       → redirect browser to redirectUrl (or fallback: {baseUrl}/payment/{token})
    // Callback query params: token, status (OK|FAILED)
    // Verify: POST /v1/payment/verify  body: { token, orderId }

    public async Task<string> InitiateTaraAsync(Guid paymentId, Guid userId)
    {
        var payment = await db.Payments.FirstOrDefaultAsync(
            p => p.Id == paymentId && p.UserId == userId && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment not found or already processed");

        if (string.IsNullOrEmpty(TaraApiKey))
            throw new InvalidOperationException("Tara:ApiKey is not configured");

        var amountToman = payment.AmountMinor / IrrToToman;
        var http = httpClientFactory.CreateClient("tara");

        using var request = new HttpRequestMessage(HttpMethod.Post, $"{TaraBaseUrl}/v1/payment/request");
        request.Headers.Add("X-Api-Key", TaraApiKey);
        request.Content = JsonContent.Create(new
        {
            amount      = amountToman,
            orderId     = paymentId.ToString(),
            callbackUrl = TaraCallbackUrl,
            description = payment.Title ?? "پرداخت فلت‌رندر",
        });

        var resp = await http.SendAsync(request);
        resp.EnsureSuccessStatusCode();

        var json = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root = json!.RootElement;

        if (!root.TryGetProperty("token", out var tokenEl))
        {
            var msg = root.TryGetProperty("message", out var m) ? m.GetString() : "Tara error";
            throw new InvalidOperationException($"Tara payment request failed: {msg}");
        }

        var token = tokenEl.GetString()!;
        payment.GatewayToken = token;
        payment.UpdatedAt    = DateTime.UtcNow;
        await db.SaveChangesAsync();

        // Use the redirectUrl Tara returns; fall back to constructed URL if absent
        if (root.TryGetProperty("redirectUrl", out var urlEl) && !string.IsNullOrEmpty(urlEl.GetString()))
            return urlEl.GetString()!;

        return $"{TaraBaseUrl}/payment/{token}";
    }

    public async Task<string> HandleTaraCallbackAsync(string token, string status)
    {
        var ok = status.Equals("OK", StringComparison.OrdinalIgnoreCase)
              || status.Equals("SUCCESS", StringComparison.OrdinalIgnoreCase);

        if (!ok)
            return "/payment/result?status=failed&gateway=tara";

        var payment = await db.Payments.Include(p => p.User)
            .FirstOrDefaultAsync(p => p.GatewayToken == token && p.Status == PaymentStatus.Pending)
            ?? throw new KeyNotFoundException("Payment record not found for this token");

        var http = httpClientFactory.CreateClient("tara");

        using var request = new HttpRequestMessage(HttpMethod.Post, $"{TaraBaseUrl}/v1/payment/verify");
        request.Headers.Add("X-Api-Key", TaraApiKey);
        request.Content = JsonContent.Create(new
        {
            token,
            orderId = payment.Id.ToString(),
        });

        var resp = await http.SendAsync(request);
        resp.EnsureSuccessStatusCode();

        var json    = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var root    = json!.RootElement;
        var verified = root.TryGetProperty("status", out var statusEl)
            && (statusEl.GetString()?.ToUpperInvariant() is "OK" or "SUCCESS" or "VERIFIED");

        if (verified)
        {
            var trackId = root.TryGetProperty("trackId", out var tid)
                ? tid.GetString() ?? token : token;

            payment.Status        = PaymentStatus.Succeeded;
            payment.GatewayTrackId = trackId;
            payment.ConfirmedAt   = DateTime.UtcNow;
            payment.UpdatedAt     = DateTime.UtcNow;

            if (payment.PlanId.HasValue)
                await ActivatePlanAsync(payment);

            await db.SaveChangesAsync();
            return $"/payment/result?status=success&ref={trackId}";
        }

        payment.Status        = PaymentStatus.Failed;
        payment.FailedAt      = DateTime.UtcNow;
        payment.FailureReason = root.TryGetProperty("message", out var msgEl)
            ? msgEl.GetString() : "Tara verification failed";
        payment.UpdatedAt     = DateTime.UtcNow;
        await db.SaveChangesAsync();
        return "/payment/result?status=failed&gateway=tara";
    }

    // ── Stripe webhook ────────────────────────────────────────────────────────────

    public async Task HandleStripeWebhookAsync(string payload, string signature)
    {
        if (!VerifyStripeSignature(payload, signature, StripeWebhookSecret))
            throw new UnauthorizedAccessException("Invalid Stripe webhook signature");

        using var json   = JsonDocument.Parse(payload);
        var eventType    = json.RootElement.GetProperty("type").GetString();

        if (eventType != "checkout.session.completed") return;

        var sessionObj   = json.RootElement.GetProperty("data").GetProperty("object");
        var metadata     = sessionObj.GetProperty("metadata");

        if (!metadata.TryGetProperty("payment_id", out var pidEl)) return;
        if (!Guid.TryParse(pidEl.GetString(), out var paymentId)) return;

        var payment = await db.Payments.Include(p => p.User).FirstOrDefaultAsync(p => p.Id == paymentId);
        if (payment is null || payment.Status != PaymentStatus.Pending) return;

        var piId = sessionObj.TryGetProperty("payment_intent", out var pi) ? pi.GetString() : null;

        payment.Status       = PaymentStatus.Succeeded;
        payment.GatewayOrderId = piId;
        payment.ConfirmedAt  = DateTime.UtcNow;
        payment.UpdatedAt    = DateTime.UtcNow;

        if (payment.PlanId.HasValue)
            await ActivatePlanAsync(payment);

        await db.SaveChangesAsync();
    }

    // ── Refunds ───────────────────────────────────────────────────────────────────

    public async Task<RefundResponse> IssueRefundAsync(
        Guid paymentId, long? amountMinor, string reason, string refundTo)
    {
        var payment = await db.Payments.Include(p => p.User)
            .FirstOrDefaultAsync(p => p.Id == paymentId)
            ?? throw new KeyNotFoundException("Payment not found");

        if (payment.Status != PaymentStatus.Succeeded)
            throw new InvalidOperationException("Only succeeded payments can be refunded");

        var refundAmount    = amountMinor ?? payment.AmountMinor;
        payment.Status      = PaymentStatus.Refunded;
        payment.RefundedAt  = DateTime.UtcNow;
        payment.RefundAmountMinor = refundAmount;
        payment.RefundReason = reason;
        payment.UpdatedAt   = DateTime.UtcNow;

        // Credit back to user balance (default) or affiliate balance
        if (string.IsNullOrEmpty(refundTo) || refundTo == "Balance")
            payment.User.BalanceMinor += refundAmount;
        else if (refundTo == "Affiliate")
            payment.User.AffiliateBalanceMinor += refundAmount;

        await db.SaveChangesAsync();
        return new RefundResponse(paymentId, "Refunded");
    }

    // ── Helpers ───────────────────────────────────────────────────────────────────

    private async Task ActivatePlanAsync(Payment payment)
    {
        var plan = await db.Plans.FindAsync(payment.PlanId!.Value);
        if (plan is null) return;

        var durationMonths = plan.MonthsDuration ?? plan.BillingPeriod switch
        {
            BillingPeriod.Monthly    => 1,
            BillingPeriod.Quarterly  => 3,
            BillingPeriod.SemiAnnual => 6,
            BillingPeriod.Annual     => 12,
            BillingPeriod.Lifetime   => 1200,
            BillingPeriod.OneTime    => 1,
            _                        => 1,
        };

        var now = DateTime.UtcNow;
        db.UserPlans.Add(new UserPlan
        {
            UserId                 = payment.UserId,
            TenantId               = payment.TenantId,
            PlanId                 = plan.Id,
            PlanCode               = plan.Code,
            PlanName               = plan.Name,
            PriceMinorPaid         = payment.AmountMinor,
            Currency               = payment.Currency,
            InitialSecondsCharge   = plan.SecondsCharge,
            RemainChargeSec        = plan.SecondsCharge,
            StartsAt               = now,
            ExpiresAt              = now.AddMonths(durationMonths),
            AutoRenew              = false,
            PaymentId              = payment.Id,
        });
    }

    private static bool VerifyStripeSignature(string payload, string header, string secret)
    {
        // Stripe header: t=<unix_ts>,v1=<hex_sig>[,...]
        if (string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(header)) return false;

        var ts  = "";
        var sig = "";
        foreach (var part in header.Split(','))
        {
            if (part.StartsWith("t="))  ts  = part[2..];
            if (part.StartsWith("v1=")) sig = part[3..];
        }
        if (string.IsNullOrEmpty(ts) || string.IsNullOrEmpty(sig)) return false;

        var message = $"{ts}.{payload}";
        using var hmac    = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var expectedBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        var expected      = Convert.ToHexString(expectedBytes).ToLowerInvariant();
        return expected == sig;
    }

    private static PaymentResponse MapPaymentResponse(Payment p) => new(
        p.Id, p.Gateway.ToString(), p.Status.ToString(), p.Action.ToString(),
        p.AmountMinor, p.Currency, p.Title, p.Description,
        p.CardLast4, p.ConfirmedAt, p.FailedAt, p.FailureReason, p.CreatedAt
    );
}
