using Meezi.API.Models.Billing;
using Meezi.Infrastructure.Services.Platform;
using Meezi.Core.Constants;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;

namespace Meezi.API.Services;

public interface IBillingService
{
    Task<IReadOnlyList<PaymentMethodDto>> GetPaymentMethodsAsync(CancellationToken cancellationToken = default);

    Task<(SubscribeResponse? Data, string? ErrorCode, string? Message)> InitiateSubscriptionAsync(
        string cafeId,
        SubscribeRequest request,
        CancellationToken cancellationToken = default);

    Task<BillingVerifyResult> VerifyZarinPalAsync(
        string authority,
        string? status,
        CancellationToken cancellationToken = default);

    Task<BillingVerifyResult> VerifySnappPayAsync(
        string? paymentToken,
        string? state,
        CancellationToken cancellationToken = default);

    Task<BillingVerifyResult> VerifyTaraAsync(
        string? traceNumber,
        string? status,
        CancellationToken cancellationToken = default);

    Task<BillingStatusDto?> GetStatusAsync(string cafeId, PlanTier currentTier, CancellationToken cancellationToken = default);

    Task<(bool Ok, string? ErrorCode, string? Message)> CancelQueuedAsync(
        string cafeId,
        string paymentId,
        CancellationToken cancellationToken = default);
}

public class BillingService : IBillingService
{
    private readonly AppDbContext _db;
    private readonly IBillingPaymentOrchestrator _payments;
    private readonly ISmsService _smsService;
    private readonly IConnectionMultiplexer _redis;
    private readonly IConfiguration _configuration;
    private readonly IPlatformCatalogService _platformCatalog;
    private readonly ILogger<BillingService> _logger;

    public BillingService(
        AppDbContext db,
        IBillingPaymentOrchestrator payments,
        ISmsService smsService,
        IConnectionMultiplexer redis,
        IConfiguration configuration,
        IPlatformCatalogService platformCatalog,
        ILogger<BillingService> logger)
    {
        _db = db;
        _payments = payments;
        _smsService = smsService;
        _redis = redis;
        _configuration = configuration;
        _platformCatalog = platformCatalog;
        _logger = logger;
    }

    private const string FeatureMenu3d = "menu_3d";
    private const string FeatureMenu3dAi = "menu_3d_ai";
    private const string FeatureDiscoverProfile = "discover_profile";

    public async Task<(SubscribeResponse? Data, string? ErrorCode, string? Message)> InitiateSubscriptionAsync(
        string cafeId,
        SubscribeRequest request,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken);
        if (cafe is null)
            return (null, "NOT_FOUND", "Cafe not found.");

        if (!await _platformCatalog.IsBillableOnlineAsync(request.PlanTier, cancellationToken))
            return (null, "NOT_BILLABLE", "This plan requires contacting sales.");

        var monthly = await _platformCatalog.GetMonthlyPriceTomanAsync(request.PlanTier, cancellationToken);
        if (monthly <= 0)
            return (null, "NOT_BILLABLE", "This plan has no online price.");

        var amountToman = monthly * request.Months;
        var amountRials = PlanPricing.ToRials(amountToman);

        var methods = await _payments.GetEnabledMethodsAsync(cancellationToken);
        var methodId = string.IsNullOrWhiteSpace(request.PaymentMethod)
            ? methods.FirstOrDefault(m => m.IsDefault)?.Id ?? PaymentProviderIds.ZarinPal
            : request.PaymentMethod.Trim().ToLowerInvariant();

        var provider = PaymentProviderIds.Parse(methodId);
        if (provider is null || methods.All(m => m.Id != methodId))
            return (null, "PAYMENT_METHOD_DISABLED", "Selected payment method is not available.");

        var payment = new SubscriptionPayment
        {
            CafeId = cafeId,
            PlanTier = request.PlanTier,
            Months = request.Months,
            AmountToman = amountToman,
            AmountRials = amountRials,
            Provider = provider.Value,
            Status = SubscriptionPaymentStatus.Pending
        };

        _db.SubscriptionPayments.Add(payment);
        await _db.SaveChangesAsync(cancellationToken);

        var apiBase = _configuration["App:PublicBaseUrl"]?.TrimEnd('/') ?? "http://localhost:5080";
        var callbackUrl = provider.Value switch
        {
            PaymentProvider.SnappPay => $"{apiBase}/api/billing/verify/snapppay",
            PaymentProvider.Tara => $"{apiBase}/api/billing/verify/tara",
            _ => $"{apiBase}/api/billing/verify"
        };

