<?php

namespace App\Services\Showroom;

use App\Models\Admin;
use App\Models\Item;
use App\Models\ManualPaymentVerification;
use App\Models\Order;
use App\Models\PaymentTransaction;
use App\Models\PurchaseItem;
use App\Models\User;
use App\Scopes\StoreScope;
use App\Services\Accounting\AccountingVoucherService;
use App\Services\Customer\PointLedgerService;
use App\Services\Inventory\StockMovementService;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;

class ShowroomPosService
{
    private array $columnCache = [];

    public function __construct(
        private readonly ShowroomCustomerService $customerService,
        private readonly StockMovementService $stockMovementService,
        private readonly PointLedgerService $pointLedgerService,
        private readonly AccountingVoucherService $accountingVoucherService,
    ) {
    }

    public function quote(array $data): array
    {
        $lines = $this->resolveLines($data['items'] ?? []);
        $gross = round((float) collect($lines)->sum('line_total'), 8);
        $pointAmount = 0;

        if (! empty($data['customer_id']) && ! empty($data['point_redeem_points'])) {
            $pointAmount = min($this->pointLedgerService->amountForPoints((int) $data['point_redeem_points']), $gross);
        }

        return [
            'items' => $lines,
            'gross_amount' => $gross,
            'point_redeem_points' => (int) ($data['point_redeem_points'] ?? 0),
            'point_redeem_amount' => $pointAmount,
            'net_payable' => max(round($gross - $pointAmount, 8), 0),
        ];
    }

    public function createSale(array $data, Admin $admin, mixed $device = null): Order
    {
        $idempotencyKey = $this->normalizeIdempotencyKey($data['idempotency_key'] ?? null);
        if ($existingOrder = $this->findExistingSaleByIdempotencyKey($idempotencyKey)) {
            return $existingOrder;
        }

        try {
            return DB::transaction(function () use ($data, $admin, $device, $idempotencyKey) {
            if ($existingOrder = $this->findExistingSaleByIdempotencyKey($idempotencyKey, true)) {
                return $existingOrder;
            }

            $customer = $this->resolveCustomer($data);
            $quote = $this->quote(array_merge($data, ['customer_id' => $customer->id]));
            $payments = $this->normalizePayments($data['payments'] ?? []);
            $paidAmount = round((float) collect($payments)->sum('amount'), 8);
            $netPayable = round((float) $quote['net_payable'], 8);

            if (abs($paidAmount - $netPayable) > 0.00001) {
                throw ValidationException::withMessages([
                    'payments' => translate('Showroom sale requires full payment. Paid amount must equal net payable.'),
                ]);
            }

            foreach ($payments as $index => $payment) {
                $payments[$index] = $this->assertPaymentIsAllowed($payment, $netPayable);
            }

            $redeemedAmount = (int) $quote['point_redeem_points'] > 0
                ? min($this->pointLedgerService->amountForPoints((int) $quote['point_redeem_points']), (float) $quote['gross_amount'])
                : 0;

            $order = $this->createOrder($customer, $quote, $payments, $data, $admin, $device, $redeemedAmount, $idempotencyKey);
            $this->linkBanglaQrPayments($order, $payments);

            if ((int) $quote['point_redeem_points'] > 0) {
                $redeemedAmount = $this->pointLedgerService->redeem($customer->id, (int) $quote['point_redeem_points'], [
                    'source_type' => 'showroom_sale',
                    'source_id' => $order->id,
                    'source_reference' => 'POS-' . $order->id,
                    'created_by_admin_id' => $admin->id,
                    'note' => 'Showroom POS point redeem',
                ]);
            }
            $orderDetails = [];
            $cogs = 0;

            foreach ($quote['items'] as $line) {
                $orderDetails[] = $this->orderDetailPayload($order, $line);
                $movement = $this->stockMovementService->decrease((int) $line['item_id'], (int) $line['quantity'], [
                    'variation_type' => $line['variation_type'],
                    'source_type' => 'showroom_sale',
                    'source_id' => $order->id,
                    'source_reference' => 'POS-' . $order->id,
                    'sale_channel' => 'showroom',
                    'created_by_admin_id' => $admin->id,
                ]);
                $cogs += $this->averagePurchaseCost((int) $line['item_id'], $line['variation_type']) * (int) $line['quantity'];
            }

            DB::table('order_details')->insert($this->filterRows('order_details', $orderDetails));
            foreach ($payments as $payment) {
                $this->insertOrderPayment($order, $payment);
            }

            $voucher = $this->accountingVoucherService->postShowroomSale($order, $payments, round($cogs, 8), $redeemedAmount, $admin->id);
            $this->updateOrderColumns($order, [
                'accounting_voucher_id' => $voucher->id,
            ]);

            $earned = $this->pointLedgerService->earn($customer->id, $netPayable, [
                'source_type' => 'showroom_sale',
                'source_id' => $order->id,
                'source_reference' => 'POS-' . $order->id,
                'created_by_admin_id' => $admin->id,
                'note' => 'Showroom POS point earn',
            ]);
            $this->updateOrderColumns($order, ['point_earned' => $earned]);

            return $order->fresh(['details', 'payments', 'customer']);
        });
        } catch (QueryException $exception) {
            if ($existingOrder = $this->findExistingSaleByIdempotencyKey($idempotencyKey)) {
                return $existingOrder;
            }

            throw $exception;
        }
    }

