using Nalu.Web.Data.Repositories;
using Stripe;
using Stripe.Checkout;
using NaluSubscription = Nalu.Web.Data.Models.Subscription;

namespace Nalu.Web.Endpoints;

public static class StripeEndpoints
{
    public static void MapStripeEndpoints(this WebApplication app)
    {
        app.MapPost("/webhooks/stripe", async (
            HttpContext ctx,
            IConfiguration config,
            WebhookEventRepository webhookEvents,
            SubscriptionRepository subscriptions,
            UserRepository users,
            ILogger<Program> logger,
            CancellationToken ct) =>
        {
            var webhookSecret = config["Stripe:WebhookSecret"] ?? "";
            if (string.IsNullOrEmpty(webhookSecret))
            {
                logger.LogError("Stripe:WebhookSecret not configured");
                return Results.StatusCode(500);
            }

            string json;
            using (var reader = new StreamReader(ctx.Request.Body))
                json = await reader.ReadToEndAsync(ct);

            var signature = ctx.Request.Headers["Stripe-Signature"].FirstOrDefault();
            if (string.IsNullOrEmpty(signature))
                return Results.BadRequest("Missing Stripe-Signature");

            Event stripeEvent;
            try
            {
                stripeEvent = EventUtility.ConstructEvent(json, signature, webhookSecret,
                    throwOnApiVersionMismatch: false);
            }
            catch (StripeException ex)
            {
                logger.LogWarning("Stripe signature validation failed: {Msg}", ex.Message);
                return Results.BadRequest("Invalid signature");
            }

            // ── Idempotency check ─────────────────────────────────────────────
            var isNew = await webhookEvents.TryInsertAsync(stripeEvent.Id, stripeEvent.Type, ct);
            if (!isNew)
            {
                logger.LogInformation("Duplicate webhook {EventId} ({Type}) — skipped", stripeEvent.Id, stripeEvent.Type);
                return Results.Ok();
            }

            logger.LogInformation("Processing webhook {EventId} {Type}", stripeEvent.Id, stripeEvent.Type);

            try
            {
                switch (stripeEvent.Type)
                {
                    case "checkout.session.completed":
                        await HandleCheckoutCompleted(stripeEvent, subscriptions, users, logger, ct);
                        break;

                    case "invoice.paid":
                        await HandleInvoicePaid(stripeEvent, subscriptions, users, logger, ct);
                        break;

                    case "invoice.payment_failed":
                        await HandleInvoicePaymentFailed(stripeEvent, subscriptions, logger, ct);
                        break;

                    case "customer.subscription.updated":
                        await HandleSubscriptionUpdated(stripeEvent, subscriptions, logger, ct);
                        break;

                    case "customer.subscription.deleted":
                        await HandleSubscriptionDeleted(stripeEvent, subscriptions, users, logger, ct);
                        break;

                    default:
                        logger.LogInformation("Unhandled event type: {Type}", stripeEvent.Type);
                        break;
                }
            }
            catch (Exception ex)
            {
                // Return 200 to Stripe to avoid retries for permanent failures.
                // Log and investigate manually.
                logger.LogError(ex, "Error processing webhook {EventId} {Type}", stripeEvent.Id, stripeEvent.Type);
            }

            return Results.Ok();
        })
        .WithName("StripeWebhook")
        .ExcludeFromDescription(); // hide from OpenAPI
    }

    // ── checkout.session.completed ────────────────────────────────────────────
    // Fires on first successful payment. Metadata contains user_id + plan.
    private static async Task HandleCheckoutCompleted(
        Event stripeEvent, SubscriptionRepository subscriptions,
        UserRepository users, ILogger logger, CancellationToken ct)
    {
        if (stripeEvent.Data.Object is not Session session) return;

        var userId = session.Metadata?.GetValueOrDefault("user_id");
        var plan   = session.Metadata?.GetValueOrDefault("plan");

        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(plan))
        {
            logger.LogWarning("checkout.session.completed missing metadata user_id/plan — session {Id}", session.Id);
            return;
        }

        var stripeSubId    = session.SubscriptionId;
        var stripeCustomId = session.CustomerId;

        if (string.IsNullOrEmpty(stripeSubId))
        {
            logger.LogWarning("checkout.session.completed has no SubscriptionId — session {Id}", session.Id);
            return;
        }

        // Upsert subscription record
        var sub = new NaluSubscription
        {
            UserId               = userId,
            StripeSubscriptionId = stripeSubId,
            StripeCustomerId     = stripeCustomId ?? "",
            Plan                 = plan,
            Status               = "active",
            CurrentPeriodStart   = session.Created,             // approximate — will be corrected by invoice.paid
            CurrentPeriodEnd     = session.Created.AddMonths(1),
        };
        await subscriptions.UpsertAsync(sub, ct);

