<?php

namespace App\Listeners;

use App\Events\InvoiceStatusChanged;
use App\Jobs\ProcessDomainRegisterJob;
use App\Jobs\ProcessDomainRenewJob;
use App\Jobs\ProcessDomainTransferJob;
use App\Jobs\ProcessProductChangeJob;
use App\Jobs\SendEmailJob;
use App\Models\Reseller;
use App\Models\ResellerCommission;
use App\Jobs\ProcessProductCreateJob;
use App\Jobs\ProcessProductUnsuspendJob;
use App\Models\ClientBalance;
use App\Models\ClientDomain;
use App\Models\ClientProduct;
use App\Models\ClientProductTrial;
use App\Models\Invoice;
use App\Models\Log;
use App\Services\Billing\InvoiceNumberService;
use Illuminate\Contracts\Queue\ShouldQueue;

class HandleInvoiceStatusChange implements ShouldQueue
{
    /**
     * Handle the event.
     */
    public function handle(InvoiceStatusChanged $event): void
    {
        $invoice = $event->invoice;
        $newStatus = $event->newStatus;
        $reason = $event->reason;

        // Log the status change as system event
        Log::create([
            'type' => Log::TYPE_SYSTEM,
            'observer' => 'HandleInvoiceStatusChange',
            'module' => 'Invoice',
            'method' => 'handle',
            'client_id' => $invoice->client_id,
            'ip_address' => request()->ip() ?? '0.0.0.0',
            'description' => "Invoice #{$invoice->invoice_number} status changed to {$newStatus}. Reason: {$reason}",
            'request' => [
                'invoice_id' => $invoice->id,
                'old_status' => $invoice->getOriginal('status'),
                'new_status' => $newStatus,
                'reason' => $reason,
            ],
        ]);

        // PRC-407 — Customer email + admin notify moved to the specialized
        // listeners (SendInvoicePaidNotification / SendInvoiceCancelledNotification)
        // to consolidate responsibilities. This listener now owns only the
        // side-effect chain (provisioning, late-fee adjust, balance, etc).
        // InvoicePaymentService now also dispatches InvoicePaid/Cancelled
        // events alongside InvoiceStatusChanged so the email listeners fire
        // on the normal payment flow.

        // Trigger provisioning when invoice is paid
        if ($newStatus === 'paid') {
            // PRC-404 / C-002 — Proforma → tax invoice conversion. Run before
            // provisioning so all downstream logs/emails reference the final
            // tax invoice number.
            if ($event->oldStatus === Invoice::STATUS_PROFORMA) {
                $this->convertProformaToTaxInvoice($invoice);
            }

            $this->handleAddFundsCredit($invoice);
            $this->triggerProvisioning($invoice);
            $this->triggerUpgrades($invoice);
            $this->unsuspendPaidServices($invoice);
            $this->generateResellerCommission($invoice);
            $this->updateRecurringAmounts($invoice);
            $this->triggerSslRenewal($invoice); // PRC-57
            $this->triggerSoftwareRenewal($invoice); // PRC-59
            $this->triggerLicenseRenewal($invoice); // PRC-60
            $this->pushNextDueFromCumulativeLines($invoice); // PRC-370 / M2
            $this->convertTrialsToActive($invoice); // PRC-536
            $this->fireServiceRecurringCompleted($invoice); // PRC-622
        }
    }

    /**
     * PRC-622 — 'ServiceRecurringCompleted' hook'u (hook-only; client renewal
     * maili ayrı sistemde — REMINDER-TIERS). Renewal'a özgü ayrı bir nokta
     * olmadığı için fatura paid + satır renewal-tipi koşuluyla fire edilir:
     * related_type='product' + related_service_id + period_end dolu satırlar
     * DueInvoiceService renewal hattının imzasıdır. PRC-436'daki initial-cycle
     * guard'ı aynen uygulanır (bugün açılan / hâlâ pending servis = ilk satın
     * alma, renewal değil).
     */
    private function fireServiceRecurringCompleted($invoice): void
    {
        $serviceIds = $invoice->items()
            ->where('related_type', 'product')
            ->whereNotNull('related_service_id')
            ->whereNotNull('period_end')
            ->pluck('related_service_id')
            ->unique()
            ->values()
            ->all();

        if (empty($serviceIds)) {
            return;
        }

        $bridge = app(\App\Core\Hook\HookBridge::class);

        foreach (ClientProduct::whereIn('id', $serviceIds)->get() as $service) {
            $createdToday = $service->create_date instanceof \Carbon\Carbon
                && $service->create_date->isSameDay(now());
            if ($createdToday || ($service->status ?? null) === 'pending') {
                continue; // initial cycle — renewal değil (PRC-436 guard'ı)
            }

            $bridge->fireAction('ServiceRecurringCompleted', $bridge->buildServiceContext($service, [
                'invoiceid' => $invoice->id,
                'invoice_id' => $invoice->id,
            ]));
        }
    }