    private function resolveCustomer(array $data): User
    {
        if (! empty($data['customer_id'])) {
            return User::findOrFail($data['customer_id']);
        }

        return $this->customerService->findOrCreateByMobile(
            (string) ($data['customer_mobile'] ?? data_get($data, 'customer.mobile')),
            (string) ($data['customer_name'] ?? data_get($data, 'customer.name'))
        );
    }

    private function resolveLines(array $items): array
    {
        if ($items === []) {
            throw ValidationException::withMessages([
                'items' => translate('Please add at least one product to continue.'),
            ]);
        }

        return collect($items)->map(function ($line, $index) {
            $item = Item::withoutGlobalScopes()->withoutGlobalScope(StoreScope::class)->findOrFail(data_get($line, 'item_id'));
            $quantity = max((int) data_get($line, 'quantity', 1), 1);
            $variationType = blank(data_get($line, 'variation_type')) ? null : (string) data_get($line, 'variation_type');
            $this->assertVariationSelection($item, $variationType, "items.$index.variation_type");
            $unitPrice = data_get($line, 'unit_price');
            $unitPrice = $unitPrice === null ? $this->priceFor($item, $variationType) : (float) $unitPrice;
            $discount = max((float) data_get($line, 'discount_amount', 0), 0);
            $taxAmount = max((float) data_get($line, 'tax_amount', 0), 0);
            $lineTotal = max(round(($unitPrice * $quantity) - $discount + $taxAmount, 8), 0);

            return [
                'item_id' => $item->id,
                'name' => $item->name,
                'image_full_url' => $item->image_full_url,
                'variation_type' => $variationType,
                'quantity' => $quantity,
                'unit_price' => $unitPrice,
                'discount_amount' => $discount,
                'tax_amount' => $taxAmount,
                'line_total' => $lineTotal,
                'available_stock' => $this->stockFor($item, $variationType),
                'item_snapshot' => $this->itemSnapshot($item),
            ];
        })->values()->all();
    }

    private function assertVariationSelection(Item $item, ?string $variationType, string $errorKey): void
    {
        $variations = is_array($item->variations) ? $item->variations : json_decode($item->variations ?: '[]', true);
        if (! is_array($variations) || count($variations) === 0) {
            return;
        }

        if (blank($variationType)) {
            throw ValidationException::withMessages([
                $errorKey => translate('Please select a product variation for this item.'),
            ]);
        }

        $matched = collect($variations)->contains(fn ($variation) => ($variation['type'] ?? null) === $variationType);
        if (! $matched) {
            throw ValidationException::withMessages([
                $errorKey => translate('Selected variation was not found for the chosen product.'),
            ]);
        }
    }

