using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Common;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformBilling;

internal sealed class PlatformBillingSettlementService(
    TikuDbContext dbContext,
    ITenantFeatureCacheInvalidator featureCacheInvalidator) : IPlatformBillingSettlementService
{
    public async Task<PlatformBillingPayment> MarkPaidAsync(
        Guid paymentId,
        string providerEventId,
        string eventType,
        string? providerTradeNo,
        DateTimeOffset paidAt,
        JsonElement payload,
        Guid? actorUserId,
        CancellationToken cancellationToken = default)
    {
        var payment = await dbContext.PlatformBillingPayments.SingleOrDefaultAsync(value => value.Id == paymentId, cancellationToken)
            ?? throw Error("Platform billing payment was not found.", "platform_billing_payment_not_found");
        if (await dbContext.PlatformBillingPaymentEvents.AnyAsync(value =>
                value.TenantId == payment.TenantId &&
                value.Provider == payment.Provider &&
                value.ProviderEventId == providerEventId, cancellationToken))
        {
            return payment;
        }

        var order = await dbContext.PlatformBillingOrders.SingleAsync(value =>
            value.TenantId == payment.TenantId && value.Id == payment.OrderId, cancellationToken);
        if (payment.AmountCents != order.TotalAmountCents)
        {
            throw Error("Payment amount does not match the order.", "platform_billing_payment_amount_mismatch");
        }

        dbContext.PlatformBillingPaymentEvents.Add(new PlatformBillingPaymentEvent
        {
            TenantId = payment.TenantId,
            PaymentId = payment.Id,
            Provider = payment.Provider,
            ProviderEventId = providerEventId,
            EventType = eventType,
            Payload = payload.ValueKind == JsonValueKind.Undefined ? JsonDefaults.Object() : payload.Clone()
        });
        if (payment.Status == PlatformBillingPaymentStatus.Succeeded)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
            return payment;
        }
        if (payment.Status != PlatformBillingPaymentStatus.Pending || order.Status != PlatformBillingOrderStatus.PendingPayment)
        {
            throw Error("Payment or order status does not allow settlement.", "platform_billing_payment_status_invalid");
        }

        payment.Status = PlatformBillingPaymentStatus.Succeeded;
        payment.ProviderTradeNo = providerTradeNo;
        payment.PaidAt = paidAt;
        order.Status = PlatformBillingOrderStatus.Paid;
        order.PaidAt = paidAt;
        var tenant = await dbContext.Tenants.SingleAsync(value => value.Id == order.TenantId, cancellationToken);
        tenant.BillingStatus = BillingStatus.Active;

        var orderItems = await dbContext.PlatformBillingOrderItems.AsNoTracking()
            .Where(value => value.TenantId == order.TenantId && value.OrderId == order.Id)
            .ToArrayAsync(cancellationToken);
        var baseItem = orderItems.SingleOrDefault(value => value.ItemType == PlatformBillingItemType.BasePlan)
            ?? throw Error("Order base plan item is missing.", "platform_billing_base_plan_missing");
        var baseVersion = await dbContext.SaasOfferingVersions.AsNoTracking()
            .SingleAsync(value => value.Id == baseItem.OfferingVersionId, cancellationToken);
        var subscription = await dbContext.TenantSaasSubscriptions
            .OrderByDescending(value => value.UpdatedAt)
            .FirstOrDefaultAsync(value => value.TenantId == order.TenantId, cancellationToken);
        var now = paidAt;
        if (subscription is null)
        {
            subscription = new TenantSaasSubscription
            {
                TenantId = order.TenantId,
                BaseOfferingVersionId = baseVersion.Id,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = now,
                CurrentPeriodStart = now,
                CurrentPeriodEnd = AddCycle(now, baseVersion.BillingCycle)
            };
            dbContext.TenantSaasSubscriptions.Add(subscription);
        }
        else if (order.Purpose == PlatformBillingOrderPurpose.Renewal)
        {
            var periodStart = subscription.CurrentPeriodEnd > now ? subscription.CurrentPeriodEnd : now;
            subscription.Status = TenantSaasSubscriptionStatus.Active;
            subscription.CurrentPeriodStart = subscription.CurrentPeriodEnd > now
                ? subscription.CurrentPeriodStart
                : now;
            subscription.CurrentPeriodEnd = AddCycle(periodStart, baseVersion.BillingCycle);
            subscription.CancelAtPeriodEnd = false;
            subscription.CancelledAt = null;
        }
        else if (order.Purpose == PlatformBillingOrderPurpose.Downgrade)
        {
            subscription.ScheduledBaseOfferingVersionId = baseVersion.Id;
        }
        else
        {
            subscription.BaseOfferingVersionId = baseVersion.Id;
            subscription.ScheduledBaseOfferingVersionId = null;
            subscription.Status = TenantSaasSubscriptionStatus.Active;
            subscription.CancelAtPeriodEnd = false;
            subscription.CancelledAt = null;
            if (subscription.CurrentPeriodEnd <= now)
            {
                subscription.CurrentPeriodStart = now;
                subscription.CurrentPeriodEnd = AddCycle(now, baseVersion.BillingCycle);
            }
        }

        if (order.Purpose != PlatformBillingOrderPurpose.Downgrade)
        {
            var existingItems = await dbContext.TenantSaasSubscriptionItems
                .Where(value => value.TenantId == order.TenantId && value.SubscriptionId == subscription.Id &&
                                value.Status == TenantSaasSubscriptionItemStatus.Active)
                .ToArrayAsync(cancellationToken);
            foreach (var existing in existingItems)
            {
                existing.Status = TenantSaasSubscriptionItemStatus.Cancelled;
                existing.EndsAt = now > existing.StartsAt ? now : existing.StartsAt.AddTicks(1);
            }
            dbContext.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
            {
                TenantId = order.TenantId,
                SubscriptionId = subscription.Id,
                OfferingVersionId = item.OfferingVersionId,
                SourceOrderItemId = item.Id,
                ItemType = item.ItemType == PlatformBillingItemType.BasePlan
                    ? TenantSaasSubscriptionItemType.BasePlan
                    : TenantSaasSubscriptionItemType.AddOn,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = now,
                EndsAt = subscription.CurrentPeriodEnd
            }));
        }
        else
        {
            var existingScheduledItems = await dbContext.TenantSaasSubscriptionItems
                .Where(value => value.TenantId == order.TenantId &&
                                value.SubscriptionId == subscription.Id &&
                                value.Status == TenantSaasSubscriptionItemStatus.Scheduled)
                .ToArrayAsync(cancellationToken);
            foreach (var existing in existingScheduledItems)
            {
                existing.Status = TenantSaasSubscriptionItemStatus.Cancelled;
                existing.EndsAt = now > existing.StartsAt ? now : existing.StartsAt.AddTicks(1);
            }
            dbContext.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
            {
                TenantId = order.TenantId,
                SubscriptionId = subscription.Id,
                OfferingVersionId = item.OfferingVersionId,
                SourceOrderItemId = item.Id,
                ItemType = item.ItemType == PlatformBillingItemType.BasePlan
                    ? TenantSaasSubscriptionItemType.BasePlan
                    : TenantSaasSubscriptionItemType.AddOn,
                Status = TenantSaasSubscriptionItemStatus.Scheduled,
                StartsAt = subscription.CurrentPeriodEnd,
                EndsAt = AddCycle(subscription.CurrentPeriodEnd, baseVersion.BillingCycle)
            }));
        }

        var invoice = new PlatformBillingInvoice
        {
            TenantId = order.TenantId,
            OrderId = order.Id,
            InvoiceNo = $"SI{now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}",
            Status = PlatformBillingInvoiceStatus.Paid,
            TotalAmountCents = order.TotalAmountCents,
            Currency = order.Currency,
            IssuedAt = now,
            PaidAt = now,
            BillingProfileSnapshot = await LoadBillingProfileSnapshotAsync(order.TenantId, cancellationToken)
        };
        dbContext.PlatformBillingInvoices.Add(invoice);
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = order.TenantId,
            ActorUserId = actorUserId,
            Action = "platform_billing.payment.settled",
            TargetType = "platform_billing_orders",
            TargetId = order.Id.ToString(),
            Details = JsonSerializer.SerializeToElement(new { order.OrderNo, payment.PaymentNo, payment.Provider, order.Purpose })
        });
        await dbContext.SaveChangesAsync(cancellationToken);
        await featureCacheInvalidator.InvalidateAsync(order.TenantId, cancellationToken);

        return payment;
    }

    private async Task<JsonElement> LoadBillingProfileSnapshotAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        var profile = await dbContext.TenantBillingProfiles.AsNoTracking()
            .SingleOrDefaultAsync(value => value.TenantId == tenantId, cancellationToken);
        return profile is null
            ? JsonDefaults.Object()
            : JsonSerializer.SerializeToElement(new
            {
                profile.BillingName,
                profile.TaxId,
                profile.ContactName,
                profile.ContactPhone,
                profile.ContactEmail,
                profile.InvoiceTitle,
                profile.InvoiceType
            });
    }

    private static DateTimeOffset AddCycle(DateTimeOffset start, PlatformBillingCycle cycle) => cycle switch
    {
        PlatformBillingCycle.Monthly => start.AddMonths(1),
        PlatformBillingCycle.Quarterly => start.AddMonths(3),
        PlatformBillingCycle.Yearly => start.AddYears(1),
        PlatformBillingCycle.OneTime => start.AddYears(100),
        _ => throw new ArgumentOutOfRangeException(nameof(cycle))
    };

    private static PlatformBillingException Error(string message, string code) => new(message, code);
}
