using Microsoft.Extensions.Options;
using PlotLine.Data;
using PlotLine.Models;
using Stripe;
using Stripe.Checkout;

namespace PlotLine.Services;

public interface IStripeBillingService
{
    Task<StripeBillingActionResult> StartCheckoutOrChangePlanAsync(AppUser user, StripeCheckoutRequest request, string successUrl, string cancelUrl);
    Task<StripeBillingActionResult> CancelAtPeriodEndAsync(AppUser user);
    Task<StripeBillingActionResult> CreateBillingPortalSessionAsync(AppUser user, string returnUrl);
    Task<StripeWebhookHandleResult> HandleWebhookAsync(string json, string signatureHeader);
}

public sealed class StripeBillingActionResult
{
    public bool Succeeded { get; init; }
    public string? RedirectUrl { get; init; }
    public string Message { get; init; } = string.Empty;
    public static StripeBillingActionResult Success(string message, string? redirectUrl = null) => new() { Succeeded = true, Message = message, RedirectUrl = redirectUrl };
    public static StripeBillingActionResult Failure(string message) => new() { Message = message };
}

public sealed class StripeWebhookHandleResult
{
    public int StatusCode { get; init; }
    public string Message { get; init; } = string.Empty;
    public static StripeWebhookHandleResult Ok(string message = "OK") => new() { StatusCode = StatusCodes.Status200OK, Message = message };
    public static StripeWebhookHandleResult BadRequest(string message) => new() { StatusCode = StatusCodes.Status400BadRequest, Message = message };
}