    private function stockFor(Item $item, ?string $variationType): int
    {
        if ($variationType) {
            $variations = is_array($item->variations) ? $item->variations : json_decode($item->variations ?: '[]', true);
            foreach ($variations ?: [] as $variation) {
                if (($variation['type'] ?? null) === $variationType) {
                    return (int) ($variation['stock'] ?? 0);
                }
            }
        }

        return (int) ($item->stock ?? 0);
    }

    private function priceFor(Item $item, ?string $variationType): float
    {
        if ($variationType) {
            $variations = is_array($item->variations) ? $item->variations : json_decode($item->variations ?: '[]', true);
            foreach ($variations ?: [] as $variation) {
                if (($variation['type'] ?? null) === $variationType) {
                    return (float) ($variation['price'] ?? $item->price);
                }
            }
        }

        return (float) $item->price;
    }

    private function normalizePayments(array $payments): array
    {
        return collect($payments)->map(fn ($payment) => [
            'payment_method' => data_get($payment, 'payment_method', data_get($payment, 'method')),
            'amount' => round((float) data_get($payment, 'amount', 0), 8),
            'transaction_ref' => data_get($payment, 'transaction_ref', data_get($payment, 'transaction_id')),
            'payment_transaction_id' => data_get($payment, 'payment_transaction_id'),
            'provider_name' => data_get($payment, 'provider_name'),
            'provider_transaction_id' => data_get($payment, 'provider_transaction_id'),
            'qr_reference' => data_get($payment, 'qr_reference'),
            'payment_payload' => data_get($payment, 'payload', []),
        ])->filter(fn ($payment) => $payment['payment_method'] && $payment['amount'] > 0)->values()->all();
    }

    private function assertPaymentIsAllowed(array $payment, float $netPayable): array
    {
        if ($payment['payment_method'] !== 'bangla_qr') {
            return $payment;
        }

        if (empty($payment['payment_transaction_id']) && empty($payment['transaction_ref']) && empty($payment['qr_reference'])) {
            throw ValidationException::withMessages([
                'bangla_qr' => translate('Bangla QR payment reference is required.'),
            ]);
        }

        $transaction = PaymentTransaction::query()
            ->where(function ($query) use ($payment) {
                if (! empty($payment['payment_transaction_id'])) {
                    $query->orWhere('id', $payment['payment_transaction_id']);
                }
                if (! empty($payment['transaction_ref'])) {
                    $query->orWhere('transaction_id', $payment['transaction_ref']);
                }
                if (! empty($payment['qr_reference'])) {
                    $query->orWhere('qr_reference', $payment['qr_reference']);
                }
            })
            ->lockForUpdate()
            ->first();

        $manuallyApproved = $transaction
            ? ManualPaymentVerification::where('payment_transaction_id', $transaction->id)->where('status', 'approved')->exists()
            : false;

        if (! $transaction || $transaction->status !== 'paid' || (! $transaction->verified_by_system && ! $manuallyApproved)) {
            throw ValidationException::withMessages([
                'bangla_qr' => translate('Bangla QR payment must be verified by provider or approved from manual verification queue before completing POS sale.'),
            ]);
        }

        if (abs((float) $transaction->amount - (float) $payment['amount']) > 0.00001) {
            throw ValidationException::withMessages([
                'bangla_qr' => translate('Bangla QR amount does not match the POS payment amount.'),
            ]);
        }

        if ($transaction->order_id || ($transaction->source_type === 'showroom_sale' && $transaction->source_id)) {
            throw ValidationException::withMessages([
                'bangla_qr' => translate('This Bangla QR payment has already been used for another invoice.'),
            ]);
        }

        $payment['payment_transaction_id'] = $transaction->id;
        $payment['transaction_ref'] = $payment['transaction_ref'] ?: $transaction->transaction_id;
        $payment['provider_name'] = $payment['provider_name'] ?: $transaction->provider_name;
        $payment['provider_transaction_id'] = $payment['provider_transaction_id'] ?: $transaction->provider_transaction_id;
        $payment['qr_reference'] = $payment['qr_reference'] ?: $transaction->qr_reference;
        $payment['payment_payload'] = array_merge($payment['payment_payload'] ?? [], [
            'transaction_id' => $transaction->transaction_id,
            'provider_transaction_id' => $transaction->provider_transaction_id,
            'verified_by_system' => (bool) $transaction->verified_by_system,
            'verified_at' => optional($transaction->verified_at)->toIso8601String(),
            'paid_at' => optional($transaction->paid_at)->toIso8601String(),
        ]);

        return $payment;
    }

