#nullable enable
using Baya.Application.Features.Invoices.Commands.IssueInvoice;
using Baya.Application.Features.Payouts.Commands.ExecutePayoutBatch;
using Baya.Application.Features.Payouts.Commands.GeneratePayoutBatch;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Messaging;
using Baya.Domain.Entities.PartnerCenters;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Payouts;
using Baya.Domain.Entities.Refunds;
using Baya.Domain.Entities.User;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Baya.Infrastructure.Persistence.Services.Seeding;

internal sealed partial class DemoLifecycleSeeder
{
    // ---------------------------------------------------------------------------------------------------
    // BNPL — the settled provider-financed order behind the "bnpl_settled" booking (net-of-fee model).
    // ---------------------------------------------------------------------------------------------------

    private async Task EnsureBnplAsync(BookingWorld bookings, DateTime now, CancellationToken cancellationToken)
    {
        if (!bookings.ByKey.TryGetValue("bnpl_settled", out var booking))
            return;

        var txn = bookings.TxnByKey["bnpl_settled"];
        if (await db.Set<BnplTransaction>().AnyAsync(b => b.PaymentTransactionId == txn.Id, cancellationToken))
            return;

        var commissionRate = await platformConfig.GetConfig<decimal>("bnpl_provider_commission_rate", cancellationToken);
        var fee = (long)Math.Round(booking.GrossPriceIrr * commissionRate, MidpointRounding.AwayFromZero);
        var settledAt = booking.ConfirmedAt ?? now;

        var bnpl = new BnplTransaction
        {
            PaymentTransactionId = txn.Id,
            ProviderCode = BnplProviderCodes.SnappPay,
            MerchantOfRecord = "platform",
            EligibilityStatus = "approved",
            OrderAmountIrr = booking.GrossPriceIrr,
            InstallmentCount = 4
        };
        bnpl.MarkTokenIssued("demo-bnpl-token-1", "demo-bnpl-order-1");
        bnpl.MarkVerified("demo-bnpl-order-1", "{\"source\":\"demo-lifecycle-seed\",\"step\":\"verify\"}");
        bnpl.MarkSettled(
            settledAmountIrr: booking.GrossPriceIrr - fee,
            commissionIrr: fee,
            settledAt: settledAt.AddDays(1),
            callbackPayloadJson: "{\"source\":\"demo-lifecycle-seed\",\"step\":\"settle\"}");
        db.Set<BnplTransaction>().Add(bnpl);
        await db.SaveChangesAsync(cancellationToken); // assigns bnpl.Id for the ledger source ref

        db.Set<LedgerEntry>().AddRange(LedgerPosting.BnplSettle(
            booking.Id, booking.NurseId, booking.GrossPriceIrr, booking.BalinyaarCommissionIrr,
            booking.NursePayoutAmount, bnplCommissionIrr: fee, bnplTransactionId: bnpl.Id, createdAt: settledAt.AddDays(1)));
        await db.SaveChangesAsync(cancellationToken);
    }

    // ---------------------------------------------------------------------------------------------------
    // Payout batches — dispatched through the REAL commands so eligibility, the first-payout IBAN gate,
    // clawback netting, and the payout ledger all come from production code, not a reimplementation.
    // ---------------------------------------------------------------------------------------------------

    private async Task EnsurePaidPayoutBatchAsync(BookingWorld bookings, DateTime now, CancellationToken cancellationToken)
    {
        if (!bookings.ByKey.TryGetValue("completed_paid", out var paidBooking))
            return;
        if (await db.Set<NursePayoutBookingLink>().AnyAsync(l => l.BookingId == paidBooking.Id, cancellationToken))
            return;

        // periodEnd T0−4d keeps the still-in-draft-window bookings (dispute end T0−2d/−3d) out of this batch.
        var generate = await sender.Send(
            new GeneratePayoutBatchCommand(Today(now, -21), Today(now, -4)) { SystemInitiated = true },
            cancellationToken);
        if (!generate.IsSuccess)
        {
            logger.LogWarning("Demo lifecycle: paid payout batch generation failed — skipping the paid-batch scenario.");
            return;
        }

        // The seeder is the only writer at boot, so the newest batch is the one just generated.
        var batchId = await db.Set<NursePayoutBatch>()
            .OrderByDescending(b => b.Id)
            .Select(b => b.Id)
            .FirstAsync(cancellationToken);

        var execute = await sender.Send(new ExecutePayoutBatchCommand(batchId), cancellationToken);
        if (!execute.IsSuccess)
            logger.LogWarning("Demo lifecycle: executing payout batch {BatchId} failed — it stays unprocessed.", batchId);
    }

