<?php

namespace App\Http\Controllers\Store;

use App\Helpers\VisitorHelper;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessProductCreateJob;
use App\Models\ClientActivityLog;
use App\Models\Currency;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\PaymentGatewaySetting;
use App\Models\Setting;
use App\Rules\ValidCaptcha;
use App\Services\Billing\InvoiceNumberService;
use App\Services\CaptchaService;
use App\Services\DiscountManager;
use App\Services\Domain\BasketExtraFieldsValidator;
use App\Services\PaymentManager;
use App\Services\PricingService;
use App\Services\Store\TrialService;
use App\Services\TaxCalculationService;
use App\Services\CitizenshipService;
use App\Services\VisitorService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class CheckoutController extends Controller
{
    protected $paymentManager;

    protected $pricingService;

    protected $discountManager;

    protected $visitorService;

    protected $taxCalculationService;

    public function __construct(
        PaymentManager $paymentManager,
        PricingService $pricingService,
        DiscountManager $discountManager,
        VisitorService $visitorService,
        TaxCalculationService $taxCalculationService
    ) {
        $this->middleware('checkout.flow');
        $this->paymentManager = $paymentManager;
        $this->pricingService = $pricingService;
        $this->taxCalculationService = $taxCalculationService;
        $this->discountManager = $discountManager;
        $this->visitorService = $visitorService;
    }

    public function index(Request $request)
    {
        $visitorId = VisitorHelper::getVisitorId($request);

        if (! $visitorId) {
            return redirect()->route('store.basket.index')->with('error', __('store.checkout_empty_basket'));
        }

        $basketData = VisitorHelper::getVisitorBasket($visitorId);
        $basketItems = $basketData['items'] ?? [];

        if (empty($basketItems)) {
            return redirect()->route('store.basket.index')->with('error', __('store.checkout_empty_basket'));
        }

        // Get authenticated client first
        $client = Auth::guard('client')->user();

        // Currency bilgisini al (öncelik: authenticated user > session > default)
        $currency = null;

        // 1. Authenticated user currency
        if ($client && $client->currency_id) {
            $currency = Currency::where('status', true)
                ->where('id', $client->currency_id)
                ->first();

            \Log::info('CheckoutController::index - Using authenticated user currency', [
                'client_id' => $client->id,
                'currency_id' => $client->currency_id,
                'currency_code' => $currency?->code,
            ]);
        }

        // 2. Session currency
        if (! $currency) {
            $currencyCode = app(\App\Services\Store\StorePreferenceResolver::class)->currentCurrencyCode();
            if ($currencyCode) {
                $currency = Currency::where('status', true)
                    ->where('code', $currencyCode)
                    ->first();
            }
        }

        // 3. Default currency
        if (! $currency) {
            $currency = Currency::where('status', true)
                ->where('is_default', true)
                ->first();
        }

        // CRITICAL: Basket items'daki fiyatları güncel currency'ye göre yeniden hesapla
        // Çünkü visitor_basket'te eski currency'nin fiyatları saklanıyor
        $basketItems = $this->recalculateBasketItemPrices($basketItems, $currency);

        // Basket summary hesapla
        $summary = $this->pricingService->calculateBasketSummaryFromItems($basketItems, $currency, $request);

        $trackingEventId = (string) Str::uuid();
        request()->headers->set('X-Event-Id', $trackingEventId);

        app(\App\Core\Hook\HookManager::class)->action('PreShoppingCartCheckout', [
            'basket' => (object) [
                'items' => $basketItems,
                'total' => $summary['total'] ?? 0,
                'currency' => $currency->code,
            ],
            'client' => $client,
        ]);

        // Payment gateway'leri al (modüler sistem)
        $paymentGateways = $this->getActivePaymentGateways();

        // Settings
        $allowNotesOnCheckout = Setting::getValue('allow_notes_on_checkout', true);

        // Billing addresses'leri al
        $billingAddresses = $client->billingAddresses()->orderBy('is_default', 'desc')->orderBy('created_at', 'desc')->get();
        $defaultBillingAddress = $client->defaultBillingAddress;

        // Custom fields that should show in order
        $orderCustomFields = \App\Models\ClientCustomField::where('show_in_order', true)
            ->orderBy('field_order')
            ->get();

        // Citizenship verification check
        $citizenshipService = app(CitizenshipService::class);
        $verificationRequired = false;
        $verificationStatus = null;
        $verificationMode = null;

        if ($citizenshipService->isEnabled() && $client) {
            $countryCode = $client->country ?? null;
            if ($countryCode && $citizenshipService->isRequiredForCountry($countryCode)) {
                $verificationStatus = $client->verification_status ?? 'unverified';
                $verificationMode = $citizenshipService->getMode();
                $verificationRequired = $verificationStatus !== 'verified';
            }
        }

        // Required contracts from basket products (per-product SLA/AUP/custom pivot)
        $basketProductIds = collect($basketItems)
            ->pluck('product_id')
            ->filter()
            ->unique();

        $requiredContracts = collect();
        if ($basketProductIds->isNotEmpty()) {
            $products = \App\Models\Product::with('contracts')
                ->whereIn('id', $basketProductIds)
                ->get();
            $requiredContracts = $products
                ->flatMap(fn ($p) => $p->contracts->filter(fn ($c) => (bool) $c->pivot->required))
                ->unique('id')
                ->values();
        }

        $enableTosAcceptance = (bool) Setting::getValue('enable_tos_acceptance', false);

        // PRC-592: genel ToS checkbox açıksa link'i sözleşme sisteminden (ToS contract)
        // çöz. Ürüne-bağlı SLA'nın kullandığı route'un aynısı (legal.contract.show).
        // $tosUrl daha önce hiç set edilmiyordu → blade'de undefined → link çıkmıyordu.
        $tosUrl = null;
        if ($enableTosAcceptance) {
            $tosContract = \App\Models\Contract::where('type', 'tos')
                ->where('status', 'active')
                ->orderByDesc('requires_acceptance')
                ->first();
            if ($tosContract) {
                $tosSlug = $tosContract->getTranslation('slug', app()->getLocale()) ?: $tosContract->slug;
                $tosUrl = route('legal.contract.show', $tosSlug);
            }
        }

        // PRC-540: Compute which gateways support card-save so the view can show/hide
        // the trial card-save opt-in checkbox per gateway selection.
        // We pass a flat set of gateway names (slugs) that have save_card=true AND
        // allow_save_card=true in their admin settings.
        $trialCCSaveProductIds = collect($basketItems)
            ->filter(fn ($i) => ! empty($i['is_trial'])
                && ($i['type'] ?? '') === 'product'
                && ! empty($i['product_id']))
            ->pluck('product_id')
            ->filter()
            ->unique();

        $hasTrialCCSaveItem = false;
        if ($trialCCSaveProductIds->isNotEmpty()) {
            $hasTrialCCSaveItem = \App\Models\Product::whereIn('id', $trialCCSaveProductIds)
                ->where('trial_ccsave', true)
                ->exists();
        }

        // Build the set of vault-capable gateway slugs for the JS side.
        $vaultGateways = [];
        foreach ($paymentGateways as $gw) {
            $gwSlug   = $gw['name'];
            $features = $gw['features'] ?? [];
            $gwSettings = $gw['settings'] ?? [];
            if (($features['save_card'] ?? false)
                && ($gwSettings['allow_save_card'] ?? false)) {
                $vaultGateways[] = $gwSlug;
            }
        }

        // PRC-622 Dalga C — kart formu dahil ödeme sayfası render edilmeden önce;
        // misafir checkout'ta boş context.
        app(\App\Core\Hook\HookBridge::class)->fireAction('ClientAreaPageCreditCardCheckout',
            $client ? app(\App\Core\Hook\HookBridge::class)->buildClientContext($client) : []
        );

        // PRC-736 — Tek-sayfa (one-page) checkout varyantı. Ayar-gated; varsayılan
        // kapalı olduğunda mevcut çok-adımlı akış birebir korunur (regresyon yok).
        // İki view de AYNI compact($data)'yı ve AYNI process/callback yolunu kullanır;
        // yalnızca UI orkestrasyonu (adım yerine tek-sayfa section) değişir.
        $onePageCheckout = (bool) Setting::getValue('one_page_checkout', false);
        $checkoutView = $onePageCheckout ? 'checkout.one-page' : 'checkout.index';

        return view($checkoutView, compact(
            'basketItems',
            'summary',
            'currency',
            'paymentGateways',
            'allowNotesOnCheckout',
            'client',
            'billingAddresses',
            'defaultBillingAddress',
            'orderCustomFields',
            'verificationRequired',
            'verificationStatus',
            'verificationMode',
            'trackingEventId',
            'requiredContracts',
            'enableTosAcceptance',
            'tosUrl',
            'hasTrialCCSaveItem',
            'vaultGateways'
        ));
    }

    public function process(Request $request)
    {
        $rules = [
            'payment_gateway_id' => ['required', 'string'],
            'billing_address_id' => ['required', 'integer', 'exists:client_billing_addresses,id'],
            'notes'              => ['nullable', 'string', 'max:1000'],
            // PRC-872 — taksit sayısı; sunucu plan listesiyle validate edilir (tamper guard aşağıda)
            'installment_count'  => ['nullable', 'integer', 'min:1', 'max:36'],
            // PRC-1057 — PaymentId-tabanlı taksit carrier'ı (iPay88 banka metodu); plan
            // listesine karşı tamper guard aşağıda doğrular.
            'installment_payment_id' => ['nullable', 'string', 'max:32'],
        ];

        $captcha = app(CaptchaService::class);
        if ($captcha->shouldShowCaptcha('shopping_cart_checkout')) {
            $rules[$captcha->getResponseFieldName()] = ['required', new ValidCaptcha('shopping_cart_checkout')];
        }

        // PRC-592: genel ToS checkbox açıksa server-side de zorunlu (HTML5 required
        // bypass edilebiliyordu). 'accepted' → checkbox işaretli olmalı.
        if ((bool) Setting::getValue('enable_tos_acceptance', false)) {
            $rules['terms_accepted'] = ['accepted'];
        }

        $validator = Validator::make($request->all(), $rules, [
            'terms_accepted.accepted' => __('store.checkout_terms_required'),
        ]);

        // Custom validation for payment gateway exists in modular system
        $validator->after(function ($validator) use ($request) {
            $gatewayName = $request->payment_gateway_id;

            if (empty($gatewayName)) {
                $validator->errors()->add('payment_gateway_id', __('store.checkout_payment_gateway_required'));

                return;
            }

            $availableGateways = $this->paymentManager->getAvailableGateways();
            $settings = PaymentGatewaySetting::getModuleSettings($gatewayName);

            if (! isset($availableGateways[$gatewayName]) || ($settings['status'] ?? 'inactive') !== 'active') {
                $validator->errors()->add('payment_gateway_id', __('store.checkout_invalid_payment_gateway'));
            }
        });

        if ($validator->fails()) {
            return back()->withErrors($validator)->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
        }

        // ShoppingCartValidateCheckout — semantic validation after FormRequest
        $client = Auth::guard('client')->user();
        $hookErrors = app(\App\Core\Hook\HookManager::class)->validate('ShoppingCartValidateCheckout', [
            'paymentmethod'  => $request->input('payment_gateway_id'),
            'notes'          => $request->input('notes'),
            'custtype'       => 'client',
            'clientId'       => $client?->id,
            'firstname'      => $client?->first_name,
            'lastname'       => $client?->last_name,
            'email'          => $client?->email,
            'address1'       => $client?->address1,
            'city'           => $client?->city,
            'country'        => $client?->country,
            'tax_id'         => $client?->tax_id,
            'phonenumber'    => $client?->phone,
            'customfield'    => [],
        ]);
        if (! empty($hookErrors)) {
            return back()->withErrors(['hook' => $hookErrors])->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
        }

        $visitorId = VisitorHelper::getVisitorId($request);

        if (! $visitorId) {
            return back()->withErrors(['error' => __('store.checkout_empty_basket')]);
        }

        $basketData = VisitorHelper::getVisitorBasket($visitorId);
        $basketItems = $basketData['items'] ?? [];

        if (empty($basketItems)) {
            return back()->withErrors(['error' => __('store.checkout_empty_basket')]);
        }

        // PRC-320 / A-1+2+3+4 — defense-in-depth. BasketController::addItem
        // already gates each insertion; this re-check catches a basket
        // built before the admin disabled an action (stale state) or a
        // basket POST that bypassed addItem. Any domain action that has
        // been toggled off since basket creation aborts checkout with 422.
        $actionSettingMap = [
            'register' => 'allow_domain_registration',
            'transfer' => 'allow_domain_transfer',
            'use' => 'allow_own_domain',
            'renew' => 'enable_renewal_orders',
        ];
        $normalizedForCheck = is_array($basketItems)
            ? $basketItems
            : (json_decode($basketItems, true) ?? []);
        foreach ($normalizedForCheck as $idx => $item) {
            $action = $item['domain_action'] ?? null;
            // Also inspect connectedDomain (hosting product + bundled domain)
            $connectedAction = $item['connectedDomain']['domain_action'] ?? null;
            foreach ([$action, $connectedAction] as $candidate) {
                if ($candidate === null) {
                    continue;
                }
                if (! isset($actionSettingMap[$candidate])) {
                    continue;
                }
                if (! \App\Models\Setting::getValue($actionSettingMap[$candidate], true)) {
                    abort(422, __('store.domain_action_not_available'));
                }
            }

            // PRC-322 / A-7 — IDN backend reject. Defense-in-depth: catches
            // baskets built before the admin disabled `allow_idn_domains`,
            // or POSTs that bypassed BasketController::addItem.
            $domainCandidates = [
                $item['domain_name'] ?? null,
            ];
            $connected = $item['connectedDomain'] ?? null;
            if (is_string($connected)) {
                $domainCandidates[] = $connected;
            } elseif (is_array($connected)) {
                $domainCandidates[] = $connected['domain_name'] ?? null;
            }
            foreach ($domainCandidates as $candidateDomain) {
                \App\Services\Domain\DomainPolicyGuard::assertIdnAllowed($candidateDomain);
            }
        }

        // Guard: required extra fields must be present before any order is created.
        // Error keys follow the `items.{index}.additional_fields.{field_key}` shape
        // so Blade can scope messages under the matching basket row.
        $extraFieldsValidator = app(BasketExtraFieldsValidator::class);
        $normalizedItems = is_array($basketItems)
            ? $basketItems
            : (json_decode($basketItems, true) ?? []);
        $extraErrors = $extraFieldsValidator->validate($normalizedItems);

        if (! empty($extraErrors)) {
            $errors = [];
            foreach ($extraErrors as $itemIndex => $itemErrors) {
                foreach (($itemErrors['missing'] ?? []) as $key) {
                    $errors["items.{$itemIndex}.additional_fields.{$key}"][]
                        = __('store.extra_field_required');
                }
                foreach (($itemErrors['invalid'] ?? []) as $key => $message) {
                    $errors["items.{$itemIndex}.additional_fields.{$key}"][]
                        = $message;
                }
            }
            throw ValidationException::withMessages($errors);
        }

        // Stock-control gate (PRC-21): refuse checkout if any basket product
        // has stock_control on and used_stock >= total_stock. We materialise the
        // expected per-product demand by counting basket rows for each product_id
        // (one ClientProduct will be created per row, so two rows = need 2 stock).
        $basketProductIds = collect($basketItems)
            ->where('type', 'product')
            ->pluck('product_id')
            ->filter();

        if ($basketProductIds->isNotEmpty()) {
            $demand = $basketProductIds->countBy()->all();
            $stockProducts = \App\Models\Product::whereIn('id', array_keys($demand))->get();

            $stockErrors = [];
            foreach ($stockProducts as $sp) {
                if (! $sp->stock_control) {
                    continue;
                }
                $available = $sp->availableStock();
                $needed    = (int) ($demand[$sp->id] ?? 0);
                if ($available !== null && $needed > $available) {
                    $stockErrors["product_{$sp->id}_out_of_stock"] = __('store.checkout_product_out_of_stock', [
                        'product'   => $sp->name,
                        'available' => $available,
                    ]);
                }
            }

            if (! empty($stockErrors)) {
                return back()->withErrors($stockErrors)->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
            }
        }

        // PRC-812 — Paket-başına sipariş limiti (free/trial suistimal kontrolü).
        // Müşterinin bu ürün için iptal-olmayan ClientProduct sayısı + sepetteki
        // aynı ürün kalemleri, products.max_orders_per_client'ı aşamaz. NULL limit
        // = sınırsız. Sunucu-taraflı son savunma hattı (sepette 2. kalem de sayılır).
        $orderLimitClient = Auth::guard('client')->user();
        if ($orderLimitClient) {
            $orderLimitViolations = app(\App\Services\Store\OrderLimitService::class)
                ->violations($orderLimitClient, $basketItems);

            if ($orderLimitViolations->isNotEmpty()) {
                $limitErrors = [];
                foreach ($orderLimitViolations as $v) {
                    $limitErrors["product_{$v['product']->id}_order_limit"] = __('store.checkout_order_limit_reached', [
                        'product' => $v['product']->name,
                        'limit'   => $v['limit'],
                    ]);
                }

                return back()->withErrors($limitErrors)->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
            }
        }

        // PRC-840 — brand katalog gate: branded storefront'ta vitrine dahil olmayan
        // ürün/TLD sipariş edilemez (vitrin sonradan daraltıldıysa sepetteki eski
        // kalem burada yakalanır — son sunucu-taraflı savunma hattı).
        $brandCtx = app(\App\Services\Store\BrandContext::class);
        if ($brandCtx->isBranded()) {
            $brandErrors = [];

            $brandProductIds = collect($basketItems)->where('type', 'product')->pluck('product_id')->filter()->unique();
            if ($brandProductIds->isNotEmpty()) {
                foreach (\App\Models\Product::whereIn('id', $brandProductIds)->get(['id', 'name', 'group_id']) as $bp) {
                    if (! $brandCtx->productGroupVisible($bp->group_id)) {
                        $brandErrors["product_{$bp->id}_not_available"] = __('store.product_not_available') . ' (' . $bp->name . ')';
                    }
                }
            }

            foreach (collect($basketItems)->where('type', 'domain') as $idx => $di) {
                $itemTld = $di['tld'] ?? null;
                if ($itemTld && ! $brandCtx->tldVisible($itemTld)) {
                    $brandErrors["domain_{$idx}_not_available"] = __('store.product_not_available') . ' (' . ($di['domain_name'] ?? $itemTld) . ')';
                }
            }

            if (! empty($brandErrors)) {
                return back()->withErrors($brandErrors)->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
            }
        }

        // Required-contracts gate: re-compute server-side (don't trust client list).
        // Pulls product_contracts pivot.required for products in the basket and
        // ensures every required contract id appears in submitted accepted_contract_ids[].
        $basketProductIds = collect($basketItems)
            ->pluck('product_id')
            ->filter()
            ->unique();

        $requiredContractIds = collect();
        if ($basketProductIds->isNotEmpty()) {
            $requiredContractIds = \App\Models\Product::with('contracts')
                ->whereIn('id', $basketProductIds)->get()
                ->flatMap(fn ($p) => $p->contracts->filter(fn ($c) => (bool) $c->pivot->required)->pluck('id'))
                ->unique()
                ->values();
        }

        $accepted = collect($request->input('accepted_contract_ids', []))
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values();

        $missing = $requiredContractIds->diff($accepted);
        if ($missing->isNotEmpty()) {
            return back()->withErrors([
                'accepted_contract_ids' => __('store.checkout_required_contracts_missing'),
            ])->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
        }

        $client = Auth::guard('client')->user();

        // Citizenship verification gate
        $citizenshipService = app(CitizenshipService::class);
        if ($citizenshipService->isEnabled() && $client) {
            $countryCode = $client->country ?? null;
            if ($countryCode && $citizenshipService->isRequiredForCountry($countryCode)) {
                $mode = $citizenshipService->getMode();
                $status = $client->verification_status ?? 'unverified';

                if ($mode === 'require_approval' && $status !== 'verified') {
                    return redirect()->route('store.checkout.index')
                        ->with('error', __('store.checkout_verification_required'));
                }
                // 'allow_with_review' mode: allow pending/verified, block unverified
                if ($mode === 'allow_with_review' && $status === 'unverified') {
                    return redirect()->route('store.checkout.index')
                        ->with('error', __('store.checkout_verification_required'));
                }
            }
        }

        $gatewayName = $request->payment_gateway_id;

        // Validate billing address belongs to client
        $billingAddress = $client->billingAddresses()->findOrFail($request->billing_address_id);

        // Get payment gateway info from modular system
        $availableGateways = $this->paymentManager->getAvailableGateways();
        $gatewaySettings = PaymentGatewaySetting::getModuleSettings($gatewayName);
        $gatewayInfo = $availableGateways[$gatewayName];

        $paymentGateway = [
            'name' => $gatewayName,
            'display_name' => $gatewaySettings['display_name'] ?? $gatewayInfo['label'] ?? ucfirst($gatewayName),
            'fee_percentage' => floatval($gatewaySettings['fee_percentage'] ?? 0),
            'fee_fixed' => floatval($gatewaySettings['fee_fixed'] ?? 0),
        ];

        // Currency bilgisini al (öncelik: authenticated user > session > default)
        $currency = null;

        // 1. Authenticated user currency
        if ($client && $client->currency_id) {
            $currency = Currency::where('status', true)
                ->where('id', $client->currency_id)
                ->first();

            \Log::info('CheckoutController::process - Using authenticated user currency', [
                'client_id' => $client->id,
                'currency_id' => $client->currency_id,
                'currency_code' => $currency?->code,
            ]);
        }

        // 2. Session currency
        if (! $currency) {
            $currencyCode = app(\App\Services\Store\StorePreferenceResolver::class)->currentCurrencyCode();
            if ($currencyCode) {
                $currency = Currency::where('status', true)
                    ->where('code', $currencyCode)
                    ->first();
            }
        }

        // 3. Default currency
        if (! $currency) {
            $currency = Currency::where('status', true)
                ->where('is_default', true)
                ->first();
        }

        // PRC-872/1057 — taksit tamper guard + sepet özeti hesabı
        $installmentCount = max(1, (int) ($request->input('installment_count') ?? 1));
        // PRC-1057 — PaymentId-tabanlı taksit carrier'ı (iPay88 banka metodu); boşsa null.
        $installmentPaymentId = trim((string) $request->input('installment_payment_id', '')) ?: null;

        if ($installmentPaymentId !== null || $installmentCount > 1) {
            $gatewayModule = $this->paymentManager->getGatewayModule($gatewayName);

            // Gateway taksit feature'ını desteklemiyorsa 422
            if (! $gatewayModule || ! ($gatewayModule->getFeatures()['installment'] ?? false)) {
                abort(422, __('store.checkout_invalid_installment'));
            }

            // Baz tutar: taksit farkı olmadan (installmentCount=1) sunucu-taraflı sepet toplamı.
            // InstallmentController ile aynı yöntem — client'tan gelen amount YOK SAYILIR.
            // Fix 2: gateway=null so gateway_fee is excluded from the base, matching what
            // InstallmentController::serverCheckoutAmount() shows to the client.
            $baseSummary = $this->pricingService->calculateBasketSummaryFromItems(
                $basketItems, $currency, $request, null, 1
            );
            $baseAmount = (float) ($baseSummary['total'] ?? 0);
            // PRC-689: sepet para birimi geçirilir → izin verilmeyen para biriminde ($currency)
            // getInstallmentPlans boş döner → count>1 plan listesinde bulunmaz → 422 (tamper guard).
            // PRC-1057: $plans hem payment_id-eşleşme hem numeric-count branch'inde kullanılır.
            $plans = $gatewayModule->getInstallmentPlans($baseAmount, $currency->code ?? null);

            if ($installmentPaymentId !== null) {
                // PRC-1057 — PaymentId-tabanlı seçim: planı payment_id ile eşleştir + count'u
                // PLANDAN al (client'ın gönderdiği count'a GÜVENME; otorite sunucu planıdır).
                $matched = null;
                foreach ($plans as $plan) {
                    if (isset($plan['payment_id']) && (string) $plan['payment_id'] === $installmentPaymentId) {
                        $matched = $plan;
                        break;
                    }
                }
                if ($matched === null) {
                    abort(422, __('store.checkout_invalid_installment'));
                }
                $installmentCount = max(1, (int) ($matched['count'] ?? 1));
            } else {
                // Numeric-count (mevcut davranış): YALNIZ payment_id=null (gerçek numeric) planlar
                // eşleşebilir — böylece PaymentId-tabanlı gateway'de (iPay88) count>1 seçimi
                // carrier olmadan REDDEDİLİR (taksit için payment_id zorunlu).
                $numericCounts = array_map(
                    'intval',
                    array_column(array_filter($plans, static fn ($p) => empty($p['payment_id'])), 'count')
                );
                if (! in_array($installmentCount, $numericCounts, true)) {
                    abort(422, __('store.checkout_invalid_installment'));
                }
            }
        }

        // Basket summary hesapla (taksit farkı dahil)
        $summary = $this->pricingService->calculateBasketSummaryFromItems(
            $basketItems, $currency, $request, $paymentGateway, $installmentCount
        );

        // PRC-1099 rank-15: fail closed. If any basket item has NO catalog Pricing
        // row (product row, or a real domain register/renew/transfer row), do NOT
        // create an orphan pending Order that would provision the item for free.
        // Redirect back to the basket so the customer/admin fixes the missing price.
        if (! empty($summary['unpriceable'])) {
            \Log::warning('Checkout aborted — unpriceable basket item(s)', [
                'client_id' => $client->id ?? null,
                'currency' => $currency->code ?? null,
                'unpriceable' => $summary['unpriceable'],
            ]);

            return redirect()->route('store.basket.index')
                ->with('error', __('store.checkout_error'));
        }

        // Applied discount bilgilerini al
        $discountInfo = $this->discountManager->getAppliedDiscountInfo($request);

        try {
            DB::beginTransaction();

            // Brand context for reseller tracking
            $checkoutBrand = app()->bound('brand') ? app('brand') : null;

            // Order oluştur - orders tablosundaki field isimleri kullan
            // PRC-198: hard-coded values, server-controlled — unsealed() block silences
            // the SealsAdminAttributes audit warning for this trusted checkout flow.
            $order = Order::unsealed(fn () => Order::create([
                'uuid' => \Illuminate\Support\Str::uuid(),
                'client_id' => $client->id,
                'brand_id' => $checkoutBrand?->id,
                'reseller_id' => $checkoutBrand?->reseller_id,
                'billing_address_id' => $billingAddress->id,
                'order_number' => $this->generateOrderNumber(),
                'status' => 'pending',
                'currency_id' => $currency->id,
                'amount' => $summary['total'],
                'subtotal' => $summary['subtotal'],
                'setup_amount' => $summary['setup_fee'] ?? 0, // orders tablosunda setup_amount
                'tax_amount' => $summary['tax_amount'],
                'discount_amount' => $summary['discount_amount'] ?? 0,
                'total' => $summary['total'],
                'payment_gateway_id' => null, // Legacy field, keeping null
                'payment_gateway_name' => $paymentGateway['name'],
                'payment_status' => 'pending',
                'notes' => $request->notes,
                'ip_address' => $request->ip(),
                'user_agent' => $request->userAgent(),
                'device_fingerprint' => $request->input('device_fingerprint') ?? $request->cookie('device_fp'),
                'referer_url' => $request->headers->get('referer'),
                'session_id' => session()->getId(),
                'ordered_at' => now(),
                'discount_info' => $discountInfo['has_discount'] ? json_encode($discountInfo) : null,
            ]));

            // Record per-order contract acceptances (audit trail for legal evidence).
            // Inside the same DB transaction so any later failure rolls back order + acceptances atomically.
            if ($accepted->isNotEmpty()) {
                $acceptanceService = app(\App\Services\Contract\ContractAcceptanceService::class);
                foreach ($accepted as $contractId) {
                    $contract = \App\Models\Contract::active()->find($contractId);
                    if ($contract) {
                        $acceptanceService->record(
                            $client,
                            $contract,
                            \App\Enums\AcceptanceContext::CHECKOUT,
                            $order->id,
                            $request
                        );

                        // PRC-622 — 'Distance Selling Contract Custom' maili.
                        // Mesafeli satış sözleşmeleri 'custom' tipiyle modellenir
                        // (ContractType'ta ayrı tip yok); yalnız o tipte fire.
                        if ($contract->type === \App\Enums\ContractType::CUSTOM->value) {
                            app(\App\Core\Hook\HookBridge::class)->fireAction('DistanceSellingContractAccepted', [
                                'order_id' => $order->id,
                                'orderid' => $order->id,
                                'client_id' => $client->id,
                                'contract_id' => $contract->id,
                                'contract_title' => (string) ($contract->getTranslation('title', app()->getLocale()) ?? $contract->title),
                            ]);
                        }
                    }
                }
            }

            // Period type mapping
            $periodTypeMapping = [
                'ay' => 'month',
                'yıl' => 'year',
                'gün' => 'day',
                'hafta' => 'week',
                'month' => 'month',
                'year' => 'year',
                'day' => 'day',
                'week' => 'week',
            ];

            // billingcycle name resolver: (period, periodType) → 'monthly|quarterly|semiannually|annually|biennially|triennially'.
            // ClientProduct.billingcycle is consumed by ProrationService (period-in-months) and reports.
            // Domain terms (year-based) get mapped to month-equivalents for consistency.
            $resolveBillingCycle = static function (int $period, string $periodType): string {
                $months = match ($periodType) {
                    'year' => $period * 12,
                    'week' => max(1, (int) round($period / 4)),
                    'day'  => max(1, (int) round($period / 30)),
                    default => $period, // 'month'
                };
                return \App\Services\ProrationService::monthsToBillingCycle($months);
            };

            // Order items oluştur ve service ID'lerini kaydet
            // IMPORTANT: Use $summary['items'] instead of $basketItems to get recalculated prices with group pricing
            // IMPORTANT: Use reference (&$item) so client_product_id / client_domain_id assignments below
            // actually persist on $summaryItems (used later to populate invoice_items.related_service_id)
            $summaryItems = $summary['items'];

            // PRC-536: PricingService recalculates prices from DB and doesn't know about
            // trial items. Force trial item prices back to 0 after summary computation,
            // then recompute summary totals so Order + Invoice carry accurate 0 amounts.
            $hadTrialZero = false;
            foreach ($summaryItems as &$sItem) {
                if (! empty($sItem['is_trial']) && ($sItem['type'] ?? '') === 'product') {
                    $sItem['price']     = 0;
                    $sItem['setup_fee'] = 0;
                    $sItem['tax_amount'] = 0;
                    $hadTrialZero = true;
                }
            }
            unset($sItem);

            // PRC-536: Paid-trial pass — detect eligible paid-trial items, apply
            // first-purchase discount, and track the per-item discount delta so it
            // lands in discount_amount on the Order + Invoice (NOT a coupon).
            // Key: product_id (string for JSON-safe basket keys) → discount amount.
            $paidTrialDiscountMap = []; // product_id => float (discount delta for this order)
            $trialService         = app(TrialService::class);
            $hadPaidTrialDiscount = false;
            // Fix 3: batch-load products for the detection loop to avoid N+1 queries.
            $paidTrialProductIds = array_filter(array_map(
                fn ($si) => (($si['type'] ?? '') === 'product' && empty($si['is_trial']))
                    ? ($si['product_id'] ?? null)
                    : null,
                $summaryItems
            ));
            $paidTrialProducts = \App\Models\Product::whereIn('id', array_values($paidTrialProductIds))
                ->get()
                ->keyBy('id');
            foreach ($summaryItems as &$sItem) {
                if (($sItem['type'] ?? '') !== 'product' || empty($sItem['product_id'])) {
                    continue;
                }
                // Skip items already zeroed as free trial
                if (! empty($sItem['is_trial'])) {
                    continue;
                }
                $paidTrialProduct = $paidTrialProducts->get($sItem['product_id']);
                if (! $paidTrialProduct || $paidTrialProduct->trial_mode !== 'paid') {
                    continue;
                }
                if (! $trialService->isEligible($client, $paidTrialProduct)) {
                    continue;
                }
                // Apply discount to this item's price (setup_fee is one-time but
                // the discount applies to the recurring amount, consistent with plan).
                $originalPrice   = (float) $sItem['price'];
                $discountedPrice = $trialService->applyPaidTrialDiscount($paidTrialProduct, $originalPrice);
                $discountDelta   = round($originalPrice - $discountedPrice, 2);

                if ($discountDelta > 0) {
                    // Store the original (full) price so ClientProduct.recurring_amount
                    // can be set to it — renewals charge the undiscounted price.
                    $sItem['_paid_trial_original_price'] = $originalPrice;
                    $sItem['price']    = $discountedPrice;
                    // Fix 1: scale tax_amount proportionally to the reduced price so that
                    // the recompute block sums correct (discounted) tax values, preventing
                    // a header tax_amount mismatch against per-line InvoiceItem.tax_amount.
                    if ($originalPrice > 0 && isset($sItem['tax_amount'])) {
                        $sItem['tax_amount'] = round((float) $sItem['tax_amount'] * ($discountedPrice / $originalPrice), 2);
                    }
                    // Mark item so the ClientProduct creation knows to call recordTrial.
                    $sItem['_paid_trial_eligible'] = true;
                    // Accumulate per product_id (one row per product in a normal basket).
                    // Fix 2: multiply by quantity so discount_amount is accurate for qty>1.
                    $paidTrialDiscountMap[$sItem['product_id']] =
                        ($paidTrialDiscountMap[$sItem['product_id']] ?? 0.0)
                        + $discountDelta * (int) ($sItem['quantity'] ?? 1);
                    $hadPaidTrialDiscount = true;
                }
            }
            unset($sItem);

            // PRC-697 — İlk-sipariş / ortak-anchor pro-rata pass.
            // Sabit yenileme günü (billing anchor) tanımlı + proration aktif ürünlerde
            // ilk fatura anchor'a kadar gün-bazlı orantılı çıkar; next_due_date = anchor.
            // Trial item'lar (fiyatı 0'a çekilmiş) atlanır. Ayar-gated: global
            // `proration_enabled` kapalıysa (varsayılan) bu pass tümüyle no-op.
            // Paid-trial pass'iyle AYNI post-summary override + recompute deseni.
            $hadProration = false;
            $prorationSvc = app(\App\Services\Billing\FirstOrderProrationService::class);
            if ($prorationSvc->isGloballyEnabled()) {
                foreach ($summaryItems as &$sItem) {
                    if (($sItem['type'] ?? '') !== 'product' || empty($sItem['product_id'])) {
                        continue;
                    }
                    // Trial item'lar prorate edilmez (fiyat zaten 0 / özel akış).
                    if (! empty($sItem['is_trial']) || ! empty($sItem['_paid_trial_eligible'])) {
                        continue;
                    }

                    $prorateProduct = $paidTrialProducts->get($sItem['product_id'])
                        ?? \App\Models\Product::with('group')->find($sItem['product_id']);
                    if (! $prorateProduct || ! $prorateProduct->relationLoaded('group')) {
                        $prorateProduct?->loadMissing('group');
                    }
                    if (! $prorateProduct || ! $prorationSvc->isEligible($prorateProduct)) {
                        continue;
                    }

                    $periodMonths = \App\Services\ProrationService::billingCycleToMonths(
                        $resolveBillingCycle(
                            (int) ($sItem['period'] ?? 1),
                            $periodTypeMapping[$sItem['period_type'] ?? 'month'] ?? 'month'
                        )
                    );

                    $result = $prorationSvc->calculateFirstCharge(
                        (float) $sItem['price'],
                        $periodMonths,
                        $prorateProduct,
                        now()
                    );

                    if (empty($result['applied'])) {
                        // Anchor tam sipariş gününe denk geldi vb. → fiyat tam kalır ama
                        // next_due_date anchor'a hizalanır (aynı anchor'da toplama için).
                        $sItem['_proration_next_due'] = $result['next_due_date'];
                        continue;
                    }

                    $originalPrice = (float) $sItem['price'];
                    $proratedPrice = (float) $result['prorated_price'];

                    // Tax'ı orantılı ölçekle (paid-trial pass'iyle aynı yaklaşım —
                    // header tax-line invariant korunur).
                    if ($originalPrice > 0 && isset($sItem['tax_amount'])) {
                        $sItem['tax_amount'] = round((float) $sItem['tax_amount'] * ($proratedPrice / $originalPrice), 2);
                    }

                    $sItem['price']                 = $proratedPrice;
                    $sItem['_proration_applied']    = true;
                    $sItem['_proration_next_due']   = $result['next_due_date'];
                    $sItem['_proration_days']       = $result['prorated_days'];
                    $sItem['_proration_total_days'] = $result['full_period_days'];
                    $sItem['_proration_note']       = $prorationSvc->buildLineNote($result);
                    $hadProration = true;
                }
                unset($sItem);
            }

            if ($hadTrialZero || $hadPaidTrialDiscount || $hadProration) {
                // Recompute summary totals from the corrected item list.
                // PRC-549 — Tax recompute item.tax_amount toplamına dayanıyordu
                // ama PricingService item-level tax_amount yazmıyor → header tax
                // 0'a düşüyordu (paid trial customer'lar KDV=0 fatura alıyordu).
                // PRC-545/546 sonrası PricingService:
                //   - paid trial: _paid_trial_discounted_price'tan vergi
                //   - coupon/promotion: item_discount'tan vergi
                // Yani $summary['tax_amount'] ZATEN indirimli base'lerle hesaplanmış.
                // Item.price override (paid trial line 657) ya da is_trial zero
                // tax base'i değiştirmediği için (PricingService meta ile baktı),
                // burada toplamak yerine summary'den AL.
                $recompSubtotal  = 0;
                $recompSetup     = 0;
                foreach ($summaryItems as $si) {
                    $recompSubtotal += (float) ($si['price'] ?? 0) * (int) ($si['quantity'] ?? 1);
                    $recompSetup    += (float) ($si['setup_fee'] ?? 0);
                }
                $recompTax = (float) ($summary['tax_amount'] ?? 0);
                // PRC-697 — proration item.tax_amount'ı orantılı küçülttü; header tax
                // artık summary'nin (tam dönem) değeri DEĞİL, per-line scaled tax
                // toplamıdır. Yalnız proration uygulandığında bu yola gir (paid-trial
                // yolu PricingService meta'ya dayandığı için summary'den okumaya devam eder).
                if ($hadProration) {
                    $recompTax = round(collect($summaryItems)->sum(fn ($si) => (float) ($si['tax_amount'] ?? 0)), 2);
                }
                // PRC-549 — paid_trial savings'i discount_amount'a EKLEME. item.price
                // üst-yazıldığı (line 657: discountedPrice) için recompSubtotal zaten
                // indirimli; ekstra discount line eklersek double-count olur (sübtotal'dan
                // bir kez, discount sat satırından bir kez daha). Coupon/promotion/client
                // ise PricingService.discount_amount'tan zaten geliyor.
                $paidTrialTotalDiscount = array_sum($paidTrialDiscountMap);
                $recompDiscount = (float) ($summary['discount_amount'] ?? 0);
                $recompGateway  = (float) ($summary['gateway_fee'] ?? 0);

                // Bug 2 fix (PRC-536): for a PURE free-trial order the billable amount
                // before gateway is 0, but the gateway fee was still added → total 3 TL,
                // defeating the zero-invoice auto-pay block below. Waive the gateway fee
                // only when ALL of the following hold:
                //   • at least one free-trial item was zeroed ($hadTrialZero)
                //   • the recomputed billable total (subtotal + setup + tax − discount) is ≤ 0
                //   • there are no paid contributions (no paid-trial discount applied — a
                //     mixed basket still owes the paid portion + its gateway fee)
                // A mixed basket (trial item + normal paid item) will have recompSubtotal > 0
                // and $hadPaidTrialDiscount may be false; it keeps the gateway fee.
                $recompBillable = $recompSubtotal + $recompSetup + $recompTax - $recompDiscount;
                if ($hadTrialZero && ! $hadPaidTrialDiscount && $recompBillable <= 0) {
                    $recompGateway = 0.0;
                    $summary['gateway_fee'] = 0.0;
                }

                // Fix 1: include installment surcharge so invoice.total_amount is not short
                // for a mixed trial+paid basket with a procp-mode installment.
                $installmentRecompSurcharge = (float) ($summary['installment_surcharge'] ?? 0);
                $recompTotal    = $recompSubtotal + $recompSetup + $recompTax - $recompDiscount + $recompGateway + $installmentRecompSurcharge;

                $summary['subtotal']        = $recompSubtotal;
                $summary['setup_fee']       = $recompSetup;
                $summary['tax_amount']      = $recompTax;
                $summary['total']           = max(0, $recompTotal);
                // Clamp discount so it never exceeds gross (subtotal + setup + tax).
                // A coupon computed against the original price would produce a stale
                // (too-large) discount_amount on a free-trial or paid-trial invoice.
                $summary['discount_amount'] = min($recompDiscount, $recompSubtotal + $recompSetup + $recompTax);
                $summary['items']           = $summaryItems;

                // Propagate corrected totals to the already-created Order as well.
                // Note: orders table has no gateway_fee column; the fee is tracked on invoices.
                Order::unsealed(fn () => $order->update([
                    'amount'          => $summary['total'],
                    'subtotal'        => $recompSubtotal,
                    'setup_amount'    => $recompSetup,
                    'tax_amount'      => $recompTax,
                    'total'           => $summary['total'],
                    'discount_amount' => $summary['discount_amount'],
                ]));
            }

            // PRC-573 — Satır maliyeti 0 olan (borcu order anında kapanan)
            // ClientProduct'ların id'si; commit sonrası faturadan bağımsız hemen
            // provizyona sokulur. Free trial bunun bir örneği (is_trial'a değil,
            // satır toplamına bakılır — genel kural).
            $settledClientProductIds = [];

            foreach ($summaryItems as $index => &$item) {
                $itemTotal = ($item['price'] * ($item['quantity'] ?? 1)) + ($item['setup_fee'] ?? 0);
                $periodType = $periodTypeMapping[$item['period_type'] ?? 'month'] ?? 'month';

                // TLD ID'yi al (domain items için basket'te zaten var)
                $tldId = null;
                if ($item['type'] === 'domain') {
                    $tldId = $item['tld_id'] ?? null;
                }

                // Domain name'i belirle (basket'ten)
                $domainName = null;
                if ($item['type'] === 'domain') {
                    $domainName = $item['name']; // Domain items için name = domain name
                } elseif ($item['type'] === 'product' && ! empty($item['connectedDomain'])) {
                    $domainName = $item['connectedDomain']; // Product'a bağlı domain
                }

                // LOG: Before OrderItem creation
                \Log::info('=== CREATING ORDER ITEM ===', [
                    'item_type' => $item['type'],
                    'item_name' => $item['name'],
                    'has_configurableOptions' => isset($item['configurableOptions']),
                    'configurableOptions_type' => isset($item['configurableOptions']) ? gettype($item['configurableOptions']) : 'not set',
                    'configurableOptions_count' => isset($item['configurableOptions']) && is_array($item['configurableOptions']) ? count($item['configurableOptions']) : 0,
                    'configurableOptions_sample' => isset($item['configurableOptions']) && is_array($item['configurableOptions']) && count($item['configurableOptions']) > 0
                        ? $item['configurableOptions'][0]
                        : null,
                ]);

                $orderItem = OrderItem::create([
                    'order_id' => $order->id,
                    'type' => $item['type'],
                    'name' => $item['name'],
                    'description' => $item['name'], // description alanı eklendi
                    'product_id' => $item['product_id'] ?? null,
                    'domain_name' => $domainName,
                    'domain_action' => $item['domain_action'] ?? null,
                    'period' => $item['period'] ?? 1,
                    'period_type' => $periodType,
                    'quantity' => $item['quantity'] ?? 1,
                    'unit_price' => $item['price'],
                    'amount' => $item['price'], // amount alanı eklendi
                    'setup_amount' => $item['setup_fee'] ?? 0,
                    'total_price' => $itemTotal,
                    'total_amount' => $itemTotal, // total_amount alanı eklendi
                    'currency_id' => $currency->id,
                    'relation_id' => $tldId ?? $item['product_id'] ?? null, // TLD ID or Product ID
                    'config_options' => $item['configurableOptions'] ?? null, // Laravel auto-encodes with array cast
                    'custom_fields' => $item['customFields'] ?? null, // Laravel auto-encodes with array cast
                    'nameservers' => $item['nameservers'] ?? null, // Laravel auto-encodes with array cast
                    'epp_code' => $item['epp_code'] ?? null,
                ]);

                // LOG: After OrderItem creation
                \Log::info('=== ORDER ITEM CREATED ===', [
                    'order_item_id' => $orderItem->id,
                    'config_options_in_model' => $orderItem->config_options,
                    'config_options_type_in_model' => gettype($orderItem->config_options),
                    'config_options_from_db_raw' => \DB::table('order_items')->where('id', $orderItem->id)->value('config_options'),
                ]);

                // Product/Domain entities oluştur (pending status ile)
                if ($item['type'] === 'product' && ! empty($item['product_id'])) {
                    // Product bilgisini al (module için)
                    $product = \App\Models\Product::find($item['product_id']);

                    // Generate unique username
                    $username = $this->generateUsername($client, $domainName);

                    // Generate strong random password (12 characters)
                    $password = \Illuminate\Support\Str::random(12);

                    $itemPeriod = (int) ($item['period'] ?? 1);
                    $billingCycleName = $resolveBillingCycle($itemPeriod, $periodType);
                    $nextDue = now()->add($itemPeriod, $periodType.'s')->toDateString();

                    // PRC-697 — proration pass anchor'a hizalı next_due_date belirlediyse
                    // onu kullan (ilk fatura yalnız anchor'a kadar; sonraki tam dönem
                    // oradan başlar → aynı anchor'daki hizmetler aynı günde yenilenir).
                    if (! empty($item['_proration_next_due'])) {
                        $nextDue = $item['_proration_next_due'];
                    }

                    // --- PRC-536: Free-trial branch ---
                    $isFreeTrialItem = ! empty($item['is_trial']) && $product && $product->trial_mode === 'free';

                    if ($isFreeTrialItem) {
                        // Re-check eligibility at checkout time (client is definitely
                        // logged in here; basket check may have been done as guest).
                        // Fix 4: reuse $trialService resolved at the paid-trial pass above.
                        if (! $trialService->isEligible($client, $product)) {
                            DB::rollBack();
                            return back()->withErrors([
                                'error' => __('store.trial_not_eligible'),
                            ])->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
                        }

                        $trialDays           = (int) ($item['trial_days'] ?? $product->trial_period_days ?? 0);
                        $postTrialPeriodRaw  = (string) ($item['post_trial_period'] ?? $product->trial_default_period ?? 'monthly');
                        // Bug 1 fix (PRC-536): the post-trial period selector in the store
                        // sends a months-count string ("1", "12") while billingcycle must hold
                        // a cycle NAME ("monthly", "annually"). Convert if numeric; otherwise
                        // treat as already a name (e.g. admin-set $product->trial_default_period
                        // such as "monthly").
                        if (is_numeric($postTrialPeriodRaw)) {
                            $postTrialMonths = (int) $postTrialPeriodRaw;
                            $postTrialPeriod = \App\Services\ProrationService::monthsToBillingCycle($postTrialMonths);
                        } else {
                            $postTrialPeriod = $postTrialPeriodRaw;
                            $postTrialMonths = \App\Services\ProrationService::billingCycleToMonths($postTrialPeriod);
                        }
                        $trialEndsAt      = now()->addDays($trialDays)->toDateString();

                        // Look up post-trial recurring_amount from Pricing table,
                        // using the same query pattern as recalculateBasketItemPrices.
                        // Post-trial recurring intentionally excludes setup_amount — trial
                        // conversion does not re-charge a setup fee.
                        $postTrialPricing = \App\Models\Pricing::where('type', 'product')
                            ->where('relation_id', $item['product_id'])
                            ->where('currency_id', $currency->id)
                            ->where('period', $postTrialMonths)
                            ->first();
                        if ($postTrialPricing === null) {
                            \Log::warning('PRC-536: post-trial pricing row not found; recurring_amount will be 0', [
                                'product_id'        => $item['product_id'],
                                'post_trial_period' => $postTrialPeriod,
                                'currency'          => $currency->code ?? $currency->id,
                            ]);
                        }
                        $postTrialRecurring = $postTrialPricing ? (float) $postTrialPricing->amount : 0.0;

                        // ClientProduct oluştur — trial overrides
                        // PRC-182 / PRC-536: unsealed() silences SealsAdminAttributes.
                        $clientProduct = \App\Models\ClientProduct::unsealed(fn () => \App\Models\ClientProduct::create([
                            'client_id'       => $client->id,
                            'order_id'        => $order->id,
                            'product_id'      => $item['product_id'],
                            'module'          => $product?->module,
                            'domain'          => $domainName,
                            'username'        => $username,
                            // PRC-584: plaintext ver — ClientProduct mutator (setPasswordAttribute)
                            // zaten encrypt eder. encrypt() eklemek çift-şifrelemeye yol açıyordu.
                            'password'        => $password,
                            'quantity'        => $item['quantity'] ?? 1,
                            'billingcycle'    => $postTrialPeriod,    // post-trial cycle
                            'recurring_amount' => $postTrialRecurring, // post-trial price
                            'status'          => 'pending',
                            'create_date'     => now()->toDateString(),
                            'expire_date'     => $trialEndsAt,
                            'next_due_date'   => $trialEndsAt,
                            'due_date'        => $trialEndsAt,
                            'is_trial'        => true,
                            'trial_ends_at'   => $trialEndsAt,
                            'auto_renew'      => false,
                            'created_at'      => now(),
                            'updated_at'      => now(),
                        ]));

                        // Record the trial (inside the transaction so a rollback cleans it up).
                        $trialService->recordTrial($client, $product, $clientProduct, 'free');

                        \Log::info('PRC-536: free trial ClientProduct created', [
                            'client_id'        => $client->id,
                            'product_id'       => $product->id,
                            'client_product_id' => $clientProduct->id,
                            'trial_days'       => $trialDays,
                            'trial_ends_at'    => $trialEndsAt,
                            'post_trial_period' => $postTrialPeriod,
                            'post_trial_recurring' => $postTrialRecurring,
                        ]);
                    } else {
                    // --- Standard (non-trial) ClientProduct ---
                    // ClientProduct oluştur
                    // PRC-182: hard-coded server-controlled values; unsealed() block
                    // silences the SealsAdminAttributes audit warning for this trusted
                    // checkout flow.
                    // PRC-536: For paid-trial items, recurring_amount must be the FULL
                    // (pre-discount) price so renewals charge the undiscounted rate.
                    // For normal items it remains null (model default).
                    $standardRecurringAmount = ! empty($item['_paid_trial_eligible'])
                        ? ($item['_paid_trial_original_price'] ?? null)
                        : null;

                    $clientProduct = \App\Models\ClientProduct::unsealed(fn () => \App\Models\ClientProduct::create([
                        'client_id' => $client->id,
                        'order_id' => $order->id,
                        'product_id' => $item['product_id'],
                        'module' => $product?->module, // Provisioning module
                        'domain' => $domainName, // connectedDomain kullan
                        'username' => $username, // Auto-generated unique username
                        // PRC-584: plaintext ver — ClientProduct mutator zaten encrypt eder
                        // (çift-şifreleme fix).
                        'password' => $password,
                        'quantity' => $item['quantity'] ?? 1,
                        'billingcycle' => $billingCycleName,
                        'recurring_amount' => $standardRecurringAmount,
                        'status' => 'pending', // suspended yerine pending
                        'create_date' => now()->toDateString(),
                        'expire_date' => $nextDue,
                        'next_due_date' => $nextDue,
                        'due_date' => $nextDue,
                        'auto_renew' => false,
                        'created_at' => now(),
                        'updated_at' => now(),
                    ]));
                        // PRC-536: Paid-trial — record the trial after standard ClientProduct
                        // is created (inside the transaction). recurring_amount is the FULL
                        // undiscounted price set above; the discount only hit the first invoice.
                        if (! empty($item['_paid_trial_eligible'])) {
                            $trialService->recordTrial($client, $product, $clientProduct, 'paid');
                            \Log::info('PRC-536: paid trial recorded', [
                                'client_id'         => $client->id,
                                'product_id'        => $product->id,
                                'client_product_id' => $clientProduct->id,
                                'discount_delta'    => $paidTrialDiscountMap[$item['product_id']] ?? 0,
                            ]);
                        }
                    } // end trial / standard branch

                    // Save custom fields
                    if (! empty($item['customFields'])) {
                        // PRC-751 — client_editable=false product CF: tahrif edilmiş
                        // sepet değeri yok sayılır, admin default_value'ya sabitlenir.
                        $item['customFields'] = app(\App\Support\CustomFieldClientGuard::class)
                            ->enforceLockedProduct($item['customFields']);

                        foreach ($item['customFields'] as $fieldId => $value) {
                            \App\Models\ClientCustomFieldValue::create([
                                'custom_field_id' => $fieldId,
                                'client_id' => $client->id,
                                'field_value' => $value,
                            ]);

                            // Also save to product-level config field values for admin/clientarea display
                            \App\Models\ClientProductConfigFieldValue::create([
                                'product_id' => $clientProduct->id,
                                'option_id' => $fieldId,
                                'value' => $value,
                            ]);
                        }
                    }

                    // Save config options
                    if (! empty($item['configurableOptions'])) {
                        foreach ($item['configurableOptions'] as $configOption) {
                            \App\Models\ClientProductConfigOptionValue::create([
                                'product_id' => $clientProduct->id,
                                'option_id' => $configOption['option_id'],
                                'value' => $configOption['value_id'], // value_id'yi value kolonuna kaydet
                                'group_id' => $configOption['group_id'] ?? null,
                            ]);
                        }
                    }

                    // Store client_product id for invoice item (REFERANS ile değiştirilecek)
                    $item['client_product_id'] = $clientProduct->id;

                    // PRC-573 — Satır maliyeti 0 ise (free trial, %100 indirimli vb.)
                    // borç order anında kapanmış demektir; commit sonrası ödeme
                    // beklemeden provizyona sokulacak CP'ler arasına ekle.
                    // PRC-1099 rank-15: BUT a zero total caused by a MISSING catalog
                    // price row (_pricing_missing) is a real bug, not a genuine free
                    // product — refuse to auto-settle/free-provision it. (Belt-and-
                    // suspenders: the order-creating path already aborts earlier when
                    // the summary reports any unpriceable item.)
                    if ((float) $itemTotal === 0.0 && empty($item['_pricing_missing'])) {
                        $settledClientProductIds[] = $clientProduct->id;
                    }
                } elseif ($item['type'] === 'domain' && ! empty($item['name'])) {
                    $domainAction = $item['domain_action'] ?? 'register';

                    if ($domainAction === 'renew' && ! empty($item['client_domain_id'])) {
                        // Renewal: use existing ClientDomain, mark provisioning pending
                        $clientDomain = \App\Models\ClientDomain::where('id', $item['client_domain_id'])
                            ->where('client_id', $client->id)
                            ->first();

                        if ($clientDomain) {
                            $clientDomain->update(['provisioning_status' => 'pending']);
                            $item['client_domain_id'] = $clientDomain->id;
                        }
                    } else {
                        // Register/Transfer: create new ClientDomain
                        $tld = \App\Models\Tld::find($tldId);

                        // PRC-644 — checkout defense: premium kuralı son kez (transfer premium'da yasak, blocked register yasak)
                        if (! empty($item['tld_id'])) {
                            $premiumTld = \App\Models\Tld::find($item['tld_id']);
                            if ($premiumTld) {
                                app(\App\Services\Domain\PremiumDomainService::class)->assertSellable(
                                    $premiumTld, $item['domain_action'] ?? 'register', (bool) ($item['is_premium'] ?? false)
                                );
                            }
                        }

                        $clientDomain = \App\Models\ClientDomain::create([
                            'client_id' => $client->id,
                            'tld_id' => $tldId,
                            'domain' => $item['name'],
                            'module' => $tld?->registrar ? strtolower($tld->registrar) : null,
                            'period' => $item['period'] ?? 1,
                            'status' => 'pending',
                            'create_date' => now()->toDateString(),
                            'next_due_date' => now()->add($item['period'] ?? 1, 'years')->toDateString(),
                            'due_date' => now()->add($item['period'] ?? 1, 'years')->toDateString(),
                            'auto_renew' => false,
                            'dns_management' => null,
                            'email_forwarding' => null,
                            'id_protection' => null,
                            // PRC-443 — epp_code boolean feature flag; null =
                            // TLD'den inherit. Transfer auth code (string) ayrı
                            // kolona yazılır; register flow'da null kalır.
                            'epp_code' => null,
                            'transfer_epp_code' => ($item['domain_action'] ?? null) === 'transfer'
                                ? ($item['epp_code'] ?? null)
                                : null,
                            'is_premium' => $item['is_premium'] ?? false,
                            'premium_purchase_amount' => ! empty($item['is_premium']) ? (float) ($item['price'] ?? 0) : null,
                            'disable_reminders' => false,
                            'created_at' => now(),
                            'updated_at' => now(),
                        ]);

                        $item['client_domain_id'] = $clientDomain->id;
                    }

                    // Persist extra fields (module-defined), only those currently visible.
                    // Wrapped in try/catch so a module bug in getExtraFields does NOT roll
                    // back a paid-for order — the domain is already persisted; extras are
                    // best-effort, loggable-but-non-fatal.
                    if ($clientDomain) {
                        try {
                            $rawExtras = $item['additional_fields'] ?? [];
                            if (! empty($rawExtras) && isset($item['tld_id'])) {
                                $tldModel = \App\Models\Tld::find($item['tld_id']);
                                if ($tldModel && $tldModel->registrar) {
                                    $registrarManager = app(\App\Core\Registrar\RegistrarManager::class);
                                    $moduleName = strtolower((string) $tldModel->registrar);
                                    if ($registrarManager->has($moduleName)) {
                                        $regMod = $registrarManager->get($moduleName);
                                        $defs = $regMod->getExtraFields(ltrim((string) $tldModel->tld, '.'));
                                        $visible = app(\App\Services\Domain\ExtraFieldEvaluator::class)
                                            ->filterVisible($defs, $rawExtras);

                                        foreach ($visible as $key => $_def) {
                                            $value = $rawExtras[$key] ?? null;
                                            if ($value === null || $value === '') {
                                                continue;
                                            }
                                            \App\Models\ClientDomainField::create([
                                                'domain_id' => $clientDomain->id,
                                                'field_key' => $key,
                                                'value'     => (string) $value,
                                            ]);
                                        }
                                    }
                                }
                            }
                        } catch (\Throwable $e) {
                            \Illuminate\Support\Facades\Log::error(
                                'CheckoutController: extra fields persistence failed',
                                [
                                    'client_domain_id' => $clientDomain->id ?? null,
                                    'tld_id' => $item['tld_id'] ?? null,
                                    'error' => $e->getMessage(),
                                ],
                            );
                            // Swallow — domain creation is the source of truth; extras
                            // can be re-entered by admin from the domain detail page.
                        }
                    }
                }
            }
            unset($item); // REFERANS CLEANUP - ÖNEMLI!

            // Fatura oluştur - invoices tablosundaki field isimleri kullan
            // PRC-195: hard-coded server-computed values; unsealed() block silences
            // the SealsAdminAttributes audit warning for this trusted checkout flow.
            $invoice = Invoice::unsealed(fn () => Invoice::create([
                'invoice_number' => $this->generateInvoiceNumber(),
                'client_id' => $client->id,
                'order_id' => $order->id,
                'billing_address_id' => $billingAddress->id,
                'currency_id' => $currency->id,
                'subtotal' => $summary['subtotal'],
                'setup_fee' => $summary['setup_fee'] ?? 0, // invoices tablosunda setup_fee
                'tax_amount' => $summary['tax_amount'],
                'tax_rate' => $summary['tax_rate'] ?? 0, // Tax rate from TaxCalculationService
                'tax_exemption_reason' => $summary['tax_exemption_reason'] ?? null,
                'discount_amount' => $summary['discount_amount'] ?? 0,
                'gateway_fee' => $summary['gateway_fee'] ?? 0, // invoices tablosunda gateway_fee
                // PRC-872 — taksit persist
                'installment_count'     => $installmentCount,
                'installment_surcharge' => $summary['installment_surcharge'] ?? 0,
                // PRC-1057 — PaymentId-tabanlı taksit carrier'ı (iPay88; numeric'te null)
                'installment_payment_id' => $installmentPaymentId,
                'total_amount' => $summary['total'], // invoices tablosunda total_amount
                'status' => 'unpaid',
                'due_date' => now()->addDays(30),
                'issued_at' => now(),
                'notes' => $request->notes,
                'custom_field_values' => $this->getInvoiceCustomFieldValues($billingAddress),
            ]));

            // Invoice items oluştur - güncellenmiş yapı
            // IMPORTANT: Use $summaryItems (already defined above) to get recalculated prices with group pricing
            foreach ($summaryItems as $index => $item) {
                $itemPrice = ($item['price'] * ($item['quantity'] ?? 1));
                $itemSetupFee = ($item['setup_fee'] ?? 0);
                // PRC-547 — per-line discount (coupon/promotion/client_specific).
                // PricingService::applyItemLevelDiscounts her etkilenen item'a
                // `item_discount` meta yazar. CheckoutController:657-672 paid-trial
                // pass'inde item.price'ı zaten override etmiş olduğundan, kalan
                // sadece coupon/promotion/client_specific axis'i. Tax base'inden
                // düşülür (WHMCS davranışı, PRC-546 ile aynı) + invoice_items'a
                // discount_amount field'ı populate edilir → header-line invariant.
                $itemLineDiscount = (float) ($item['item_discount'] ?? 0);
                $itemTaxableBase = max(0.0, $itemPrice - $itemLineDiscount);
                $itemSubtotal = $itemTaxableBase + $itemSetupFee;
                $periodType = $periodTypeMapping[$item['period_type'] ?? 'month'] ?? 'month';

                // Calculate tax for this specific item using TaxCalculationService
                $productType = ($item['type'] === 'domain') ? 'domain' : 'hosting';
                $product = null;

                if ($item['type'] === 'product' && isset($item['product_id'])) {
                    $product = \App\Models\Product::find($item['product_id']);
                }

                $itemTaxInfo = $this->taxCalculationService->calculateTax(
                    client: $client,
                    productType: $productType,
                    product: $product,
                    countryCode: null // Will use client's country
                );

                $itemTaxRate = $itemTaxInfo['taxable'] ? $itemTaxInfo['rate'] : 0;
                if ($itemTaxInfo['taxable']) {
                    $taxComputed = \App\Services\TaxCalculationService::computeTax($itemSubtotal, $itemTaxInfo['rate'], $itemTaxInfo['tax_method'] ?? 'exclusive');
                    $itemTaxAmount = $taxComputed['tax_amount'];
                    $itemLineTotal = $taxComputed['total'];
                } else {
                    $itemTaxAmount = 0;
                    $itemLineTotal = $itemSubtotal;
                }
                // PRC-547 — per-line discount_rate hesabı (audit/raporlama için).
                // item_discount > 0 ve original_price varsa, oran = discount/original × 100.
                $itemDiscountRate = 0.0;
                if ($itemLineDiscount > 0 && ! empty($item['original_price']) && $item['original_price'] > 0) {
                    $itemDiscountRate = round($itemLineDiscount / (float) $item['original_price'] * 100, 2);
                }

                // Domain name'i belirle (invoice item için de aynı logic)
                $invoiceDomainName = null;
                if ($item['type'] === 'domain') {
                    $invoiceDomainName = $item['name']; // Domain items için name = domain name
                } elseif ($item['type'] === 'product' && ! empty($item['connectedDomain'])) {
                    $invoiceDomainName = $item['connectedDomain']; // Product'a bağlı domain
                }

                // Domain name ile beraber description oluştur
                $description = $item['name'];
                if ($item['type'] === 'domain' && ! empty($item['name'])) {
                    $actionText = ucfirst($item['domain_action'] ?? 'register');
                    $description = "{$actionText}: {$item['name']}";
                } elseif ($item['type'] === 'product' && ! empty($item['connectedDomain'])) {
                    $description = "{$item['name']} ({$item['connectedDomain']})";
                }

                // PRC-697 — oransal ilk fatura satırına açıklayıcı not (ilk tahsilat
                // ≠ reklam fiyatı bounce'unu azaltmak için).
                if (! empty($item['_proration_applied']) && ! empty($item['_proration_note'])) {
                    $description .= ' — ' . $item['_proration_note'];
                }

                // Domain ise: related_type=domain, related_id=tld_id
                // Product ise: related_type=product, related_id=products.id
                $relatedType = $item['type']; // 'domain' veya 'product'
                $relatedId = null;

                if ($item['type'] === 'domain') {
                    $relatedId = $item['tld_id'] ?? null; // TLD ID
                } elseif ($item['type'] === 'product') {
                    $relatedId = $item['product_id'] ?? null; // Products table ID
                }

                // Config options toplamını hesapla
                $configOptionsTotal = 0;
                if (!empty($item['configurableOptions']) && is_array($item['configurableOptions'])) {
                    foreach ($item['configurableOptions'] as $option) {
                        $configOptionsTotal += $option['price'] ?? 0;
                    }
                }

                // Capture custom field values where visible_on_invoice is true
                $invoiceCustomFieldValues = null;
                $clientServiceId = $item['client_product_id'] ?? null;
                if ($clientServiceId && ($item['product_id'] ?? null)) {
                    $visibleFields = \App\Models\ProductCustomField::where('product_id', $item['product_id'])
                        ->where('visible_on_invoice', true)
                        ->where('status', true)
                        ->ordered()
                        ->get();

                    if ($visibleFields->isNotEmpty()) {
                        $fieldValues = \App\Models\ClientProductConfigFieldValue::where('product_id', $clientServiceId)
                            ->whereIn('option_id', $visibleFields->pluck('id'))
                            ->pluck('value', 'option_id');

                        if ($fieldValues->isNotEmpty()) {
                            // PRC-836 — TEK KAYNAK snapshot (type+field_id+dropdown-resolve
                            // dahil); InvoiceService ile aynı şekil → display yüzeyleri
                            // checkbox'ı Evet/Hayır formatlayabilir.
                            $invoiceCustomFieldValues = $visibleFields->filter(fn ($f) => $fieldValues->has($f->id))
                                ->map(fn ($f) => $f->toSnapshotEntry($fieldValues->get($f->id)))
                                ->values()
                                ->toArray();
                        }
                    }
                }

                InvoiceItem::create([
                    'invoice_id' => $invoice->id,
                    'type' => $item['type'],
                    'description' => $description,
                    'quantity' => $item['quantity'] ?? 1,
                    'unit_price' => $item['price'],
                    'setup_fee' => $itemSetupFee,
                    'total' => $itemSubtotal,
                    'tax_rate' => $itemTaxRate,
                    'tax_amount' => round($itemTaxAmount, 2),
                    // PRC-547 — per-line discount populate (header-line invariant).
                    'discount_rate' => $itemDiscountRate,
                    'discount_amount' => round($itemLineDiscount, 2),
                    'line_total' => round($itemLineTotal, 2),
                    'product_id' => $item['product_id'] ?? null,
                    'domain_name' => $invoiceDomainName,
                    'period' => $item['period'] ?? 1,
                    'period_type' => $periodType,
                    'related_type' => $relatedType,
                    'related_id' => $relatedId,
                    'related_service_id' => $clientServiceId ?? ($item['client_domain_id'] ?? null),
                    'config_options' => $item['configurableOptions'] ?? null,
                    'config_options_total' => $configOptionsTotal,
                    'custom_field_values' => $invoiceCustomFieldValues,
                ]);
            }

            // PRC-647 — müşterinin (login) kullandığı NS'i hatırla (servis default'tan farklıysa saklar).
            if ($client) {
                $nsItem = $order->items()
                    ->where('type', 'domain')
                    ->whereIn('domain_action', ['register', 'transfer'])
                    ->whereNotNull('nameservers')
                    ->first();
                if ($nsItem && ! empty($nsItem->nameservers)) {
                    app(\App\Services\Domain\NameserverPreferenceService::class)
                        ->remember($client->id, (array) $nsItem->nameservers);
                }
            }

            // Kupon/Promosyon kullanımını kaydet ve usage_count'u artır (DB::commit'ten ÖNCE)
            if ($discountInfo['has_discount'] && !empty($discountInfo['coupon_code'])) {
                $discountCode = $discountInfo['coupon_code'];

                // Önce Promotion'da ara, sonra Coupon'da ara (BasketController ile aynı mantık)
                $promotion = \App\Models\Promotion::where('code', $discountCode)->first();
                $isPromotion = false;

                if ($promotion) {
                    $isPromotion = true;

                    // PromotionUsage ledger satırı (order_id ile) — böylece bir
                    // satır ⇔ bir increment ilişkisi promotion için de geçerli olur
                    // ve DiscountUsageReverter row-based geri alım bu increment'i
                    // sipariş iptalinde görebilir.
                    \App\Models\PromotionUsage::create([
                        'promotion_id' => $promotion->id,
                        'client_id' => $client->id,
                        'order_id' => $order->id,
                        'discount_amount' => $summary['discount_amount'] ?? 0,
                        'original_amount' => $summary['subtotal'] ?? 0,
                        'final_amount' => $summary['total'] ?? 0,
                        'currency_id' => $currency->id,
                        'used_at' => now(),
                        'metadata' => [
                            'basket_items_count' => count($basketItems),
                            'invoice_id' => $invoice->id ?? null,
                            'coupon_code' => $promotion->code,
                        ],
                    ]);

                    // Promosyon kullanım sayısını artır
                    $promotion->incrementUsage();

                    \Log::info('Promotion usage recorded', [
                        'promotion_code' => $promotion->code,
                        'promotion_id' => $promotion->id,
                        'client_id' => $client->id,
                        'order_id' => $order->id,
                        'discount_amount' => $summary['discount_amount'] ?? 0,
                    ]);
                } else {
                    // Coupon modelini bul
                    $coupon = \App\Models\Coupon::where('code', $discountCode)->first();

                    if ($coupon) {
                        // CouponUsage kaydı oluştur
                        \App\Models\CouponUsage::create([
                            'coupon_id' => $coupon->id,
                            'client_id' => $client->id,
                            'order_id' => $order->id,
                            'discount_amount' => $summary['discount_amount'] ?? 0,
                            'original_amount' => $summary['subtotal'] ?? 0,
                            'final_amount' => $summary['total'] ?? 0,
                            'currency_id' => $currency->id,
                            'used_at' => now(),
                            'metadata' => [
                                'basket_items_count' => count($basketItems),
                                'invoice_id' => $invoice->id,
                                'payment_gateway' => $paymentGateway['name'],
                            ],
                        ]);

                        // Kupon usage_count'u artır
                        $coupon->incrementUsage();

                        \Log::info('Coupon usage recorded', [
                            'coupon_code' => $coupon->code,
                            'coupon_id' => $coupon->id,
                            'client_id' => $client->id,
                            'order_id' => $order->id,
                            'discount_amount' => $summary['discount_amount'] ?? 0,
                        ]);
                    }
                }
            }

            // PRC-579 — Auto-applied promotion usage tracking. Yukarıdaki blok
            // yalnız basket kuponunu (getAppliedDiscountInfo->coupon_code) görür;
            // kod girilmeden uygulanan promotion summary.discount'tan alınır.
            // coupon_code boşsa (gerçek auto-promotion) çift sayım olmadan kaydedilir.
            $appliedDiscount = $summary['discount'] ?? null;
            if (empty($discountInfo['coupon_code'])
                && is_array($appliedDiscount)
                && ($appliedDiscount['type'] ?? null) === 'promotion'
                && ! empty($appliedDiscount['promotion_code'])) {
                $autoPromo = \App\Models\Promotion::where('code', $appliedDiscount['promotion_code'])->first();
                if ($autoPromo) {
                    \App\Models\PromotionUsage::create([
                        'promotion_id' => $autoPromo->id,
                        'client_id' => $client->id,
                        'order_id' => $order->id,
                        'discount_amount' => $summary['discount_amount'] ?? 0,
                        'original_amount' => $summary['subtotal'] ?? 0,
                        'final_amount' => $summary['total'] ?? 0,
                        'currency_id' => $currency->id,
                        'used_at' => now(),
                        'metadata' => ['invoice_id' => $invoice->id ?? null, 'auto_applied' => true],
                    ]);
                    $autoPromo->incrementUsage();
                }
            }

            DB::commit();

            // Trigger on_order provisioning jobs
            $this->triggerOnOrderProvisioningJobs($order);

            // PRC-573 — Borcu order anında kapanan (satır maliyeti 0) hizmetleri
            // faturadan bağımsız hemen provizyona sok. Karışık sepette free-trial
            // satırı ücretli kalemlerin ödenmesini beklemez. 'order'-setup zaten
            // yukarıda; bu yalnız 'payment'-setup CP'leri kapsar (dispatcher kararı).
            // Atomik pending→queued sayesinde, saf-trial sepette aşağıdaki
            // zero-total → triggerProvisioning yolu ile çakışsa bile job tek kez
            // dispatch edilir.
            if (! empty($settledClientProductIds)) {
                $provisioningDispatcher = app(\App\Services\Provisioning\ProductProvisioningDispatcher::class);
                foreach ($settledClientProductIds as $settledCpId) {
                    $settledCp = \App\Models\ClientProduct::with('product')->find($settledCpId);
                    if ($settledCp) {
                        $provisioningDispatcher->dispatch($settledCp, $order->id, 'zero-cost line at checkout');
                    }
                }
            }

            // PRC-536 / PRC-854: Zero-total invoice auto-pay.
            // ANY checkout invoice whose total is 0 — free trial, %100 coupon, a
            // genuinely free/0-priced product, or a full balance credit — has nothing
            // to pay. The normal payment flow never fires for a 0-total invoice, so
            // without this the client lands on a useless payment page (PRC-854). Mark
            // the invoice paid and dispatch InvoiceStatusChanged here — after
            // DB::commit() so the queued HandleInvoiceStatusChange::triggerProvisioning
            // listener (products with auto_setup_option==='payment') sees a fully
            // committed row. The InvoiceObserver also fires InvoicePaid on this status
            // transition, which drives PRC-673 accounting-exclusion. Free-trial-only
            // side effects (card-save opt-in below) stay gated on $hadTrialZero.
            if ((float) $invoice->total_amount === 0.0) {
                try {
                    Invoice::unsealed(fn () => $invoice->update([
                        'status'    => 'paid',
                        'paid_date' => now(),
                    ]));
                    event(new \App\Events\InvoiceStatusChanged(
                        $invoice,
                        'paid',
                        'Auto-marked paid: zero-total order at checkout',
                        'unpaid'
                    ));
                    \Log::info('PRC-854: zero-total invoice auto-marked paid', [
                        'invoice_id' => $invoice->id,
                        'order_id'   => $order->id,
                    ]);
                } catch (\Exception $e) {
                    \Log::error('PRC-854: failed to auto-pay zero-total invoice', [
                        'invoice_id' => $invoice->id,
                        'error'      => $e->getMessage(),
                    ]);
                    // Non-fatal: service already created; provisioning can be
                    // triggered manually from admin if needed.
                }
            }

            // PRC-540: Free-trial card-save opt-in.
            // If the basket contained at least one free-trial item whose product has
            // trial_ccsave=true, AND the customer checked the opt-in box, AND the chosen
            // gateway supports save_card — call gateway->saveCard() and persist a
            // SavedPaymentMethod (is_default=true) so ProcessAutoPayJob can charge it at
            // trial-end. Entirely best-effort: a failure must NOT block provisioning.
            if ($hadTrialZero
                && (bool) $request->input('trial_save_card_consent', false)) {
                $this->maybeTrialSaveCard($client, $request, $gatewayName, $summaryItems);
            }

            // Sepeti temizle
            VisitorHelper::clearVisitorBasket($visitorId);

            // Kullanılan coupon kodu varsa temizle (tek kullanımlık)
            if ($discountInfo['has_discount'] && ! empty($discountInfo['coupon_code'])) {
                \App\Helpers\VisitorHelper::removeVisitorBasketCoupon($visitorId);
            }

            // Send order confirmation email asynchronously using SendEmailJob
            // Template: "Order Confirmation" (ID: 4)
            \App\Jobs\SendEmailJob::dispatch(
                'Order Confirmation',
                'order',
                $order->id,
                [
                    'ORDER_NUMBER' => $order->order_number,
                    'ORDER_DATE' => $order->created_at->format('d.m.Y H:i'),
                    'ORDER_TOTAL' => number_format($order->total, 2),
                    'CURRENCY' => $order->currency->code ?? 'TRY',
                    'INVOICE_NUMBER' => $invoice->invoice_number,
                    'INVOICE_URL' => route('store.payment.show', $invoice->id),
                    'PAYMENT_URL' => route('store.payment.show', $invoice->id),
                ]
            );

            // Payment gateway'e yönlendir
            return $this->redirectToPayment($order, $paymentGateway, $invoice, $request);

        } catch (\Exception $e) {
            DB::rollBack();
            // PRC-644 — kasıtlı HTTP hatalarını (ör. premium 422) generic "checkout failed"e dönüştürme.
            if ($e instanceof \Symfony\Component\HttpKernel\Exception\HttpException) {
                throw $e;
            }
            logger()->error('Checkout process failed', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);

            return back()->withErrors(['error' => __('store.checkout_process_failed')])->withInput($request->except(['trial_card_number', 'trial_expiry_month', 'trial_expiry_year']));
        }
    }

    public function updateBillingInfo(Request $request)
    {
        $client = Auth::guard('client')->user();

        $validator = Validator::make($request->all(), [
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'company_name' => ['nullable', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', 'unique:clients,email,'.$client->id],
            'phone' => ['required', 'string', 'max:20'],
            'country_id' => ['required', 'exists:countries,id'],
            'city' => ['required', 'string', 'max:255'],
            'address1' => ['required', 'string', 'max:255'],
            'address2' => ['nullable', 'string', 'max:255'],
            'state' => ['nullable', 'string', 'max:255'],
            'postcode' => ['required', 'string', 'max:20'],
            'tax_id' => ['nullable', 'string', 'max:50'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => __('store.validation_please_fill_fields'),
                'errors' => $validator->errors(),
            ], 422);
        }

        try {
            $client->update($validator->validated());

            return response()->json([
                'success' => true,
                'message' => __('store.checkout_billing_updated'),
                'data' => [
                    'first_name' => $client->first_name,
                    'last_name' => $client->last_name,
                    'company_name' => $client->company_name,
                    'email' => $client->email,
                    'phone' => $client->phone,
                    'country_name' => $client->country?->common_name,
                    'city' => $client->city,
                    'address1' => $client->address1,
                    'address2' => $client->address2,
                    'state' => $client->state,
                    'postcode' => $client->postcode,
                    'tax_id' => $client->tax_id,
                ],
            ]);
        } catch (\Exception $e) {
            logger()->error('Billing info update failed', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'message' => __('store.messages_error_occurred'),
            ], 500);
        }
    }

    private function generateOrderNumber(): string
    {
        $prefix = Setting::getValue('order_number_prefix', 'ORD');
        $counter = Setting::getValue('order_number_counter', 1000);

        $defaultOrderNumber = $prefix.str_pad($counter, 6, '0', STR_PAD_LEFT);

        // Counter'ı artır
        Setting::setValue('order_number_counter', $counter + 1);

        // OverrideOrderNumberGeneration — allows a plugin to supply a custom order number.
        // Handler receives the generated default and must return a string (or the default).
        $orderNumber = (string) app(\App\Core\Hook\HookManager::class)->filter(
            'OverrideOrderNumberGeneration',
            $defaultOrderNumber
        );

        return $orderNumber;
    }

    private function generateInvoiceNumber(): string
    {
        return app(InvoiceNumberService::class)->generate();
    }

    private function getInvoiceCustomFieldValues($billingAddress)
    {
        // Billing address'ten custom field değerlerini al (show_in_invoice = true olanlar)
        $customFieldValues = [];
        $invoiceCustomFields = \App\Models\ClientCustomField::where('show_in_invoice', true)->get();

        if ($billingAddress->custom_field_values) {
            $addressCustomFields = is_string($billingAddress->custom_field_values)
                ? json_decode($billingAddress->custom_field_values, true)
                : $billingAddress->custom_field_values;

            foreach ($invoiceCustomFields as $field) {
                if (isset($addressCustomFields[$field->id])) {
                    $customFieldValues[$field->id] = $addressCustomFields[$field->id];
                }
            }
        }

        return $customFieldValues;
    }

    private function redirectToPayment($order, $paymentGateway, $invoice, $request)
    {
        // PRC-536: Free-trial zero-total invoices are auto-paid before this redirect.
        // The payment show page queries `status = unpaid` and would throw a 404 for
        // an already-paid invoice. Send paid invoices directly to the success page.
        if ($invoice->status === 'paid') {
            return redirect()->route('store.payment.success', $invoice->id);
        }

        // Payment sayfasına yönlendir - invoice ID ile
        return redirect()->route('store.payment.show', $invoice->id);
    }

    // Billing Address Management Methods

    public function getBillingAddresses(Request $request)
    {
        $client = Auth::guard('client')->user();
        $addresses = $client->billingAddresses()->with('country')->orderBy('is_default', 'desc')->orderBy('created_at', 'desc')->get();

        return response()->json([
            'success' => true,
            'addresses' => $addresses,
        ]);
    }

    public function getBillingAddress(Request $request, $addressId)
    {
        $client = Auth::guard('client')->user();
        $address = $client->billingAddresses()->with('country')->findOrFail($addressId);

        return response()->json([
            'success' => true,
            'address' => $address,
        ]);
    }

    public function storeBillingAddress(Request $request)
    {
        $client = Auth::guard('client')->user();

        $taxIdRule = Setting::getValue('tax_id_required', false)
            ? ['required', 'string', 'max:50']
            : ['nullable', 'string', 'max:50'];

        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'company_name' => ['nullable', 'string', 'max:255'],
            'tax_id' => $taxIdRule,
            'email' => ['required', 'email', 'max:255'],
            'phone' => ['required', 'string', 'max:20'],
            'country_id' => ['required', 'exists:countries,id'],
            'city' => ['required', 'string', 'max:255'],
            'address1' => ['required', 'string', 'max:255'],
            'address2' => ['nullable', 'string', 'max:255'],
            'state' => ['nullable', 'string', 'max:255'],
            'postcode' => ['required', 'string', 'max:20'],
            'is_default' => ['nullable', 'boolean'],
        ]);

        // Validate custom fields
        $orderCustomFields = \App\Models\ClientCustomField::where('show_in_order', true)->get();
        foreach ($orderCustomFields as $field) {
            $fieldKey = "custom_field_{$field->id}";
            $fieldRules = $field->is_required ? ['required'] : ['nullable'];

            switch ($field->type) {
                case 'text':
                case 'input':
                case 'textarea':
                case 'textbox':
                    $fieldRules[] = 'string';
                    $fieldRules[] = 'max:1000';
                    break;
                case 'select':
                case 'radio':
                case 'options':
                    if ($field->values && is_array($field->values)) {
                        $fieldRules[] = 'in:'.implode(',', array_keys($field->values));
                    }
                    break;
                case 'checkbox':
                    $fieldRules[] = 'in:0,1';
                    break;
            }

            $validator->addRules([$fieldKey => $fieldRules]);
        }

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => __('store.validation_please_fill_fields'),
                'errors' => $validator->errors(),
            ], 422);
        }

        try {
            DB::beginTransaction();

            // Collect custom field values
            $customFieldValues = [];
            foreach ($orderCustomFields as $field) {
                // PRC-751 — client_editable=false alan salt-okunur (anti-tamper).
                if (! $field->client_editable) {
                    continue;
                }

                $fieldKey = "custom_field_{$field->id}";
                $fieldValue = $request->input($fieldKey);
                if ($fieldValue !== null && $fieldValue !== '') {
                    $customFieldValues[$field->id] = $fieldValue;
                }
            }

            $addressData = $validator->validated();
            $addressData['client_id'] = $client->id;
            $addressData['custom_field_values'] = $customFieldValues;

            $address = \App\Models\ClientBillingAddress::create($addressData);

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => __('store.checkout_address_saved'),
                'address' => $address->load('country'),
            ]);

        } catch (\Exception $e) {
            DB::rollBack();
            logger()->error('Billing address creation failed', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'message' => __('store.messages_error_occurred'),
            ], 500);
        }
    }

    public function updateBillingAddress(Request $request, $addressId)
    {
        $client = Auth::guard('client')->user();
        $address = $client->billingAddresses()->findOrFail($addressId);

        $taxIdRule = Setting::getValue('tax_id_required', false)
            ? ['required', 'string', 'max:50']
            : ['nullable', 'string', 'max:50'];

        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'company_name' => ['nullable', 'string', 'max:255'],
            'tax_id' => $taxIdRule,
            'email' => ['required', 'email', 'max:255'],
            'phone' => ['required', 'string', 'max:20'],
            'country_id' => ['required', 'exists:countries,id'],
            'city' => ['required', 'string', 'max:255'],
            'address1' => ['required', 'string', 'max:255'],
            'address2' => ['nullable', 'string', 'max:255'],
            'state' => ['nullable', 'string', 'max:255'],
            'postcode' => ['required', 'string', 'max:20'],
            'is_default' => ['nullable', 'boolean'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => __('store.validation_please_fill_fields'),
                'errors' => $validator->errors(),
            ], 422);
        }

        try {
            $address->update($validator->validated());

            return response()->json([
                'success' => true,
                'message' => __('store.checkout_address_updated'),
                'address' => $address->load('country'),
            ]);

        } catch (\Exception $e) {
            logger()->error('Billing address update failed', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'message' => __('store.messages_error_occurred'),
            ], 500);
        }
    }

    public function deleteBillingAddress(Request $request, $addressId)
    {
        $client = Auth::guard('client')->user();
        $address = $client->billingAddresses()->findOrFail($addressId);

        // Cannot delete the only address
        if ($client->billingAddresses()->count() <= 1) {
            return response()->json([
                'success' => false,
                'message' => __('store.checkout_cannot_delete_only_address'),
            ], 400);
        }

        try {
            $address->delete();

            return response()->json([
                'success' => true,
                'message' => __('store.checkout_address_deleted'),
            ]);

        } catch (\Exception $e) {
            logger()->error('Billing address deletion failed', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'message' => __('store.messages_error_occurred'),
            ], 500);
        }
    }

    public function setDefaultBillingAddress(Request $request, $addressId)
    {
        $client = Auth::guard('client')->user();
        $address = $client->billingAddresses()->findOrFail($addressId);

        try {
            // The model mutator will handle setting others to false
            $address->update(['is_default' => true]);

            return response()->json([
                'success' => true,
                'message' => __('store.checkout_default_address_set'),
                'address' => $address->load('country'),
            ]);

        } catch (\Exception $e) {
            logger()->error('Default billing address setting failed', ['error' => $e->getMessage()]);

            return response()->json([
                'success' => false,
                'message' => __('store.messages_error_occurred'),
            ], 500);
        }
    }

    /**
     * Trigger provisioning jobs for on_order setup products
     */
    private function triggerOnOrderProvisioningJobs(Order $order): void
    {
        try {
            $order->load(['items.product']);

            foreach ($order->items as $orderItem) {
                // Check for product provisioning (on_order only)
                if ($orderItem->type === 'product' && $orderItem->product_id) {
                    $product = $orderItem->product;

                    // Only trigger if auto_setup_option is 'order'
                    if ($product && $product->auto_setup_option === 'order') {
                        $clientProduct = \App\Models\ClientProduct::where('order_id', $order->id)
                            ->where('product_id', $orderItem->product_id)
                            ->where('provisioning_status', 'pending')
                            ->first();

                        if ($clientProduct) {
                            // Check if there's a setup delay
                            $delay = $product->setup_delay > 0
                                ? now()->addMinutes($product->setup_delay)
                                : null;

                            // Dispatch with order_id (3rd param) - job will create log automatically
                            ProcessProductCreateJob::dispatch($clientProduct->id, null, $order->id)
                                ->delay($delay);

                            Log::info('Dispatched product provisioning job (order)', [
                                'client_product_id' => $clientProduct->id,
                                'order_id' => $order->id,
                                'product_id' => $product->id,
                                'delay_minutes' => $product->setup_delay,
                            ]);
                        }
                    }
                }

                // Note: Domain provisioning happens after payment in PaymentController
                // Products with auto_setup_option === 'payment' also provision after payment
            }
        } catch (\Exception $e) {
            Log::error('Failed to trigger provisioning jobs (order)', [
                'order_id' => $order->id,
                'error' => $e->getMessage(),
            ]);
            // Don't throw - we don't want to break the checkout flow
        }
    }

    /**
     * Generate unique username for client product
     *
     * @param \App\Models\Client $client
     * @param string|null $domain
     * @return string
     */
    private function generateUsername($client, $domain = null): string
    {
        if ($domain) {
            // Domain varsa, domain'den türet (uzantı olmadan)
            $base = preg_replace('/\.[a-z]+$/i', '', $domain); // example.com -> example
            $base = preg_replace('/[^a-z0-9]/i', '', $base); // Özel karakterleri temizle
            $base = strtolower(substr($base, 0, 8)); // İlk 8 karakter
        } else {
            // Domain yoksa, firma ismi veya ad-soyad'dan türet
            if (!empty($client->company_name)) {
                $base = preg_replace('/[^a-z0-9]/i', '', $client->company_name);
            } else {
                $base = preg_replace('/[^a-z0-9]/i', '', $client->first_name . $client->last_name);
            }
            $base = strtolower(substr($base, 0, 6)); // İlk 6 karakter
        }

        // Boşsa fallback
        if (empty($base)) {
            $base = 'user';
        }

        // Unique olana kadar dene
        $username = $base;
        $counter = 1;

        while (\App\Models\ClientProduct::where('username', $username)->exists()) {
            $username = $base . $counter;
            $counter++;

            // Çok uzun olmasın
            if (strlen($username) > 10) {
                $base = substr($base, 0, 6);
                $username = $base . $counter;
            }
        }

        return $username;
    }

    /**
     * Get active payment gateways for checkout (modular system)
     */
    private function getActivePaymentGateways()
    {
        $availableGateways = $this->paymentManager->getAvailableGateways();
        $activeGateways = [];

        foreach ($availableGateways as $gatewayName => $gatewayInfo) {
            $settings = PaymentGatewaySetting::getModuleSettings($gatewayName);

            // Sadece aktif olan gateway'leri dahil et
            if (($settings['status'] ?? 'inactive') === 'active') {
                // Modül bilgilerini settings ile birleştir
                $gateway = array_merge($gatewayInfo, [
                    'name' => $gatewayName,
                    'display_name' => $settings['display_name'] ?? $gatewayInfo['label'] ?? ucfirst($gatewayName),
                    'status' => $settings['status'] ?? 'inactive',
                    'sort_order' => intval($settings['sort_order'] ?? 0),
                    'fee_percentage' => floatval($settings['fee_percentage'] ?? 0),
                    'fee_fixed' => floatval($settings['fee_fixed'] ?? 0),
                    'minimum_amount' => floatval($settings['minimum_amount'] ?? 0),
                    'maximum_amount' => floatval($settings['maximum_amount'] ?? 0),
                    'settings' => $settings,
                ]);

                $activeGateways[] = $gateway;
            }
        }

        // Sort order'a göre sırala
        usort($activeGateways, function ($a, $b) {
            return $a['sort_order'] <=> $b['sort_order'];
        });

        return $activeGateways;
    }

    /**
     * Sepetteki item'ların fiyatlarını güncel currency'ye göre yeniden hesapla
     * CRITICAL: visitor_basket'te eski currency'nin fiyatları saklanıyor
     */
    private function recalculateBasketItemPrices(array $basketItems, $currency): array
    {
        $recalculatedItems = [];

        foreach ($basketItems as $item) {
            // Item'ı kopyala
            $recalculatedItem = $item;

            if ($item['type'] === 'product') {
                // PRC-536: Free-trial items carry zeroed prices — do not recalculate.
                if (! empty($item['is_trial'])) {
                    $recalculatedItem['price']     = 0;
                    $recalculatedItem['setup_fee'] = 0;
                    $recalculatedItem['tax_amount'] = 0;
                    $recalculatedItems[] = $recalculatedItem;
                    continue;
                }

                // Ürün fiyatını güncel currency ile al
                $productPrice = \App\Models\Pricing::where('type', 'product')
                    ->where('relation_id', $item['product_id'])
                    ->where('currency_id', $currency->id)
                    ->where('time', 'month')
                    ->where('period', $item['period'])
                    ->first();

                if ($productPrice) {
                    $basePrice = (float) $productPrice->amount;
                    $recalculatedItem['setup_fee'] = (float) ($productPrice->setup_amount ?? 0);

                    // Apply client group pricing discount if applicable
                    $client = auth()->guard('client')->user();
                    if ($client && $client->group_id && $client->group && $client->group->hasCustomPricing()) {
                        // Check for product-specific discount
                        $productDiscount = $client->group->getProductDiscountPercentage($item['product_id'], $item['period']);
                        if ($productDiscount !== null) {
                            $basePrice = $basePrice * (1 - ($productDiscount / 100));
                        } else {
                            // Check for global percentage discount
                            $globalDiscount = $client->group->getDiscountPercentage();
                            if ($globalDiscount !== null) {
                                $basePrice = $basePrice * (1 - ($globalDiscount / 100));
                            } else {
                                // Check for global fixed discount
                                $fixedDiscount = $client->group->getDiscountAmount();
                                if ($fixedDiscount !== null) {
                                    $basePrice = max(0, $basePrice - $fixedDiscount);
                                }
                            }
                        }
                    }

                    $recalculatedItem['price'] = $basePrice;

                    \Log::info('Recalculated product price', [
                        'product_id' => $item['product_id'],
                        'old_price' => $item['price'] ?? 'N/A',
                        'new_price' => $recalculatedItem['price'],
                        'currency' => $currency->code,
                    ]);
                } else {
                    // PRC-1099 rank-15: no product Pricing row for this
                    // product/period/currency → flag fail-closed so the summary's
                    // unpriceable list is populated and the order aborts.
                    $recalculatedItem['_pricing_missing'] = true;
                }

                // Configurable options'ları da yeniden hesapla
                if (isset($item['configurableOptions']) && is_array($item['configurableOptions'])) {
                    $totalConfigPrice = 0;
                    $recalculatedConfigs = [];

                    foreach ($item['configurableOptions'] as $configOption) {
                        $optionPrice = \App\Models\Pricing::where('type', 'product_config')
                            ->where('relation_id', $configOption['value_id'])
                            ->where('currency_id', $currency->id)
                            ->where('time', 'month')
                            ->where('period', $item['period'])
                            ->first(['amount', 'setup_amount']);

                        if ($optionPrice) {
                            $optionAmount = (float) $optionPrice->amount;

                            // Number tipi için quantity ile çarp
                            if (isset($configOption['quantity']) && $configOption['quantity'] > 1) {
                                $optionAmount = $optionAmount * (float) $configOption['quantity'];
                            }

                            $totalConfigPrice += $optionAmount;

                            // Config option'ı güncelle
                            $recalculatedConfig = $configOption;
                            $recalculatedConfig['price'] = $optionAmount;
                            $recalculatedConfigs[] = $recalculatedConfig;
                        }
                    }

                    $recalculatedItem['configurableOptions'] = $recalculatedConfigs;
                    $recalculatedItem['price'] += $totalConfigPrice;
                }

                // Connected domain varsa
                if (isset($item['connectedDomain']) && is_array($item['connectedDomain'])) {
                    $connectedAction = $item['connectedDomain']['domain_action'] ?? 'register';
                    $domainPriceRecord = \App\Models\Pricing::where('type', 'domain')
                        ->where('relation_id', $item['connectedDomain']['tld_id'])
                        ->where('currency_id', $currency->id)
                        ->where('segment', $connectedAction)
                        ->where('period', 1)
                        ->first();

                    if ($domainPriceRecord) {
                        $connectedDomainPrice = (float) $domainPriceRecord->amount;

                        // Apply client group TLD discount if applicable
                        if ($client && $client->group_id && $client->group && $client->group->hasCustomPricing()) {
                            $connectedAction = $item['connectedDomain']['domain_action'] ?? 'register';
                            $tldDiscount = $client->group->getTldDiscountPercentage($item['connectedDomain']['tld_id'], $connectedAction, 1);
                            if ($tldDiscount !== null) {
                                $connectedDomainPrice = $connectedDomainPrice * (1 - ($tldDiscount / 100));
                            } elseif ($client->group->getDiscountPercentage() !== null) {
                                $connectedDomainPrice = $connectedDomainPrice * (1 - ($client->group->getDiscountPercentage() / 100));
                            }
                        }

                        $recalculatedItem['price'] += $connectedDomainPrice;
                    }
                }
            } elseif ($item['type'] === 'domain') {
                // PRC-1099 rank-3 — premium domain price is authoritative
                // (resolved server-side in BasketController at add-time). Do NOT
                // overwrite it with a standard Pricing row for display; keep it
                // in lockstep with the checkout summary path (PricingService
                // calculateBasketSummaryFromItems). Source from the same field
                // the persistence path reads (L1302) so the shown price == the
                // invoiced price == premium_purchase_amount.
                $isPremiumDomain = ! empty($item['is_premium']);
                if ($isPremiumDomain) {
                    $recalculatedItem['price'] = (float) ($item['premium_pricing']['register'] ?? $item['price'] ?? 0);
                }

                // Domain fiyatını güncel currency ile al
                $tldId = $item['tld_id'] ?? null;
                if ($tldId) {
                    $domainAction = $item['domain_action'] ?? 'register';

                    $domainPrice = \App\Models\Pricing::where('type', 'domain')
                        ->where('relation_id', $tldId)
                        ->where('currency_id', $currency->id)
                        ->where('segment', $domainAction)
                        ->where('period', $item['period'] ?? 1)
                        ->first();

                    if ($domainPrice && ! $isPremiumDomain) {
                        $basePrice = (float) $domainPrice->amount;

                        // Apply client group TLD discount if applicable
                        $client = auth()->guard('client')->user();
                        if ($client && $client->group_id && $client->group && $client->group->hasCustomPricing()) {
                            $tldDiscount = $client->group->getTldDiscountPercentage($tldId, $domainAction, $item['period'] ?? 1);
                            if ($tldDiscount !== null) {
                                $basePrice = $basePrice * (1 - ($tldDiscount / 100));
                            } else {
                                // Check for global percentage discount
                                $globalDiscount = $client->group->getDiscountPercentage();
                                if ($globalDiscount !== null) {
                                    $basePrice = $basePrice * (1 - ($globalDiscount / 100));
                                }
                            }
                        }

                        $recalculatedItem['price'] = $basePrice;

                        \Log::info('Recalculated domain price', [
                            'tld_id' => $tldId,
                            'old_price' => $item['price'] ?? 'N/A',
                            'new_price' => $recalculatedItem['price'],
                            'currency' => $currency->code,
                        ]);
                    } elseif (! $isPremiumDomain && $domainAction !== 'use') {
                        // PRC-1099 rank-15: register/transfer/renew domain with NO
                        // standard Pricing row → flag fail-closed. Premium prices from
                        // the premium field; domain_action==='use' is legitimate-zero.
                        $recalculatedItem['_pricing_missing'] = true;
                    }
                }
            }

            $recalculatedItems[] = $recalculatedItem;
        }

        return $recalculatedItems;
    }

    /**
     * PRC-540 — Persist a SavedPaymentMethod for a free-trial customer who opted in
     * to card-save at checkout.  Best-effort: any failure is logged but does NOT
     * roll back the already-committed trial order.
     *
     * Conditions checked here (belt-and-suspenders on top of the view guard):
     *  - At least one basket item is a free-trial product with trial_ccsave=true.
     *  - The chosen gateway module reports getFeatures()['save_card'] === true.
     *  - The gateway admin setting allow_save_card is truthy.
     *
     * For Payu (the concrete gateway wired in this PR) the card details come from
     * the request; sandbox mode returns a SANDBOX_CARD_* token without hitting the
     * real network.  For other gateways that later add saveCard support the same
     * path activates automatically once they meet the two feature-flag conditions.
     *
     * @param  \App\Models\Client  $client
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $gatewayName
     * @param  array  $summaryItems  Already-computed summary items (used to check trial_ccsave).
     */
    /**
     * PRC-540 — Minimal IIN-prefix card-brand detection (mirrors PaymentController).
     * Not exhaustive — good enough for display ("VISA •••• 1111").
     */
    private function detectCardBrand(string $cardNumber): ?string
    {
        $digits = preg_replace('/\D/', '', $cardNumber);
        if ($digits === '') {
            return null;
        }
        if ($digits[0] === '4') {
            return 'visa';
        }
        $first2 = (int) substr($digits, 0, 2);
        if ($first2 >= 51 && $first2 <= 55) {
            return 'mastercard';
        }
        $first4 = (int) substr($digits, 0, 4);
        if ($first4 >= 2221 && $first4 <= 2720) {
            return 'mastercard';
        }
        if (in_array(substr($digits, 0, 2), ['34', '37'], true)) {
            return 'amex';
        }
        if (str_starts_with($digits, '9792') || str_starts_with($digits, '65')) {
            return 'troy';
        }
        return 'card';
    }

    private function maybeTrialSaveCard(
        $client,
        \Illuminate\Http\Request $request,
        string $gatewayName,
        array $summaryItems
    ): void {
        try {
            // 1. Confirm at least one free-trial product in the basket has trial_ccsave=true.
            $trialProductIds = collect($summaryItems)
                ->filter(fn ($i) => ! empty($i['is_trial']) && ($i['type'] ?? '') === 'product')
                ->pluck('product_id')
                ->filter()
                ->unique()
                ->values()
                ->all();

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

            $needsCCSave = \App\Models\Product::whereIn('id', $trialProductIds)
                ->where('trial_ccsave', true)
                ->exists();

            if (! $needsCCSave) {
                return;
            }

            // 2. Gateway must support save_card and have it enabled in admin settings.
            $gatewayModule = $this->paymentManager->getGatewayModule($gatewayName);
            if (! $gatewayModule) {
                return;
            }

            $features = $gatewayModule->getFeatures();
            if (empty($features['save_card'])) {
                return;
            }

            $gwSettings = PaymentGatewaySetting::getModuleSettings($gatewayName);
            if (empty($gwSettings['allow_save_card'])) {
                return;
            }

            // 3. Build the params the gateway's saveCard() expects.
            //    We collect card details from the request (same fields used by handleCreditCard2D/3D).
            //    Raw PAN / CVV must NOT be logged or persisted beyond this call.
            $cardNumber = (string) $request->input('trial_card_number', '');
            $cardHolder = (string) $request->input('trial_card_holder', '');
            $expiryMonth = str_pad((string) $request->input('trial_expiry_month', ''), 2, '0', STR_PAD_LEFT);
            $expiryYear  = (string) $request->input('trial_expiry_year', '');

            if ($cardNumber === '' || $expiryMonth === '' || $expiryYear === '') {
                \Log::info('PRC-540: trial_save_card_consent=1 but card fields missing — skipping', [
                    'client_id'  => $client->id,
                    'gateway'    => $gatewayName,
                ]);
                return;
            }

            $saveResult = $gatewayModule->saveCard([
                'client_id'        => $client->id,
                'card_number'      => $cardNumber,
                'card_holder_name' => $cardHolder,
                'expiry_month'     => $expiryMonth,
                'expiry_year'      => $expiryYear,
            ]);

            if (($saveResult['status'] ?? null) !== 'success') {
                \Log::warning('PRC-540: gateway saveCard returned non-success for trial', [
                    'client_id' => $client->id,
                    'gateway'   => $gatewayName,
                    'result'    => array_diff_key($saveResult, ['token' => true, 'card_token' => true]),
                ]);
                return;
            }

            // PRC-540 review fix: gateway return contract'i tek tip değil — Eway
            // `data.token_customer_id` ile, SagePay `data.card_identifier` ile döner.
            // AbstractPayment'ta bir gün standardize edilirse fallback'ler kaldırılabilir.
            $token = $saveResult['token']
                ?? $saveResult['card_token']
                ?? $saveResult['data']['token_customer_id'] ?? null  // Eway
                ?? $saveResult['data']['card_identifier'] ?? null    // SagePay
                ?? null;
            if (! $token) {
                \Log::warning('PRC-540: saveCard succeeded but returned no token', [
                    'client_id' => $client->id,
                    'gateway'   => $gatewayName,
                ]);
                return;
            }

            $gatewaySlug = strtolower($gatewayName);

            // 4. Idempotent upsert: skip if the same token is already stored.
            $exists = \App\Models\SavedPaymentMethod::where('client_id', $client->id)
                ->where('gateway_slug', $gatewaySlug)
                ->where('token', $token)
                ->exists();

            if ($exists) {
                return;
            }

            // Detect card brand from PAN prefix (same helper used in PaymentController).
            $cardBrand = $this->detectCardBrand($cardNumber);
            $lastFour  = substr(preg_replace('/\D/', '', $cardNumber), -4);

            // Mark is_default=true; demote any existing default for this gateway first.
            \App\Models\SavedPaymentMethod::where('client_id', $client->id)
                ->where('gateway_slug', $gatewaySlug)
                ->where('is_default', true)
                ->update(['is_default' => false]);

            \App\Models\SavedPaymentMethod::create([
                'client_id'    => $client->id,
                'gateway_slug' => $gatewaySlug,
                'token'        => $token,
                'card_brand'   => $cardBrand,
                'last_four'    => $lastFour,
                'expiry_month' => $expiryMonth,
                'expiry_year'  => $expiryYear,
                'is_default'   => true,
                'status'       => 'active',
            ]);

            \Log::info('PRC-540: trial card-save persisted', [
                'client_id'  => $client->id,
                'gateway'    => $gatewaySlug,
                'last_four'  => $lastFour,
            ]);

        } catch (\Throwable $e) {
            // Best-effort — never let card-save failure surface to the customer as
            // a checkout error. Trial provisioning already completed at this point.
            \Log::error('PRC-540: trial card-save failed (provisioning unaffected)', [
                'client_id' => $client->id ?? null,
                'gateway'   => $gatewayName,
                'error'     => $e->getMessage(),
            ]);
        }
    }
}
