<?php

namespace App\Services;

use App\Jobs\SendEmailJob;
use App\Models\Affiliate;
use App\Models\AffiliateCommission;
use App\Models\AffiliateLink;
use App\Models\AffiliatePayout;
use App\Models\AffiliateProductOverride;
use App\Models\AffiliateReferral;
use App\Models\ClientBalance;
use App\Models\Currency;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Product;
use App\Models\Setting;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class AffiliateService
{
    /**
     * Create a commission record for an invoice item.
     *
     * Calculates the rate, prevents self-referral, creates the commission
     * with status=pending, updates affiliate.total_earned, and dispatches
     * the commission earned email.
     */
    public function createCommission(
        InvoiceItem $item,
        Affiliate $affiliate,
        string $type = 'initial'
    ): ?AffiliateCommission {
        $invoice = $item->invoice;

        // Self-referral prevention: affiliate cannot earn from own purchases
        if ($affiliate->client_id === $invoice->client_id) {
            Log::info('AffiliateService: Self-referral prevented', [
                'affiliate_id' => $affiliate->id,
                'client_id' => $invoice->client_id,
                'invoice_id' => $invoice->id,
            ]);

            return null;
        }

        // Get the product for rate calculation
        $product = $item->product;

        // If no product found, skip commission
        if (!$product) {
            Log::info('AffiliateService: No product found for invoice item, skipping', [
                'invoice_item_id' => $item->id,
                'affiliate_id' => $affiliate->id,
            ]);

            return null;
        }

        // Calculate commission rate
        $rate = $this->getCommissionRate($product, $affiliate);

        if ($rate <= 0) {
            Log::info('AffiliateService: Commission rate is zero, skipping', [
                'product_id' => $product->id,
                'affiliate_id' => $affiliate->id,
            ]);

            return null;
        }

        // Calculate commission amount from the item's line_total (before tax)
        $itemAmount = (float) $item->line_total;
        $commissionAmount = round($itemAmount * ($rate / 100), 2);

        // PRC-622 — 'CalcAffiliateCommission' filter'ı: handler'lar hesaplanan
        // tutarı override edebilir (WHMCS semantiği). Handler yoksa değer
        // aynen geri döner.
        $filterCtx = [
            'affiliate_id' => $affiliate->id,
            'client_id' => $invoice->client_id,
            'invoice_id' => $invoice->id,
            'invoice_item_id' => $item->id,
            'product_id' => $product->id,
            'rate' => $rate,
            'type' => $type,
            'invoice_amount' => $itemAmount,
        ];
        $filtered = app(\App\Core\Hook\HookManager::class)
            ->filter('CalcAffiliateCommission', $commissionAmount, $filterCtx);
        if (is_numeric($filtered)) {
            $commissionAmount = round((float) $filtered, 2);
        }

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

        // Determine the affiliate link that generated this sale (if available)
        $affiliateLinkId = null;
        $client = $invoice->client;
        if ($client && $client->affiliate_id === $affiliate->id) {
            // Try to find the most recent link for this affiliate
            $latestLink = AffiliateLink::where('affiliate_id', $affiliate->id)
                ->where('is_active', true)
                ->latest()
                ->first();
            $affiliateLinkId = $latestLink?->id;
        }

        // PRC-1094 — affiliate_commissions.referral_id is NOT NULL FK to
        // affiliate_referrals. Resolve (or create) the referral row for this
        // affiliate↔client pair and mark it converted. firstOrCreate is a
        // safety-net: even if the middleware attribution path never ran (e.g.
        // affiliate_id set via import/tinker), a valid referral id is
        // guaranteed here so the commission insert can't violate the FK.
        $referral = AffiliateReferral::firstOrCreate(
            [
                'affiliate_id' => $affiliate->id,
                'referred_client_id' => $invoice->client_id,
            ],
            [
                'status' => 'converted',
            ]
        );
        if ($referral->status !== 'converted') {
            $referral->update(['status' => 'converted']);
        }

        // PRC-1101 — komisyon fatura currency'sinde hesaplandı; affiliate'in TEK
        // payout currency'sine FX ile çevir + çevrilmiş tutarı satırda sakla
        // (tahakkuk/onay/reverse simetrik, FX-drift yok). Skaler total_earned/
        // pending_balance artık payout currency cinsindendir.
        $payoutAmount = $this->convertToPayoutCurrency($commissionAmount, (int) $invoice->currency_id, $affiliate);

        $commission = AffiliateCommission::create([
            'affiliate_id' => $affiliate->id,
            'referral_id' => $referral->id,
            'invoice_id' => $invoice->id,
            'affiliate_link_id' => $affiliateLinkId,
            'client_id' => $invoice->client_id,
            'order_id' => $invoice->order_id,
            'product_id' => $product->id,
            'type' => $type,
            'amount' => $commissionAmount,
            'payout_amount' => $payoutAmount,
            'commission_rate' => $rate,
            'invoice_amount' => $itemAmount,
            'currency_id' => $invoice->currency_id,
            'status' => 'pending',
        ]);

        // Update affiliate total_earned (payout currency cinsinden).
        $affiliate->increment('total_earned', $payoutAmount);

        // Dispatch commission earned email
        SendEmailJob::dispatch(
            'Commission Earned',
            'affiliate',
            $affiliate->id,
            [
                'AMOUNT' => number_format($commissionAmount, 2),
                'PRODUCT' => $product->name ?? $item->description,
                'STATUS' => 'pending',
            ]
        );

        // PRC-622 — 'AffiliateCommission' action hook'u (hook-only; komisyon
        // maili yukarıda zaten dispatch ediliyor).
        app(\App\Core\Hook\HookBridge::class)->fireAction('AffiliateCommission', array_merge($filterCtx, [
            'commission_id' => $commission->id,
            'amount' => $commissionAmount,
        ]));

        Log::info('AffiliateService: Commission created', [
            'commission_id' => $commission->id,
            'affiliate_id' => $affiliate->id,
            'amount' => $commissionAmount,
            'rate' => $rate,
            'type' => $type,
        ]);

        return $commission;
    }

    /**
     * Get the commission rate for a product and affiliate.
     *
     * Priority:
     * 1. AffiliateProductOverride for the product (if not enabled, return 0)
     * 2. Override rate on the product override
     * 3. Affiliate's own commission_rate
     * 4. Global setting: affiliate_earning_percentage
     */
    public function getCommissionRate(Product $product, Affiliate $affiliate): float
    {
        // Check product-level override
        $override = AffiliateProductOverride::where('product_id', $product->id)->first();

        if ($override) {
            // If the product is explicitly disabled for affiliates
            if (!$override->enabled) {
                return 0;
            }

            // If the override has a specific rate, use it
            if ($override->commission_rate !== null) {
                return (float) $override->commission_rate;
            }
        }

        // Check affiliate's own commission rate
        if ($affiliate->commission_rate !== null && (float) $affiliate->commission_rate > 0) {
            return (float) $affiliate->commission_rate;
        }

        // Fallback to global setting
        return (float) Setting::get('affiliate_earning_percentage', 10);
    }

    /**
     * Process pending commissions that have passed the delay period.
     *
     * Finds pending commissions where created_at + delay < now,
     * updates them to approved, adds amounts to affiliate.pending_balance,
     * and sends commission_approved email.
     *
     * @return int Number of commissions processed
     */
    public function processApprovedCommissions(): int
    {
        $delayDays = (int) Setting::get('affiliate_commission_delay', 30);
        $cutoffDate = now()->subDays($delayDays);

        $pendingCommissions = AffiliateCommission::where('status', 'pending')
            ->where('created_at', '<=', $cutoffDate)
            ->with('affiliate')
            ->get();

        $processedCount = 0;

        foreach ($pendingCommissions as $commission) {
            DB::transaction(function () use ($commission) {
                $commission->update([
                    'status' => 'approved',
                    'approved_at' => now(),
                ]);

                // Add to affiliate balance — PRC-1101: payout currency cinsinden
                // (payout_amount), ham commission amount değil (çok-currency karışmasın).
                $affiliate = $commission->affiliate;
                $affiliate->increment('pending_balance', (float) ($commission->payout_amount ?? $commission->amount));

                // Send commission approved email
                SendEmailJob::dispatch(
                    'Commission Approved',
                    'affiliate',
                    $affiliate->id,
                    [
                        'AMOUNT' => number_format($commission->amount, 2),
                        'BALANCE' => number_format($affiliate->fresh()->pending_balance, 2),
                    ]
                );
            });

            $processedCount++;
        }

        Log::info('AffiliateService: Processed pending commissions', [
            'count' => $processedCount,
            'delay_days' => $delayDays,
        ]);

        return $processedCount;
    }

    /**
     * Request a payout for an affiliate.
     *
     * If method=credit: transfers via ClientBalance, status=paid immediately.
     * If method=payout_request: status=pending, email sent to admin.
     */
    public function requestPayout(Affiliate $affiliate, float $amount, string $method): AffiliatePayout
    {
        $minPayout = (float) Setting::get('affiliate_payout_amount', 50);

        // Stateless validations (no balance dependency) stay outside the lock.
        if ($amount <= 0) {
            throw new \InvalidArgumentException('Payout amount must be greater than zero.');
        }
        if ($method === 'payout_request' && $amount < $minPayout) {
            throw new \InvalidArgumentException("Minimum payout amount is {$minPayout}.");
        }

        // PRC-1099 rank-14: the available-balance check must be serialized with
        // the write under a pessimistic lock. Pre-fix the check ran outside the
        // transaction on a stale in-memory model, so two concurrent requests
        // both read the same balance and each created a payout (TOCTOU double
        // payout / negative pending_balance). Re-load + lock the affiliate and
        // recompute available balance from locked reads INSIDE the transaction.
        return DB::transaction(function () use ($affiliate, $amount, $method) {
            $affiliate = Affiliate::whereKey($affiliate->id)->lockForUpdate()->firstOrFail();

            $pendingPayoutsTotal = (float) AffiliatePayout::where('affiliate_id', $affiliate->id)
                ->where('status', 'pending')
                ->sum('amount');
            $availableBalance = (float) $affiliate->pending_balance - $pendingPayoutsTotal;

            if ($amount > $availableBalance) {
                throw new \InvalidArgumentException('Requested amount exceeds available balance.');
            }

            if ($method === 'credit') {
                // Instant credit transfer
                $payout = AffiliatePayout::create([
                    'affiliate_id' => $affiliate->id,
                    'amount' => $amount,
                    'status' => 'paid',
                    'payment_method' => 'credit',
                    'method' => 'credit',
                    'paid_at' => now(),
                ]);

                // Transfer to client balance
                $this->transferToCredit($affiliate, $amount);

                return $payout;
            }

            // Payout request — needs admin approval
            $payout = AffiliatePayout::create([
                'affiliate_id' => $affiliate->id,
                'amount' => $amount,
                'status' => 'pending',
                'payment_method' => $affiliate->payout_details['method'] ?? 'bank_transfer',
                'method' => 'payout_request',
            ]);

            // Send email notification to admin
            SendEmailJob::dispatch(
                'Payout Requested',
                'affiliate',
                $affiliate->id,
                [
                    'AFFILIATE_NAME' => $affiliate->client->first_name . ' ' . $affiliate->client->last_name,
                    'AMOUNT' => number_format($amount, 2),
                    'METHOD' => $method,
                ]
            );

            // PRC-622 — 'AffiliateWithdrawalRequest' action hook'u (hook-only;
            // 'Payout Requested' maili yukarıda zaten dispatch ediliyor
            // → communication_events eşlemesi YOK).
            app(\App\Core\Hook\HookBridge::class)->fireAction('AffiliateWithdrawalRequest', [
                'affiliate_id' => $affiliate->id,
                'client_id' => $affiliate->client_id,
                'payout_id' => $payout->id,
                'amount' => $amount,
                'method' => $method,
            ]);

            return $payout;
        });
    }

    /**
     * Approve a payout request.
     *
     * Sets status=paid, records admin_id and paid_at,
     * subtracts from affiliate.pending_balance, adds to total_paid,
     * and sends payout_paid email.
     */
    public function approvePayout(AffiliatePayout $payout, $admin): void
    {
        if ($payout->status !== 'pending') {
            throw new \InvalidArgumentException('Only pending payouts can be approved.');
        }

        DB::transaction(function () use ($payout, $admin) {
            $payout->update([
                'status' => 'paid',
                'admin_id' => $admin->id,
                'paid_at' => now(),
            ]);

            $affiliate = $payout->affiliate;

            // Subtract from balance and add to total_paid
            $affiliate->decrement('pending_balance', $payout->amount);
            $affiliate->increment('total_paid', $payout->amount);

            // Send payout paid email
            SendEmailJob::dispatch(
                'Payout Paid',
                'affiliate',
                $affiliate->id,
                [
                    'AMOUNT' => number_format($payout->amount, 2),
                    'REFERENCE' => $payout->transaction_reference ?? '-',
                ]
            );
        });
    }

    /**
     * Reject a payout request.
     *
     * Sets status=rejected, records admin_id and reason,
     * sends payout_rejected email.
     */
    public function rejectPayout(AffiliatePayout $payout, $admin, string $reason): void
    {
        if ($payout->status !== 'pending') {
            throw new \InvalidArgumentException('Only pending payouts can be rejected.');
        }

        DB::transaction(function () use ($payout, $admin, $reason) {
            $payout->update([
                'status' => 'rejected',
                'admin_id' => $admin->id,
                'notes' => $reason,
            ]);

            // Send payout rejected email
            SendEmailJob::dispatch(
                'Payout Rejected',
                'affiliate',
                $payout->affiliate_id,
                [
                    'AMOUNT' => number_format($payout->amount, 2),
                    'REASON' => $reason,
                ]
            );
        });
    }

    /**
     * Transfer affiliate balance to client credit (ClientBalance ledger entry).
     *
     * Creates a credit entry in the client_balances table and subtracts
     * from the affiliate's pending_balance.
     */
    /**
     * PRC-1101 — Affiliate'in payout para birimi id'si. payout_currency_id set
     * değilse client currency'sine, o da yoksa default'a düşer.
     */
    private function payoutCurrencyId(Affiliate $affiliate): int
    {
        return (int) ($affiliate->payout_currency_id
            ?? $affiliate->client?->currency_id
            ?? Currency::where('is_default', true)->value('id')
            ?? 1);
    }

    /**
     * PRC-1101 — Bir komisyon tutarını ($amount, $fromCurrencyId cinsinden)
     * affiliate'in payout currency'sine FX ile çevir. Aynı currency veya eksik/
     * bayat FX (153'ün 145'i rate=0) → ham tutar + uyarı (fail-safe, bozulma yok).
     */
    private function convertToPayoutCurrency(float $amount, int $fromCurrencyId, Affiliate $affiliate): float
    {
        $toCurrencyId = $this->payoutCurrencyId($affiliate);
        if ($fromCurrencyId === $toCurrencyId) {
            return round($amount, 2);
        }

        $fromRate = (float) (Currency::find($fromCurrencyId)?->rate ?? 0);
        $toRate = (float) (Currency::find($toCurrencyId)?->rate ?? 0);

        if ($fromRate > 0 && $toRate > 0) {
            return round(($amount / $fromRate) * $toRate, 2);
        }

        Log::warning('Affiliate commission FX conversion skipped — stale/missing rate', [
            'affiliate_id' => $affiliate->id,
            'from_currency_id' => $fromCurrencyId,
            'to_currency_id' => $toCurrencyId,
            'from_rate' => $fromRate,
            'to_rate' => $toRate,
        ]);

        return round($amount, 2);
    }

    public function transferToCredit(Affiliate $affiliate, float $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('Transfer amount must be greater than zero.');
        }

        if ($amount > (float) $affiliate->pending_balance) {
            throw new \InvalidArgumentException('Transfer amount exceeds available balance.');
        }

        $client = $affiliate->client;

        // PRC-1101 — pending_balance affiliate.payout_currency_id cinsindendir;
        // krediyi o currency'de ver (client currency sonradan değişmiş olabilir).
        $currencyId = $this->payoutCurrencyId($affiliate);

        // Create credit entry in client balance ledger
        ClientBalance::addCredit(
            $client->id,
            $currencyId,
            $amount,
            [
                'description' => 'Affiliate commission credit transfer',
                'reference_type' => 'affiliate',
                'reference_id' => $affiliate->id,
            ]
        );

        // Subtract from affiliate balance
        $affiliate->decrement('pending_balance', $amount);
        $affiliate->increment('total_paid', $amount);
    }

    /**
     * KVKK-compliant name masking.
     *
     * "Bunyamin" -> "B***"
     * "Bunyamin Akcay" -> "B*** A***"
     */
    public function maskName(string $name): string
    {
        if (empty($name)) {
            return '';
        }

        $parts = explode(' ', trim($name));
        $masked = [];

        foreach ($parts as $part) {
            if (mb_strlen($part) === 0) {
                continue;
            }
            $masked[] = mb_strtoupper(mb_substr($part, 0, 1)) . '***';
        }

        return implode(' ', $masked);
    }

    /**
     * KVKK-compliant email masking.
     *
     * "test@example.com" -> "t***@example.com"
     * "bunyamin@company.com" -> "b***@company.com"
     */
    public function maskEmail(string $email): string
    {
        if (empty($email) || !str_contains($email, '@')) {
            return '';
        }

        [$local, $domain] = explode('@', $email, 2);

        if (mb_strlen($local) === 0) {
            return '***@' . $domain;
        }

        return mb_strtolower(mb_substr($local, 0, 1)) . '***@' . $domain;
    }

    /**
     * Generate a monthly report for an affiliate.
     *
     * Returns an array with:
     * clicks, unique_clicks, conversions, conversion_rate, earnings,
     * approved, balance, top_link, month_over_month_change
     */
    public function generateMonthlyReport(Affiliate $affiliate, Carbon $month): array
    {
        $startOfMonth = $month->copy()->startOfMonth();
        $endOfMonth = $month->copy()->endOfMonth();

        // Previous month for comparison
        $prevStart = $month->copy()->subMonth()->startOfMonth();
        $prevEnd = $month->copy()->subMonth()->endOfMonth();

        // Clicks this month (via affiliate_links -> affiliate_clicks)
        $linkIds = AffiliateLink::where('affiliate_id', $affiliate->id)->pluck('id');

        $clicks = 0;
        $uniqueClicks = 0;

        if ($linkIds->isNotEmpty()) {
            $clicks = DB::table('affiliate_clicks')
                ->whereIn('affiliate_link_id', $linkIds)
                ->whereBetween('created_at', [$startOfMonth, $endOfMonth])
                ->count();

            $uniqueClicks = DB::table('affiliate_clicks')
                ->whereIn('affiliate_link_id', $linkIds)
                ->whereBetween('created_at', [$startOfMonth, $endOfMonth])
                ->where('is_unique', true)
                ->count();
        }

        // Conversions (commissions created this month)
        $conversions = AffiliateCommission::where('affiliate_id', $affiliate->id)
            ->whereBetween('created_at', [$startOfMonth, $endOfMonth])
            ->count();

        // Conversion rate
        $conversionRate = $uniqueClicks > 0 ? round(($conversions / $uniqueClicks) * 100, 2) : 0;

        // Earnings this month (total commission amount)
        $earnings = AffiliateCommission::where('affiliate_id', $affiliate->id)
            ->whereBetween('created_at', [$startOfMonth, $endOfMonth])
            ->sum('amount');

        // Approved commissions this month
        $approved = AffiliateCommission::where('affiliate_id', $affiliate->id)
            ->where('status', 'approved')
            ->whereBetween('approved_at', [$startOfMonth, $endOfMonth])
            ->sum('amount');

        // Current balance
        $balance = (float) $affiliate->pending_balance;

        // Top performing link this month
        $topLink = null;
        if ($linkIds->isNotEmpty()) {
            $topLinkId = DB::table('affiliate_clicks')
                ->select('affiliate_link_id', DB::raw('COUNT(*) as click_count'))
                ->whereIn('affiliate_link_id', $linkIds)
                ->whereBetween('created_at', [$startOfMonth, $endOfMonth])
                ->groupBy('affiliate_link_id')
                ->orderByDesc('click_count')
                ->first();

            if ($topLinkId) {
                $topLink = AffiliateLink::find($topLinkId->affiliate_link_id);
            }
        }

        // Month-over-month change (earnings comparison)
        $prevEarnings = AffiliateCommission::where('affiliate_id', $affiliate->id)
            ->whereBetween('created_at', [$prevStart, $prevEnd])
            ->sum('amount');

        $monthOverMonthChange = $prevEarnings > 0
            ? round((($earnings - $prevEarnings) / $prevEarnings) * 100, 2)
            : ($earnings > 0 ? 100 : 0);

        return [
            'clicks' => $clicks,
            'unique_clicks' => $uniqueClicks,
            'conversions' => $conversions,
            'conversion_rate' => $conversionRate,
            'earnings' => round($earnings, 2),
            'approved' => round($approved, 2),
            'balance' => $balance,
            'top_link' => $topLink,
            'month_over_month_change' => $monthOverMonthChange,
        ];
    }

    /**
     * Apply bonus deposit to affiliate on activation.
     *
     * If the affiliate_bonus_deposit setting is > 0, creates a ClientBalance
     * credit entry for the affiliate's client.
     */
    public function applyBonusDeposit(Affiliate $affiliate): void
    {
        $bonusAmount = (float) Setting::get('affiliate_bonus_deposit', 0);

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

        $client = $affiliate->client;

        // Determine the client's currency (fall back to default)
        $currencyId = $client->currency_id
            ?? Currency::where('is_default', true)->first()?->id
            ?? 1;

        ClientBalance::addCredit(
            $client->id,
            $currencyId,
            $bonusAmount,
            [
                'description' => 'Affiliate activation bonus deposit',
                'reference_type' => 'affiliate',
                'reference_id' => $affiliate->id,
            ]
        );

        // PRC-628 — Referral Bonus: aktivasyon bonusu kredilendiğinde affiliate'a
        // bildirim. 'Commission Earned'den AYRI bir bonus-kredi olayı (komisyon
        // değil, affiliate_bonus_deposit setting'i). Önceden sessizce kredi
        // ediliyordu — tetiksiz 'Referral Bonus' şablonu burada canlanır.
        SendEmailJob::dispatch('Referral Bonus', 'affiliate', $affiliate->id, [
            'BONUSAMOUNT' => number_format($bonusAmount, 2),
        ]);

        Log::info('AffiliateService: Bonus deposit applied', [
            'affiliate_id' => $affiliate->id,
            'client_id' => $client->id,
            'amount' => $bonusAmount,
        ]);
    }

    /**
     * Process affiliate commissions when an invoice is paid.
     *
     * Called after invoice payment. Checks client.affiliate_id for attribution,
     * creates a commission per invoice item, and respects recurring settings
     * for renewal invoices.
     */
    public function processInvoicePayment(Invoice $invoice): void
    {
        // Ensure the invoice is loaded with required relationships
        $invoice->loadMissing(['client', 'items.product']);

        $client = $invoice->client;

        if (!$client || !$client->affiliate_id) {
            return;
        }

        // Load the affiliate
        $affiliate = Affiliate::where('id', $client->affiliate_id)
            ->where('status', 'active')
            ->first();

        if (!$affiliate) {
            Log::info('AffiliateService: Affiliate not found or inactive for client', [
                'client_id' => $client->id,
                'affiliate_id' => $client->affiliate_id,
            ]);

            return;
        }

        // PRC-1099 rank-21: renewal-vs-first is PER LINE ITEM, not an
        // invoice-wide boolean. A returning client's invoice can legitimately
        // mix a brand-new first-order item (→ 'initial') with a renewal item
        // (→ 'recurring'). Previously the whole invoice was classified from
        // "client has any prior initial commission", and the
        // recurring-enabled + max-recurring-months gate was applied
        // invoice-wide with a method-level `return`, silently dropping the
        // genuinely-new item. Classify each item and apply the gate per item
        // with an early `continue`.
        $recurringEnabled = (bool) Setting::get('affiliate_recurring_commission', false);
        $maxMonths = (int) Setting::get('affiliate_max_recurring_months', 0);

        // Count DISTINCT prior renewal periods once (one renewal invoice =
        // one billing cycle). The current invoice's commissions are written
        // below, so this is the number of PRIOR renewal periods and stays
        // stable across the loop.
        $existingRecurring = AffiliateCommission::where('affiliate_id', $affiliate->id)
            ->where('client_id', $client->id)
            ->where('type', 'recurring')
            ->where('invoice_id', '!=', $invoice->id)
            ->distinct()
            ->count('invoice_id');

        foreach ($invoice->items as $item) {
            $isRenewal = $this->isRenewalItem($item, $invoice);

            if ($isRenewal) {
                if (!$recurringEnabled) {
                    Log::info('AffiliateService: Recurring commissions disabled, skipping renewal item', [
                        'invoice_id' => $invoice->id,
                        'invoice_item_id' => $item->id,
                        'affiliate_id' => $affiliate->id,
                    ]);

                    continue;
                }

                if ($maxMonths > 0 && $existingRecurring >= $maxMonths) {
                    Log::info('AffiliateService: Max recurring months reached, skipping renewal item', [
                        'affiliate_id' => $affiliate->id,
                        'client_id' => $client->id,
                        'invoice_item_id' => $item->id,
                        'max_months' => $maxMonths,
                        'existing' => $existingRecurring,
                    ]);

                    continue;
                }
            }

            $this->createCommission($item, $affiliate, $isRenewal ? 'recurring' : 'initial');
        }
    }

    /**
     * PRC-1099 rank-21 — Determine per invoice item whether it represents a
     * renewal of an already-existing service (→ 'recurring' commission) or a
     * brand-new first purchase (→ 'initial').
     *
     * A renewal item is one whose referenced service already existed before
     * this invoice. Signals, in priority order:
     *   1. Domain-lifecycle invoice: DomainLifecycleService stamps
     *      `invoices.domain_lifecycle_type` as renewal/grace/redemption on
     *      renewal/grace/redemption invoices — any domain line on such an
     *      invoice is a renewal.
     *   2. Prior paid reference: DueInvoiceService links renewal lines to the
     *      existing service via `invoice_items.related_service_id`
     *      (+ `related_type`). Both first-purchase AND renewal lines can carry
     *      it (checkout links the freshly-created service too), so the
     *      distinguishing test is whether that same service has a PRIOR PAID
     *      invoice item on a DIFFERENT invoice — true only for renewals.
     *   3. Fallback: the client-history heuristic in isRenewalInvoice().
     */
    private function isRenewalItem(InvoiceItem $item, Invoice $invoice): bool
    {
        // Signal 1 — domain lifecycle stamp on the invoice.
        $lifecycle = $invoice->domain_lifecycle_type;
        if ($item->related_type === 'domain'
            && in_array($lifecycle, ['renewal', 'grace', 'redemption'], true)) {
            return true;
        }

        // Signal 2 — the referenced service has a prior PAID invoice item.
        if ($item->related_service_id !== null && $item->related_type !== null) {
            $hasPriorPaidItem = InvoiceItem::query()
                ->where('related_type', $item->related_type)
                ->where('related_service_id', $item->related_service_id)
                ->where('invoice_id', '!=', $invoice->id)
                ->whereHas('invoice', function ($q) {
                    $q->where('status', Invoice::STATUS_PAID);
                })
                ->exists();

            if ($hasPriorPaidItem) {
                return true;
            }
        }

        // Signal 3 — last-resort client-history heuristic.
        return $this->isRenewalInvoice($invoice);
    }

    /**
     * PRC-1094 — Reverse affiliate commissions when an invoice is
     * refunded/cancelled (called from InvoiceObserver status transitions).
     *
     * Reverses only commissions that have NOT been paid out yet:
     *   - pending:  only total_earned was incremented at creation → decrement it.
     *   - approved: total_earned + pending_balance both include it → decrement both.
     *   - paid:     already paid out to the affiliate; no automatic clawback —
     *               logged for manual admin follow-up (refunding paid money is a
     *               financial decision, not a silent ledger edit).
     *   - rejected: already reversed → skipped.
     *
     * The commission status enum has no `cancelled`; reversal uses `rejected`
     * plus the cancelled_at / cancellation_reason columns (no enum migration).
     *
     * @return int Number of commissions reversed
     */
    public function reverseCommissionsForInvoice(Invoice $invoice, string $reason = 'Invoice refunded/cancelled'): int
    {
        $commissions = AffiliateCommission::where('invoice_id', $invoice->id)
            ->whereIn('status', ['pending', 'approved'])
            ->with('affiliate')
            ->get();

        $reversed = 0;

        foreach ($commissions as $commission) {
            DB::transaction(function () use ($commission, $reason) {
                $wasApproved = $commission->status === 'approved';

                $commission->update([
                    'status' => 'rejected',
                    'cancelled_at' => now(),
                    'cancellation_reason' => $reason,
                ]);

                $affiliate = $commission->affiliate;
                if ($affiliate) {
                    // PRC-1101 — tahakkuk payout_amount (payout currency) ile yapıldı;
                    // reverse de aynı tutarla simetrik olmalı (ham amount değil).
                    $reverseAmount = (float) ($commission->payout_amount ?? $commission->amount);
                    // total_earned was incremented at creation for every commission.
                    $affiliate->decrement('total_earned', $reverseAmount);
                    // approved commissions were additionally added to pending_balance.
                    if ($wasApproved) {
                        $affiliate->decrement('pending_balance', $reverseAmount);
                    }
                }
            });

            $reversed++;
        }

        // Paid commissions can't be auto-clawed back — surface for manual handling
        // rather than silently leaving commission on refunded money.
        $paidCount = AffiliateCommission::where('invoice_id', $invoice->id)
            ->where('status', 'paid')
            ->count();

        if ($paidCount > 0) {
            Log::warning('AffiliateService: paid commissions on refunded/cancelled invoice need manual clawback', [
                'invoice_id' => $invoice->id,
                'paid_commissions' => $paidCount,
            ]);
        }

        if ($reversed > 0) {
            Log::info('AffiliateService: reversed affiliate commissions', [
                'invoice_id' => $invoice->id,
                'reversed' => $reversed,
                'reason' => $reason,
            ]);
        }

        return $reversed;
    }

    /**
     * Determine if an invoice is a renewal/recurring invoice.
     *
     * Checks whether the client already has initial commissions for the same affiliate,
     * which would indicate this is a renewal purchase.
     */
    private function isRenewalInvoice(Invoice $invoice): bool
    {
        // If there are previous paid commissions for this client from this affiliate,
        // it means this is a recurring purchase
        $hasInitialCommission = AffiliateCommission::where('client_id', $invoice->client_id)
            ->where('type', 'initial')
            ->where('invoice_id', '!=', $invoice->id)
            ->exists();

        return $hasInitialCommission;
    }
}