        // Link Stripe customer to user + upgrade plan
        if (!string.IsNullOrEmpty(stripeCustomId))
            await users.SetStripeCustomerAsync(userId, stripeCustomId, ct);

        await users.UpdatePlanAsync(userId, plan, ct);

        logger.LogInformation("Checkout completed: user {UserId} upgraded to {Plan}", userId, plan);
    }

    // ── invoice.paid ──────────────────────────────────────────────────────────
    // Fires on every successful billing cycle (including first payment).
    // First-cycle invoice.paid fires after checkout.session.completed,
    // so idempotency protects the user plan from being double-written.
    private static async Task HandleInvoicePaid(
        Event stripeEvent, SubscriptionRepository subscriptions,
        UserRepository users, ILogger logger, CancellationToken ct)
    {
        if (stripeEvent.Data.Object is not Invoice invoice) return;

        var stripeSubId = invoice.SubscriptionId;
        if (string.IsNullOrEmpty(stripeSubId)) return;

        var sub = await subscriptions.FindByStripeIdAsync(stripeSubId, ct);
        if (sub == null)
        {
            logger.LogWarning("invoice.paid: no subscription found for {SubId}", stripeSubId);
            return;
        }

        sub.Status               = "active";
        sub.CurrentPeriodStart   = invoice.PeriodStart;
        sub.CurrentPeriodEnd     = invoice.PeriodEnd;
        await subscriptions.UpsertAsync(sub, ct);

        // Re-apply plan in case it was downgraded by a failed payment
        await users.UpdatePlanAsync(sub.UserId, sub.Plan, ct);

        logger.LogInformation("Invoice paid: subscription {SubId} renewed for user {UserId}", stripeSubId, sub.UserId);
    }

    // ── invoice.payment_failed ────────────────────────────────────────────────
    // Stripe will retry automatically. Mark as past_due but keep plan active
    // until customer.subscription.deleted fires (after all retries exhausted).
    private static async Task HandleInvoicePaymentFailed(
        Event stripeEvent, SubscriptionRepository subscriptions,
        ILogger logger, CancellationToken ct)
    {
        if (stripeEvent.Data.Object is not Invoice invoice) return;

        var stripeSubId = invoice.SubscriptionId;
        if (string.IsNullOrEmpty(stripeSubId)) return;

        await subscriptions.UpdateStatusAsync(stripeSubId, "past_due", ct);

        logger.LogWarning("Invoice payment failed: subscription {SubId} marked past_due", stripeSubId);
    }

    // ── customer.subscription.updated ────────────────────────────────────────
    // Plan upgrades/downgrades via Stripe portal, or CancelAtPeriodEnd toggled.
    private static async Task HandleSubscriptionUpdated(
        Event stripeEvent, SubscriptionRepository subscriptions,
        ILogger logger, CancellationToken ct)
    {
        if (stripeEvent.Data.Object is not Subscription stripeSub) return;

        var sub = await subscriptions.FindByStripeIdAsync(stripeSub.Id, ct);
        if (sub == null)
        {
            logger.LogWarning("subscription.updated: no local record for {SubId}", stripeSub.Id);
            return;
        }

        sub.Status             = stripeSub.Status;
        sub.CancelAtPeriodEnd  = stripeSub.CancelAtPeriodEnd;
        sub.CurrentPeriodStart = stripeSub.CurrentPeriodStart;
        sub.CurrentPeriodEnd   = stripeSub.CurrentPeriodEnd;
        await subscriptions.UpsertAsync(sub, ct);

        logger.LogInformation("Subscription updated: {SubId} status={Status} cancelAtEnd={Cancel}",
            stripeSub.Id, stripeSub.Status, stripeSub.CancelAtPeriodEnd);
    }

    // ── customer.subscription.deleted ────────────────────────────────────────
    // All retries exhausted, or user cancelled immediately.
    private static async Task HandleSubscriptionDeleted(
        Event stripeEvent, SubscriptionRepository subscriptions,
        UserRepository users, ILogger logger, CancellationToken ct)
    {
        if (stripeEvent.Data.Object is not Subscription stripeSub) return;

        var sub = await subscriptions.FindByStripeIdAsync(stripeSub.Id, ct);
        if (sub == null)
        {
            logger.LogWarning("subscription.deleted: no local record for {SubId}", stripeSub.Id);
            return;
        }

        await subscriptions.UpdateStatusAsync(stripeSub.Id, "canceled", ct);
        await users.UpdatePlanAsync(sub.UserId, "free", ct);

        logger.LogInformation("Subscription deleted: user {UserId} downgraded to free", sub.UserId);
    }
}