    private function linkBanglaQrPayments(Order $order, array $payments): void
    {
        foreach ($payments as $payment) {
            if ($payment['payment_method'] !== 'bangla_qr') {
                continue;
            }

            $transaction = PaymentTransaction::query()
                ->where(function ($query) use ($payment) {
                    if (! empty($payment['payment_transaction_id'])) {
                        $query->orWhere('id', $payment['payment_transaction_id']);
                    }
                    if (! empty($payment['transaction_ref'])) {
                        $query->orWhere('transaction_id', $payment['transaction_ref']);
                    }
                    if (! empty($payment['qr_reference'])) {
                        $query->orWhere('qr_reference', $payment['qr_reference']);
                    }
                })
                ->lockForUpdate()
                ->first();

            if (! $transaction || $transaction->order_id || ($transaction->source_type === 'showroom_sale' && $transaction->source_id)) {
                throw ValidationException::withMessages([
                    'bangla_qr' => translate('This Bangla QR payment can not be linked to the POS invoice.'),
                ]);
            }

            $transaction->forceFill([
                'source_type' => 'showroom_sale',
                'source_id' => $order->id,
                'source_reference' => 'POS-' . $order->id,
                'order_id' => $order->id,
            ])->save();
        }
    }

    private function createOrder(User $customer, array $quote, array $payments, array $data, Admin $admin, mixed $device, float $redeemedAmount, ?string $idempotencyKey = null): Order
    {
        $method = count($payments) > 1 ? 'multiple_payment' : ($payments[0]['payment_method'] ?? 'cash');
        $payload = [
            'user_id' => $customer->id,
            'customer_id' => (string) $customer->id,
            'is_guest' => 0,
            'customer_type' => 'customer',
            'payment_status' => 'paid',
            'order_status' => 'delivered',
            'payment_method' => $method,
            'transaction_reference' => data_get($payments, '0.transaction_ref'),
            'transaction_ref' => data_get($payments, '0.transaction_ref'),
            'order_amount' => $quote['net_payable'],
            'paid_amount' => $quote['net_payable'],
            'delivery_charge' => 0,
            'original_delivery_charge' => 0,
            'store_id' => $data['store_id'] ?? data_get($quote, 'items.0.item_snapshot.store_id'),
            'module_id' => $data['module_id'] ?? data_get($quote, 'items.0.item_snapshot.module_id'),
            'order_type' => 'pos',
            'checked' => 1,
            'confirmed' => now(),
            'delivered' => now(),
            'sale_channel' => 'showroom',
            'pos_idempotency_key' => $idempotencyKey,
            'pos_created_by_admin_id' => $admin->id,
            'pos_device_id' => $device?->id,
            'showroom_id' => $data['showroom_id'] ?? $device?->showroom_id,
            'point_redeemed_points' => (int) $quote['point_redeem_points'],
            'point_redeemed_amount' => $redeemedAmount,
            'created_at' => now(),
            'updated_at' => now(),
        ];

        $order = new Order();
        foreach ($this->filterRow('orders', $payload) as $key => $value) {
            $order->setAttribute($key, $value);
        }
        $order->save();

        return $order;
    }