    private async Task EnsureDraftPayoutBatchAsync(BookingWorld bookings, DateTime now, CancellationToken cancellationToken)
    {
        if (!bookings.ByKey.TryGetValue("completed_eligible", out var eligibleBooking))
            return;
        if (await db.Set<NursePayoutBookingLink>().AnyAsync(l => l.BookingId == eligibleBooking.Id, cancellationToken))
            return;

        // Runs AFTER the refunds: the BNPL-refunded booking is genuinely excluded by the real eligibility
        // query, and this batch deliberately stays draft so the admin "process" action is demoable.
        var generate = await sender.Send(
            new GeneratePayoutBatchCommand(Today(now, -3), Today(now, -1)) { SystemInitiated = true },
            cancellationToken);
        if (!generate.IsSuccess)
            logger.LogWarning("Demo lifecycle: draft payout batch generation failed — skipping the draft-batch scenario.");
    }

    // ---------------------------------------------------------------------------------------------------
    // Refunds — one per channel/fork: succeeded card, processing BNPL revert, post-payout clawback.
    // Every refund is ticket-linked (b15: refunds.ticket_id is always non-null on the real path).
    // ---------------------------------------------------------------------------------------------------

    private async Task EnsureRefundsAsync(
        PersonaWorld world, BookingWorld bookings, DateTime now, CancellationToken cancellationToken)
    {
        var admin = world.AdminUserId;

        // 1) Cancelled booking → full (100%) card refund, already succeeded.
        if (bookings.ByKey.TryGetValue("cancelled_refunded", out var cancelled)
            && !await db.Set<Refund>().AnyAsync(r => r.BookingId == cancelled.Id, cancellationToken))
        {
            var customer = world.Customers[0];
            var ticket = await EnsureRefundTicketAsync(
                DemoLifecycleDefinitions.RefundTicketCard, cancelled.Id, customer.UserId, admin,
                "استرداد وجه رزرو لغوشده", now.AddDays(-1), cancellationToken);

            var refund = new Refund
            {
                PaymentTransactionId = bookings.TxnByKey["cancelled_refunded"].Id,
                BookingId = cancelled.Id,
                RequestedByCustomerId = customer.ProfileId,
                TicketId = ticket.Id,
                Amount = cancelled.GrossPriceIrr,
                PlatformFeeRefundedIrr = cancelled.BalinyaarCommissionIrr,
                NursePayoutRefundedIrr = cancelled.NursePayoutAmount,
                RefundPercentage = 1.0m,
                RefundChannel = RefundChannel.PspCard,
                ReasonCategory = "customer_cancelled",
                CancellationPolicyCode = cancelled.CancellationPolicyCode,
                RefundPercentageApplied = cancelled.CancellationRefundPercentage,
                ApprovedByAdminId = admin
            };
            refund.MarkSucceededCard("demo-psp-ref-1", now.AddDays(-1));
            db.Set<Refund>().Add(refund);
            await db.SaveChangesAsync(cancellationToken);

            ticket.RefundId = refund.Id;
            db.Set<LedgerEntry>().AddRange(LedgerPosting.RefundReversalPrePayout(
                cancelled.Id, cancelled.NurseId, refund.PlatformFeeRefundedIrr, refund.NursePayoutRefundedIrr,
                refund.Id, now.AddDays(-1)));
            db.Set<LedgerEntry>().AddRange(LedgerPosting.RefundPayableClearing(
                cancelled.Id, refund.Amount, refund.Id, now.AddDays(-1)));
            await db.SaveChangesAsync(cancellationToken);
        }

        // 2) Completed BNPL booking disputed → 50% refund via bnpl_revert, still processing (the
        //    admin confirm_settlement action stays demoable; the ~10-business-day ETA is surfaced).
        if (bookings.ByKey.TryGetValue("bnpl_settled", out var bnplBooking)
            && !await db.Set<Refund>().AnyAsync(r => r.BookingId == bnplBooking.Id, cancellationToken))
        {
            var customer = world.Customers[1];
            var ticket = await EnsureRefundTicketAsync(
                DemoLifecycleDefinitions.RefundTicketBnpl, bnplBooking.Id, customer.UserId, admin,
                "اعتراض به کیفیت خدمت و درخواست استرداد", now.AddDays(-1), cancellationToken);

            var amount = bnplBooking.GrossPriceIrr / 2;
            var feeLeg = (long)Math.Round(bnplBooking.BalinyaarCommissionIrr * 0.5m, MidpointRounding.AwayFromZero);
            var etaBusinessDays = await platformConfig.GetConfig<int>("bnpl_refund_eta_business_days", cancellationToken);

            var refund = new Refund
            {
                PaymentTransactionId = bookings.TxnByKey["bnpl_settled"].Id,
                BookingId = bnplBooking.Id,
                RequestedByCustomerId = customer.ProfileId,
                TicketId = ticket.Id,
                Amount = amount,
                PlatformFeeRefundedIrr = feeLeg,
                NursePayoutRefundedIrr = amount - feeLeg,
                RefundPercentage = 0.5m,
                RefundChannel = RefundChannel.BnplRevert,
                ReasonCategory = "service_quality",
                ApprovedByAdminId = admin
            };
            refund.MarkProcessing("demo-revert-1", Today(now, etaBusinessDays + 4));
            db.Set<Refund>().Add(refund);
            await db.SaveChangesAsync(cancellationToken);

            ticket.RefundId = refund.Id;
            db.Set<LedgerEntry>().AddRange(LedgerPosting.RefundReversalPrePayout(
                bnplBooking.Id, bnplBooking.NurseId, refund.PlatformFeeRefundedIrr, refund.NursePayoutRefundedIrr,
                refund.Id, now.AddDays(-1)));
            // No clearing leg yet — that posts when the admin confirms the settlement (the demoable action).

            var bnpl = await db.Set<BnplTransaction>()
                .FirstOrDefaultAsync(b => b.PaymentTransactionId == refund.PaymentTransactionId, cancellationToken);
            bnpl?.MarkReverted("demo-revert-txn-1", amount, providerCommissionReversedAmount: null,
                revertedAt: now.AddDays(-1), callbackPayloadJson: "{\"source\":\"demo-lifecycle-seed\",\"step\":\"revert\"}");
            await db.SaveChangesAsync(cancellationToken);
        }

        // 3) Paid-out booking refunded afterwards → the post-payout clawback fork (IBAN transfers are
        //    irreversible, so the nurse's already-paid share becomes a pending receivable).
        if (bookings.ByKey.TryGetValue("clawback", out var clawedBooking)
            && !await db.Set<Refund>().AnyAsync(r => r.BookingId == clawedBooking.Id, cancellationToken))
        {
            var customer = world.Customers[0];
            var ticket = await EnsureRefundTicketAsync(
                DemoLifecycleDefinitions.RefundTicketClawback, clawedBooking.Id, customer.UserId, admin,
                "شکایت پس از پرداخت و درخواست استرداد", now.AddHours(-20), cancellationToken);

            var refund = new Refund
            {
                PaymentTransactionId = bookings.TxnByKey["clawback"].Id,
                BookingId = clawedBooking.Id,
                RequestedByCustomerId = customer.ProfileId,
                TicketId = ticket.Id,
                Amount = clawedBooking.GrossPriceIrr,
                PlatformFeeRefundedIrr = clawedBooking.BalinyaarCommissionIrr,
                NursePayoutRefundedIrr = clawedBooking.NursePayoutAmount,
                RefundPercentage = 1.0m,
                RefundChannel = RefundChannel.PspCard,
                ReasonCategory = "service_quality",
                ApprovedByAdminId = admin
            };
            refund.MarkSucceededCard("demo-psp-ref-2", now.AddHours(-20));
            db.Set<Refund>().Add(refund);
            await db.SaveChangesAsync(cancellationToken);

            ticket.RefundId = refund.Id;
            db.Set<LedgerEntry>().AddRange(LedgerPosting.ClawbackReversalPostPayout(
                clawedBooking.Id, clawedBooking.NurseId, refund.PlatformFeeRefundedIrr, refund.NursePayoutRefundedIrr,
                refund.Id, now.AddHours(-20)));
            db.Set<LedgerEntry>().AddRange(LedgerPosting.RefundPayableClearing(
                clawedBooking.Id, refund.Amount, refund.Id, now.AddHours(-20)));

            var originalPayoutId = await db.Set<NursePayoutBookingLink>()
                .Where(l => l.BookingId == clawedBooking.Id)
                .Select(l => (long?)l.PayoutId)
                .FirstOrDefaultAsync(cancellationToken);

            db.Set<NurseClawback>().Add(new NurseClawback
            {
                NurseId = clawedBooking.NurseId,
                BookingId = clawedBooking.Id,
                RefundId = refund.Id,
                OriginalPayoutId = originalPayoutId,
                AmountIrr = refund.NursePayoutRefundedIrr
            });
            await db.SaveChangesAsync(cancellationToken);
        }
    }