    /**
     * PRC-404 / C-002 — When a proforma invoice transitions to paid, issue a
     * new tax invoice number using `tax_invoice_number_format`. The original
     * proforma number is preserved in the audit log for legal traceability.
     */
    private function convertProformaToTaxInvoice(Invoice $invoice): void
    {
        $proformaNumber = (string) $invoice->invoice_number;
        $taxNumber = app(InvoiceNumberService::class)->generate(['type' => 'tax']);

        $invoice->update(['invoice_number' => $taxNumber]);

        Log::create([
            'type' => Log::TYPE_SYSTEM,
            'observer' => 'HandleInvoiceStatusChange',
            'module' => 'Invoice',
            'method' => 'convertProformaToTaxInvoice',
            'client_id' => $invoice->client_id,
            'ip_address' => request()->ip() ?? '0.0.0.0',
            'description' => "Proforma {$proformaNumber} converted to tax invoice {$taxNumber}",
            'request' => [
                'invoice_id' => $invoice->id,
                'proforma_number' => $proformaNumber,
                'tax_number' => $taxNumber,
            ],
        ]);
    }

    /**
     * PRC-370 / M2 — When a cumulative invoice is paid, push each
     * affected service's `next_due_date` to the latest paid line's
     * `period_end`. Without this, services with multi-cycle cumulative
     * invoices would still show their original (pre-billing) next_due
     * and the cron would re-bill the cycles next day.
     *
     * Only applies to lines with `period_end` set — legacy or ad-hoc
     * lines are untouched.
     */
    private function pushNextDueFromCumulativeLines($invoice): void
    {
        $items = $invoice->items()
            ->whereNotNull('period_end')
            ->whereIn('related_type', ['product', 'domain'])
            ->whereNotNull('related_service_id')
            ->get();

        // Group by service so we only run one update per service
        // (with the MAX period_end across the invoice's lines for it).
        $byService = [];
        foreach ($items as $item) {
            $key = $item->related_type.':'.$item->related_service_id;
            $end = $item->period_end instanceof \Carbon\Carbon
                ? $item->period_end
                : \Carbon\Carbon::parse($item->period_end);
            if (! isset($byService[$key]) || $end->gt($byService[$key]['end'])) {
                $byService[$key] = [
                    'type' => $item->related_type,
                    'service_id' => $item->related_service_id,
                    'end' => $end,
                ];
            }
        }

        foreach ($byService as $entry) {
            $model = $entry['type'] === 'product'
                ? \App\Models\ClientProduct::find($entry['service_id'])
                : \App\Models\ClientDomain::find($entry['service_id']);
            if (! $model) {
                continue;
            }

            // next_due_date = day after period_end (next cycle starts).
            $newNext = $entry['end']->copy()->addDay();

            // PRC-436 — Initial-cycle guard. Cumulative push is for *renewal*
            // invoices; if the service was just created (today) the original
            // CheckoutController next_due is correct and pushing here would
            // double-count the period. Also skip if the service is still
            // pending (provisioning not yet activated).
            $createdToday = $model->create_date instanceof \Carbon\Carbon
                && $model->create_date->isSameDay(now());
            if ($createdToday || ($model->status ?? null) === 'pending') {
                Log::info('PRC-436 skip cumulative push for initial cycle', [
                    'invoice_id' => $invoice->id,
                    'service_type' => $entry['type'],
                    'service_id' => $entry['service_id'],
                    'status' => $model->status ?? null,
                    'create_date' => $model->create_date?->toDateString(),
                ]);
                continue;
            }

            if (! $model->next_due_date || $newNext->gt($model->next_due_date)) {
                $model->next_due_date = $newNext;
                $model->save();

                Log::info('PRC-370 next_due_date pushed from cumulative payment', [
                    'invoice_id' => $invoice->id,
                    'service_type' => $entry['type'],
                    'service_id' => $entry['service_id'],
                    'new_next_due_date' => $newNext->toDateString(),
                ]);
            }
        }
    }