        var description = $"میزی — اشتراک {request.PlanTier} ({request.Months} ماه)";
        var init = await _payments.InitiateAsync(
            provider.Value,
            amountRials,
            payment.Id,
            description,
            callbackUrl,
            cancellationToken);

        if (!init.Success || string.IsNullOrEmpty(init.Authority) || string.IsNullOrEmpty(init.PaymentUrl))
        {
            payment.Status = SubscriptionPaymentStatus.Failed;
            await _db.SaveChangesAsync(cancellationToken);
            return (null, "PAYMENT_FAILED", init.ErrorMessage ?? "Could not start payment.");
        }

        payment.Authority = init.Authority;
        await _db.SaveChangesAsync(cancellationToken);

        return (new SubscribeResponse(payment.Id, init.PaymentUrl), null, null);
    }

    public Task<IReadOnlyList<PaymentMethodDto>> GetPaymentMethodsAsync(CancellationToken cancellationToken = default) =>
        _payments.GetEnabledMethodsAsync(cancellationToken);

    public Task<BillingVerifyResult> VerifyZarinPalAsync(
        string authority,
        string? status,
        CancellationToken cancellationToken = default) =>
        CompletePaymentAsync(
            PaymentProvider.ZarinPal,
            authority,
            status is null or "" or "OK",
            cancellationToken);

    public Task<BillingVerifyResult> VerifySnappPayAsync(
        string? paymentToken,
        string? state,
        CancellationToken cancellationToken = default) =>
        CompletePaymentAsync(
            PaymentProvider.SnappPay,
            paymentToken,
            !string.IsNullOrWhiteSpace(paymentToken)
            && (string.IsNullOrWhiteSpace(state) || state.Equals("OK", StringComparison.OrdinalIgnoreCase)),
            cancellationToken);

    public Task<BillingVerifyResult> VerifyTaraAsync(
        string? traceNumber,
        string? status,
        CancellationToken cancellationToken = default) =>
        CompletePaymentAsync(
            PaymentProvider.Tara,
            traceNumber,
            status is null or "" or "OK",
            cancellationToken);

    private async Task<BillingVerifyResult> CompletePaymentAsync(
        PaymentProvider provider,
        string? externalId,
        bool callbackOk,
        CancellationToken cancellationToken)
    {
        var dashboardBase = _configuration["Billing:DashboardBaseUrl"]?.TrimEnd('/')
            ?? _configuration.GetSection("Cors:Origins").Get<string[]>()?.FirstOrDefault()
            ?? "http://localhost:3101";
        var failUrl = $"{dashboardBase}/fa/subscription?billing=failed";
        var successUrl = $"{dashboardBase}/fa/subscription?billing=success";

        if (!callbackOk || string.IsNullOrWhiteSpace(externalId))
            return new BillingVerifyResult(false, failUrl);

        var payment = await _db.SubscriptionPayments
            .Include(p => p.Cafe)
            .FirstOrDefaultAsync(
                p => p.Authority == externalId && p.Provider == provider,
                cancellationToken);

        if (payment is null)
            return new BillingVerifyResult(false, failUrl);

        if (payment.Status == SubscriptionPaymentStatus.Completed)
            return new BillingVerifyResult(true, successUrl);

        var verify = await _payments.VerifyAsync(provider, externalId, payment.AmountRials, cancellationToken);
        if (!verify.Success)
        {
            payment.Status = SubscriptionPaymentStatus.Failed;
            await _db.SaveChangesAsync(cancellationToken);
            return new BillingVerifyResult(false, failUrl);
        }

        payment.RefId = verify.RefId;

        var cafe = payment.Cafe;
        var now = DateTime.UtcNow;

        // Where does the current paid coverage end? = the latest of the active plan's expiry
        // and the furthest-out already-queued period. A new purchase is appended to that.
        var coverageEnd = await ComputeCoverageEndAsync(cafe, payment.Id, now, cancellationToken);

        payment.EffectiveFrom = coverageEnd;
        payment.EffectiveTo = coverageEnd.AddMonths(payment.Months);

        var queued = coverageEnd > now;
        if (queued)
        {
            // The owner already has active/queued coverage → book this one after it.
            payment.Status = SubscriptionPaymentStatus.Scheduled;
        }
        else
        {
            // No active coverage → activate immediately.
            payment.Status = SubscriptionPaymentStatus.Completed;
            cafe.PlanTier = payment.PlanTier;
            cafe.PlanExpiresAt = payment.EffectiveTo;
        }

        await _db.SaveChangesAsync(cancellationToken);

        await TrySendConfirmationSmsAsync(cafe, payment, queued, cancellationToken);

        return new BillingVerifyResult(true, successUrl);
    }

    /// <summary>End of the cafe's current paid coverage: the later of its active plan expiry
    /// and the furthest-out scheduled (queued) period. Returns <paramref name="now"/> if neither
    /// extends past now (i.e. nothing active/queued).</summary>
    private async Task<DateTime> ComputeCoverageEndAsync(
        Cafe cafe, string? excludePaymentId, DateTime now, CancellationToken ct)
    {
        var end = now;
        if (cafe.PlanTier != PlanTier.Free && cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt.Value > end)
            end = cafe.PlanExpiresAt.Value;

        var lastScheduledEnd = await _db.SubscriptionPayments
            .Where(p => p.CafeId == cafe.Id
                && p.Status == SubscriptionPaymentStatus.Scheduled
                && (excludePaymentId == null || p.Id != excludePaymentId)
                && p.EffectiveTo != null)
            .OrderByDescending(p => p.EffectiveTo)
            .Select(p => p.EffectiveTo)
            .FirstOrDefaultAsync(ct);

        if (lastScheduledEnd.HasValue && lastScheduledEnd.Value > end)
            end = lastScheduledEnd.Value;

        return end;
    }

    /// <summary>When the active plan has lapsed, promote due queued periods to active.
    /// Loops so a fully-elapsed short queued period doesn't strand the next one.</summary>
    private async Task PromoteDueScheduledAsync(string cafeId, CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct);
        if (cafe is null) return;

        var changed = false;
        while (!(cafe.PlanTier != PlanTier.Free && cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt.Value > now))
        {
            var next = await _db.SubscriptionPayments
                .Where(p => p.CafeId == cafeId
                    && p.Status == SubscriptionPaymentStatus.Scheduled
                    && p.EffectiveFrom != null && p.EffectiveFrom <= now)
                .OrderBy(p => p.EffectiveFrom)
                .FirstOrDefaultAsync(ct);
            if (next is null) break;

            cafe.PlanTier = next.PlanTier;
            cafe.PlanExpiresAt = next.EffectiveTo;
            next.Status = SubscriptionPaymentStatus.Completed;
            changed = true;
        }

        if (changed) await _db.SaveChangesAsync(ct);
    }

    public async Task<(bool Ok, string? ErrorCode, string? Message)> CancelQueuedAsync(
        string cafeId,
        string paymentId,
        CancellationToken cancellationToken = default)
    {
        var payment = await _db.SubscriptionPayments
            .FirstOrDefaultAsync(p => p.Id == paymentId && p.CafeId == cafeId, cancellationToken);
        if (payment is null)
            return (false, "NOT_FOUND", "Subscription not found.");

        // Only a queued (not-yet-started) subscription can be cancelled. The active prepaid
        // plan keeps running until its paid time ends.
        if (payment.Status != SubscriptionPaymentStatus.Scheduled)
            return (false, "NOT_CANCELLABLE", "Only a queued subscription can be cancelled.");

        payment.Status = SubscriptionPaymentStatus.Cancelled;
        await _db.SaveChangesAsync(cancellationToken);

        // Re-pack the remaining queue so later periods slide earlier to fill the gap.
        await RecomputeQueueAsync(cafeId, cancellationToken);
        return (true, null, null);
    }

    /// <summary>Re-sequences the remaining queued periods contiguously after the active plan
    /// (purchase order preserved), so cancelling one in the middle doesn't leave a gap.</summary>
    private async Task RecomputeQueueAsync(string cafeId, CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct);
        if (cafe is null) return;

        var anchor = (cafe.PlanTier != PlanTier.Free && cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt.Value > now)
            ? cafe.PlanExpiresAt.Value
            : now;

        var scheduled = await _db.SubscriptionPayments
            .Where(p => p.CafeId == cafeId && p.Status == SubscriptionPaymentStatus.Scheduled)
            .OrderBy(p => p.CreatedAt)
            .ToListAsync(ct);

        foreach (var s in scheduled)
        {
            s.EffectiveFrom = anchor;
            s.EffectiveTo = anchor.AddMonths(s.Months);
            anchor = s.EffectiveTo.Value;
        }

        if (scheduled.Count > 0) await _db.SaveChangesAsync(ct);
    }

    public async Task<BillingStatusDto?> GetStatusAsync(
        string cafeId,
        PlanTier currentTier,
        CancellationToken cancellationToken = default)
    {
        // Lazily activate any queued plan whose start date has passed before reading status.
        await PromoteDueScheduledAsync(cafeId, cancellationToken);

        var cafe = await _db.Cafes.AsNoTracking().FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken);
        if (cafe is null) return null;

        var queuedPlans = await _db.SubscriptionPayments.AsNoTracking()
            .Where(p => p.CafeId == cafeId && p.Status == SubscriptionPaymentStatus.Scheduled
                && p.EffectiveFrom != null && p.EffectiveTo != null)
            .OrderBy(p => p.EffectiveFrom)
            .Select(p => new QueuedPlanDto(
                p.Id, p.PlanTier, p.Months, p.EffectiveFrom!.Value, p.EffectiveTo!.Value, p.AmountToman))
            .ToListAsync(cancellationToken);

        var todayStart = DateTime.UtcNow.Date;
        var ordersToday = await _db.Orders.CountAsync(
            o => o.CafeId == cafeId && o.CreatedAt >= todayStart,
            cancellationToken);

        var customersCount = await _db.Customers.CountAsync(c => c.CafeId == cafeId, cancellationToken);

        var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier);
        var maxCustomers = PlanLimits.MaxCustomers(cafe.PlanTier);
        var maxSms = PlanLimits.MaxSmsPerMonth(cafe.PlanTier);

        var monthKey = $"sms:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}";
        var redis = _redis.GetDatabase();
        var smsUsed = await redis.StringGetAsync(monthKey);
        var smsUsedCount = smsUsed.HasValue ? (int)smsUsed : 0;

        var menu3d = await _platformCatalog.IsFeatureEnabledForCafeAsync(
            cafeId, cafe.PlanTier, FeatureMenu3d, cancellationToken);
        var menuAi3d = await _platformCatalog.IsFeatureEnabledForCafeAsync(
            cafeId, cafe.PlanTier, FeatureMenu3dAi, cancellationToken);
        var discoverProfile = await _platformCatalog.IsFeatureEnabledForCafeAsync(
            cafeId, cafe.PlanTier, FeatureDiscoverProfile, cancellationToken);
        var isExpired = cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt.Value < DateTime.UtcNow;

        var ai3dKey = $"ai3d:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}";
        var ai3dUsed = await redis.StringGetAsync(ai3dKey);
        var ai3dUsedCount = ai3dUsed.HasValue && int.TryParse(ai3dUsed.ToString(), out var aiN) ? aiN : 0;
        var ai3dLimit = menuAi3d ? PlanLimits.MaxMenuAi3dPerMonth(cafe.PlanTier) : 0;

        return new BillingStatusDto(
            cafe.PlanTier,
            cafe.PlanExpiresAt,
            ordersToday,
            maxOrders == int.MaxValue ? null : maxOrders,
            customersCount,
            maxCustomers == int.MaxValue ? null : maxCustomers,
            smsUsedCount,
            maxSms == int.MaxValue ? -1 : maxSms,
            menu3d,
            menuAi3d,
            ai3dUsedCount,
            ai3dLimit,
            discoverProfile,
            isExpired,
            queuedPlans);
    }

    private async Task TrySendConfirmationSmsAsync(
        Cafe cafe,
        SubscriptionPayment payment,
        bool queued,
        CancellationToken cancellationToken)
    {
        var ownerPhone = await _db.Employees
            .Where(e => e.CafeId == cafe.Id && e.Role == EmployeeRole.Owner)
            .Select(e => e.Phone)
            .FirstOrDefaultAsync(cancellationToken);

        if (string.IsNullOrEmpty(ownerPhone)) return;

        var message = queued
            ? $"میزی: اشتراک {payment.PlanTier} ثبت شد و از {payment.EffectiveFrom:yyyy-MM-dd} (پس از پایان اشتراک فعلی) آغاز می‌شود. مبلغ: {payment.AmountToman:N0} ت"
            : $"میزی: اشتراک {payment.PlanTier} فعال شد تا {cafe.PlanExpiresAt:yyyy-MM-dd}. مبلغ: {payment.AmountToman:N0} ت";
        try
        {
            await _smsService.SendMessageAsync(ownerPhone, message, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to send subscription confirmation SMS for cafe {CafeId}", cafe.Id);
        }
    }
}