public sealed class StripeBillingService(
    ISubscriptionRepository subscriptions,
    ISubscriptionService subscriptionService,
    IOptions<StripeSettings> stripeOptions,
    ILogger<StripeBillingService> logger) : IStripeBillingService
{
    private readonly StripeSettings settings = stripeOptions.Value;

    public async Task<StripeBillingActionResult> StartCheckoutOrChangePlanAsync(AppUser user, StripeCheckoutRequest request, string successUrl, string cancelUrl)
    {
        var level = await subscriptions.GetSubscriptionLevelAsync(request.SubscriptionLevelID);
        if (level is null || !level.IsActive)
        {
            return StripeBillingActionResult.Failure("The selected subscription plan is not available.");
        }

        if (!level.RequiresStripeSubscription)
        {
            return StripeBillingActionResult.Failure("Draft Desk is managed inside PlotDirector and does not require Stripe Checkout.");
        }

        var billingInterval = NormalizeBillingInterval(request.BillingInterval);
        var priceId = ResolvePriceId(level, billingInterval);
        if (string.IsNullOrWhiteSpace(priceId))
        {
            return StripeBillingActionResult.Failure("The selected billing interval is not available for this plan.");
        }

        if (string.IsNullOrWhiteSpace(settings.SecretKey))
        {
            return StripeBillingActionResult.Failure("Stripe test configuration is not available.");
        }

        StripeConfiguration.ApiKey = settings.SecretKey;

        var currentSubscription = await subscriptionService.GetCurrentSubscriptionAsync(user.UserID);
        if (!string.IsNullOrWhiteSpace(currentSubscription?.StripeSubscriptionId)
            && IsStripeSubscriptionActive(currentSubscription.StripeStatus))
        {
            var updateResult = await ChangeExistingSubscriptionAsync(currentSubscription, level, priceId, billingInterval);
            return updateResult;
        }

        var customerId = await GetOrCreateCustomerAsync(user, currentSubscription);
        var sessionService = new Stripe.Checkout.SessionService();
        var session = await sessionService.CreateAsync(new Stripe.Checkout.SessionCreateOptions
        {
            Mode = "subscription",
            Customer = customerId,
            SuccessUrl = successUrl,
            CancelUrl = cancelUrl,
            ClientReferenceId = user.UserID.ToString(),
            Metadata = new Dictionary<string, string>
            {
                ["UserID"] = user.UserID.ToString(),
                ["SubscriptionLevelID"] = level.SubscriptionLevelID.ToString(),
                ["BillingInterval"] = billingInterval
            },
            SubscriptionData = new SessionSubscriptionDataOptions
            {
                Metadata = new Dictionary<string, string>
                {
                    ["UserID"] = user.UserID.ToString(),
                    ["SubscriptionLevelID"] = level.SubscriptionLevelID.ToString(),
                    ["BillingInterval"] = billingInterval
                }
            },
            LineItems =
            [
                new SessionLineItemOptions
                {
                    Price = priceId,
                    Quantity = 1
                }
            ]
        });

        return StripeBillingActionResult.Success("Redirecting to Stripe Checkout.", session.Url);
    }

    public async Task<StripeBillingActionResult> CancelAtPeriodEndAsync(AppUser user)
    {
        var currentSubscription = await subscriptionService.GetCurrentSubscriptionAsync(user.UserID);
        if (string.IsNullOrWhiteSpace(currentSubscription?.StripeSubscriptionId))
        {
            return StripeBillingActionResult.Failure("No paid Stripe subscription is active for this account.");
        }

        if (string.IsNullOrWhiteSpace(settings.SecretKey))
        {
            return StripeBillingActionResult.Failure("Stripe test configuration is not available.");
        }

        StripeConfiguration.ApiKey = settings.SecretKey;
        var stripeSubscription = await new Stripe.SubscriptionService().UpdateAsync(
            currentSubscription.StripeSubscriptionId,
            new SubscriptionUpdateOptions { CancelAtPeriodEnd = true });

        await subscriptions.ApplyStripeStateAsync(BuildSyncState(stripeSubscription, user.UserID, currentSubscription.SubscriptionLevelID, currentSubscription.BillingInterval, null));
        return StripeBillingActionResult.Success("Your subscription will cancel at the end of the current billing period.");
    }

    public async Task<StripeBillingActionResult> CreateBillingPortalSessionAsync(AppUser user, string returnUrl)
    {
        var currentSubscription = await subscriptionService.GetCurrentSubscriptionAsync(user.UserID);
        if (string.IsNullOrWhiteSpace(currentSubscription?.StripeCustomerId))
        {
            return StripeBillingActionResult.Failure("No Stripe customer is linked to this account yet.");
        }

        if (string.IsNullOrWhiteSpace(settings.SecretKey))
        {
            return StripeBillingActionResult.Failure("Stripe test configuration is not available.");
        }

        StripeConfiguration.ApiKey = settings.SecretKey;
        try
        {
            var portalSession = await new Stripe.BillingPortal.SessionService().CreateAsync(new Stripe.BillingPortal.SessionCreateOptions
            {
                Customer = currentSubscription.StripeCustomerId,
                ReturnUrl = returnUrl
            });

            return StripeBillingActionResult.Success("Redirecting to Stripe Billing Portal.", portalSession.Url);
        }
        catch (StripeException ex)
        {
            logger.LogWarning(ex, "Stripe Billing Portal session could not be created for user {UserID}.", user.UserID);
            return StripeBillingActionResult.Failure("Stripe Billing Portal is not available yet.");
        }
    }

    public async Task<StripeWebhookHandleResult> HandleWebhookAsync(string json, string signatureHeader)
    {
        if (string.IsNullOrWhiteSpace(settings.WebhookSecret))
        {
            return StripeWebhookHandleResult.BadRequest("Stripe webhook configuration is not available.");
        }

        Event stripeEvent;
        try
        {
            stripeEvent = EventUtility.ConstructEvent(json, signatureHeader, settings.WebhookSecret);
        }
        catch (StripeException ex)
        {
            logger.LogWarning(ex, "Invalid Stripe webhook signature.");
            return StripeWebhookHandleResult.BadRequest("Invalid Stripe signature.");
        }

        var shouldProcess = await subscriptions.TryBeginStripeWebhookEventAsync(stripeEvent.Id, stripeEvent.Type);
        if (!shouldProcess)
        {
            return StripeWebhookHandleResult.Ok("Duplicate event ignored.");
        }

        try
        {
            await ProcessWebhookEventAsync(stripeEvent);
            await subscriptions.CompleteStripeWebhookEventAsync(stripeEvent.Id, "Processed");
            return StripeWebhookHandleResult.Ok();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Stripe webhook {EventId} failed.", stripeEvent.Id);
            await subscriptions.CompleteStripeWebhookEventAsync(stripeEvent.Id, "Failed", ex.Message);
            return StripeWebhookHandleResult.BadRequest("Webhook processing failed.");
        }
    }

    private async Task<StripeBillingActionResult> ChangeExistingSubscriptionAsync(UserSubscriptionDetail currentSubscription, SubscriptionLevel level, string priceId, string billingInterval)
    {
        var stripeSubscriptionService = new Stripe.SubscriptionService();
        var stripeSubscription = await stripeSubscriptionService.GetAsync(currentSubscription.StripeSubscriptionId);
        var itemId = stripeSubscription.Items?.Data?.FirstOrDefault()?.Id;
        if (string.IsNullOrWhiteSpace(itemId))
        {
            return StripeBillingActionResult.Failure("The current Stripe subscription could not be updated.");
        }

        var updated = await stripeSubscriptionService.UpdateAsync(
            currentSubscription.StripeSubscriptionId,
            new SubscriptionUpdateOptions
            {
                Items =
                [
                    new SubscriptionItemOptions
                    {
                        Id = itemId,
                        Price = priceId
                    }
                ],
                Metadata = new Dictionary<string, string>
                {
                    ["UserID"] = currentSubscription.UserID.ToString(),
                    ["SubscriptionLevelID"] = level.SubscriptionLevelID.ToString(),
                    ["BillingInterval"] = billingInterval
                }
            });

        await subscriptions.ApplyStripeStateAsync(BuildSyncState(updated, currentSubscription.UserID, level.SubscriptionLevelID, billingInterval, null));
        return StripeBillingActionResult.Success("Your subscription change has been sent to Stripe.");
    }

    private async Task<string> GetOrCreateCustomerAsync(AppUser user, UserSubscriptionDetail? currentSubscription)
    {
        if (!string.IsNullOrWhiteSpace(currentSubscription?.StripeCustomerId))
        {
            return currentSubscription.StripeCustomerId;
        }

        var customer = await new CustomerService().CreateAsync(new CustomerCreateOptions
        {
            Email = user.Email,
            Name = user.DisplayName,
            Metadata = new Dictionary<string, string> { ["UserID"] = user.UserID.ToString() }
        });

        await subscriptions.SetStripeCustomerAsync(user.UserID, customer.Id);
        return customer.Id;
    }

    private async Task ProcessWebhookEventAsync(Event stripeEvent)
    {
        switch (stripeEvent.Type)
        {
            case "checkout.session.completed":
                await HandleCheckoutSessionCompletedAsync(stripeEvent);
                break;
            case "customer.subscription.created":
            case "customer.subscription.updated":
                await HandleSubscriptionUpdatedAsync(stripeEvent);
                break;
            case "customer.subscription.deleted":
                await HandleSubscriptionDeletedAsync(stripeEvent);
                break;
            case "invoice.payment_failed":
                await HandleInvoicePaymentFailedAsync(stripeEvent);
                break;
            case "invoice.payment_succeeded":
                await HandleInvoicePaymentSucceededAsync(stripeEvent);
                break;
            default:
                logger.LogInformation("Stripe webhook event {EventType} ignored.", stripeEvent.Type);
                break;
        }
    }

    private async Task HandleCheckoutSessionCompletedAsync(Event stripeEvent)
    {
        var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
        if (session is null || string.IsNullOrWhiteSpace(session.SubscriptionId))
        {
            return;
        }

        int.TryParse(session.Metadata.GetValueOrDefault("UserID"), out var userId);
        int.TryParse(session.Metadata.GetValueOrDefault("SubscriptionLevelID"), out var subscriptionLevelId);
        var billingInterval = session.Metadata.GetValueOrDefault("BillingInterval");
        var stripeSubscription = await new Stripe.SubscriptionService().GetAsync(session.SubscriptionId);
        await subscriptions.ApplyStripeStateAsync(BuildSyncState(stripeSubscription, userId == 0 ? null : userId, subscriptionLevelId == 0 ? null : subscriptionLevelId, billingInterval, stripeEvent.Id));
    }

    private async Task HandleSubscriptionUpdatedAsync(Event stripeEvent)
    {
        var stripeSubscription = stripeEvent.Data.Object as Subscription;
        if (stripeSubscription is null)
        {
            return;
        }

        int.TryParse(stripeSubscription.Metadata.GetValueOrDefault("UserID"), out var userId);
        int.TryParse(stripeSubscription.Metadata.GetValueOrDefault("SubscriptionLevelID"), out var subscriptionLevelId);
        var billingInterval = stripeSubscription.Metadata.GetValueOrDefault("BillingInterval");
        await subscriptions.ApplyStripeStateAsync(BuildSyncState(stripeSubscription, userId == 0 ? null : userId, subscriptionLevelId == 0 ? null : subscriptionLevelId, billingInterval, stripeEvent.Id));
    }

    private async Task HandleSubscriptionDeletedAsync(Event stripeEvent)
    {
        var stripeSubscription = stripeEvent.Data.Object as Subscription;
        if (stripeSubscription is null)
        {
            return;
        }

        await subscriptions.DowngradeToDraftDeskAsync(stripeSubscription.CustomerId, stripeSubscription.Id, stripeEvent.Id);
    }

    private async Task HandleInvoicePaymentFailedAsync(Event stripeEvent)
    {
        var invoice = stripeEvent.Data.Object as Invoice;
        if (invoice is null)
        {
            return;
        }

        await subscriptions.RecordPaymentFailureAsync(invoice.CustomerId, invoice.Parent?.SubscriptionDetails?.SubscriptionId, stripeEvent.Id);
    }

    private async Task HandleInvoicePaymentSucceededAsync(Event stripeEvent)
    {
        var invoice = stripeEvent.Data.Object as Invoice;
        if (invoice is null)
        {
            return;
        }

        await subscriptions.ClearPaymentFailureAsync(invoice.CustomerId, invoice.Parent?.SubscriptionDetails?.SubscriptionId, stripeEvent.Id);
    }

    private static StripeSubscriptionSyncState BuildSyncState(Subscription stripeSubscription, int? userId, int? subscriptionLevelId, string? billingInterval, string? eventId)
    {
        var item = stripeSubscription.Items?.Data?.FirstOrDefault();
        var price = item?.Price;
        return new StripeSubscriptionSyncState
        {
            UserID = userId,
            SubscriptionLevelID = subscriptionLevelId,
            StripeCustomerId = stripeSubscription.CustomerId ?? string.Empty,
            StripeSubscriptionId = stripeSubscription.Id,
            StripePriceId = price?.Id ?? string.Empty,
            BillingInterval = billingInterval ?? StripeIntervalToBillingInterval(price?.Recurring?.Interval),
            StripeStatus = stripeSubscription.Status ?? string.Empty,
            CurrentPeriodStartUTC = item?.CurrentPeriodStart,
            CurrentPeriodEndUTC = item?.CurrentPeriodEnd,
            CancelAtPeriodEnd = stripeSubscription.CancelAtPeriodEnd,
            LastStripeEventId = eventId
        };
    }

    private static string NormalizeBillingInterval(string? value) =>
        string.Equals(value, "Annual", StringComparison.OrdinalIgnoreCase) ? "Annual" : "Monthly";

    private static string? ResolvePriceId(SubscriptionLevel level, string billingInterval) =>
        string.Equals(billingInterval, "Annual", StringComparison.OrdinalIgnoreCase)
            ? level.StripeAnnualPriceId
            : level.StripeMonthlyPriceId;

    private static string? StripeIntervalToBillingInterval(string? interval) =>
        string.Equals(interval, "year", StringComparison.OrdinalIgnoreCase) ? "Annual" :
        string.Equals(interval, "month", StringComparison.OrdinalIgnoreCase) ? "Monthly" :
        null;

    private static bool IsStripeSubscriptionActive(string? status) =>
        string.Equals(status, "active", StringComparison.OrdinalIgnoreCase)
        || string.Equals(status, "trialing", StringComparison.OrdinalIgnoreCase);
}
