using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using Stripe;
using Stripe.Checkout;

namespace PowderCoating.Infrastructure.Services;

/// <summary>
/// Manages Stripe subscription billing for the platform: creates checkout sessions for plan
/// upgrades, fulfills completed checkouts, syncs subscription state, opens the customer billing
/// portal, and handles inbound Stripe webhook events. All webhook events are verified with
/// HMAC signature validation before processing to ensure idempotent, tamper-proof delivery.
/// </summary>
public class StripeService : IStripeService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IConfiguration _configuration;
    private readonly IInAppNotificationService _inApp;
    private readonly ILogger<StripeService> _logger;
    private readonly string _webhookSecret;

    /// <summary>
    /// Constructs the service and sets the Stripe global API key from configuration.
    /// The webhook secret is cached at construction time rather than read per-request
    /// because <see cref="HandleWebhookAsync"/> is called on every inbound webhook and
    /// configuration reads are synchronous/cheap but the intent is to make it obvious
    /// where the secret comes from.
    /// </summary>
    public StripeService(
        IUnitOfWork unitOfWork,
        IConfiguration configuration,
        IInAppNotificationService inApp,
        ILogger<StripeService> logger)
    {
        _unitOfWork = unitOfWork;
        _configuration = configuration;
        _inApp = inApp;
        _logger = logger;

        StripeConfiguration.ApiKey = configuration["Stripe:SecretKey"] ?? string.Empty;
        _webhookSecret = configuration["Stripe:WebhookSecret"] ?? string.Empty;
    }

    /// <summary>
    /// Creates a Stripe Checkout session for an existing tenant company that is upgrading
    /// or changing its subscription plan. Persists the chosen billing period preference
    /// (<paramref name="isAnnual"/>) before creating the session so renewals default to the same
    /// cadence. Creates a Stripe Customer record on first checkout and stores the resulting
    /// <c>StripeCustomerId</c> on the company. The price ID is resolved from
    /// <c>SubscriptionPlanConfig</c> in the database; the method validates that the stored ID
    /// starts with <c>price_</c> (not <c>prod_</c>) and provides a human-readable error message
    /// if misconfigured. Returns the Stripe-hosted checkout URL to which the caller should redirect.
    /// </summary>
    public async Task<string> CreateCheckoutSessionAsync(
        int companyId,
        int newPlan,
        bool isAnnual,
        string successUrl,
        string cancelUrl)
    {
        var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true)
            ?? throw new InvalidOperationException($"Company {companyId} not found");

        // Persist the billing period preference so future renewals default to the same
        company.IsAnnualBilling = isAnnual;
        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        // Get plan config for Stripe price ID
        var planConfig = await _unitOfWork.SubscriptionPlanConfigs.FirstOrDefaultAsync(
            c => c.Plan == newPlan && c.IsActive,
            ignoreQueryFilters: true);

        // Use annual price ID when requested, fall back to monthly
        var planName = planConfig?.DisplayName ?? newPlan.ToString();
        var priceId = isAnnual
            ? (!string.IsNullOrWhiteSpace(planConfig?.StripePriceIdAnnual) ? planConfig!.StripePriceIdAnnual : null)
            : (!string.IsNullOrWhiteSpace(planConfig?.StripePriceIdMonthly)
                ? planConfig!.StripePriceIdMonthly
                : _configuration[$"Stripe:Prices:{planName}"]);

        if (string.IsNullOrWhiteSpace(priceId))
            throw new InvalidOperationException(
                $"No Stripe price ID configured for the {planName} plan. " +
                "Go to Platform Management → Subscription Plans → Edit and enter the price ID.");

        // Product IDs (prod_...) are a common mistake — price IDs must start with price_
        if (!priceId!.StartsWith("price_"))
            throw new InvalidOperationException(
                $"The value '{priceId}' saved for the {planName} plan is a Stripe product ID, not a price ID. " +
                "In your Stripe dashboard, open the product, find the specific price under the Pricing section, " +
                "and copy the ID that starts with 'price_'. " +
                "Then update it in Platform Management → Subscription Plans → Edit.");

        // Create Stripe customer if needed
        var stripeCustomerId = company.StripeCustomerId;
        if (string.IsNullOrEmpty(stripeCustomerId))
        {
            var customerService = new CustomerService();
            var customer = await customerService.CreateAsync(new CustomerCreateOptions
            {
                Email = company.PrimaryContactEmail,
                Name = company.CompanyName,
                Metadata = new Dictionary<string, string>
                {
                    ["companyId"] = companyId.ToString()
                }
            });
            stripeCustomerId = customer.Id;

            // Persist customer ID
            company.StripeCustomerId = stripeCustomerId;
            await _unitOfWork.Companies.UpdateAsync(company);
            await _unitOfWork.CompleteAsync();
        }

        var sessionService = new SessionService();
        var session = await sessionService.CreateAsync(new SessionCreateOptions
        {
            Customer = stripeCustomerId,
            Mode = "subscription",
            PaymentMethodTypes = new List<string> { "card" },
            LineItems = new List<SessionLineItemOptions>
            {
                new SessionLineItemOptions
                {
                    Price = priceId,
                    Quantity = 1
                }
            },
            // Stripe replaces {CHECKOUT_SESSION_ID} with the real session ID on redirect
            SuccessUrl = (successUrl.Contains('?') ? successUrl + "&" : successUrl + "?") + "session_id={CHECKOUT_SESSION_ID}",
            CancelUrl = cancelUrl,
            Metadata = new Dictionary<string, string>
            {
                ["companyId"] = companyId.ToString(),
                ["plan"] = newPlan.ToString()   // stored as int string
            }
        });

        return session.Url;
    }

    /// <summary>
    /// Creates a Stripe Checkout session for a new registrant who has not yet been issued a
    /// company record in the database. Unlike <see cref="CreateCheckoutSessionAsync"/>, this
    /// session is keyed by <paramref name="email"/> (via <c>CustomerEmail</c>) rather than a
    /// stored <c>StripeCustomerId</c>, and carries a <c>registration=true</c> metadata flag so
    /// the subsequent <see cref="FulfillRegistrationCheckoutAsync"/> call can distinguish
    /// registration sessions from plan-upgrade sessions in webhook handlers.
    /// Returns the Stripe-hosted checkout URL.
    /// </summary>
    public async Task<string> CreateRegistrationCheckoutSessionAsync(
        int plan, bool isAnnual, string email, string companyName, string successUrl, string cancelUrl)
    {
        var planConfig = await _unitOfWork.SubscriptionPlanConfigs.FirstOrDefaultAsync(
            c => c.Plan == plan && c.IsActive, ignoreQueryFilters: true);

        var planName = planConfig?.DisplayName ?? plan.ToString();
        var priceId = isAnnual
            ? (!string.IsNullOrWhiteSpace(planConfig?.StripePriceIdAnnual) ? planConfig!.StripePriceIdAnnual : null)
            : (!string.IsNullOrWhiteSpace(planConfig?.StripePriceIdMonthly) ? planConfig!.StripePriceIdMonthly : _configuration[$"Stripe:Prices:{planName}"]);

        if (string.IsNullOrWhiteSpace(priceId))
            throw new InvalidOperationException(
                $"No Stripe price ID configured for the {planName} plan. " +
                "Go to Platform Management → Subscription Plans → Edit and enter the price ID.");

        if (!priceId!.StartsWith("price_"))
            throw new InvalidOperationException(
                $"The value '{priceId}' for the {planName} plan is not a valid Stripe price ID (must start with 'price_').");

        var sessionService = new SessionService();
        var session = await sessionService.CreateAsync(new SessionCreateOptions
        {
            CustomerEmail = email,
            Mode = "subscription",
            PaymentMethodTypes = new List<string> { "card" },
            LineItems = new List<SessionLineItemOptions>
            {
                new() { Price = priceId, Quantity = 1 }
            },
            SuccessUrl = (successUrl.Contains('?') ? successUrl + "&" : successUrl + "?") + "session_id={CHECKOUT_SESSION_ID}",
            CancelUrl = cancelUrl,
            Metadata = new Dictionary<string, string>
            {
                ["registration"] = "true",
                ["plan"]         = plan.ToString()
            }
        });

        return session.Url;
    }

    /// <summary>
    /// Verifies that the supplied Stripe Checkout session belongs to the registration flow and has
    /// reached the paid/complete state. Returns <c>false</c> for any missing/invalid/unpaid session
    /// so the caller can safely stop before creating any local company or user records.
    /// </summary>
    public async Task<bool> IsRegistrationCheckoutPaidAsync(string sessionId)
    {
        try
        {
            var sessionService = new SessionService();
            var session = await sessionService.GetAsync(sessionId);

            if (!session.Metadata.TryGetValue("registration", out var isRegistration) ||
                !string.Equals(isRegistration, "true", StringComparison.OrdinalIgnoreCase))
            {
                _logger.LogWarning(
                    "Registration checkout validation failed for session {SessionId}: missing registration metadata",
                    sessionId);
                return false;
            }

            var isPaidAndComplete = session.PaymentStatus == "paid" && session.Status == "complete";
            if (!isPaidAndComplete)
            {
                _logger.LogWarning(
                    "Registration checkout validation failed for session {SessionId}: paymentStatus={PaymentStatus}, status={Status}",
                    sessionId, session.PaymentStatus, session.Status);
            }

            return isPaidAndComplete;
        }
        catch (StripeException ex)
        {
            _logger.LogWarning(ex, "Stripe rejected registration checkout validation for session {SessionId}", sessionId);
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error validating registration checkout session {SessionId}", sessionId);
            return false;
        }
    }

    /// <summary>
    /// Finalizes a registration checkout after payment is confirmed. Called from the
    /// registration success redirect (not the webhook path). Retrieves the session from Stripe
    /// with the subscription expanded to access the real <c>CurrentPeriodEnd</c>, then updates
    /// the newly created company record with the Stripe customer ID, subscription ID, plan, and
    /// end date. Silently returns if the session is not yet paid or the company record is missing.
    /// </summary>
    public async Task FulfillRegistrationCheckoutAsync(string sessionId, int companyId, int plan)
    {
        var sessionService = new SessionService();
        var session = await sessionService.GetAsync(sessionId, new SessionGetOptions
        {
            Expand = new List<string> { "subscription" }
        });

        if (session.PaymentStatus != "paid" && session.Status != "complete")
        {
            _logger.LogWarning("FulfillRegistrationCheckoutAsync: session {SessionId} not paid/complete", sessionId);
            return;
        }

        var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
        if (company == null) return;

        var subscription = session.Subscription;
        var periodEnd = subscription?.Items?.Data?.FirstOrDefault()?.CurrentPeriodEnd
            ?? DateTime.UtcNow.AddMonths(1);

        company.SubscriptionPlan = plan;
        company.SubscriptionStatus = SubscriptionStatus.Active;
        company.StripeCustomerId = session.CustomerId;
        company.StripeSubscriptionId = session.SubscriptionId;
        company.SubscriptionStartDate = DateTime.UtcNow;
        company.SubscriptionEndDate = periodEnd;

        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation(
            "Registration checkout fulfilled for company {CompanyId}: plan={Plan}, ends={EndDate}",
            companyId, plan, periodEnd);
    }

    /// <summary>
    /// Fulfills an existing-tenant plan-upgrade checkout after Stripe confirms payment.
    /// This method is the shared fulfillment path used by both the success-redirect controller
    /// action and the <c>checkout.session.completed</c> webhook handler — keeping both paths
    /// identical prevents divergence if only one fires (e.g. user closes browser before redirect).
    /// The subscription is expanded in the Stripe API call so the real <c>CurrentPeriodEnd</c>
    /// is available without a second round-trip. The company ID and target plan are read from
    /// session metadata set by <see cref="CreateCheckoutSessionAsync"/>.
    /// </summary>
    public async Task FulfillCheckoutAsync(string sessionId)
    {
        // Retrieve the session and expand the subscription so we get the real period end date
        var sessionService = new SessionService();
        var session = await sessionService.GetAsync(sessionId, new SessionGetOptions
        {
            Expand = new List<string> { "subscription" }
        });

        if (session.PaymentStatus != "paid" && session.Status != "complete")
        {
            _logger.LogWarning("FulfillCheckoutAsync called for unpaid/incomplete session {SessionId}", sessionId);
            return;
        }

        if (!session.Metadata.TryGetValue("companyId", out var companyIdStr) ||
            !int.TryParse(companyIdStr, out var companyId))
        {
            _logger.LogWarning("FulfillCheckoutAsync: missing companyId metadata on session {SessionId}", sessionId);
            return;
        }

        var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true);
        if (company == null) return;

        // Parse the plan from metadata
        var plan = company.SubscriptionPlan;
        if (session.Metadata.TryGetValue("plan", out var planStr) &&
            int.TryParse(planStr, out var parsedPlan))
            plan = parsedPlan;

        // Get the real subscription period end from the expanded subscription
        var subscription = session.Subscription;
        var periodEnd = subscription?.Items?.Data?.FirstOrDefault()?.CurrentPeriodEnd ?? DateTime.UtcNow.AddMonths(1);

        company.SubscriptionPlan = plan;
        company.SubscriptionStatus = SubscriptionStatus.Active;
        company.StripeCustomerId = session.CustomerId;
        company.StripeSubscriptionId = session.SubscriptionId;
        company.SubscriptionStartDate = DateTime.UtcNow;
        company.SubscriptionEndDate = periodEnd;

        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation(
            "Company {CompanyId} subscription fulfilled: plan={Plan}, ends={EndDate}",
            companyId, plan, periodEnd);
    }

    /// <summary>
    /// Pulls the current subscription state from Stripe and overwrites the local company record
    /// with the authoritative Stripe values (status, plan, period end). Used by SuperAdmins via
    /// the Platform Management UI when the local record is believed to be stale. Matches the
    /// Stripe price ID back to a <c>SubscriptionPlanConfig</c> row so the <c>SubscriptionPlan</c>
    /// int is kept in sync even if the price ID was rotated. Logs a warning (but does not throw)
    /// when no plan config matches the active price ID.
    /// </summary>
    public async Task SyncSubscriptionAsync(int companyId)
    {
        var company = await _unitOfWork.Companies.GetByIdAsync(companyId, ignoreQueryFilters: true)
            ?? throw new InvalidOperationException($"Company {companyId} not found");

        if (string.IsNullOrEmpty(company.StripeSubscriptionId))
            throw new InvalidOperationException("No Stripe subscription ID on record. Complete a checkout first.");

        var subscriptionService = new Stripe.SubscriptionService();
        var subscription = await subscriptionService.GetAsync(company.StripeSubscriptionId, new Stripe.SubscriptionGetOptions
        {
            Expand = new List<string> { "items.data.price" }
        });

        // Map Stripe status to our enum
        company.SubscriptionStatus = subscription.Status switch
        {
            "active"   => SubscriptionStatus.Active,
            "past_due" => SubscriptionStatus.GracePeriod,
            "canceled" => SubscriptionStatus.Canceled,
            "unpaid"   => SubscriptionStatus.GracePeriod,
            _          => SubscriptionStatus.Active
        };
        company.SubscriptionEndDate = subscription.Items?.Data?.FirstOrDefault()?.CurrentPeriodEnd ?? DateTime.UtcNow.AddMonths(1);

        // Match the Stripe price ID back to one of our plan configs
        var stripePriceId = subscription.Items?.Data?.FirstOrDefault()?.Price?.Id;
        if (!string.IsNullOrEmpty(stripePriceId))
        {
            var planConfigs = await _unitOfWork.SubscriptionPlanConfigs.FindAsync(
                c => c.IsActive, ignoreQueryFilters: true);

            var matchedConfig = planConfigs.FirstOrDefault(c =>
                c.StripePriceIdMonthly == stripePriceId ||
                c.StripePriceIdAnnual  == stripePriceId);

            if (matchedConfig != null)
                company.SubscriptionPlan = matchedConfig.Plan;  // int
            else
                _logger.LogWarning(
                    "SyncSubscriptionAsync: price {PriceId} did not match any plan config for company {CompanyId}",
                    stripePriceId, companyId);
        }

        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation(
            "Company {CompanyId} synced from Stripe: plan={Plan}, status={Status}, ends={EndDate}",
            companyId, company.SubscriptionPlan, company.SubscriptionStatus, company.SubscriptionEndDate);
    }

    /// <summary>
    /// Creates a Stripe Billing Portal session that lets a tenant self-manage their subscription
    /// (update payment method, view invoices, cancel) without surfacing Stripe credentials to the
    /// tenant. Returns the portal URL to which the controller should redirect the user. The portal
    /// session is short-lived (minutes) and bound to the Stripe customer ID; the
    /// <paramref name="returnUrl"/> is where Stripe sends the user when they close the portal.
    /// </summary>
    public async Task<string> CreateCustomerPortalSessionAsync(string stripeCustomerId, string returnUrl)
    {
        var service = new Stripe.BillingPortal.SessionService();
        var session = await service.CreateAsync(new Stripe.BillingPortal.SessionCreateOptions
        {
            Customer = stripeCustomerId,
            ReturnUrl = returnUrl
        });
        return session.Url;
    }

    /// <summary>
    /// Verifies and dispatches an inbound Stripe webhook event. The HMAC signature in
    /// <paramref name="stripeSignature"/> is validated against <c>_webhookSecret</c> before any
    /// processing; a <see cref="StripeException"/> is re-thrown so the caller (StripeController)
    /// can return HTTP 400, which tells Stripe the event was rejected and it should retry.
    /// Four event types are handled: <c>checkout.session.completed</c>,
    /// <c>customer.subscription.updated</c>, <c>customer.subscription.deleted</c>, and
    /// <c>invoice.payment_failed</c>. Unknown event types are logged and ignored, as Stripe
    /// delivers many event types that are irrelevant to this platform.
    /// </summary>
    public async Task HandleWebhookAsync(string json, string stripeSignature)
    {
        Event stripeEvent;
        try
        {
            stripeEvent = EventUtility.ConstructEvent(json, stripeSignature, _webhookSecret);
        }
        catch (StripeException ex)
        {
            _logger.LogError(ex, "Stripe webhook signature verification failed");
            throw;
        }

        _logger.LogInformation("Processing Stripe webhook event: {EventType}", stripeEvent.Type);

        switch (stripeEvent.Type)
        {
            case EventTypes.CheckoutSessionCompleted:
                await HandleCheckoutSessionCompletedAsync(stripeEvent);
                break;

            case EventTypes.CustomerSubscriptionUpdated:
                await HandleSubscriptionUpdatedAsync(stripeEvent);
                break;

            case EventTypes.CustomerSubscriptionDeleted:
                await HandleSubscriptionDeletedAsync(stripeEvent);
                break;

            case EventTypes.InvoicePaymentFailed:
                await HandleInvoicePaymentFailedAsync(stripeEvent);
                break;

            default:
                _logger.LogInformation("Unhandled Stripe event type: {EventType}", stripeEvent.Type);
                break;
        }
    }

    /// <summary>
    /// Handles the <c>checkout.session.completed</c> webhook by delegating to
    /// <see cref="FulfillCheckoutAsync"/>. Using the same fulfillment method as the
    /// success-redirect path ensures webhook delivery and browser redirect are idempotent —
    /// the second call is a no-op because Stripe subscription IDs do not change between calls.
    /// </summary>
    private async Task HandleCheckoutSessionCompletedAsync(Event stripeEvent)
    {
        var session = stripeEvent.Data.Object as Session;
        if (session == null) return;

        // Reuse FulfillCheckoutAsync so the webhook path has identical logic to the success redirect
        await FulfillCheckoutAsync(session.Id);
    }

    /// <summary>
    /// Handles the <c>customer.subscription.updated</c> webhook, which fires on any subscription
    /// change (plan switch, renewal, trial ending, status change). Looks up the company first by
    /// subscription ID, then falls back to customer ID in case the subscription ID was not yet
    /// persisted (e.g. during initial provisioning race conditions). Updates <c>SubscriptionStatus</c>
    /// and <c>SubscriptionEndDate</c> from the live Stripe subscription object.
    ///
    /// When a customer cancels via the billing portal, Stripe keeps the subscription in
    /// <c>"active"</c> status but sets <c>cancel_at_period_end = true</c>. Without this check the
    /// handler would leave the company as Active. We treat <c>cancel_at_period_end</c> as
    /// <c>Canceled</c> immediately so the status reflects the customer's intent, while
    /// <c>IsActive</c> and <c>SubscriptionEndDate</c> are left unchanged so they retain access
    /// until the period actually ends (at which point <c>customer.subscription.deleted</c> fires
    /// and <see cref="HandleSubscriptionDeletedAsync"/> deactivates the company).
    /// </summary>
    private async Task HandleSubscriptionUpdatedAsync(Event stripeEvent)
    {
        var subscription = stripeEvent.Data.Object as Subscription;
        if (subscription == null) return;

        var company = await _unitOfWork.Companies.FirstOrDefaultAsync(
            c => c.StripeSubscriptionId == subscription.Id,
            ignoreQueryFilters: true);

        if (company == null)
        {
            company = await _unitOfWork.Companies.FirstOrDefaultAsync(
                c => c.StripeCustomerId == subscription.CustomerId,
                ignoreQueryFilters: true);
        }

        if (company == null) return;

        company.StripeSubscriptionId = subscription.Id;
        company.SubscriptionEndDate = subscription.Items?.Data?.FirstOrDefault()?.CurrentPeriodEnd ?? DateTime.UtcNow.AddMonths(1);

        // cancel_at_period_end means the customer cancelled via the portal — reflect that
        // immediately even though Stripe still reports status "active" until the period ends.
        if (subscription.CancelAtPeriodEnd)
        {
            company.SubscriptionStatus = SubscriptionStatus.Canceled;
        }
        else
        {
            company.SubscriptionStatus = subscription.Status switch
            {
                "active"   => SubscriptionStatus.Active,
                "past_due" => SubscriptionStatus.GracePeriod,
                "canceled" => SubscriptionStatus.Canceled,
                "unpaid"   => SubscriptionStatus.GracePeriod,
                _          => SubscriptionStatus.Active
            };
        }

        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("Company {CompanyId} subscription updated: Stripe status={StripeStatus}, CancelAtPeriodEnd={CancelAtPeriodEnd}, LocalStatus={LocalStatus}",
            company.Id, subscription.Status, subscription.CancelAtPeriodEnd, company.SubscriptionStatus);
    }

    /// <summary>
    /// Handles the <c>customer.subscription.deleted</c> webhook, which fires when a subscription
    /// is fully ended in Stripe — either because the cancel-at-period-end date passed or because
    /// it was immediately cancelled. Sets <c>SubscriptionStatus</c> to
    /// <see cref="SubscriptionStatus.Canceled"/> and <c>IsActive = false</c> to block access, then
    /// fires a SuperAdmin in-app notification so the team can follow up with the churned tenant.
    /// The notification is fire-and-forget (discarded task) because a failure there should not roll
    /// back the deactivation.
    /// </summary>
    private async Task HandleSubscriptionDeletedAsync(Event stripeEvent)
    {
        var subscription = stripeEvent.Data.Object as Subscription;
        if (subscription == null) return;

        var company = await _unitOfWork.Companies.FirstOrDefaultAsync(
            c => c.StripeSubscriptionId == subscription.Id,
            ignoreQueryFilters: true);

        if (company == null) return;

        company.SubscriptionStatus = SubscriptionStatus.Canceled;
        company.IsActive = false;
        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogInformation("Company {CompanyId} subscription deleted — deactivated", company.Id);

        _ = _inApp.CreateForSuperAdminsAsync(
            "Subscription Canceled",
            $"{company.CompanyName} subscription was canceled via Stripe.",
            "SubscriptionCanceled",
            $"/Companies/Details/{company.Id}");
    }

    /// <summary>
    /// Handles the <c>invoice.payment_failed</c> webhook, which fires when Stripe cannot charge
    /// the tenant's saved payment method during renewal. Moves the company to
    /// <see cref="SubscriptionStatus.GracePeriod"/> rather than immediately canceling, giving the
    /// tenant time to update their payment method via the Stripe Billing Portal. A SuperAdmin
    /// in-app notification is created as a fire-and-forget task so the team can proactively
    /// reach out to the at-risk tenant.
    /// </summary>
    private async Task HandleInvoicePaymentFailedAsync(Event stripeEvent)
    {
        var invoice = stripeEvent.Data.Object as Invoice;
        if (invoice == null || string.IsNullOrEmpty(invoice.CustomerId)) return;

        var company = await _unitOfWork.Companies.FirstOrDefaultAsync(
            c => c.StripeCustomerId == invoice.CustomerId,
            ignoreQueryFilters: true);

        if (company == null) return;

        company.SubscriptionStatus = SubscriptionStatus.GracePeriod;
        await _unitOfWork.Companies.UpdateAsync(company);
        await _unitOfWork.CompleteAsync();

        _logger.LogWarning("Company {CompanyId} payment failed — set to grace period", company.Id);

        _ = _inApp.CreateForSuperAdminsAsync(
            "Subscription Payment Failed",
            $"{company.CompanyName} subscription payment failed — moved to grace period.",
            "PaymentFailed",
            $"/Companies/Details/{company.Id}");
    }
}