    /**
     * Sync client_products.recurring_amount from the invoice's recurring service items.
     *
     * PRC-57 — Payment-driven SSL renewal.
     *
     * Renewal invoice ödendiğinde, faturalanan SSL ClientProduct'ları
     * için `expires_at` push'lanır ve modüle bağlı olarak şu davranış:
     *   - ACME (Letsencrypt, requiresCustomerInput=false): yeniden auto
     *     install — yeni period sonuna kadar geçerli yeni cert
     *   - Reseller (Comodo, requiresCustomerInput=true): cert.installed=false
     *     yapılır + `installation_input` temizlenir; müşteri client area'da
     *     yeniden CSR girer
     *
     * Period sonu fatura mantığı `DueInvoiceService` jenerik akışıyla aynı
     * — bir SSL ClientProduct için renewal invoice oluşur, müşteri öder,
     * bu listener tetiklenir. CA TTL ayrı bir konu (ACME modülünün iç işi).
     */
    /**
     * PRC-59: Software (self-hosted yazılım lisansı) renewal trigger.
     *
     * Subscription ürünlerde period sonu push + key refresh gerekir; lifetime
     * ürünlerde renewal yok (zaten expires_at null).
     */
    private function triggerSoftwareRenewal($invoice): void
    {
        $serviceIds = $invoice->items()
            ->where('related_type', 'product')
            ->whereNotNull('related_service_id')
            ->pluck('related_service_id')
            ->unique()
            ->values()
            ->all();

        if (empty($serviceIds)) {
            return;
        }

        $licenses = \App\Models\ClientSoftwareLicense::query()
            ->whereIn('client_product_id', $serviceIds)
            ->whereNull('revoked_at')
            ->where('license_type', \App\Models\ClientSoftwareLicense::TYPE_SUBSCRIPTION)
            ->with('clientProduct')
            ->get();

        foreach ($licenses as $license) {
            $cp = $license->clientProduct;
            if (! $cp) {
                continue;
            }
            $license->update(['expires_at' => $cp->next_due_date ?? now()->addYear()]);
            ProcessProductCreateJob::dispatch($cp->id, null, $invoice->order_id);
            \Log::info('Software license renewal dispatched', [
                'invoice_id' => $invoice->id,
                'license_id' => $license->id,
            ]);
        }
    }

    /**
     * PRC-60: 3rd-party license renewal trigger.
     */
    private function triggerLicenseRenewal($invoice): void
    {
        $serviceIds = $invoice->items()
            ->where('related_type', 'product')
            ->whereNotNull('related_service_id')
            ->pluck('related_service_id')
            ->unique()
            ->values()
            ->all();

        if (empty($serviceIds)) {
            return;
        }

        $licenses = \App\Models\ClientLicense::query()
            ->whereIn('client_product_id', $serviceIds)
            ->whereNull('revoked_at')
            ->with('clientProduct')
            ->get();

        foreach ($licenses as $license) {
            $cp = $license->clientProduct;
            if (! $cp) {
                continue;
            }
            $license->update(['expires_at' => $cp->next_due_date ?? now()->addYear()]);
            ProcessProductCreateJob::dispatch($cp->id, null, $invoice->order_id);
            \Log::info('3rd-party license renewal dispatched', [
                'invoice_id' => $invoice->id,
                'license_id' => $license->id,
            ]);
        }
    }

