<?php

namespace App\Http\Controllers\Api\V1\Showroom;

use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\ManualPaymentVerification;
use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\PaymentTransaction;
use App\Models\PurchaseItem;
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\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class ReturnExchangeController extends Controller
{
    public function __construct(
        private readonly StockMovementService $stockMovementService,
        private readonly AccountingVoucherService $accountingVoucherService,
        private readonly PointLedgerService $pointLedgerService,
    ) {
    }

    public function returnSale(Request $request)
    {
        $data = $request->validate([
            'order_id' => 'required|integer',
            'idempotency_key' => 'nullable|string|max:120',
            'refund_method' => 'required|string|in:cash,bkash,nagad,bank,card,bank_card,bangla_qr',
            'items' => 'required|array|min:1',
            'items.*.order_detail_id' => 'required|integer',
            'items.*.quantity' => 'required|integer|min:1',
            'reason' => 'nullable|string|max:500',
        ]);
        $admin = $request->attributes->get('showroom_admin');
        $idempotencyKey = $data['idempotency_key'] ?? (string) Str::uuid();

        try {
            $result = DB::transaction(function () use ($data, $admin, $idempotencyKey) {
            $this->assertReturnTables();

            $existing = DB::table('refunds')->where('idempotency_key', $idempotencyKey)->first();
            if ($existing) {
                return $this->returnResponseFromExisting($existing);
            }

            $order = Order::where('sale_channel', 'showroom')->lockForUpdate()->findOrFail($data['order_id']);
            if ($order->payment_status !== 'paid') {
                throw ValidationException::withMessages([
                    'order_id' => translate('Only paid showroom invoices can be returned.'),
                ]);
            }

            $returnLines = collect($data['items'])->map(fn ($line) => $this->resolveRefundReturnLine($order, $line))->values();
            $grossReturnedAmount = round((float) $returnLines->sum('line_refund_amount'), 8);
            $returnedCogs = round((float) $returnLines->sum('cogs'), 8);
            $grossOrderAmount = $this->grossOrderAmount($order);
            $redeemedAmountToReverse = $this->proportional((float) ($order->point_redeemed_amount ?? 0), $grossReturnedAmount, $grossOrderAmount);
            $cashRefundAmount = max(round($grossReturnedAmount - $redeemedAmountToReverse, 8), 0);

            $refundId = DB::table('refunds')->insertGetId($this->filterRow('refunds', [
                'idempotency_key' => $idempotencyKey,
                'order_id' => $order->id,
                'user_id' => (int) ($order->user_id ?? 0),
                'order_status' => (string) ($order->order_status ?? 'delivered'),
                'customer_reason' => $data['reason'] ?? null,
                'customer_note' => $data['reason'] ?? null,
                'admin_note' => $redeemedAmountToReverse > 0
                    ? 'Gross return ' . $grossReturnedAmount . ', point discount reversed ' . $redeemedAmountToReverse
                    : null,
                'refund_amount' => $cashRefundAmount,
                'refund_status' => 'refunded',
                'refund_method' => $data['refund_method'],
                'created_by_admin_id' => $admin?->id,
                'refunded_at' => now(),
                'created_at' => now(),
                'updated_at' => now(),
            ]));
            $returnNo = 'RETURN-POS-' . $refundId;

            foreach ($returnLines as $line) {
                $movement = $this->stockMovementService->reverseSale($line['item_id'], $line['quantity'], [
                    'variation_type' => $line['variation_type'],
                    'source_type' => 'showroom_return',
                    'source_id' => $refundId,
                    'source_reference' => $returnNo,
                    'sale_channel' => 'showroom',
                    'created_by_admin_id' => $admin?->id,
                    'note' => $data['reason'] ?? null,
                ]);

                DB::table('showroom_return_items')->insert($this->filterRow('showroom_return_items', [
                    'refund_id' => $refundId,
                    'order_id' => $order->id,
                    'order_detail_id' => $line['order_detail_id'],
                    'item_id' => $line['item_id'],
                    'item_name' => $line['item_name'],
                    'variation_type' => $line['variation_type'],
                    'quantity' => $line['quantity'],
                    'unit_price' => $line['unit_price'],
                    'discount_amount' => $line['discount_amount'],
                    'tax_amount' => $line['tax_amount'],
                    'line_refund_amount' => $line['line_refund_amount'],
                    'cogs_amount' => $line['cogs'],
                    'stock_movement_id' => $movement->id,
                    'source_reference' => $returnNo,
                    'created_by_admin_id' => $admin?->id,
                    'created_at' => now(),
                    'updated_at' => now(),
                ]));
            }

            $voucherLines = [
                ['account_key' => 'sales_return', 'debit' => $grossReturnedAmount, 'description' => 'Showroom return'],
            ];
            if ($redeemedAmountToReverse > 0) {
                $voucherLines[] = ['account_key' => 'loyalty_discount', 'credit' => $redeemedAmountToReverse, 'description' => 'Reverse point redeem discount'];
            }
            if ($cashRefundAmount > 0) {
                $voucherLines[] = [
                    'account_key' => $this->accountingVoucherService->paymentAccountKey($data['refund_method']),
                    'credit' => $cashRefundAmount,
                    'description' => 'Return refund',
                ];
            }
            if ($returnedCogs > 0) {
                $voucherLines[] = ['account_key' => 'inventory', 'debit' => $returnedCogs, 'description' => 'Returned inventory'];
                $voucherLines[] = ['account_key' => 'cogs', 'credit' => $returnedCogs, 'description' => 'Reverse COGS'];
            }

            $voucher = $this->accountingVoucherService->post([
                'source_type' => 'showroom_return',
                'source_id' => $refundId,
                'source_reference' => $returnNo,
                'sale_channel' => 'showroom',
                'created_by_admin_id' => $admin?->id,
                'lines' => $voucherLines,
            ]);

            if (Schema::hasColumn('refunds', 'voucher_id')) {
                $refundUpdate = [
                    'voucher_id' => $voucher->id,
                ];
                if (Schema::hasColumn('refunds', 'updated_at')) {
                    $refundUpdate['updated_at'] = now();
                }

                DB::table('refunds')->where('id', $refundId)->update($refundUpdate);
            }

            if ($order->user_id && (int) ($order->point_redeemed_points ?? 0) > 0) {
                $restoreRedeemed = (int) floor($this->proportional((float) ($order->point_redeemed_points ?? 0), $grossReturnedAmount, $grossOrderAmount));
                if ($restoreRedeemed > 0) {
                    $this->pointLedgerService->write((int) $order->user_id, $restoreRedeemed, [
                        'source_type' => 'showroom_return',
                        'source_id' => $refundId,
                        'source_reference' => $returnNo,
                        'point_type' => 'adjustment',
                        'amount_value' => $this->pointLedgerService->amountForPoints($restoreRedeemed),
                        'created_by_admin_id' => $admin?->id,
                        'note' => 'Restore redeemed points for returned item',
                    ]);
                }
            }

            $reversePoints = $order->order_amount > 0
                ? (int) floor(((int) ($order->point_earned ?? 0)) * ($cashRefundAmount / (float) $order->order_amount))
                : 0;
            if ($reversePoints > 0 && $order->user_id) {
                $this->pointLedgerService->reverseEarn((int) $order->user_id, $reversePoints, [
                    'source_type' => 'showroom_return',
                    'source_id' => $refundId,
                    'source_reference' => $returnNo,
                    'created_by_admin_id' => $admin?->id,
                ]);
            }

            return [
                'refund_id' => $refundId,
                'return_no' => $returnNo,
                'order_id' => $order->id,
                'gross_returned_amount' => $grossReturnedAmount,
                'point_discount_reversed_amount' => $redeemedAmountToReverse,
                'refund_amount' => $cashRefundAmount,
                'voucher_id' => $voucher->id,
            ];
            });
        } catch (QueryException $exception) {
            if ($existing = $this->findExistingReturn($idempotencyKey)) {
                $result = $this->returnResponseFromExisting($existing);
            } else {
                throw $exception;
            }
        }

        return response()->json($result, empty($result['duplicate']) ? 201 : 200);
    }

    public function exchange(Request $request)
    {
        $data = $request->validate([
            'order_id' => 'required|integer',
            'idempotency_key' => 'nullable|string|max:120',
            'return_items' => 'required_without:items|array|min:1',
            'items' => 'required_without:return_items|array|min:1',
            'return_items.*.order_detail_id' => 'required_with:return_items|integer',
            'items.*.order_detail_id' => 'required_with:items|integer',
            'return_items.*.quantity' => 'required_with:return_items|integer|min:1',
            'items.*.quantity' => 'required_with:items|integer|min:1',
            'replacement_items' => 'required_without:new_items|array|min:1',
            'new_items' => 'required_without:replacement_items|array|min:1',
            'replacement_items.*.item_id' => 'required_with:replacement_items|integer',
            'new_items.*.item_id' => 'required_with:new_items|integer',
            'replacement_items.*.quantity' => 'required_with:replacement_items|integer|min:1',
            'new_items.*.quantity' => 'required_with:new_items|integer|min:1',
            'replacement_items.*.variation_type' => 'nullable|string|max:180',
            'new_items.*.variation_type' => 'nullable|string|max:180',
            'replacement_items.*.unit_price' => 'nullable|numeric|min:0',
            'new_items.*.unit_price' => 'nullable|numeric|min:0',
            'replacement_items.*.discount_amount' => 'nullable|numeric|min:0',
            'new_items.*.discount_amount' => 'nullable|numeric|min:0',
            'replacement_items.*.tax_amount' => 'nullable|numeric|min:0',
            'new_items.*.tax_amount' => 'nullable|numeric|min:0',
            'payments' => 'nullable|array',
            'payments.*.payment_method' => 'required_with:payments|string|in:cash,bkash,nagad,bank,card,bank_card,bangla_qr',
            'payments.*.amount' => 'required_with:payments|numeric|min:0.000001',
            'payments.*.transaction_ref' => 'nullable|string|max:180',
            'payments.*.transaction_id' => 'nullable|string|max:180',
            'payments.*.payment_transaction_id' => 'nullable|integer',
            'payments.*.provider_name' => 'nullable|string|max:80',
            'payments.*.provider_transaction_id' => 'nullable|string|max:180',
            'payments.*.qr_reference' => 'nullable|string|max:180',
            'refund_method' => 'nullable|string|in:cash,bkash,nagad,bank,card,bank_card,bangla_qr',
            'reason' => 'nullable|string|max:500',
        ]);

        $admin = $request->attributes->get('showroom_admin');
        $returnItems = $data['return_items'] ?? $data['items'] ?? [];
        $replacementItems = $data['replacement_items'] ?? $data['new_items'] ?? [];
        $idempotencyKey = $data['idempotency_key'] ?? (string) Str::uuid();

        try {
            $result = DB::transaction(function () use ($data, $admin, $returnItems, $replacementItems, $idempotencyKey) {
            $this->assertExchangeTables();

            $existing = DB::table('order_exchanges')->where('idempotency_key', $idempotencyKey)->first();
            if ($existing) {
                return $this->exchangeResponseFromExisting($existing);
            }

            $order = Order::where('sale_channel', 'showroom')->lockForUpdate()->findOrFail($data['order_id']);
            if ($order->payment_status !== 'paid') {
                throw ValidationException::withMessages([
                    'order_id' => translate('Only paid showroom invoices can be exchanged.'),
                ]);
            }

            $returnLines = collect($returnItems)->map(fn ($line) => $this->resolveReturnLine($order, $line))->values();
            $replacementLines = collect($replacementItems)->map(fn ($line) => $this->resolveReplacementLine($line))->values();

            $grossReturnedAmount = round((float) $returnLines->sum('line_total'), 8);
            $returnedCogs = round((float) $returnLines->sum('cogs'), 8);
            $replacementAmount = round((float) $replacementLines->sum('line_total'), 8);
            $replacementCogs = round((float) $replacementLines->sum('cogs'), 8);
            $grossOrderAmount = $this->grossOrderAmount($order);
            $redeemedAmountToReverse = $this->proportional((float) ($order->point_redeemed_amount ?? 0), $grossReturnedAmount, $grossOrderAmount);
            $returnedCreditAmount = max(round($grossReturnedAmount - $redeemedAmountToReverse, 8), 0);
            $differenceAmount = round($replacementAmount - $returnedCreditAmount, 8);
            $payments = $this->normalizePayments($data['payments'] ?? []);
            $paidAmount = round((float) collect($payments)->sum('amount'), 8);

            if ($differenceAmount > 0 && abs($paidAmount - $differenceAmount) > 0.00001) {
                throw ValidationException::withMessages([
                    'payments' => translate('Exchange payment must equal the payable difference. Due sale is not allowed.'),
                ]);
            }

            if ($differenceAmount <= 0 && $paidAmount > 0) {
                throw ValidationException::withMessages([
                    'payments' => translate('Payment is not allowed when exchange difference is zero or refundable.'),
                ]);
            }

            if ($differenceAmount < 0 && empty($data['refund_method'])) {
                throw ValidationException::withMessages([
                    'refund_method' => translate('Refund method is required when exchange amount is refundable.'),
                ]);
            }

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

            $exchangeNo = 'EXC-' . now()->format('ymdHis') . '-' . random_int(1000, 9999);
            $exchangeId = DB::table('order_exchanges')->insertGetId($this->filterRow('order_exchanges', [
                'exchange_no' => $exchangeNo,
                'idempotency_key' => $idempotencyKey,
                'order_id' => $order->id,
                'user_id' => $order->user_id,
                'returned_amount' => $grossReturnedAmount,
                'replacement_amount' => $replacementAmount,
                'difference_amount' => $differenceAmount,
                'affects_collectable' => true,
                'exchange_status' => 'posted',
                'exchanged_at' => now(),
                'notes' => $data['reason'] ?? null,
                'created_by_admin_id' => $admin?->id,
                'created_at' => now(),
                'updated_at' => now(),
            ]));

            foreach ($returnLines as $line) {
                DB::table('order_exchange_items')->insert($this->filterRow('order_exchange_items', [
                    'order_exchange_id' => $exchangeId,
                    'direction' => 'return',
                    'order_detail_id' => $line['order_detail_id'],
                    'item_id' => $line['item_id'],
                    'item_name' => $line['item_name'],
                    'variation' => json_encode($line['variation_payload']),
                    'quantity' => $line['quantity'],
                    'unit_price' => $line['unit_price'],
                    'line_total' => $line['line_total'],
                    'created_at' => now(),
                    'updated_at' => now(),
                ]));

                $this->stockMovementService->reverseSale($line['item_id'], $line['quantity'], [
                    'variation_type' => $line['variation_type'],
                    'source_type' => 'showroom_exchange_return',
                    'source_id' => $exchangeId,
                    'source_reference' => $exchangeNo,
                    'sale_channel' => 'showroom',
                    'created_by_admin_id' => $admin?->id,
                    'note' => $data['reason'] ?? null,
                ]);
            }

            foreach ($replacementLines as $line) {
                DB::table('order_exchange_items')->insert($this->filterRow('order_exchange_items', [
                    'order_exchange_id' => $exchangeId,
                    'direction' => 'replacement',
                    'order_detail_id' => null,
                    'item_id' => $line['item_id'],
                    'item_name' => $line['item_name'],
                    'variation' => json_encode($line['variation_payload']),
                    'quantity' => $line['quantity'],
                    'unit_price' => $line['unit_price'],
                    'line_total' => $line['line_total'],
                    'created_at' => now(),
                    'updated_at' => now(),
                ]));

                $this->stockMovementService->decrease($line['item_id'], $line['quantity'], [
                    'variation_type' => $line['variation_type'],
                    'source_type' => 'showroom_exchange_sale',
                    'source_id' => $exchangeId,
                    'source_reference' => $exchangeNo,
                    'sale_channel' => 'showroom',
                    'created_by_admin_id' => $admin?->id,
                ]);
            }

            if ($differenceAmount > 0) {
                foreach ($payments as $payment) {
                    $this->linkBanglaQrPayment($exchangeId, $exchangeNo, $payment);
                    $this->insertExchangePayment($order, $payment);
                }
            }

            $voucher = $this->postExchangeVoucher(
                $exchangeId,
                $exchangeNo,
                $payments,
                $data['refund_method'] ?? null,
                $grossReturnedAmount,
                $returnedCogs,
                $replacementAmount,
                $replacementCogs,
                $redeemedAmountToReverse,
                $differenceAmount,
                $admin?->id
            );

            if (Schema::hasColumn('order_exchanges', 'voucher_id')) {
                $exchangeUpdate = [
                    'voucher_id' => $voucher->id,
                ];
                if (Schema::hasColumn('order_exchanges', 'updated_at')) {
                    $exchangeUpdate['updated_at'] = now();
                }

                DB::table('order_exchanges')->where('id', $exchangeId)->update($exchangeUpdate);
            }

            $this->adjustExchangePoints($order, $exchangeId, $exchangeNo, $grossReturnedAmount, $returnedCreditAmount, $replacementAmount, $grossOrderAmount, $admin?->id);

            return [
                'exchange_id' => $exchangeId,
                'exchange_no' => $exchangeNo,
                'order_id' => $order->id,
                'returned_amount' => $grossReturnedAmount,
                'point_discount_reversed_amount' => $redeemedAmountToReverse,
                'returned_credit_amount' => $returnedCreditAmount,
                'replacement_amount' => $replacementAmount,
                'difference_amount' => $differenceAmount,
                'payment_collected' => max($differenceAmount, 0),
                'refund_amount' => abs(min($differenceAmount, 0)),
                'voucher_id' => $voucher->id,
            ];
            });
        } catch (QueryException $exception) {
            if ($existing = $this->findExistingExchange($idempotencyKey)) {
                $result = $this->exchangeResponseFromExisting($existing);
            } else {
                throw $exception;
            }
        }

        return response()->json($result, empty($result['duplicate']) ? 201 : 200);
    }

    private function assertExchangeTables(): void
    {
        if (! Schema::hasTable('order_exchanges') || ! Schema::hasTable('order_exchange_items')) {
            throw ValidationException::withMessages([
                'exchange' => translate('Exchange tables are not available. Please run the required migrations.'),
            ]);
        }

        $requiredExchangeColumns = [
            'idempotency_key',
            'order_id',
            'user_id',
            'returned_amount',
            'replacement_amount',
            'difference_amount',
            'exchange_status',
            'exchanged_at',
        ];
        $requiredItemColumns = [
            'order_exchange_id',
            'direction',
            'order_detail_id',
            'item_id',
            'quantity',
            'unit_price',
            'line_total',
        ];

        if (! $this->tableHasColumns('order_exchanges', $requiredExchangeColumns)
            || ! $this->tableHasColumns('order_exchange_items', $requiredItemColumns)) {
            throw ValidationException::withMessages([
                'exchange' => translate('Exchange columns are not available. Please run the showroom exchange compatibility migration.'),
            ]);
        }
    }

    private function assertReturnTables(): void
    {
        if (! Schema::hasTable('refunds') || ! Schema::hasTable('showroom_return_items')) {
            throw ValidationException::withMessages([
                'return' => translate('Showroom return tables are not available. Please run the required migrations.'),
            ]);
        }

        if (! Schema::hasColumn('refunds', 'idempotency_key') || ! Schema::hasColumn('refunds', 'voucher_id')) {
            throw ValidationException::withMessages([
                'return' => translate('Showroom return columns are not available. Please run the required migrations.'),
            ]);
        }
    }

    private function resolveRefundReturnLine(Order $order, array $line): array
    {
        $detail = OrderDetail::where('order_id', $order->id)->lockForUpdate()->findOrFail($line['order_detail_id']);
        $quantity = (int) $line['quantity'];
        $soldQuantity = (int) ($detail->quantity ?? $detail->qty ?? 0);
        $alreadyReturned = $this->returnedQuantityForDetail($order, (int) $detail->id);

        if ($quantity > max($soldQuantity - $alreadyReturned, 0)) {
            throw ValidationException::withMessages([
                'quantity' => translate('Return quantity can not exceed available sold quantity.'),
            ]);
        }

        $unitPrice = (float) ($detail->price ?? 0);
        $ratio = $soldQuantity > 0 ? ($quantity / $soldQuantity) : 1;
        $lineDiscount = max((float) ($detail->discount_on_item ?? 0), 0) * $ratio;
        $lineTax = max((float) ($detail->tax_amount ?? 0), 0) * $ratio;
        $lineRefund = max(round(($unitPrice * $quantity) - $lineDiscount + $lineTax, 8), 0);
        $itemId = (int) ($detail->item_id ?? $detail->product_id);
        $variationType = $detail->variant ?: data_get(json_decode($detail->variation ?: '[]', true), '0.type');

        return [
            'order_detail_id' => $detail->id,
            'item_id' => $itemId,
            'item_name' => $this->itemNameFromDetail($detail),
            'variation_type' => blank($variationType) ? null : $variationType,
            'quantity' => $quantity,
            'unit_price' => $unitPrice,
            'discount_amount' => round($lineDiscount, 8),
            'tax_amount' => round($lineTax, 8),
            'line_refund_amount' => $lineRefund,
            'cogs' => round($this->averagePurchaseCost($itemId, $variationType) * $quantity, 8),
        ];
    }

    private function resolveReturnLine(Order $order, array $line): array
    {
        $detail = OrderDetail::where('order_id', $order->id)->lockForUpdate()->findOrFail($line['order_detail_id']);
        $quantity = (int) $line['quantity'];
        $soldQuantity = (int) ($detail->quantity ?? $detail->qty ?? 0);
        $alreadyReturned = $this->returnedQuantityForDetail($order, (int) $detail->id);

        if ($quantity > max($soldQuantity - $alreadyReturned, 0)) {
            throw ValidationException::withMessages([
                'quantity' => translate('Exchange return quantity can not exceed available sold quantity.'),
            ]);
        }

        $unitPrice = (float) ($detail->price ?? 0);
        $ratio = $soldQuantity > 0 ? ($quantity / $soldQuantity) : 1;
        $lineDiscount = max((float) ($detail->discount_on_item ?? 0), 0) * $ratio;
        $lineTax = max((float) ($detail->tax_amount ?? 0), 0) * $ratio;
        $lineTotal = max(round(($unitPrice * $quantity) - $lineDiscount + $lineTax, 8), 0);
        $itemId = (int) ($detail->item_id ?? $detail->product_id);
        $variationType = $detail->variant ?: data_get(json_decode($detail->variation ?: '[]', true), '0.type');

        return [
            'order_detail_id' => $detail->id,
            'item_id' => $itemId,
            'item_name' => $this->itemNameFromDetail($detail),
            'variation_type' => blank($variationType) ? null : $variationType,
            'variation_payload' => $variationType ? [['type' => $variationType]] : [],
            'quantity' => $quantity,
            'unit_price' => $unitPrice,
            'line_total' => $lineTotal,
            'cogs' => round($this->averagePurchaseCost($itemId, $variationType) * $quantity, 8),
        ];
    }

    private function returnedQuantityForDetail(Order $order, int $orderDetailId): int
    {
        $quantity = 0;

        if (Schema::hasTable('showroom_return_items')) {
            $quantity += (int) DB::table('showroom_return_items')
                ->where('order_id', $order->id)
                ->where('order_detail_id', $orderDetailId)
                ->sum('quantity');
        }

        if (Schema::hasTable('order_exchanges') && Schema::hasTable('order_exchange_items')) {
            $quantity += (int) DB::table('order_exchange_items')
                ->join('order_exchanges', 'order_exchanges.id', '=', 'order_exchange_items.order_exchange_id')
                ->where('order_exchanges.order_id', $order->id)
                ->where('order_exchanges.exchange_status', 'posted')
                ->where('order_exchange_items.direction', 'return')
                ->where('order_exchange_items.order_detail_id', $orderDetailId)
                ->sum('order_exchange_items.quantity');
        }

        return $quantity;
    }

    private function resolveReplacementLine(array $line): array
    {
        $item = Item::withoutGlobalScopes()->withoutGlobalScope(StoreScope::class)->findOrFail($line['item_id']);
        $quantity = (int) $line['quantity'];
        $variationType = blank($line['variation_type'] ?? null) ? null : (string) $line['variation_type'];
        $this->assertVariationSelection($item, $variationType);

        $unitPrice = array_key_exists('unit_price', $line) && $line['unit_price'] !== null
            ? (float) $line['unit_price']
            : $this->priceFor($item, $variationType);
        $discount = max((float) ($line['discount_amount'] ?? 0), 0);
        $tax = max((float) ($line['tax_amount'] ?? 0), 0);
        $lineTotal = max(round(($unitPrice * $quantity) - $discount + $tax, 8), 0);

        return [
            'item_id' => $item->id,
            'item_name' => $item->name,
            'variation_type' => $variationType,
            'variation_payload' => $variationType ? [['type' => $variationType]] : [],
            'quantity' => $quantity,
            'unit_price' => $unitPrice,
            'line_total' => $lineTotal,
            'cogs' => round($this->averagePurchaseCost((int) $item->id, $variationType) * $quantity, 8),
        ];
    }

    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): array
    {
        if ($payment['payment_method'] !== 'bangla_qr') {
            return $payment;
        }

        $transaction = $this->findBanglaQrTransaction($payment);
        $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 exchange.'),
            ]);
        }

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

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

        $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 linkBanglaQrPayment(int $exchangeId, string $exchangeNo, array $payment): void
    {
        if ($payment['payment_method'] !== 'bangla_qr') {
            return;
        }

        $transaction = $this->findBanglaQrTransaction($payment);
        if (! $transaction) {
            throw ValidationException::withMessages([
                'bangla_qr' => translate('Bangla QR payment was not found.'),
            ]);
        }

        $transaction->forceFill([
            'source_type' => 'showroom_exchange',
            'source_id' => $exchangeId,
            'source_reference' => $exchangeNo,
        ])->save();
    }

    private function findBanglaQrTransaction(array $payment): ?PaymentTransaction
    {
        if (empty($payment['payment_transaction_id']) && empty($payment['transaction_ref']) && empty($payment['qr_reference'])) {
            return null;
        }

        return PaymentTransaction::query()
            ->where('payment_method', 'bangla_qr')
            ->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();
    }

    private function insertExchangePayment(Order $order, array $payment): void
    {
        DB::table('order_payments')->insert($this->filterRow('order_payments', [
            '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(),
        ]));
    }

    private function postExchangeVoucher(
        int $exchangeId,
        string $exchangeNo,
        array $payments,
        ?string $refundMethod,
        float $grossReturnedAmount,
        float $returnedCogs,
        float $replacementAmount,
        float $replacementCogs,
        float $redeemedAmountToReverse,
        float $differenceAmount,
        ?int $adminId
    ) {
        $lines = [
            ['account_key' => 'sales_return', 'debit' => $grossReturnedAmount, 'description' => 'Showroom exchange return'],
            ['account_key' => 'sales_income', 'credit' => $replacementAmount, 'description' => 'Showroom exchange replacement'],
        ];

        if ($redeemedAmountToReverse > 0) {
            $lines[] = ['account_key' => 'loyalty_discount', 'credit' => $redeemedAmountToReverse, 'description' => 'Reverse point redeem discount'];
        }

        if ($differenceAmount > 0) {
            foreach ($payments as $payment) {
                $lines[] = [
                    'account_key' => $this->accountingVoucherService->paymentAccountKey($payment['payment_method']),
                    'debit' => $payment['amount'],
                    'description' => 'Exchange difference collection via ' . $payment['payment_method'],
                ];
            }
        } elseif ($differenceAmount < 0) {
            $lines[] = [
                'account_key' => $this->accountingVoucherService->paymentAccountKey($refundMethod ?: 'cash'),
                'credit' => abs($differenceAmount),
                'description' => 'Exchange refund payable',
            ];
        }

        if ($returnedCogs > 0) {
            $lines[] = ['account_key' => 'inventory', 'debit' => $returnedCogs, 'description' => 'Exchange return inventory in'];
            $lines[] = ['account_key' => 'cogs', 'credit' => $returnedCogs, 'description' => 'Reverse COGS for exchange return'];
        }

        if ($replacementCogs > 0) {
            $lines[] = ['account_key' => 'cogs', 'debit' => $replacementCogs, 'description' => 'Exchange replacement COGS'];
            $lines[] = ['account_key' => 'inventory', 'credit' => $replacementCogs, 'description' => 'Exchange replacement inventory out'];
        }

        return $this->accountingVoucherService->post([
            'source_type' => 'showroom_exchange',
            'source_id' => $exchangeId,
            'source_reference' => $exchangeNo,
            'sale_channel' => 'showroom',
            'created_by_admin_id' => $adminId,
            'lines' => $lines,
        ]);
    }

    private function adjustExchangePoints(Order $order, int $exchangeId, string $exchangeNo, float $grossReturnedAmount, float $returnedCreditAmount, float $replacementAmount, float $grossOrderAmount, ?int $adminId): void
    {
        if (! $order->user_id) {
            return;
        }

        $restoreRedeemed = (int) floor($this->proportional((float) ($order->point_redeemed_points ?? 0), $grossReturnedAmount, $grossOrderAmount));
        if ($restoreRedeemed > 0) {
            $this->pointLedgerService->write((int) $order->user_id, $restoreRedeemed, [
                'source_type' => 'showroom_exchange',
                'source_id' => $exchangeId,
                'source_reference' => $exchangeNo,
                'point_type' => 'adjustment',
                'amount_value' => $this->pointLedgerService->amountForPoints($restoreRedeemed),
                'created_by_admin_id' => $adminId,
                'note' => 'Restore redeemed points for exchanged item',
            ]);
        }

        $reverseEarned = (int) floor($this->proportional((float) ($order->point_earned ?? 0), $returnedCreditAmount, (float) $order->order_amount));
        if ($reverseEarned > 0) {
            $this->pointLedgerService->reverseEarn((int) $order->user_id, $reverseEarned, [
                'source_type' => 'showroom_exchange',
                'source_id' => $exchangeId,
                'source_reference' => $exchangeNo,
                'created_by_admin_id' => $adminId,
                'note' => 'Reverse earned points for exchanged item',
            ]);
        }

        $earned = $this->pointLedgerService->earn((int) $order->user_id, $replacementAmount, [
            'source_type' => 'showroom_exchange_replacement',
            'source_id' => $exchangeId,
            'source_reference' => $exchangeNo,
            'created_by_admin_id' => $adminId,
            'note' => 'Earn points for exchange replacement item',
        ]);

        if ($earned > 0 && Schema::hasColumn('orders', 'point_earned')) {
            $order->forceFill(['point_earned' => (int) ($order->point_earned ?? 0) + $earned])->save();
        }
    }

    private function itemNameFromDetail(OrderDetail $detail): string
    {
        $details = is_string($detail->item_details) ? json_decode($detail->item_details, true) : null;

        return is_array($details) ? ($details['name'] ?? $details['item_name'] ?? ('Item #' . ($detail->item_id ?? $detail->product_id))) : ('Item #' . ($detail->item_id ?? $detail->product_id));
    }

    private function grossOrderAmount(Order $order): float
    {
        return max(round((float) $order->order_amount + (float) ($order->point_redeemed_amount ?? 0), 8), 0.00001);
    }

    private function proportional(float $value, float $part, float $base): float
    {
        return $base > 0 ? round($value * ($part / $base), 8) : 0;
    }

    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 assertVariationSelection(Item $item, ?string $variationType): void
    {
        $variations = is_array($item->variations) ? $item->variations : json_decode($item->variations ?: '[]', true);
        if (! is_array($variations) || count($variations) === 0) {
            return;
        }

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

    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 filterRow(string $table, array $row): array
    {
        return Schema::hasTable($table)
            ? array_intersect_key($row, array_flip(Schema::getColumnListing($table)))
            : $row;
    }

    private function findExistingReturn(string $idempotencyKey): ?object
    {
        if (! Schema::hasTable('refunds') || ! Schema::hasColumn('refunds', 'idempotency_key')) {
            return null;
        }

        return DB::table('refunds')->where('idempotency_key', $idempotencyKey)->first();
    }

    private function returnResponseFromExisting(object $existing): array
    {
        return [
            'refund_id' => $existing->id,
            'order_id' => $existing->order_id,
            'refund_amount' => (float) $existing->refund_amount,
            'voucher_id' => $existing->voucher_id ?? null,
            'duplicate' => true,
            'message' => translate('Return already posted for this request.'),
        ];
    }

    private function findExistingExchange(string $idempotencyKey): ?object
    {
        if (! Schema::hasTable('order_exchanges') || ! Schema::hasColumn('order_exchanges', 'idempotency_key')) {
            return null;
        }

        return DB::table('order_exchanges')->where('idempotency_key', $idempotencyKey)->first();
    }

    private function exchangeResponseFromExisting(object $existing): array
    {
        return [
            'exchange_id' => $existing->id,
            'exchange_no' => $existing->exchange_no,
            'voucher_id' => $existing->voucher_id ?? null,
            'duplicate' => true,
            'message' => translate('Exchange already posted for this request.'),
        ];
    }

    private function tableHasColumns(string $table, array $columns): bool
    {
        if (! Schema::hasTable($table)) {
            return false;
        }

        foreach ($columns as $column) {
            if (! Schema::hasColumn($table, $column)) {
                return false;
            }
        }

        return true;
    }
}