    private async Task<Ticket> EnsureRefundTicketAsync(
        string referenceCode, long bookingId, int customerUserId, int adminUserId,
        string subject, DateTime openedAt, CancellationToken cancellationToken)
    {
        var existing = await db.Set<Ticket>()
            .FirstOrDefaultAsync(t => t.ReferenceCode == referenceCode, cancellationToken);
        if (existing is not null)
            return existing;

        var ticket = new Ticket
        {
            ReferenceCode = referenceCode,
            Subject = subject,
            Category = TicketCategory.Refund,
            BookingId = bookingId,
            OpenedById = customerUserId,
            Participants =
            [
                new TicketParticipant { UserId = customerUserId, RoleOnTicket = TicketParticipantRole.Customer },
                new TicketParticipant { UserId = adminUserId, RoleOnTicket = TicketParticipantRole.Admin, AddedById = adminUserId }
            ],
            Messages =
            [
                new TicketMessage
                {
                    SenderId = customerUserId,
                    Body = "سلام، لطفاً وضعیت استرداد وجه این رزرو را بررسی کنید.",
                    SentAt = openedAt
                },
                new TicketMessage
                {
                    SenderId = adminUserId,
                    Body = "درخواست شما ثبت شد و در حال بررسی است. نتیجه از همین گفتگو اطلاع‌رسانی می‌شود.",
                    SentAt = openedAt.AddMinutes(45)
                }
            ]
        };
        db.Set<Ticket>().Add(ticket);
        await db.SaveChangesAsync(cancellationToken);
        return ticket;
    }