    private function triggerSslRenewal($invoice): void
    {
        $serviceIds = $invoice->items()
            ->where('related_type', 'product')
            ->whereNotNull('related_service_id')
            ->pluck('related_service_id')
            ->unique()
            ->values()
            ->all();

        if (empty($serviceIds)) {
            return;
        }

        $certs = \App\Models\ClientSslCertificate::query()
            ->whereIn('client_product_id', $serviceIds)
            ->whereNull('revoked_at')
            ->with('clientProduct.product')
            ->get();

        foreach ($certs as $cert) {
            $cp = $cert->clientProduct;
            if (! $cp) {
                continue;
            }

            // Period sonunu ClientProduct.next_due_date'e push'la
            $cert->update([
                'expires_at' => $cp->next_due_date ?? now()->addYear(),
            ]);

            $sslMgr = app(\App\Core\Ssl\SslManager::class);
            $module = strtolower($cert->module ?? '');
            if (! $sslMgr->has($module)) {
                continue;
            }

            $instance = $sslMgr->get($module);

            if (! $instance->requiresCustomerInput()) {
                // ACME: re-install otomatik
                ProcessProductCreateJob::dispatch($cp->id, null, $invoice->order_id);
                \Log::info('SSL renewal dispatched (ACME auto)', [
                    'invoice_id' => $invoice->id,
                    'cert_id' => $cert->id,
                ]);
            } else {
                // Reseller: müşteri yeniden form girmeli — installed flag'ini
                // düşür, customer kurulum başlatma ekranını görür.
                $cert->update([
                    'installed' => false,
                    'installed_at' => null,
                ]);

                // PRC-622 — 'SSL Certificate Configuration Required' client maili
                // (config/communication_events.php eşlemesi).
                app(\App\Core\Hook\HookBridge::class)->fireAction('SSLCertificateConfigurationRequired', [
                    'ssl_certificate_id' => $cert->id,
                    'client_product_id' => $cert->client_product_id,
                    'service_id' => $cert->client_product_id,
                    'client_id' => $cert->client_id,
                    'domain' => $cert->common_name,
                ]);

                \Log::info('SSL renewal — awaiting customer input', [
                    'invoice_id' => $invoice->id,
                    'cert_id' => $cert->id,
                ]);
            }
        }
    }

    /**
     * Triggered when an invoice transitions to `paid`. Iterates each renewal-type
     * invoice item that targets a ClientProduct (related_service_id) and copies
     * the item's `total` to the service's `recurring_amount`. Upgrade/downgrade
     * items are skipped — those carry prorated differences, not the new
     * renewal price (which Task 24 writes from ProrationService).
     */
    private function updateRecurringAmounts($invoice): void
    {
        $items = $invoice->items()
            ->where('related_type', 'product')
            ->whereNotNull('related_service_id')
            ->whereNull('upgrade_type')
            ->get();

        foreach ($items as $item) {
            $service = ClientProduct::find($item->related_service_id);
            if (! $service) {
                continue;
            }

            $service->update([
                // PRC-1099 rank-6 — createInvoiceFromOrder (line 146) populates
                // line_total (=total_price) but NOT total (defaults 0), so an
                // order/renewal item whose total was never set zeroed
                // recurring_amount and corrupted downstream proration. Guard the
                // zero-case by falling back to line_total ONLY when total is unset;
                // items that already carry a non-zero total keep it (verified: live
                // product items have total populated and != line_total, so we must
                // NOT unconditionally prefer line_total or we'd change existing rows).
                'recurring_amount' => (float) ($item->total ?: $item->line_total),
                // Unlock plan-changes once a renewal invoice is paid. The lock
                // is set by ProcessProductChangeJob when an upgrade/downgrade
                // is applied; we release it here so the customer can change
                // plan again in the new billing period.
                'upgrade_locked' => false,
            ]);
        }
    }

