<?php

namespace App\Services;

use App\Models\BillPayment;
use App\Models\Customer;
use App\Models\SavingsAccount;
use App\Models\SavingsTransaction;
use App\Models\Tenant;
use App\Models\User;
use App\Models\UserWallet;
use App\Models\UserWalletTransaction;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class BillPaymentService
{
    public function __construct(
        private readonly AccountingService $accounting,
        private readonly VtpassBillPaymentService $vtpass,
        private readonly FlutterwaveBillPaymentService $flutterwave,
        private readonly VtuWalletService $vtuWallets,
        private readonly PlatformSettings $platformSettings,
    ) {
    }

    public static function serviceTypes(): array
    {
        return [
            'airtime' => ['label' => 'Airtime', 'icon' => 'bi-phone-vibrate'],
            'data' => ['label' => 'Data bundle', 'icon' => 'bi-wifi'],
            'electricity' => ['label' => 'Electricity', 'icon' => 'bi-lightning-charge'],
            'cable_tv' => ['label' => 'Cable TV', 'icon' => 'bi-tv'],
            'internet' => ['label' => 'Internet', 'icon' => 'bi-router'],
        ];
    }

    public static function providers(): array
    {
        return [
            'airtime' => ['MTN', 'Airtel', 'Glo', '9mobile'],
            'data' => ['MTN', 'Airtel', 'Glo', '9mobile', 'Spectranet', 'Smile'],
            'electricity' => ['EKEDC', 'IKEDC', 'AEDC', 'PHED', 'KEDCO', 'IBEDC', 'EEDC', 'BEDC'],
            'cable_tv' => ['DSTV', 'GOtv', 'Startimes'],
            'internet' => ['Spectranet', 'Smile', 'Swift', 'FiberOne'],
        ];
    }

    public static function packages(): array
    {
        return [
            'airtime' => [],
            'data' => [],
            'electricity' => ['Prepaid token', 'Postpaid bill'],
            'cable_tv' => [],
            'internet' => [],
        ];
    }

    public function memberPurchase(Customer $customer, SavingsAccount $sourceAccount, array $payload, ?int $userId = null): BillPayment
    {
        $amount = round((float) $payload['amount'], 2);
        $fee = $this->feeForTenant($customer->tenant, (string) $payload['service_type'], $amount);
        $commission = $this->commissionForTenant($customer->tenant, (string) $payload['service_type'], $amount);
        $total = round($amount + $fee + $commission, 2);

        if ($amount <= 0) {
            throw new InvalidArgumentException('Bill amount must be greater than zero.');
        }

        $payment = DB::transaction(function () use ($customer, $sourceAccount, $payload, $amount, $fee, $commission, $total, $userId): BillPayment {
            $account = SavingsAccount::whereKey($sourceAccount->id)->lockForUpdate()->firstOrFail();
            $driver = $this->driverForTenant($customer->tenant);
            $providerReference = $this->providerReferenceFor($driver);

            if ((int) $account->customer_id !== (int) $customer->id || (int) $account->tenant_id !== (int) $customer->tenant_id) {
                throw new InvalidArgumentException('Selected savings account does not belong to this member.');
            }

            if ($account->status !== 'active') {
                throw new InvalidArgumentException('Only active savings accounts can be used for bill payment.');
            }

            $before = (float) $account->balance;

            if ($total > $before) {
                throw new InvalidArgumentException('Insufficient savings balance for this bill payment.');
            }

            $after = round($before - $total, 2);
            $transaction = SavingsTransaction::create([
                'tenant_id' => $customer->tenant_id,
                'savings_account_id' => $account->id,
                'customer_id' => $customer->id,
                'created_by' => $userId,
                'approved_by' => null,
                'type' => 'withdrawal',
                'amount' => $total,
                'processing_fee_amount' => 0,
                'net_amount' => $total,
                'balance_before' => $before,
                'balance_after' => $after,
                'reference' => NumberGenerator::reference('BILLDR'),
                'method' => 'member_balance',
                'status' => 'posted',
                'description' => 'Bill payment debit from member balance',
                'transacted_at' => now(),
                'approved_at' => now(),
            ]);

            $payment = BillPayment::create([
                'tenant_id' => $customer->tenant_id,
                'customer_id' => $customer->id,
                'recorded_by' => $userId,
                'savings_transaction_id' => $transaction->id,
                'initiated_by_member' => true,
                'reference' => NumberGenerator::reference('BILL'),
                'service_type' => $payload['service_type'],
                'provider' => $payload['provider'],
                'customer_identifier' => $payload['customer_identifier'],
                'customer_name' => $customer->fullName(),
                'package_name' => $payload['package_name'] ?? null,
                'amount' => $amount,
                'fee' => $fee,
                'commission' => $commission,
                'total_amount' => $total,
                'payment_method' => 'member_balance',
                'status' => $this->isLiveProvider($driver) ? BillPayment::STATUS_PENDING : BillPayment::STATUS_SUCCESSFUL,
                'provider_reference' => $providerReference,
                'token' => $this->isLiveProvider($driver) ? null : $this->tokenFor((string) $payload['service_type']),
                'notes' => $payload['notes'] ?? null,
                'provider_response' => [
                    'driver' => $driver,
                    'message' => $this->isLiveProvider($driver)
                        ? 'Debited from member savings balance and sent to '.$this->providerLabel($driver).' for fulfillment.'
                        : 'Processed from member savings balance. Connect a live VTU provider in settings for real-time provider fulfillment.',
                ],
                'processed_at' => $this->isLiveProvider($driver) ? null : now(),
            ]);

            if ($this->isLiveProvider($driver)) {
                $vtuDebit = $this->vtuWallets->debitForBillPayment($customer->tenant, $payment, $amount, $userId);
                $payment->update([
                    'provider_response' => [
                        ...($payment->provider_response ?? []),
                        'vtu_wallet_transaction_id' => $vtuDebit->id,
                        'vtu_wallet_debited' => (float) $vtuDebit->amount,
                    ],
                ]);
            }

            $account->update(['balance' => $after]);
            $this->accounting->recordSavings($transaction->fresh());

            return $payment->fresh(['customer', 'savingsTransaction']);
        });

        return $this->fulfillBillPayment($payment);
    }

    public function userWalletPurchase(User $user, UserWallet $wallet, array $payload): BillPayment
    {
        $amount = round((float) $payload['amount'], 2);
        $fee = $this->feeForTenant($wallet->tenant, (string) $payload['service_type'], $amount);
        $commission = $this->commissionForTenant($wallet->tenant, (string) $payload['service_type'], $amount);
        $total = round($amount + $fee + $commission, 2);

        if ($amount <= 0) {
            throw new InvalidArgumentException('Bill amount must be greater than zero.');
        }

        $payment = DB::transaction(function () use ($user, $wallet, $payload, $amount, $fee, $commission, $total): BillPayment {
            $lockedWallet = UserWallet::whereKey($wallet->id)->lockForUpdate()->firstOrFail();
            $driver = $this->driverForTenant($lockedWallet->tenant);
            $providerReference = $this->providerReferenceFor($driver);

            if ((int) $lockedWallet->user_id !== (int) $user->id || (int) $lockedWallet->tenant_id !== (int) $user->tenant_id) {
                throw new InvalidArgumentException('Selected wallet does not belong to this staff account.');
            }

            if ($lockedWallet->status !== 'active') {
                throw new InvalidArgumentException('Your wallet is not active.');
            }

            $before = (float) $lockedWallet->balance;

            if ($total > $before) {
                throw new InvalidArgumentException('Insufficient wallet balance for this bill payment.');
            }

            $after = round($before - $total, 2);
            $transaction = UserWalletTransaction::create([
                'tenant_id' => $lockedWallet->tenant_id,
                'user_wallet_id' => $lockedWallet->id,
                'user_id' => $user->id,
                'initiated_by' => $user->id,
                'type' => UserWalletTransaction::TYPE_DEBIT,
                'amount' => $total,
                'fee' => round($fee + $commission, 2),
                'balance_before' => $before,
                'balance_after' => $after,
                'reference' => NumberGenerator::reference('WDR'),
                'method' => 'bill_payment',
                'status' => UserWalletTransaction::STATUS_POSTED,
                'description' => 'Bill payment from staff wallet',
                'posted_at' => now(),
            ]);

            $payment = BillPayment::create([
                'tenant_id' => $lockedWallet->tenant_id,
                'customer_id' => null,
                'recorded_by' => $user->id,
                'user_wallet_id' => $lockedWallet->id,
                'wallet_transaction_id' => $transaction->id,
                'initiated_by_member' => false,
                'reference' => NumberGenerator::reference('BILL'),
                'service_type' => $payload['service_type'],
                'provider' => $payload['provider'],
                'customer_identifier' => $payload['customer_identifier'],
                'customer_name' => $user->name,
                'package_name' => $payload['package_name'] ?? null,
                'amount' => $amount,
                'fee' => $fee,
                'commission' => $commission,
                'total_amount' => $total,
                'payment_method' => 'staff_wallet',
                'status' => $this->isLiveProvider($driver) ? BillPayment::STATUS_PENDING : BillPayment::STATUS_SUCCESSFUL,
                'provider_reference' => $providerReference,
                'token' => $this->isLiveProvider($driver) ? null : $this->tokenFor((string) $payload['service_type']),
                'notes' => $payload['notes'] ?? null,
                'provider_response' => [
                    'driver' => $driver,
                    'message' => $this->isLiveProvider($driver)
                        ? 'Debited from staff wallet and sent to '.$this->providerLabel($driver).' for fulfillment.'
                        : 'Processed from staff wallet balance. Connect a live VTU provider in settings for real-time provider fulfillment.',
                ],
                'processed_at' => $this->isLiveProvider($driver) ? null : now(),
            ]);

            if ($this->isLiveProvider($driver)) {
                $vtuDebit = $this->vtuWallets->debitForBillPayment($lockedWallet->tenant, $payment, $amount, $user->id);
                $payment->update([
                    'provider_response' => [
                        ...($payment->provider_response ?? []),
                        'vtu_wallet_transaction_id' => $vtuDebit->id,
                        'vtu_wallet_debited' => (float) $vtuDebit->amount,
                    ],
                ]);
            }

            $lockedWallet->update(['balance' => $after]);
            $this->accounting->ensureChartOfAccounts($lockedWallet->tenant);
            $this->accounting->post($lockedWallet->tenant, '2100', 'debit', $total, 'Staff wallet bill payment '.$payment->reference, $transaction);
            $this->accounting->post($lockedWallet->tenant, '1000', 'credit', $total, 'Cash paid for staff bill '.$payment->reference, $transaction);

            return $payment->fresh(['recordedBy', 'userWallet', 'walletTransaction']);
        });

        return $this->fulfillBillPayment($payment);
    }

    public function counterPurchase(User $user, array $payload): BillPayment
    {
        $tenant = $user->tenant;

        if (! $tenant) {
            throw new InvalidArgumentException('A tenant account is required to sell bills.');
        }

        $amount = round((float) $payload['amount'], 2);
        $fee = round((float) ($payload['fee'] ?? 0), 2);
        $commission = round((float) ($payload['commission'] ?? 0), 2);
        $total = round($amount + $fee + $commission, 2);

        if ($amount <= 0) {
            throw new InvalidArgumentException('Bill amount must be greater than zero.');
        }

        $payment = DB::transaction(function () use ($user, $tenant, $payload, $amount, $fee, $commission, $total): BillPayment {
            $driver = $this->driverForTenant($tenant);
            $providerReference = $this->providerReferenceFor($driver);
            $customer = null;

            if (! empty($payload['customer_id'])) {
                $customer = Customer::where('tenant_id', $tenant->id)->findOrFail($payload['customer_id']);
            }

            $manualStatus = $payload['status'] ?? BillPayment::STATUS_SUCCESSFUL;

            $payment = BillPayment::create([
                'tenant_id' => $tenant->id,
                'customer_id' => $customer?->id,
                'recorded_by' => $user->id,
                'initiated_by_member' => false,
                'reference' => NumberGenerator::reference('BILL'),
                'service_type' => $payload['service_type'],
                'provider' => $payload['provider'],
                'customer_identifier' => $payload['customer_identifier'],
                'customer_name' => ($payload['customer_name'] ?? null) ?: $customer?->fullName(),
                'package_name' => $payload['package_name'] ?? null,
                'amount' => $amount,
                'fee' => $fee,
                'commission' => $commission,
                'total_amount' => $total,
                'payment_method' => $payload['payment_method'],
                'status' => $this->isLiveProvider($driver) ? BillPayment::STATUS_PENDING : $manualStatus,
                'provider_reference' => $this->isLiveProvider($driver) ? $providerReference : ($payload['provider_reference'] ?? $providerReference),
                'token' => $this->isLiveProvider($driver) ? null : ($payload['token'] ?? null),
                'notes' => $payload['notes'] ?? null,
                'provider_response' => [
                    'driver' => $driver,
                    'message' => $this->isLiveProvider($driver)
                        ? 'Counter bill sale received and sent to '.$this->providerLabel($driver).' for fulfillment.'
                        : 'Bill payment recorded locally.',
                ],
                'processed_at' => (! $this->isLiveProvider($driver) && in_array($manualStatus, [BillPayment::STATUS_SUCCESSFUL, BillPayment::STATUS_FAILED], true))
                    ? now()
                    : null,
            ]);

            if ($this->isLiveProvider($driver)) {
                $vtuDebit = $this->vtuWallets->debitForBillPayment($tenant, $payment, $amount, $user->id);
                $payment->update([
                    'provider_response' => [
                        ...($payment->provider_response ?? []),
                        'vtu_wallet_transaction_id' => $vtuDebit->id,
                        'vtu_wallet_debited' => (float) $vtuDebit->amount,
                    ],
                ]);
            }

            return $payment->fresh(['customer', 'recordedBy']);
        });

        return $this->fulfillBillPayment($payment);
    }

    public function requeryProvider(BillPayment $payment): BillPayment
    {
        $response = $payment->provider_response ?? [];
        $driver = $response['driver'] ?? $this->driverForTenant($payment->tenant);

        if (! $this->isLiveProvider($driver)) {
            throw new InvalidArgumentException('This bill payment was not sent through a live VTU provider.');
        }

        return $this->applyProviderResult($payment, $this->providerService($driver)->requery($payment), $driver);
    }

    public function feeFor(Customer $customer, string $serviceType, float $amount): float
    {
        return $this->feeForTenant($customer->tenant, $serviceType, $amount);
    }

    public function feeForTenant(?Tenant $tenant, string $serviceType, float $amount): float
    {
        $settings = $tenant?->settings ?? [];
        $fee = (float) ($settings['bill_payment_fee_'.$serviceType] ?? $settings['bill_payment_fee'] ?? 0);

        return round(max(0, $fee), 2);
    }

    public function commissionFor(Customer $customer, string $serviceType, float $amount): float
    {
        return $this->commissionForTenant($customer->tenant, $serviceType, $amount);
    }

    public function commissionForTenant(?Tenant $tenant, string $serviceType, float $amount): float
    {
        $settings = $tenant?->settings ?? [];
        $rate = (float) ($settings['bill_payment_commission_'.$serviceType] ?? $settings['bill_payment_commission_rate'] ?? 0);

        return round(max(0, $amount * ($rate / 100)), 2);
    }

    private function fulfillBillPayment(BillPayment $payment): BillPayment
    {
        $payment->loadMissing('tenant');
        $driver = $payment->provider_response['driver'] ?? $this->driverForTenant($payment->tenant);

        if (! $this->isLiveProvider($driver)) {
            $this->postBillPaymentMargin($payment);
            ActivityLogger::log('bill_payment_successful', 'Bill payment '.$payment->reference.' posted from local balance.', $payment);

            return $this->freshPayment($payment);
        }

        return $this->applyProviderResult($payment, $this->providerService($driver)->purchase($payment), $driver);
    }

    private function applyProviderResult(BillPayment $payment, array $result, string $driver): BillPayment
    {
        $payment = BillPayment::whereKey($payment->id)->firstOrFail();
        $existingResponse = $payment->provider_response ?? [];
        $status = $result['status'] ?? BillPayment::STATUS_PENDING;
        $providerReference = $result['provider_reference'] ?: $payment->provider_reference;
        $providerLabel = $this->providerLabel($driver);
        $activityPrefix = str_replace(' ', '_', strtolower($providerLabel));

        $payment->update([
            'status' => $status,
            'provider_reference' => $providerReference,
            'token' => $result['token'] ?: $payment->token,
            'provider_response' => [
                ...$existingResponse,
                'driver' => $driver,
                'message' => $result['message'] ?? $providerLabel.' response received.',
                'last_response' => $result['raw'] ?? [],
                'last_request' => $result['request'] ?? [],
                'last_checked_at' => now()->toDateTimeString(),
            ],
            'processed_at' => in_array($status, [BillPayment::STATUS_SUCCESSFUL, BillPayment::STATUS_FAILED], true)
                ? now()
                : $payment->processed_at,
        ]);

        $payment = $payment->fresh();

        if ($status === BillPayment::STATUS_SUCCESSFUL) {
            $this->postBillPaymentMargin($payment);
            ActivityLogger::log($activityPrefix.'_bill_payment_successful', $providerLabel.' bill payment '.$payment->reference.' delivered.', $payment);

            return $this->freshPayment($payment);
        }

        if ($status === BillPayment::STATUS_PENDING) {
            ActivityLogger::log($activityPrefix.'_bill_payment_pending', $providerLabel.' bill payment '.$payment->reference.' is pending provider confirmation.', $payment);

            return $this->freshPayment($payment);
        }

        $this->refundBillPaymentDebit($payment, (string) ($result['message'] ?? $providerLabel.' failed the transaction.'));
        ActivityLogger::log($activityPrefix.'_bill_payment_failed', $providerLabel.' bill payment '.$payment->reference.' failed and was refunded.', $payment);

        throw new InvalidArgumentException(($result['message'] ?? $providerLabel.' failed this bill payment.').' Local balance has been refunded. Reference: '.$payment->reference);
    }

    private function refundBillPaymentDebit(BillPayment $payment, string $reason): void
    {
        DB::transaction(function () use ($payment, $reason): void {
            $lockedPayment = BillPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            $response = $lockedPayment->provider_response ?? [];

            if (filter_var($response['refunded'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
                return;
            }

            $amount = round((float) $lockedPayment->total_amount, 2);

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

            if ($lockedPayment->savings_transaction_id) {
                $sourceTransaction = SavingsTransaction::whereKey($lockedPayment->savings_transaction_id)->lockForUpdate()->first();

                if ($sourceTransaction) {
                    $account = SavingsAccount::whereKey($sourceTransaction->savings_account_id)->lockForUpdate()->first();

                    if ($account) {
                        $before = (float) $account->balance;
                        $after = round($before + $amount, 2);

                        $refund = SavingsTransaction::create([
                            'tenant_id' => $lockedPayment->tenant_id,
                            'savings_account_id' => $account->id,
                            'customer_id' => $lockedPayment->customer_id,
                            'created_by' => $lockedPayment->recorded_by,
                            'approved_by' => $lockedPayment->recorded_by,
                            'type' => 'deposit',
                            'amount' => $amount,
                            'processing_fee_amount' => 0,
                            'net_amount' => $amount,
                            'balance_before' => $before,
                            'balance_after' => $after,
                            'reference' => NumberGenerator::reference('BILLRF'),
                            'method' => 'bill_payment_refund',
                            'status' => 'posted',
                            'description' => 'Refund for failed bill payment '.$lockedPayment->reference,
                            'transacted_at' => now(),
                            'approved_at' => now(),
                        ]);

                        $account->update(['balance' => $after]);
                        $this->accounting->recordSavings($refund->fresh());
                    }
                }
            }

            if ($lockedPayment->wallet_transaction_id && $lockedPayment->user_wallet_id) {
                $wallet = UserWallet::whereKey($lockedPayment->user_wallet_id)->lockForUpdate()->first();

                if ($wallet) {
                    $before = (float) $wallet->balance;
                    $after = round($before + $amount, 2);

                    $refund = UserWalletTransaction::create([
                        'tenant_id' => $lockedPayment->tenant_id,
                        'user_wallet_id' => $wallet->id,
                        'user_id' => $wallet->user_id,
                        'initiated_by' => $lockedPayment->recorded_by,
                        'type' => UserWalletTransaction::TYPE_CREDIT,
                        'amount' => $amount,
                        'fee' => 0,
                        'balance_before' => $before,
                        'balance_after' => $after,
                        'reference' => NumberGenerator::reference('WCR'),
                        'method' => 'bill_payment_refund',
                        'status' => UserWalletTransaction::STATUS_POSTED,
                        'provider_reference' => $lockedPayment->provider_reference,
                        'description' => 'Refund for failed bill payment '.$lockedPayment->reference,
                        'posted_at' => now(),
                    ]);

                    $wallet->update(['balance' => $after]);
                    $this->accounting->ensureChartOfAccounts($lockedPayment->tenant);
                    $this->accounting->post($lockedPayment->tenant, '1000', 'debit', $amount, 'Staff bill payment refund '.$lockedPayment->reference, $refund);
                    $this->accounting->post($lockedPayment->tenant, '2100', 'credit', $amount, 'Staff wallet refund '.$lockedPayment->reference, $refund);
                }
            }

            $this->vtuWallets->refundBillPaymentDebit($lockedPayment, $reason);

            $lockedPayment->update([
                'provider_response' => [
                    ...$response,
                    'refunded' => true,
                    'refund_reason' => $reason,
                    'refunded_at' => now()->toDateTimeString(),
                ],
            ]);
        });
    }

    private function postBillPaymentMargin(BillPayment $payment): void
    {
        $payment->loadMissing('tenant');
        $response = $payment->provider_response ?? [];

        if (filter_var($response['margin_posted'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
            return;
        }

        $margin = round((float) $payment->fee + (float) $payment->commission, 2);

        if ($margin > 0 && $payment->tenant) {
            $this->accounting->post($payment->tenant, '1000', 'debit', $margin, 'Bill payment margin retained '.$payment->reference, $payment);
            $this->accounting->post($payment->tenant, '4100', 'credit', $margin, 'Bill payment income '.$payment->reference, $payment);
        }

        $payment->update([
            'provider_response' => [
                ...$response,
                'margin_posted' => true,
                'margin_posted_at' => now()->toDateTimeString(),
            ],
        ]);
    }

    private function freshPayment(BillPayment $payment): BillPayment
    {
        return $payment->fresh(['customer', 'savingsTransaction', 'recordedBy', 'userWallet', 'walletTransaction']);
    }

    private function driverForTenant(?Tenant $tenant): string
    {
        return $this->platformSettings->billPaymentDriver();
    }

    private function isLiveProvider(string $driver): bool
    {
        return in_array($driver, ['vtpass', 'flutterwave'], true);
    }

    private function providerReferenceFor(string $driver): string
    {
        return match ($driver) {
            'vtpass' => $this->vtpass->requestId(),
            'flutterwave' => $this->flutterwave->requestId(),
            default => NumberGenerator::reference('VTU'),
        };
    }

    private function providerService(string $driver): VtpassBillPaymentService|FlutterwaveBillPaymentService
    {
        return match ($driver) {
            'flutterwave' => $this->flutterwave,
            default => $this->vtpass,
        };
    }

    private function providerLabel(string $driver): string
    {
        return match ($driver) {
            'flutterwave' => 'Flutterwave',
            'vtpass' => 'VTpass',
            default => 'Bill provider',
        };
    }

    private function tokenFor(string $serviceType): ?string
    {
        if ($serviceType !== 'electricity') {
            return null;
        }

        return implode('-', str_split((string) random_int(100000000000, 999999999999), 4));
    }
}