    // ---------------------------------------------------------------------------------------------------
    // Partner center — the licensed merchant-of-record sponsor, linked to the second nurse so the MoR
    // invoice resolution and the partner portal have a real subject.
    // ---------------------------------------------------------------------------------------------------

    private async Task<long> EnsurePartnerCenterAsync(PersonaWorld world, CancellationToken cancellationToken)
    {
        var existing = await db.Set<PartnerCenter>()
            .FirstOrDefaultAsync(c => c.MohEstablishmentPermitNo == DemoLifecycleDefinitions.PartnerCenterPermitNo, cancellationToken);
        if (existing is not null)
            return existing.Id;

        var portalUser = await userManager.GetUserByPhoneNumber(DemoLifecycleDefinitions.PartnerAdminPhone);
        if (portalUser is null)
        {
            portalUser = new User
            {
                UserName = DemoLifecycleDefinitions.PartnerAdminUserName,
                PhoneNumber = DemoLifecycleDefinitions.PartnerAdminPhone,
                PhoneNumberConfirmed = true,
                PhoneVerifiedAt = clock.UtcNow,
                Name = DemoLifecycleDefinitions.PartnerAdminName,
                FamilyName = DemoLifecycleDefinitions.PartnerAdminFamilyName,
                Gender = "male",
                IsActive = true
            };

            var createResult = await userManager.CreateUser(portalUser);
            if (!createResult.Succeeded)
                throw new InvalidOperationException(
                    $"Demo lifecycle seed failed to create the partner portal user: {string.Join("; ", createResult.Errors.Select(e => e.Description))}");

            // The portal keys off the center's admin_user_id, not a role; the customer role only gives the
            // login a sane landing screen (there is deliberately no partner role in the vocabulary).
            var role = await db.Set<Role>().FirstAsync(r => r.Name == RoleNames.Customer, cancellationToken);
            await userManager.AddUserToRoleAsync(portalUser, role);
        }

        var center = new PartnerCenter
        {
            Name = DemoLifecycleDefinitions.PartnerCenterName,
            LegalEntityType = "private_center",
            MohEstablishmentPermitNo = DemoLifecycleDefinitions.PartnerCenterPermitNo,
            EnamadCode = DemoLifecycleDefinitions.PartnerCenterEnamad,
            SettlementIban = "IR120170000000555566667777", // encrypted at rest by the EF converter
            IsMerchantOfRecord = true,
            CommissionRate = 0.05m,
            AdminUserId = portalUser.Id
        };
        center.Verify(clock.UtcNow.AddDays(-30));
        db.Set<PartnerCenter>().Add(center);
        await db.SaveChangesAsync(cancellationToken);

        // Karimi becomes the center's sponsored nurse — his bookings' invoices resolve to the center as MoR.
        world.Nurses[1].Profile.PartnerCenterId = center.Id;
        await db.SaveChangesAsync(cancellationToken);

        return center.Id;
    }

    // ---------------------------------------------------------------------------------------------------
    // Invoices — dispatched through the real IssueInvoice (sequential number, VAT-on-commission,
    // merchant-of-record resolution). Idempotent per booking on the real path.
    // ---------------------------------------------------------------------------------------------------

    private async Task EnsureInvoicesAsync(BookingWorld bookings, CancellationToken cancellationToken)
    {
        foreach (var (key, booking) in bookings.ByKey)
        {
            var result = await sender.Send(new IssueInvoiceCommand(booking.Id), cancellationToken);
            if (!result.IsSuccess)
                logger.LogDebug("Demo lifecycle: invoice for booking scenario '{Key}' not issued (already issued or ineligible).", key);
        }
    }
}