    /**
     * Handle add funds credit when invoice is paid
     *
     * When an add-funds invoice is paid, the InvoicePaymentService already creates
     * CREDIT (payment in) and DEBIT (applied to invoice) entries which net to zero.
     * This method adds an additional CREDIT entry to actually top up the client's balance.
     */
    private function handleAddFundsCredit($invoice): void
    {
        // Check if this is an add-funds invoice (identified by notes field)
        if ($invoice->notes !== 'add_funds') {
            return;
        }

        // PRC-1103 — idempotency: InvoiceStatusChanged 9 yerden dispatch edilir ve
        // handle() 'oldStatus zaten paid mi' kontrol etmez; gateway webhook çift-
        // teslimatı / re-mark bu krediyi ikinci kez yazıp cüzdanı şişirebilir
        // (yoktan para). Bu fatura için zaten bir add_funds credit varsa çık
        // (reseller yolundaki exists-guard ile aynı sözleşme).
        if (ClientBalance::where('invoice_id', $invoice->id)->where('reference_type', 'add_funds')->exists()) {
            return;
        }

        try {
            // PRC-695 — Bakiyeye kredi edilecek tutar DAİMA vergisiz net tutardır
            // (subtotal). Add-funds vergisi açıkken total_amount = subtotal + KDV;
            // KDV bakiyeye eklenmemeli (çifte-vergilendirme/şişme önlenir). Vergi
            // kapalıyken subtotal == total_amount olduğundan davranış korunur.
            $creditAmount = round((float) $invoice->subtotal, 2);
            if ($creditAmount <= 0) {
                $creditAmount = round((float) $invoice->total_amount, 2);
            }

            // Add credit to client's balance
            ClientBalance::addCredit(
                $invoice->client_id,
                $invoice->currency_id,
                $creditAmount,
                [
                    'description' => 'Account credit top-up via invoice #' . $invoice->invoice_number,
                    'invoice_id' => $invoice->id,
                    'order_id' => $invoice->order_id,
                    'reference_type' => 'add_funds',
                    'reference_id' => $invoice->id,
                    'metadata' => [
                        'invoice_number' => $invoice->invoice_number,
                        'type' => 'add_funds',
                    ],
                ]
            );

            \Log::info('Add funds credit applied to client balance', [
                'invoice_id' => $invoice->id,
                'invoice_number' => $invoice->invoice_number,
                'client_id' => $invoice->client_id,
                'amount' => $creditAmount,
            ]);
        } catch (\Exception $e) {
            \Log::error('Failed to apply add funds credit', [
                'invoice_id' => $invoice->id,
                'client_id' => $invoice->client_id,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Handle upgrade invoice items — dispatch package change jobs.
     */
    /**
     * Generate reseller commission when a reseller's client pays an invoice.
     * Uses brand-based detection + ClientGroup granular commission rates.
     */
    private function generateResellerCommission($invoice): void
    {
        $order = $invoice->order;

        // Commission only for brand-based orders
        if (! $order || ! $order->brand_id) {
            return;
        }

        $brand = \App\Models\Brand::find($order->brand_id);
        if (! $brand || ! $brand->reseller_id) {
            return;
        }

        $reseller = Reseller::find($brand->reseller_id);
        if (! $reseller || ! $reseller->isActive()) {
            return;
        }

        // Commission must be enabled on reseller's client group
        $group = $reseller->group;
        if (! $group || ! $group->commission_enabled) {
            return;
        }

        if (ResellerCommission::where('invoice_id', $invoice->id)->exists()) {
            return;
        }

        // PRC-845 (edge #22) — self-dealing engeli: reseller kendi siparişinden
        // komisyon KAZANMAZ (order sahibi = reseller'ın client'ı ise atla).
        if ($order->client_id && $reseller->client_id
            && (int) $order->client_id === (int) $reseller->client_id) {
            \Illuminate\Support\Facades\Log::info('Reseller self-dealing: commission skipped', [
                'invoice_id' => $invoice->id,
                'reseller_id' => $reseller->id,
                'client_id' => $order->client_id,
            ]);
            return;
        }

        $commissionService = app(\App\Services\CommissionCalculationService::class);

        // PRC-845 — kalem-bazlı komisyon YALNIZ percentage tipte. Fixed tipte
        // her satır için sabit tutar uygulamak satır×N regresyonu üretir; o yüzden
        // fixed tip tek-uygulama (fatura toplamı) davranışını korur.
        if ($group->commission_type === 'percentage') {
            [$commissionAmount, $base] = $this->calculateLineBasedCommission($invoice, $group, $commissionService);
            // Harmanlanmış efektif oran (kolon tek oran tutar; satır oranları karışık olabilir)
            $rate = $base > 0 ? round($commissionAmount / $base * 100, 2) : (float) $group->commission_rate;
            $description = "Line-based commission for order #{$order->order_number}";
        } else {
            $commissionAmount = $commissionService->calculateCommission($group, (float) $invoice->total_amount);
            $rate = $commissionService->resolveRate($group);
            $description = "Commission for order #{$order->order_number}";
        }

        if ($commissionAmount <= 0) {
            return;
        }

        ResellerCommission::create([
            'reseller_id' => $reseller->id,
            'order_id' => $order->id,
            'invoice_id' => $invoice->id,
            'client_id' => $invoice->client_id,
            'order_amount' => $invoice->total_amount,
            'commission_amount' => $commissionAmount,
            'commission_type' => $group->commission_type,
            'commission_rate' => $rate,
            'status' => 'pending',
            'description' => $description,
        ]);
    }

    /**
     * PRC-845 — fatura satırlarını dolaşıp satır başına oran çözerek komisyonu
     * toplar. Satır çözümü: related_type='product' → related_id=Product id +
     * Product.group_id; related_type='domain' → Tld.tld (lider noktalı, ör
     * '.com'); ikisi de yoksa/eşleşmezse CommissionCalculationService grup
     * default oranına düşer. Dönen: [toplam_komisyon, komisyon_tabanı].
     */
    private function calculateLineBasedCommission($invoice, $group, $commissionService): array
    {
        $totalCommission = 0.0;
        $base = 0.0;

        foreach ($invoice->items as $item) {
            $lineAmount = (float) $item->line_total;
            if ($lineAmount <= 0) {
                continue;
            }

            $productId = null;
            $productGroupId = null;
            $tld = null;

            if ($item->related_type === 'product') {
                $productId = $item->related_id ?: $item->product_id;
                if ($productId) {
                    $productGroupId = \App\Models\Product::where('id', $productId)->value('group_id');
                }
            } elseif ($item->related_type === 'domain') {
                $tld = $this->resolveItemTld($item);
            } elseif ($item->product_id) {
                // related_type set değilse legacy product_id'ye düş
                $productId = $item->product_id;
                $productGroupId = \App\Models\Product::where('id', $productId)->value('group_id');
            }

            $totalCommission += $commissionService->calculateCommission(
                $group, $lineAmount, $productId, $productGroupId, $tld
            );
            $base += $lineAmount;
        }

        return [round($totalCommission, 2), $base];
    }

    /**
     * PRC-845 — domain satırının TLD anahtarını çöz.
     *
     * PRC-839 (Module-is-Truth, CLAUDE.md Domain §2): TLD DAİMA domain string'inden
     * türetilir; `related_id`/`tld_id` drift edebilir (belgelenmiş `moen.info →
     * .net.tr` bug'ı) ve drift ederse YANLIŞ TLD'nin komisyon oranı uygulanır.
     * tld_id yalnız domain_name yoksa (fallback) kullanılır. Lider noktalı ör '.com'.
     */
    private function resolveItemTld($item): ?string
    {
        if ($item->domain_name && str_contains($item->domain_name, '.')) {
            return '.' . explode('.', $item->domain_name, 2)[1];
        }

        if ($item->related_id) {
            $tldStr = \App\Models\Tld::where('id', $item->related_id)->value('tld');
            if ($tldStr) {
                return $tldStr;
            }
        }

        return null;
    }

    private function triggerUpgrades($invoice): void
    {
        $upgradeItems = $invoice->items()
            ->where('upgrade_type', 'upgrade')
            ->whereNotNull('related_service_id')
            ->whereNotNull('product_id')
            ->get();

        foreach ($upgradeItems as $item) {
            $service = \App\Models\ClientProduct::find($item->related_service_id);

            if (! $service || ! $service->pending_product_id) {
                continue;
            }

            ProcessProductChangeJob::dispatch(
                clientProductId: $service->id,
                targetProductId: $item->product_id,
                newPeriod:       \App\Services\ProrationService::billingCycleToMonths($service->pending_billingcycle ?? 'monthly'),
                skipRefund:      false,
            );
        }
    }

    /**
     * Unsuspend services that were auto-suspended due to overdue payment.
     * Only unsuspends products with suspend_reason = 'overdue'; manually suspended services are left untouched.
     */
    private function unsuspendPaidServices($invoice): void
    {
        $items = $invoice->items()->with('clientProduct')->get();

        foreach ($items as $item) {
            $clientProduct = $item->clientProduct;

            if (! $clientProduct) {
                continue;
            }

            // Only unsuspend services that were auto-suspended for overdue payment
            if ($clientProduct->status !== 'suspended' || $clientProduct->suspend_reason !== 'overdue') {
                continue;
            }

            $product = $clientProduct->product;

            // If the product has a module configured, dispatch the unsuspend job
            if ($product && $product->module) {
                ProcessProductUnsuspendJob::dispatch($clientProduct->id);

                $clientProduct->update(['suspend_reason' => null]);

                Log::create([
                    'type' => Log::TYPE_SYSTEM,
                    'observer' => 'HandleInvoiceStatusChange',
                    'module' => 'ClientProduct',
                    'method' => 'unsuspendPaidServices',
                    'client_id' => $invoice->client_id,
                    'ip_address' => request()->ip() ?? '0.0.0.0',
                    'description' => "Auto-unsuspend dispatched for service #{$clientProduct->id} (invoice #{$invoice->invoice_number} paid)",
                    'request' => [
                        'invoice_id' => $invoice->id,
                        'client_product_id' => $clientProduct->id,
                        'product_id' => $clientProduct->product_id,
                    ],
                ]);

                \Log::info('Product unsuspend job dispatched (overdue invoice paid)', [
                    'invoice_id' => $invoice->id,
                    'client_product_id' => $clientProduct->id,
                ]);
            } else {
                // No module configured — just reactivate directly
                $clientProduct->update([
                    'status' => 'active',
                    'suspend_reason' => null,
                ]);

                Log::create([
                    'type' => Log::TYPE_SYSTEM,
                    'observer' => 'HandleInvoiceStatusChange',
                    'module' => 'ClientProduct',
                    'method' => 'unsuspendPaidServices',
                    'client_id' => $invoice->client_id,
                    'ip_address' => request()->ip() ?? '0.0.0.0',
                    'description' => "Auto-unsuspended service #{$clientProduct->id} directly (no module, invoice #{$invoice->invoice_number} paid)",
                    'request' => [
                        'invoice_id' => $invoice->id,
                        'client_product_id' => $clientProduct->id,
                        'product_id' => $clientProduct->product_id,
                    ],
                ]);

                \Log::info('Product unsuspended directly (no module, overdue invoice paid)', [
                    'invoice_id' => $invoice->id,
                    'client_product_id' => $clientProduct->id,
                ]);
            }
        }
    }

    /**
     * PRC-536 — Free trial → active conversion.
     *
     * When the post-trial renewal invoice (total_amount > 0) is paid, transition
     * each trial client_product on the invoice out of trial:
     *   - client_products.is_trial → false
     *   - client_product_trials (mode=free, status=active) → status=converted
     *
     * The signup invoice (total_amount = 0, created at checkout) MUST be skipped;
     * the `total_amount > 0` guard prevents premature trial exit.
     * Non-trial services are unaffected (no is_trial products found → no-op).
     */
    private function convertTrialsToActive(Invoice $invoice): void
    {
        // Guard: skip the 0-amount signup invoice — only the post-trial renewal
        // invoice (non-zero amount) should trigger the conversion.
        if ((float) $invoice->total_amount <= 0) {
            return;
        }

        try {
            // Only collect service IDs whose line-item amount is > 0.
            // Filtering on `line_total` because `DueInvoiceService`/`BillingManager`
            // populates `line_total` (not `total`, which defaults to 0) on renewal
            // invoice items. In a mixed basket the free-trial product's signup
            // line is 0; the post-trial renewal line is the full recurring amount
            // (> 0). Filtering here prevents premature trial exit on the signup
            // invoice when a paid item in the same basket makes total_amount > 0.
            $serviceIds = $invoice->items()
                ->where('related_type', 'product')
                ->whereNotNull('related_service_id')
                ->where('line_total', '>', 0)
                ->pluck('related_service_id')
                ->unique()
                ->values()
                ->all();

            if (empty($serviceIds)) {
                return;
            }

            $trialProducts = ClientProduct::whereIn('id', $serviceIds)
                ->where('is_trial', true)
                ->get();

            foreach ($trialProducts as $cp) {
                $cp->is_trial = false;
                $cp->save();

                ClientProductTrial::where('client_product_id', $cp->id)
                    ->where('trial_mode', 'free')
                    ->where('status', 'active')
                    ->update(['status' => 'converted']);

                \Log::info('PRC-536 trial converted to active on invoice payment', [
                    'invoice_id'        => $invoice->id,
                    'client_product_id' => $cp->id,
                    'client_id'         => $cp->client_id,
                ]);

                // PRC-539 — trial_converted email
                try {
                    $cp->loadMissing(['client', 'product']);
                    $client = $cp->client;
                    $product = $cp->product;
                    if ($client && $product) {
                        $currencyCode = $client->currency?->code ?? 'USD';
                        SendEmailJob::dispatch(
                            'Trial Converted',
                            'client_product',
                            $cp->id,
                            [
                                'CLIENT_NAME'      => trim($client->first_name . ' ' . $client->last_name),
                                'PRODUCT_NAME'     => $product->name,
                                'TRIAL_ENDS_AT'    => $cp->trial_ends_at
                                    ? $cp->trial_ends_at->format('Y-m-d')
                                    : '—',
                                'TRIAL_DAYS'       => $product->trial_period_days ?? '—',
                                'RECURRING_AMOUNT' => number_format((float) $cp->recurring_amount, 2) . ' ' . $currencyCode,
                                'RECURRING_CYCLE'  => $cp->billingcycle ?? 'monthly',
                            ]
                        );
                    }
                } catch (\Throwable $e) {
                    \Log::warning('PRC-539 trial_converted email gönderilemedi', [
                        'client_product_id' => $cp->id,
                        'error' => $e->getMessage(),
                    ]);
                }
            }
        } catch (\Exception $e) {
            // A trial-conversion failure must NOT break invoice payment processing.
            \Log::error('PRC-536 convertTrialsToActive failed', [
                'invoice_id' => $invoice->id,
                'error'      => $e->getMessage(),
                'trace'      => $e->getTraceAsString(),
            ]);
        }
    }

    /**
     * Trigger provisioning jobs for paid invoice
     */
    private function triggerProvisioning($invoice): void
    {
        // Check if invoice has an order
        $order = $invoice->order;

        if (!$order) {
            \Log::info('Invoice has no order, skipping provisioning', [
                'invoice_id' => $invoice->id,
                'invoice_number' => $invoice->invoice_number,
            ]);
            return;
        }

        $order->load(['items.product', 'items.tld']);

        // PRC-573 — Ürün provizyonu order'ın pending ClientProduct'larını tek tek
        // gezer (order item product_id + first() yerine). Aynı product_id'den iki
        // kalem (örn. 1 free-trial + 1 ücretli) olduğunda eski first() en düşük
        // id'yi seçip birini çift-dispatch, diğerini hiç dispatch etmiyordu.
        // Per-CP iterasyon her satırı tam 1 kez dispatch eder ve checkout'ta
        // zaten queue'lanmış (artık pending olmayan) trial CP'yi doğal olarak
        // atlar. Dispatch kararı tek otorite ProductProvisioningDispatcher'da.
        $dispatcher = app(\App\Services\Provisioning\ProductProvisioningDispatcher::class);
        $pendingClientProducts = ClientProduct::where('order_id', $order->id)
            ->where('provisioning_status', 'pending')
            ->with('product')
            ->get();

        foreach ($pendingClientProducts as $clientProduct) {
            $dispatcher->dispatch($clientProduct, $order->id, 'invoice paid');
        }

        foreach ($order->items as $orderItem) {
            // Handle domain items
            if ($orderItem->type === 'domain' && $orderItem->domain_name) {
                $domainAction = $orderItem->domain_action ?? 'register';

                if ($domainAction === 'renew') {
                    // Renewal: find existing active domain by name + client
                    $clientDomain = ClientDomain::where('client_id', $order->client_id)
                        ->where('domain', $orderItem->domain_name)
                        ->whereIn('status', ['active', 'expired'])
                        ->first();

                    if ($clientDomain) {
                        ProcessDomainRenewJob::dispatch($clientDomain->id, null, $order->id);
                        \Log::info('Domain renewal job dispatched (invoice paid)', [
                            'invoice_id' => $invoice->id,
                            'order_id' => $order->id,
                            'client_domain_id' => $clientDomain->id,
                            'domain' => $clientDomain->domain,
                        ]);
                    }
                } else {
                    // Register/Transfer: find pending domain and queue it
                    $domainAffected = ClientDomain::where('client_id', $order->client_id)
                        ->where('domain', $orderItem->domain_name)
                        ->where('provisioning_status', 'pending')
                        ->update(['provisioning_status' => 'queued']);

                    if ($domainAffected > 0) {
                        $clientDomain = ClientDomain::where('client_id', $order->client_id)
                            ->where('domain', $orderItem->domain_name)
                            ->first();

                        match ($domainAction) {
                            'register' => (function () use ($clientDomain, $order, $invoice) {
                                ProcessDomainRegisterJob::dispatch($clientDomain->id, null, $order->id);
                                \Log::info('Domain registration job dispatched (invoice paid)', [
                                    'invoice_id' => $invoice->id,
                                    'order_id' => $order->id,
                                    'client_domain_id' => $clientDomain->id,
                                    'domain' => $clientDomain->domain,
                                ]);
                            })(),
                            'transfer' => (function () use ($clientDomain, $order, $invoice) {
                                ProcessDomainTransferJob::dispatch($clientDomain->id, null, $order->id);
                                \Log::info('Domain transfer job dispatched (invoice paid)', [
                                    'invoice_id' => $invoice->id,
                                    'order_id' => $order->id,
                                    'client_domain_id' => $clientDomain->id,
                                    'domain' => $clientDomain->domain,
                                ]);
                            })(),
                            default => \Log::warning('Unknown domain action, skipping provisioning', [
                                'invoice_id' => $invoice->id,
                                'order_id' => $order->id,
                                'domain_action' => $domainAction,
                            ]),
                        };
                    }
                }
            }
        }
    }
}