    private function orderDetailPayload(Order $order, array $line): array
    {
        return [
            'order_id' => $order->id,
            'item_id' => $line['item_id'],
            'product_id' => $line['item_id'],
            'item_campaign_id' => null,
            'food_details' => json_encode($line['item_snapshot']),
            'item_details' => json_encode($line['item_snapshot']),
            'quantity' => $line['quantity'],
            'qty' => $line['quantity'],
            'price' => $line['unit_price'],
            'tax_amount' => $line['tax_amount'],
            'discount_on_item' => $line['discount_amount'],
            'discount_type' => 'discount_on_product',
            'variant' => $line['variation_type'],
            'variation' => json_encode($line['variation_type'] ? [['type' => $line['variation_type']]] : []),
            'add_ons' => json_encode([]),
            'total_add_on_price' => 0,
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }

    private function insertOrderPayment(Order $order, array $payment): void
    {
        $payload = [
            'order_id' => $order->id,
            'transaction_ref' => $payment['transaction_ref'],
            'amount' => $payment['amount'],
            'payment_status' => 'paid',
            'payment_method' => $payment['payment_method'],
            'payment_transaction_id' => $payment['payment_transaction_id'],
            'provider_name' => $payment['provider_name'],
            'provider_transaction_id' => $payment['provider_transaction_id'],
            'qr_reference' => $payment['qr_reference'],
            'payment_payload' => json_encode($payment['payment_payload'] ?? []),
            'paid_at' => now(),
            'created_at' => now(),
            'updated_at' => now(),
        ];

        DB::table('order_payments')->insert($this->filterRow('order_payments', $payload));
    }

    private function itemSnapshot(Item $item): array
    {
        return [
            'id' => $item->id,
            'item_id' => $item->id,
            'name' => $item->name,
            'price' => (float) $item->price,
            'stock' => (int) ($item->stock ?? 0),
            'store_id' => $item->store_id,
            'module_id' => $item->module_id,
            'image_full_url' => $item->image_full_url,
            'variations' => is_array($item->variations) ? $item->variations : json_decode($item->variations ?: '[]', true),
        ];
    }

    private function averagePurchaseCost(int $itemId, ?string $variationType): float
    {
        $query = PurchaseItem::where('item_id', $itemId);
        if ($variationType) {
            $query->where('variation_type', $variationType);
        }

        return (float) ($query->avg('purchase_price') ?: 0);
    }

    private function updateOrderColumns(Order $order, array $payload): void
    {
        $filtered = $this->filterRow('orders', $payload);
        if ($filtered === []) {
            return;
        }

        $order->forceFill($filtered)->save();
    }

    private function normalizeIdempotencyKey(?string $key): ?string
    {
        $key = trim((string) $key);

        return $key === '' ? null : $key;
    }

    private function findExistingSaleByIdempotencyKey(?string $idempotencyKey, bool $lock = false): ?Order
    {
        if (! $idempotencyKey || ! in_array('pos_idempotency_key', $this->columns('orders'), true)) {
            return null;
        }

        $query = Order::where('sale_channel', 'showroom')
            ->where('pos_idempotency_key', $idempotencyKey);

        if ($lock) {
            $query->lockForUpdate();
        }

        return $query->with(['details', 'payments', 'customer'])->first();
    }

    private function filterRows(string $table, array $rows): array
    {
        return array_map(fn ($row) => $this->filterRow($table, $row), $rows);
    }

    private function filterRow(string $table, array $row): array
    {
        $columns = $this->columns($table);

        return array_intersect_key($row, array_flip($columns));
    }

    private function columns(string $table): array
    {
        return $this->columnCache[$table] ??= (Schema::hasTable($table) ? Schema::getColumnListing($table) : []);
    }
}
