<?php

namespace App\Http\Controllers\Shop;

use App\Exceptions\Vtu\InsufficientFundsException;
use App\Http\Controllers\Controller;
use App\Models\CashbackRule;
use App\Models\Category;
use App\Models\Network;
use App\Models\Plan;
use App\Models\ServiceProvider;
use App\Models\Transaction;
use App\Models\User;
use App\Services\AirtimePricingService;
use App\Services\CashbackExpiryService;
use App\Services\CashbackWalletService;
use App\Services\PlanPricingService;
use App\Services\ServiceAvailabilityService;
use App\Services\TransactionService;
use App\Services\UserAccessService;
use App\Services\VendorRouter;
use App\Services\WalletService;
use App\Services\Providers\ProviderFactory;
use App\Support\NigeriaPhone;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;

class PurchaseController extends Controller
{
    public function __construct(
        protected PlanPricingService $planPricingService,
        protected ServiceAvailabilityService $serviceAvailabilityService,
        protected AirtimePricingService $airtimePricingService,
        protected WalletService $walletService,
        protected CashbackWalletService $cashbackWalletService,
        protected CashbackExpiryService $cashbackExpiryService,
        protected UserAccessService $userAccessService
    ) {
    }

    public function data(Request $request): Response
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'data', 'Your account cannot buy data currently.');

        return $this->renderPurchasePage('App/Buy/Data', [
            'categorySlugs' => ['data_sme', 'data_gifting', 'data_cg', 'data_direct'],
        ], $request);
    }

    public function airtime(Request $request): Response
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'airtime', 'Your account cannot buy airtime currently.');

        return $this->renderPurchasePage('App/Buy/Airtime', [
            'categorySlugs' => ['airtime_vtu'],
            'loadPlans' => false,
        ], $request);
    }

    public function cable(Request $request): Response
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'cable', 'Your account cannot buy cable currently.');

        return $this->renderPurchasePage('App/Buy/Cable', [
            'categorySlugs' => ['cable'],
        ], $request);
    }

    public function electricity(Request $request): Response
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'electricity', 'Your account cannot pay electricity bills currently.');

        return $this->renderPurchasePage('App/Buy/Electricity', [
            'categorySlugs' => ['electricity'],
        ], $request);
    }

    public function epin(Request $request): Response
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'epin', 'Your account cannot buy exam pins currently.');

        return $this->renderPurchasePage('App/Buy/Epin', [
            'categorySlugs' => ['epin'],
        ], $request);
    }

    public function purchaseData(Request $request, TransactionService $transactionService): RedirectResponse
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'data', 'Your account cannot buy data currently.');

        $data = $request->validate([
            'category_slug' => ['required', Rule::in(['data_sme', 'data_gifting', 'data_cg', 'data_direct'])],
            'network_id' => ['nullable', 'exists:networks,id'],
            'plan_id' => ['required', 'exists:plans,id'],
            'phone' => ['required', 'string', 'max:40', 'regex:/^[0-9+\s().-]+$/'],
            'coupon_code' => ['nullable', 'string', 'max:64'],
            'use_cashback' => ['nullable', 'boolean'],
            'validate_network_match' => ['nullable', 'boolean'],
            'transaction_pin' => ['required', 'digits:4'],
        ]);
        $this->assertValidTransactionPin($request, (string) $data['transaction_pin']);
        unset($data['transaction_pin']);

        $selectedPlan = Plan::query()->with(['network', 'category'])->findOrFail($data['plan_id']);
        if (! empty($data['network_id']) && (int) $selectedPlan->network_id !== (int) $data['network_id']) {
            return back()->withErrors([
                'plan_id' => 'Selected plan does not belong to the chosen network.',
            ])->withInput();
        }

        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $selectedPlan->category,
            network: $selectedPlan->network,
            providerSlug: null,
            plan: $selectedPlan
        );

        $normalizedPhone = $this->resolveValidatedNigeriaPhone(
            data: $data,
            required: true,
            selectedNetworkSlug: (string) ($selectedPlan->network?->slug ?? '')
        );

        return $this->executePurchase($request, $transactionService, 'Data', [
            ...$data,
            'phone' => $normalizedPhone,
            'mobile_number' => $normalizedPhone,
            'use_cashback' => (bool) ($data['use_cashback'] ?? false),
            'internal_reference' => $this->idempotencyReference($request),
        ]);
    }

    public function purchaseAirtime(Request $request, TransactionService $transactionService): RedirectResponse
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'airtime', 'Your account cannot buy airtime currently.');

        $data = $request->validate([
            'network_id' => ['required', 'exists:networks,id'],
            'plan_id' => ['nullable', 'exists:plans,id'],
            'phone' => ['required', 'string', 'max:40', 'regex:/^[0-9+\s().-]+$/'],
            'amount' => ['required', 'numeric', 'min:1', 'max:100000000'],
            'coupon_code' => ['nullable', 'string', 'max:64'],
            'use_cashback' => ['nullable', 'boolean'],
            'validate_network_match' => ['nullable', 'boolean'],
            'transaction_pin' => ['required', 'digits:4'],
        ]);
        $this->assertValidTransactionPin($request, (string) $data['transaction_pin']);
        unset($data['transaction_pin']);

        $category = Category::query()->where('slug', 'airtime_vtu')->firstOrFail();
        $network = Network::query()->findOrFail((int) $data['network_id']);
        $plan = null;

        if (! empty($data['plan_id'])) {
            $plan = Plan::query()->with(['network', 'category'])->findOrFail((int) $data['plan_id']);

            if ((int) $plan->category_id !== (int) $category->id) {
                return back()->withErrors([
                    'plan_id' => 'Selected plan does not belong to Airtime VTU category.',
                ])->withInput();
            }

            if ((int) $plan->network_id !== (int) $network->id) {
                return back()->withErrors([
                    'plan_id' => 'Selected plan does not belong to the chosen network.',
                ])->withInput();
            }
        }

        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $category,
            network: $network,
            providerSlug: null,
            plan: $plan
        );
        $this->airtimePricingService->assertWithinLimits((float) $data['amount']);

        $normalizedPhone = $this->resolveValidatedNigeriaPhone(
            data: $data,
            required: true,
            selectedNetworkSlug: (string) $network->slug
        );

        return $this->executePurchase($request, $transactionService, 'Airtime', [
            ...$data,
            'category_slug' => 'airtime_vtu',
            'network_slug' => $network->slug,
            'phone' => $normalizedPhone,
            'mobile_number' => $normalizedPhone,
            'use_cashback' => (bool) ($data['use_cashback'] ?? false),
            'internal_reference' => $this->idempotencyReference($request),
        ]);
    }

    public function purchaseCable(Request $request, TransactionService $transactionService): RedirectResponse
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'cable', 'Your account cannot buy cable currently.');

        $data = $request->validate([
            'category_slug' => ['required', Rule::in(['cable'])],
            'plan_id' => ['required', 'exists:plans,id'],
            'smart_card_number' => ['required', 'string', 'min:5', 'max:50'],
            'phone' => ['required', 'string', 'max:40', 'regex:/^[0-9+\s().-]+$/'],
            'cablename' => ['nullable', 'string', 'max:50'],
            'cableplan' => ['nullable', 'string', 'max:100'],
            'subscription_type' => ['nullable', Rule::in(['change', 'renew', 'renewal'])],
            'quantity' => ['nullable', 'integer', 'min:1', 'max:12'],
            'coupon_code' => ['nullable', 'string', 'max:64'],
            'use_cashback' => ['nullable', 'boolean'],
            'validate_network_match' => ['nullable', 'boolean'],
            'transaction_pin' => ['required', 'digits:4'],
        ]);
        $this->assertValidTransactionPin($request, (string) $data['transaction_pin']);
        unset($data['transaction_pin']);

        $plan = Plan::query()->with(['category', 'network'])->findOrFail($data['plan_id']);
        $meta = $plan->meta_json ?? [];
        $cablename = (string) ($data['cablename'] ?? data_get($meta, 'cablename', ''));
        $subscriptionType = (string) ($data['subscription_type'] ?? data_get($meta, 'vtpass_subscription_type', ''));
        $quantity = (int) ($data['quantity'] ?? data_get($meta, 'vtpass_quantity', 1));

        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $plan->category,
            network: $plan->network,
            providerSlug: $cablename,
            plan: $plan
        );

        $normalizedPhone = $this->resolveValidatedNigeriaPhone(
            data: $data,
            required: true,
            selectedNetworkSlug: null
        );

        return $this->executePurchase($request, $transactionService, 'Cable', [
            ...$data,
            'cablename' => $cablename !== '' ? $cablename : null,
            'cableplan' => $data['cableplan'] ?? $plan->value,
            'subscription_type' => $subscriptionType !== '' ? $subscriptionType : null,
            'quantity' => $quantity > 0 ? $quantity : 1,
            'phone' => $normalizedPhone,
            'mobile_number' => $normalizedPhone,
            'use_cashback' => (bool) ($data['use_cashback'] ?? false),
            'internal_reference' => $this->idempotencyReference($request),
        ]);
    }

    public function purchaseElectricity(Request $request, TransactionService $transactionService): RedirectResponse
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'electricity', 'Your account cannot pay electricity bills currently.');

        $data = $request->validate([
            'category_slug' => ['required', Rule::in(['electricity'])],
            'plan_id' => ['nullable', 'exists:plans,id'],
            'amount' => ['required', 'numeric', 'min:100', 'max:1000000'],
            'meter_number' => ['required', 'string', 'min:5', 'max:50'],
            'phone' => ['nullable', 'string', 'max:40', 'regex:/^[0-9+\s().-]+$/'],
            'disco_name' => ['required', 'string', 'max:50'],
            'meter_type' => ['required', Rule::in([1, 2, '1', '2'])],
            'coupon_code' => ['nullable', 'string', 'max:64'],
            'use_cashback' => ['nullable', 'boolean'],
            'validate_network_match' => ['nullable', 'boolean'],
            'transaction_pin' => ['required', 'digits:4'],
        ]);
        $this->assertValidTransactionPin($request, (string) $data['transaction_pin']);
        unset($data['transaction_pin']);

        $category = Category::query()->where('slug', 'electricity')->firstOrFail();
        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $category,
            network: null,
            providerSlug: (string) $data['disco_name'],
            plan: null
        );

        $normalizedPhone = $this->resolveValidatedNigeriaPhone(
            data: $data,
            required: false,
            selectedNetworkSlug: null
        );

        return $this->executePurchase($request, $transactionService, 'Electricity', [
            ...$data,
            'MeterType' => (int) $data['meter_type'],
            'phone' => $normalizedPhone,
            'mobile_number' => $normalizedPhone,
            'use_cashback' => (bool) ($data['use_cashback'] ?? false),
            'internal_reference' => $this->idempotencyReference($request),
        ]);
    }

    public function validateElectricityMeter(
        Request $request,
        VendorRouter $vendorRouter,
        ProviderFactory $providerFactory
    ): JsonResponse {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'electricity', 'Your account cannot pay electricity bills currently.');

        $data = $request->validate([
            'meter_number' => ['required', 'string', 'min:5', 'max:50'],
            'disco_name' => ['required', 'string', 'max:50'],
            'meter_type' => ['required', Rule::in([1, 2, '1', '2'])],
        ]);

        $category = Category::query()->where('slug', 'electricity')->firstOrFail();
        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $category,
            network: null,
            providerSlug: (string) $data['disco_name'],
            plan: null
        );

        try {
            $candidates = $vendorRouter->resolveCandidates(null, $category, null);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'No active electricity vendor route found. Ask admin to configure routing.',
            ], 422);
        }

        $attempts = [];

        foreach ($candidates as $candidate) {
            $vendor = $candidate['vendor'] ?? null;
            if (! $vendor) {
                continue;
            }

            try {
                $provider = $providerFactory->forVendor($vendor);
                $payload = $this->buildMeterValidationPayloadForVendor((string) $vendor->type, $data);
                $result = $provider->validateMeter($payload, null);
                $normalizedValidation = $this->normalizeMeterValidationResult($result, $data, (string) $vendor->type);
                $success = (bool) ($result['success'] ?? false)
                    && $this->isMeterValidationAccepted($result, $normalizedValidation, (string) $vendor->type);

                $attempts[] = [
                    'vendor' => $vendor->name,
                    'vendor_type' => $vendor->type,
                    'success' => $success,
                    'http_code' => $result['http_code'] ?? null,
                ];

                if ($success) {
                    return response()->json([
                        'success' => true,
                        'message' => 'Meter validated successfully.',
                        'vendor' => [
                            'id' => (int) $vendor->id,
                            'name' => (string) $vendor->name,
                            'type' => (string) $vendor->type,
                        ],
                        'validation' => $normalizedValidation,
                        'attempts' => $attempts,
                    ]);
                }
            } catch (\Throwable $e) {
                $attempts[] = [
                    'vendor' => $vendor->name,
                    'vendor_type' => $vendor->type,
                    'success' => false,
                    'error' => $e->getMessage(),
                ];
                Log::warning('Electricity meter validation attempt failed.', [
                    'vendor_id' => $vendor->id,
                    'vendor_name' => $vendor->name,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return response()->json([
            'success' => false,
            'message' => 'Meter validation failed. Confirm meter details and try again.',
            'attempts' => $attempts,
        ], 422);
    }

    public function purchaseEpin(Request $request, TransactionService $transactionService): RedirectResponse
    {
        $this->assertUserCanPurchase($request);
        $this->assertServiceAllowed($request, 'epin', 'Your account cannot buy exam pins currently.');

        $data = $request->validate([
            'category_slug' => ['required', Rule::in(['epin'])],
            'plan_id' => ['required', 'exists:plans,id'],
            'quantity' => ['required', 'integer', 'min:1', 'max:20'],
            'phone' => ['required', 'string', 'max:40', 'regex:/^[0-9+\s().-]+$/'],
            'biller_code' => ['nullable', 'string', 'max:120'],
            'coupon_code' => ['nullable', 'string', 'max:64'],
            'use_cashback' => ['nullable', 'boolean'],
            'validate_network_match' => ['nullable', 'boolean'],
            'transaction_pin' => ['required', 'digits:4'],
        ]);
        $this->assertValidTransactionPin($request, (string) $data['transaction_pin']);
        unset($data['transaction_pin']);

        $plan = Plan::query()->with(['category', 'network'])->findOrFail($data['plan_id']);
        $examName = (string) data_get($plan->meta_json, 'exam_name', $plan->value);

        $this->serviceAvailabilityService->assertPurchaseAllowed(
            category: $plan->category,
            network: $plan->network,
            providerSlug: $examName,
            plan: $plan
        );

        $normalizedPhone = $this->resolveValidatedNigeriaPhone(
            data: $data,
            required: true,
            selectedNetworkSlug: null
        );

        return $this->executePurchase($request, $transactionService, 'Education pin', [
            ...$data,
            'exam_name' => $examName,
            'phone' => $normalizedPhone,
            'mobile_number' => $normalizedPhone,
            'use_cashback' => (bool) ($data['use_cashback'] ?? false),
            'internal_reference' => $this->idempotencyReference($request),
        ]);
    }

    protected function executePurchase(Request $request, TransactionService $transactionService, string $serviceLabel, array $payload): RedirectResponse
    {
        try {
            $transaction = $transactionService->purchase($request->user(), $payload);
        } catch (InsufficientFundsException $e) {
            return back()->withErrors([
                'amount' => 'Insufficient wallet balance for this purchase.',
            ])->withInput();
        }

        [$flashType, $flashMessage] = $this->purchaseFlash($serviceLabel, $transaction);
        $purchaseResult = $this->purchaseResultPayload($serviceLabel, $transaction);

        return back()
            ->with($flashType, $flashMessage)
            ->with('purchase_result', $purchaseResult);
    }

    /**
     * @param  array<string,mixed>  $data
     */
    protected function resolveValidatedNigeriaPhone(array $data, bool $required, ?string $selectedNetworkSlug = null): ?string
    {
        $rawPhone = trim((string) ($data['phone'] ?? ''));
        if ($rawPhone === '') {
            if ($required) {
                throw ValidationException::withMessages([
                    'phone' => 'Enter a valid Nigerian phone number (e.g. 07065620433).',
                ]);
            }

            return null;
        }

        $result = NigeriaPhone::validateVtuPhoneNumber(
            phoneInput: $rawPhone,
            selectedNetwork: $selectedNetworkSlug,
            options: [
                'validate_network_match' => (bool) ($data['validate_network_match'] ?? false),
                'mismatch_as_error' => true,
            ]
        );

        if (! (bool) ($result['is_valid'] ?? false)) {
            throw ValidationException::withMessages([
                'phone' => (string) (($result['errors'][0] ?? null) ?: 'Enter a valid Nigerian phone number (e.g. 07065620433).'),
            ]);
        }

        $normalized = $result['normalized_number'] ?? null;
        if (! is_string($normalized) || $normalized === '') {
            throw ValidationException::withMessages([
                'phone' => 'Enter a valid Nigerian phone number (e.g. 07065620433).',
            ]);
        }

        return $normalized;
    }

    protected function renderPurchasePage(string $page, array $filter, Request $request): Response
    {
        $categories = Category::query()
            ->whereIn('slug', $filter['categorySlugs'])
            ->get(['id', 'name', 'slug']);
        $categoryIds = $categories->pluck('id');

        $user = $request->user();
        $loadPlans = (bool) ($filter['loadPlans'] ?? true);
        $plans = collect();
        if ($loadPlans) {
            $plans = Plan::query()
                ->with(['network:id,name,slug', 'category:id,name,slug', 'tierPrices:id,plan_id,tier,price'])
                ->whereIn('category_id', $categoryIds)
                ->where('status', 'available')
                ->where(function ($query) {
                    $query->whereExists(function ($subQuery) {
                        $subQuery->selectRaw('1')
                            ->from('plan_vendor_mappings')
                            ->join('vendors', 'vendors.id', '=', 'plan_vendor_mappings.vendor_id')
                            ->whereColumn('plan_vendor_mappings.plan_id', 'plans.id')
                            ->where('plan_vendor_mappings.status', 'active')
                            ->where('vendors.status', 'active');
                    })->orWhereExists(function ($subQuery) {
                        $subQuery->selectRaw('1')
                            ->from('service_vendor_mappings')
                            ->join('vendors', 'vendors.id', '=', 'service_vendor_mappings.vendor_id')
                            ->whereColumn('service_vendor_mappings.category_id', 'plans.category_id')
                            ->where('service_vendor_mappings.status', 'active')
                            ->where('vendors.status', 'active')
                            ->where(function ($networkQuery) {
                                $networkQuery->whereNull('service_vendor_mappings.network_id')
                                    ->orWhereColumn('service_vendor_mappings.network_id', 'plans.network_id');
                            });
                    });
                })
                ->orderBy('sort_order')
                ->get()
                ->filter(fn (Plan $plan) => $this->isPlanVisibleForListing($plan))
                ->values()
                ->map(function (Plan $plan) use ($user) {
                    $data = $plan->toArray();
                    $effectivePrice = $this->planPricingService->resolveSellingPrice($plan, $user);
                    $data['base_selling_price'] = (float) $plan->selling_price;
                    $data['selling_price'] = $effectivePrice;
                    $data['tier_prices'] = $this->planPricingService->allTierPrices($plan);

                    return $data;
                })
                ->values();
        }

        $this->cashbackExpiryService->expireUserIfDue($user);
        $cashbackPolicy = $this->cashbackExpiryService->policyForUser($user);

        $wallet = $this->walletService->getWallet($user);
        $serviceProviders = ServiceProvider::query()
            ->orderBy('name')
            ->get(['id', 'service_group', 'name', 'slug', 'meta_json'])
            ->filter(fn (ServiceProvider $provider) => $this->isProviderVisibleForListing($provider, $categories))
            ->values()
            ->groupBy('service_group')
            ->map(fn ($items) => $items->values())
            ->toArray();

        $networks = Network::query()->orderBy('name')->get(['id', 'name', 'slug']);
        if ($this->isAirtimeFilter($filter)) {
            $airtimeCategory = $categories->firstWhere('slug', 'airtime_vtu');
            $networks = $networks->filter(function (Network $network) use ($airtimeCategory) {
                if (! $airtimeCategory) {
                    return false;
                }

                return $this->isServiceListingEnabled($airtimeCategory, $network);
            })->values();
        } elseif ($this->isDataFilter($filter)) {
            $visibleNetworkIds = collect($plans)
                ->pluck('network.id')
                ->filter()
                ->map(fn ($id) => (int) $id)
                ->unique()
                ->values();

            $networks = $networks
                ->whereIn('id', $visibleNetworkIds)
                ->values();
        }

        $cashbackHints = $this->cashbackListingHints(
            user: $user,
            categories: $categories,
            plans: $plans,
            networks: $networks
        );

        return Inertia::render($page, [
            'networks' => $networks,
            'categories' => $categories->values(),
            'plans' => $plans,
            'recentRecipients' => $this->recentRecipientsForUser($user),
            'serviceProviders' => [
                'cable' => $serviceProviders['cable'] ?? [],
                'electricity' => $serviceProviders['electricity'] ?? [],
                'epin' => $serviceProviders['epin'] ?? [],
            ],
            'balances' => [
                'wallet' => round((float) $wallet->balance, 2),
                'cashback' => $this->cashbackWalletService->balance($user),
            ],
            'cashbackPolicy' => $cashbackPolicy,
            'cashbackHints' => $cashbackHints,
            'serviceControlMeta' => [
                'statuses' => ServiceAvailabilityService::STATUSES ?? ['enabled', 'disabled', 'maintenance'],
            ],
            'airtimePricing' => $this->airtimePricingService->settings(),
        ]);
    }

    /**
     * @return array<int,array{phone:string,label:string,source:string,created_at:?string}>
     */
    protected function recentRecipientsForUser(?User $user): array
    {
        if (! $user) {
            return [];
        }

        $recentTransactions = Transaction::query()
            ->with([
                'category:id,name',
                'plan:id,name',
            ])
            ->where('user_id', $user->id)
            ->latest('id')
            ->limit(150)
            ->get(['id', 'category_id', 'plan_id', 'meta_json', 'created_at']);

        $seen = [];
        $recipients = [];

        foreach ($recentTransactions as $transaction) {
            $meta = (array) ($transaction->meta_json ?? []);
            $rawPhone = data_get($meta, 'phone') ?? data_get($meta, 'mobile_number');
            if (! is_scalar($rawPhone)) {
                continue;
            }

            $normalizedPhone = NigeriaPhone::normalize((string) $rawPhone);
            if (! is_string($normalizedPhone) || $normalizedPhone === '') {
                continue;
            }

            if (isset($seen[$normalizedPhone])) {
                continue;
            }

            $seen[$normalizedPhone] = true;

            $labelParts = array_filter([
                $transaction->category?->name,
                $transaction->plan?->name,
            ]);

            $recipients[] = [
                'phone' => $normalizedPhone,
                'label' => count($labelParts) > 0 ? implode(' • ', $labelParts) : 'Recent recipient',
                'source' => 'history',
                'created_at' => optional($transaction->created_at)->toIso8601String(),
            ];

            if (count($recipients) >= 30) {
                break;
            }
        }

        return $recipients;
    }

    protected function isDataFilter(array $filter): bool
    {
        $slugs = (array) ($filter['categorySlugs'] ?? []);

        return collect($slugs)
            ->intersect(['data_sme', 'data_gifting', 'data_cg', 'data_direct'])
            ->isNotEmpty();
    }

    protected function isAirtimeFilter(array $filter): bool
    {
        $slugs = array_values((array) ($filter['categorySlugs'] ?? []));

        return count($slugs) === 1 && $slugs[0] === 'airtime_vtu';
    }

    protected function isPlanVisibleForListing(Plan $plan): bool
    {
        if (! $plan->category) {
            return false;
        }

        $providerSlug = $this->providerSlugForPlan($plan);

        return $this->isServiceListingEnabled(
            category: $plan->category,
            network: $plan->network,
            providerSlug: $providerSlug,
            plan: $plan
        );
    }

    protected function isProviderVisibleForListing(ServiceProvider $provider, $categories): bool
    {
        $categorySlug = match ($provider->service_group) {
            'cable' => 'cable',
            'electricity' => 'electricity',
            'epin' => 'epin',
            default => null,
        };

        if ($categorySlug === null) {
            return true;
        }

        /** @var Category|null $category */
        $category = $categories->firstWhere('slug', $categorySlug);
        if (! $category) {
            return true;
        }

        return $this->isServiceListingEnabled(
            category: $category,
            network: null,
            providerSlug: $provider->slug
        );
    }

    protected function isServiceListingEnabled(
        Category $category,
        ?Network $network = null,
        ?string $providerSlug = null,
        ?Plan $plan = null
    ): bool {
        $resolved = $this->serviceAvailabilityService->resolveStatus(
            category: $category,
            network: $network,
            providerSlug: $providerSlug,
            plan: $plan
        );

        return (string) ($resolved['status'] ?? 'enabled') === 'enabled';
    }

    protected function providerSlugForPlan(Plan $plan): ?string
    {
        $categorySlug = (string) ($plan->category?->slug ?? '');
        $meta = (array) ($plan->meta_json ?? []);

        return match ($categorySlug) {
            'cable' => filled($meta['cablename'] ?? null) ? (string) $meta['cablename'] : null,
            'electricity' => filled($meta['disco_name'] ?? null) ? (string) $meta['disco_name'] : null,
            'epin' => filled($meta['exam_name'] ?? null) ? (string) $meta['exam_name'] : null,
            default => null,
        };
    }

    /**
     * @param  \Illuminate\Support\Collection<int,Category>  $categories
     * @param  \Illuminate\Support\Collection<int,Plan>  $plans
     * @param  \Illuminate\Support\Collection<int,Network>  $networks
     * @return array{enabled:bool,percent_by_category:array<string,float>}
     */
    protected function cashbackListingHints($user, $categories, $plans, $networks): array
    {
        $enabled = (bool) app(\App\Services\SystemSettingService::class)->get('features', 'cashback_enabled', false);
        if (! $enabled) {
            return ['enabled' => false, 'percent_by_category' => []];
        }

        $now = now();
        $rules = CashbackRule::query()
            ->where('status', CashbackRule::STATUS_ACTIVE)
            ->where(function ($query) use ($now) {
                $query->whereNull('starts_at')->orWhere('starts_at', '<=', $now);
            })
            ->where(function ($query) use ($now) {
                $query->whereNull('ends_at')->orWhere('ends_at', '>=', $now);
            })
            ->orderBy('priority')
            ->orderBy('id')
            ->get([
                'id',
                'type',
                'value',
                'applicable_category_ids_json',
                'applicable_network_ids_json',
                'applicable_customer_tiers_json',
                'applicable_user_ids_json',
            ]);

        $userTier = strtolower((string) ($user->customer_tier ?? 'subscriber'));
        $userId = (int) $user->id;
        $allNetworkIds = $networks->pluck('id')->map(fn ($id) => (int) $id)->values();
        $percentByCategory = [];

        foreach ($categories as $category) {
            $categoryId = (int) $category->id;
            $categorySlug = (string) $category->slug;
            $categoryNetworkIds = $plans
                ->filter(fn ($plan) => (int) ($plan->category_id ?? 0) === $categoryId)
                ->pluck('network_id')
                ->filter()
                ->map(fn ($id) => (int) $id)
                ->unique()
                ->values();

            if ($categoryNetworkIds->isEmpty() && in_array($categorySlug, ['airtime_vtu', 'data_sme', 'data_gifting', 'data_cg', 'data_direct'], true)) {
                $categoryNetworkIds = $allNetworkIds;
            }

            $percentValues = [];
            foreach ($rules as $rule) {
                $allowedUserIds = collect((array) ($rule->applicable_user_ids_json ?? []))
                    ->map(fn ($value) => (int) $value)
                    ->filter(fn ($value) => $value > 0)
                    ->values();
                if ($allowedUserIds->isNotEmpty() && ! $allowedUserIds->contains($userId)) {
                    continue;
                }

                $allowedTiers = collect((array) ($rule->applicable_customer_tiers_json ?? []))
                    ->map(fn ($value) => strtolower((string) $value))
                    ->filter()
                    ->values();
                if ($allowedTiers->isNotEmpty() && ! $allowedTiers->contains($userTier)) {
                    continue;
                }

                $allowedCategoryIds = collect((array) ($rule->applicable_category_ids_json ?? []))
                    ->map(fn ($value) => (int) $value)
                    ->filter(fn ($value) => $value > 0)
                    ->values();
                if ($allowedCategoryIds->isNotEmpty() && ! $allowedCategoryIds->contains($categoryId)) {
                    continue;
                }

                $allowedNetworkIds = collect((array) ($rule->applicable_network_ids_json ?? []))
                    ->map(fn ($value) => (int) $value)
                    ->filter(fn ($value) => $value > 0)
                    ->values();
                if ($allowedNetworkIds->isNotEmpty()) {
                    $hasNetworkOverlap = $categoryNetworkIds->intersect($allowedNetworkIds)->isNotEmpty();
                    if (! $hasNetworkOverlap) {
                        continue;
                    }
                }

                if ((string) $rule->type !== CashbackRule::TYPE_PERCENT) {
                    continue;
                }

                $value = round((float) $rule->value, 2);
                if ($value > 0) {
                    $percentValues[] = $value;
                }
            }

            if ($percentValues !== []) {
                $percentByCategory[$categorySlug] = round(max($percentValues), 2);
            }
        }

        return [
            'enabled' => true,
            'percent_by_category' => $percentByCategory,
        ];
    }

    protected function idempotencyReference(Request $request): ?string
    {
        $idempotencyKey = trim((string) $request->header('X-Idempotency-Key', ''));

        if ($idempotencyKey === '') {
            return null;
        }

        return 'CID-'.substr(hash('sha256', $request->user()->id.'|'.$idempotencyKey), 0, 32);
    }

    protected function assertUserCanPurchase(Request $request): void
    {
        $user = $request->user();

        if ($user && ($user->status ?? 'active') !== 'active') {
            abort(403, 'Your account is currently suspended from purchases. Contact support.');
        }

    }

    protected function assertServiceAllowed(Request $request, string $serviceKey, string $message): void
    {
        if ($request->user()) {
            $this->userAccessService->assertCanUse($request->user(), $serviceKey, $message);
        }
    }

    protected function assertValidTransactionPin(Request $request, string $pin): void
    {
        $user = $request->user();

        if (! $user) {
            throw ValidationException::withMessages([
                'transaction_pin' => 'Unable to verify transaction PIN for guest user.',
            ]);
        }

        if (! $user->hasTransactionPin()) {
            throw ValidationException::withMessages([
                'transaction_pin' => 'Set your transaction PIN in Profile before making a purchase.',
            ]);
        }

        if (! $user->verifyTransactionPin($pin)) {
            throw ValidationException::withMessages([
                'transaction_pin' => 'Invalid transaction PIN.',
            ]);
        }
    }

    protected function buildMeterValidationPayloadForVendor(string $vendorType, array $data): array
    {
        $meterNumber = (string) ($data['meter_number'] ?? '');
        $discoName = Str::lower(trim((string) ($data['disco_name'] ?? '')));
        $meterTypeNumeric = (string) ($data['meter_type'] ?? '1') === '2' ? 2 : 1;
        $meterTypeWord = $meterTypeNumeric === 1 ? 'prepaid' : 'postpaid';

        if (in_array($vendorType, ['msorg', 'vtbox'], true)) {
            return [
                'meternumber' => $meterNumber,
                'meter_number' => $meterNumber,
                'disconame' => $discoName,
                'disco_name' => $discoName,
                'mtype' => $meterTypeNumeric,
                'MeterType' => $meterTypeNumeric,
            ];
        }

        if ($vendorType === 'vtpass') {
            $serviceId = $this->vtpassElectricityServiceIdForValidation($discoName);

            return [
                'billersCode' => $meterNumber,
                'meter_number' => $meterNumber,
                'serviceID' => $serviceId,
                'disconame' => $discoName,
                'disco_name' => $discoName,
                'type' => $meterTypeWord,
                'mtype' => $meterTypeWord,
                'meter_type' => $meterTypeWord,
            ];
        }

        return [
            'meter_number' => $meterNumber,
            'disco_name' => $discoName,
            'meter_type' => $meterTypeNumeric,
        ];
    }

    protected function normalizeMeterValidationResult(array $result, array $input, string $vendorType): array
    {
        $body = (array) ($result['body'] ?? []);
        $content = (array) data_get($body, 'content', []);

        $customerName = $this->sanitizeValidationText(
            data_get($content, 'Customer_Name')
            ?: data_get($content, 'customer_name')
            ?: data_get($content, 'name')
            ?: data_get($body, 'name')
            ?: data_get($body, 'customer_name')
        );

        $address = $this->sanitizeValidationText(
            data_get($content, 'Address')
            ?: data_get($content, 'address')
            ?: data_get($body, 'address')
        );

        $meterNumber = $this->sanitizeValidationText(
            data_get($content, 'Meter_Number')
            ?: data_get($content, 'MeterNumber')
            ?: data_get($content, 'meter_number')
            ?: data_get($body, 'meter_number')
        );
        if ($meterNumber === '') {
            $meterNumber = trim((string) ($input['meter_number'] ?? ''));
        }

        $meterType = $this->sanitizeValidationText(
            data_get($content, 'Meter_Type')
            ?: data_get($content, 'meter_type')
        );
        if ($meterType === '') {
            $meterType = (string) ($input['meter_type'] ?? '1') === '2' ? 'POSTPAID' : 'PREPAID';
        }

        $minimumAmount = data_get($content, 'Minimum_Amount');
        if ($minimumAmount === null) {
            $minimumAmount = data_get($content, 'Min_Purchase_Amount');
        }

        $serviceId = (string) (
            data_get($content, 'Service')
            ?: data_get($body, 'serviceID')
            ?: ($vendorType === 'vtpass'
                ? $this->vtpassElectricityServiceIdForValidation((string) ($input['disco_name'] ?? ''))
                : (string) ($input['disco_name'] ?? ''))
        );

        return [
            'customer_name' => $customerName,
            'address' => $address,
            'meter_number' => $meterNumber,
            'meter_type' => Str::upper($meterType),
            'minimum_amount' => is_numeric($minimumAmount) ? (float) $minimumAmount : null,
            'service_id' => $serviceId,
            'provider_slug' => (string) ($input['disco_name'] ?? ''),
            'validated_at' => now()->toDateTimeString(),
            'raw' => $body,
        ];
    }

    /**
     * @param  array<string,mixed>  $result
     * @param  array<string,mixed>  $normalizedValidation
     */
    protected function isMeterValidationAccepted(array $result, array $normalizedValidation, string $vendorType): bool
    {
        $customerName = $this->sanitizeValidationText($normalizedValidation['customer_name'] ?? null);
        if ($customerName === '') {
            return false;
        }

        $body = (array) ($result['body'] ?? []);
        $bodyStatus = Str::lower(trim((string) (
            data_get($body, 'status')
            ?? data_get($body, 'Status')
            ?? data_get($body, 'api_response')
            ?? ''
        )));
        $bodyMessage = Str::lower(trim((string) (
            data_get($body, 'error')
            ?? data_get($body, 'message')
            ?? data_get($body, 'response_description')
            ?? data_get($body, 'detail')
            ?? ''
        )));

        if (in_array($vendorType, ['vtpass'], true)) {
            $code = trim((string) data_get($body, 'code', ''));
            $description = Str::lower((string) data_get($body, 'response_description', ''));

            if (! in_array($code, ['000', '0000'], true) && ! str_contains($description, 'successful')) {
                return false;
            }
        }

        if (in_array($bodyStatus, ['failed', 'error', 'invalid'], true)) {
            return false;
        }

        if (
            $bodyMessage !== ''
            && Str::contains($bodyMessage, ['invalid', 'not found', 'not exist', 'error', 'failed'])
            && ! Str::contains($bodyMessage, ['successful', 'success'])
        ) {
            return false;
        }

        return true;
    }

    protected function sanitizeValidationText(mixed $value): string
    {
        $text = trim((string) $value);
        if ($text === '') {
            return '';
        }

        $normalized = Str::lower($text);
        if (in_array($normalized, ['-', '--', 'n/a', 'na', 'null', 'none', 'unknown'], true)) {
            return '';
        }

        return $text;
    }

    protected function vtpassElectricityServiceIdForValidation(string $providerSlug): string
    {
        $normalized = Str::of($providerSlug)
            ->lower()
            ->replace('&', 'and')
            ->replaceMatches('/[^a-z0-9]+/', '')
            ->trim()
            ->toString();

        $map = [
            'ikedc' => 'ikeja-electric',
            'ikejaelectric' => 'ikeja-electric',
            'ikejaelectricity' => 'ikeja-electric',
            'ekedc' => 'eko-electric',
            'ekoelectric' => 'eko-electric',
            'ekoelectricity' => 'eko-electric',
            'kedco' => 'kano-electric',
            'kanoelectric' => 'kano-electric',
            'phed' => 'portharcourt-electric',
            'portharcourtelectric' => 'portharcourt-electric',
            'jed' => 'jos-electric',
            'joselectric' => 'jos-electric',
            'ibedc' => 'ibadan-electric',
            'ibadanelectric' => 'ibadan-electric',
            'kaedco' => 'kaduna-electric',
            'kadunaelectric' => 'kaduna-electric',
            'aedc' => 'abuja-electric',
            'abujaelectric' => 'abuja-electric',
            'eedc' => 'enugu-electric',
            'enuguelectric' => 'enugu-electric',
            'bedc' => 'benin-electric',
            'beninelectric' => 'benin-electric',
            'abaelectric' => 'aba-electric',
            'aba' => 'aba-electric',
            'yedc' => 'yola-electric',
            'yolaelectric' => 'yola-electric',
        ];

        return $map[$normalized] ?? $providerSlug;
    }

    protected function purchaseSuccessMessage(string $serviceLabel, \App\Models\Transaction $transaction): string
    {
        $message = "{$serviceLabel} purchase submitted ({$transaction->status}).";
        $discountAmount = round((float) ($transaction->discount_amount ?? 0), 2);

        if ($discountAmount > 0) {
            $formatted = number_format($discountAmount, 2);
            $couponCode = (string) ($transaction->coupon_code ?? 'coupon');
            $message .= " {$couponCode} saved you NGN {$formatted}.";
        }

        return $message;
    }

    /**
     * @return array{0:'success'|'error',1:string}
     */
    protected function purchaseFlash(string $serviceLabel, \App\Models\Transaction $transaction): array
    {
        $status = strtolower((string) $transaction->status);

        if (in_array($status, ['failed', 'reversed', 'stuck'], true)) {
            return ['error', "{$serviceLabel} purchase failed."];
        }

        return ['success', $this->purchaseSuccessMessage($serviceLabel, $transaction)];
    }

    /**
     * @return array<string,mixed>
     */
    protected function purchaseResultPayload(string $serviceLabel, \App\Models\Transaction $transaction): array
    {
        $transaction->loadMissing(['category:id,name,slug', 'plan:id,name']);
        $meta = (array) ($transaction->meta_json ?? []);
        $categorySlug = (string) ($transaction->category?->slug ?? '');
        $latestVendorResponse = $this->latestVendorResponsePayload($transaction);
        $receiptExtras = $this->extractReceiptExtras(
            categorySlug: $categorySlug,
            meta: $meta,
            response: $latestVendorResponse
        );
        $networkSlug = (string) data_get($meta, 'network_slug', '');
        $networkName = $networkSlug !== '' ? strtoupper($networkSlug) : null;

        return [
            'transaction_id' => $transaction->id,
            'status' => (string) $transaction->status,
            'service_label' => $serviceLabel,
            'category' => $transaction->category?->name,
            'category_slug' => $transaction->category?->slug,
            'plan_name' => $transaction->plan?->name,
            'network_name' => $networkName,
            'phone' => data_get($meta, 'phone') ?? data_get($meta, 'mobile_number'),
            'amount_charged' => round((float) ($transaction->amount_charged ?? 0), 2),
            'internal_reference' => (string) $transaction->internal_reference,
            'created_at' => optional($transaction->created_at)->toDateTimeString(),
            'meter_token' => $receiptExtras['meter_token'],
            'utility_units' => $receiptExtras['utility_units'],
            'utility_customer_name' => $receiptExtras['utility_customer_name'],
            'epin_pins' => $receiptExtras['epin_pins'],
        ];
    }

    /**
     * @return array<string,mixed>
     */
    protected function latestVendorResponsePayload(\App\Models\Transaction $transaction): array
    {
        $latestLog = $transaction->vendorLogs()
            ->latest('id')
            ->first(['response_json']);

        return is_array($latestLog?->response_json) ? $latestLog->response_json : [];
    }

    /**
     * @return array{
     *     meter_token:?string,
     *     utility_units:?string,
     *     utility_customer_name:?string,
     *     epin_pins:array<int,string>,
     *     receipt_message:?string
     * }
     */
    protected function extractReceiptExtras(string $categorySlug, array $meta, array $response): array
    {
        $receiptMessage = $this->toNonEmptyString(
            data_get($response, 'response_description')
                ?? data_get($response, 'message')
                ?? data_get($response, 'response')
                ?? data_get($response, 'memo')
                ?? null
        );

        $meterToken = null;
        $utilityUnits = null;
        $utilityCustomerName = null;
        $epinPins = [];

        if ($categorySlug === 'electricity') {
            $meterToken = $this->toNonEmptyString(
                data_get($response, 'token')
                    ?? data_get($response, 'Token')
                    ?? data_get($response, 'content.token')
                    ?? data_get($response, 'content.Token')
                    ?? data_get($response, 'content.tokens')
                    ?? data_get($response, 'content.transactions.token')
                    ?? data_get($response, 'content.transactions.Token')
                    ?? data_get($response, 'data.token')
                    ?? data_get($response, 'data.Token')
                    ?? data_get($meta, 'meter_token')
                    ?? null
            );

            $utilityUnits = $this->toNonEmptyString(
                data_get($response, 'units')
                    ?? data_get($response, 'unitsPurchased')
                    ?? data_get($response, 'units_purchased')
                    ?? data_get($response, 'content.units')
                    ?? data_get($response, 'content.unitsPurchased')
                    ?? data_get($response, 'content.transactions.units')
                    ?? data_get($response, 'content.transactions.unitsPurchased')
                    ?? data_get($meta, 'utility_units')
                    ?? null
            );

            $utilityCustomerName = $this->toNonEmptyString(
                data_get($response, 'content.Customer_Name')
                    ?? data_get($response, 'content.customer_name')
                    ?? data_get($response, 'content.transactions.customer_name')
                    ?? data_get($response, 'name')
                    ?? data_get($meta, 'customer_name')
                    ?? null
            );
        }

        if ($categorySlug === 'epin') {
            $epinPins = $this->extractEpinPins($response);
        }

        return [
            'meter_token' => $meterToken,
            'utility_units' => $utilityUnits,
            'utility_customer_name' => $utilityCustomerName,
            'epin_pins' => $epinPins,
            'receipt_message' => $receiptMessage,
        ];
    }

    /**
     * @return array<int,string>
     */
    protected function extractEpinPins(array $response): array
    {
        $candidates = [
            data_get($response, 'pins'),
            data_get($response, 'pin'),
            data_get($response, 'pin_code'),
            data_get($response, 'content.pins'),
            data_get($response, 'content.pin'),
            data_get($response, 'content.pin_code'),
            data_get($response, 'content.transactions.pins'),
            data_get($response, 'content.transactions.pin'),
            data_get($response, 'data.pins'),
            data_get($response, 'data.pin'),
            data_get($response, 'transaction.pins'),
            data_get($response, 'transaction.pin'),
        ];

        $pins = [];

        foreach ($candidates as $candidate) {
            if ($candidate === null || $candidate === '') {
                continue;
            }

            if (is_array($candidate)) {
                foreach ($candidate as $entry) {
                    $formatted = $this->formatPinEntry($entry);
                    if ($formatted !== null) {
                        $pins[] = $formatted;
                    }
                }
                continue;
            }

            $formatted = $this->formatPinEntry($candidate);
            if ($formatted !== null) {
                $pins[] = $formatted;
            }
        }

        return array_values(array_unique($pins));
    }

    protected function formatPinEntry(mixed $entry): ?string
    {
        if (is_array($entry)) {
            $serial = $this->toNonEmptyString(
                data_get($entry, 'serial')
                    ?? data_get($entry, 'serial_no')
                    ?? data_get($entry, 'serialNumber')
                    ?? null
            );
            $pin = $this->toNonEmptyString(
                data_get($entry, 'pin')
                    ?? data_get($entry, 'pin_code')
                    ?? data_get($entry, 'code')
                    ?? data_get($entry, 'value')
                    ?? null
            );

            if ($serial && $pin) {
                return "{$serial}: {$pin}";
            }

            return $pin ?: $serial;
        }

        return $this->toNonEmptyString($entry);
    }

    protected function toNonEmptyString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        if (is_array($value)) {
            return null;
        }

        $string = trim((string) $value);

        return $string !== '' ? $string : null;
    }
}
