<?php

namespace App\Services;

use App\Exceptions\OrderFinancialException;
use App\Models\AccountingAccount;
use App\Models\AccountingVoucher;
use App\Models\CustomerLedgerEntry;
use App\Models\Item;
use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\OrderExchange;
use App\Models\OrderExchangeItem;
use App\Models\OrderPayment;
use App\Models\OrderReceipt;
use App\Services\Accounting\AccountingVoucherService;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class OrderFinancialService
{
    public function __construct(private readonly AccountingVoucherService $accountingVoucherService)
    {
    }

    public function recordAdvance(Order $order, array $data, ?int $adminId): OrderReceipt
    {
        return DB::transaction(function () use ($order, $data, $adminId) {
            $lockedOrder = $this->lockedOrder($order->id);
            $existingReceipt = OrderReceipt::where('idempotency_key', $data['idempotency_key'])->first();
            if ($existingReceipt) {
                if ((int) $existingReceipt->order_id !== (int) $lockedOrder->id
                    || $existingReceipt->account_type !== OrderReceipt::ACCOUNT_TYPE_CUSTOMER_ADVANCE) {
                    throw new OrderFinancialException('Invalid advance request token.');
                }

                return $existingReceipt->load(['order.customer', 'createdByAdmin']);
            }

            $this->assertOrderCanReceiveAdvance($lockedOrder);
            $this->assertCourierCollectableIsMutable($lockedOrder);
            $this->materializeLegacyPartialPayment($lockedOrder);

            $amount = round((float) $data['amount'], 3);
            $summary = $this->summary($lockedOrder);

            if ($amount <= 0 || $amount > $summary['remaining_due'] + 0.0005) {
                throw new OrderFinancialException('Advance amount must be greater than zero and cannot exceed the remaining due.');
            }

            $transactionRef = trim((string) ($data['transaction_ref'] ?? '')) ?: null;
            if ($transactionRef && OrderReceipt::where('order_id', $lockedOrder->id)
                ->where('transaction_ref', $transactionRef)
                ->exists()) {
                throw new OrderFinancialException('This transaction reference has already been used for the order.');
            }

            $account = $this->resolveReceiptAccount($data, $data['payment_method']);
            $receivedAt = $this->resolveReceivedAt($data['received_at'] ?? null);

            $receipt = $lockedOrder->receipts()->create([
                'idempotency_key' => $data['idempotency_key'],
                'user_id' => $lockedOrder->is_guest ? null : $lockedOrder->user_id,
                'amount' => $amount,
                'payment_method' => $data['payment_method'],
                'account_id' => $account->id,
                'transaction_ref' => $transactionRef,
                'account_type' => OrderReceipt::ACCOUNT_TYPE_CUSTOMER_ADVANCE,
                'receipt_status' => 'posted',
                'received_at' => $receivedAt,
                'notes' => trim((string) ($data['notes'] ?? '')) ?: null,
                'created_by_admin_id' => $adminId,
            ]);

            $receipt->forceFill([
                'voucher_no' => sprintf('RV-%s-%06d', now()->format('Ymd'), $receipt->id),
            ])->save();

            $payment = new OrderPayment();
            $payment->forceFill([
                'order_id' => $lockedOrder->id,
                'order_receipt_id' => $receipt->id,
                'transaction_ref' => $transactionRef ?: $receipt->voucher_no,
                'amount' => $amount,
                'payment_status' => 'paid',
                'payment_method' => $data['payment_method'],
            ])->save();

            $this->syncOrderPaymentState($lockedOrder);
            $this->syncCustomerLedger($lockedOrder, $receipt, $adminId);
            $this->syncAdvanceAccounting($receipt, $account, $adminId);

            return $receipt->fresh(['order.customer', 'createdByAdmin', 'account', 'ledgerEntry']);
        });
    }

    public function updateAdvance(OrderReceipt $receipt, array $data, ?int $adminId): OrderReceipt
    {
        return DB::transaction(function () use ($receipt, $data, $adminId) {
            $lockedReceipt = OrderReceipt::lockForUpdate()->findOrFail($receipt->id);
            $lockedOrder = $this->lockedOrder($lockedReceipt->order_id);

            $this->assertCustomerAdvanceReceipt($lockedReceipt);
            if ($lockedReceipt->receipt_status !== 'posted') {
                throw new OrderFinancialException('Only a posted advance can be edited.');
            }

            $this->assertOrderCanReceiveAdvance($lockedOrder, allowExistingAmount: (float) $lockedReceipt->amount);
            $this->assertCourierCollectableIsMutable($lockedOrder);

            $amount = round((float) $data['amount'], 3);
            $maximum = round($this->summary($lockedOrder)['remaining_due'] + (float) $lockedReceipt->amount, 3);
            if ($amount <= 0 || $amount > $maximum + 0.0005) {
                throw new OrderFinancialException('Advance amount must be greater than zero and cannot exceed the remaining due.');
            }

            $transactionRef = trim((string) ($data['transaction_ref'] ?? '')) ?: null;
            if ($transactionRef && OrderReceipt::where('order_id', $lockedOrder->id)
                ->where('transaction_ref', $transactionRef)
                ->whereKeyNot($lockedReceipt->id)
                ->exists()) {
                throw new OrderFinancialException('This transaction reference has already been used for the order.');
            }

            $account = $this->resolveReceiptAccount($data, $data['payment_method']);
            $lockedReceipt->forceFill([
                'amount' => $amount,
                'payment_method' => $data['payment_method'],
                'account_id' => $account->id,
                'transaction_ref' => $transactionRef,
                'received_at' => $this->resolveReceivedAt($data['received_at'] ?? null),
                'notes' => trim((string) ($data['notes'] ?? '')) ?: null,
            ])->save();

            OrderPayment::where('order_receipt_id', $lockedReceipt->id)->update([
                'transaction_ref' => $transactionRef ?: $lockedReceipt->voucher_no,
                'amount' => $amount,
                'payment_status' => 'paid',
                'payment_method' => $data['payment_method'],
            ]);

            $this->syncOrderPaymentState($lockedOrder);
            $this->syncCustomerLedger($lockedOrder, $lockedReceipt, $adminId);
            $this->syncAdvanceAccounting($lockedReceipt, $account, $adminId);

            return $lockedReceipt->fresh(['order.customer', 'createdByAdmin', 'account', 'ledgerEntry']);
        });
    }

    public function voidReceipt(OrderReceipt $receipt, ?int $adminId, ?string $reason = null): void
    {
        DB::transaction(function () use ($receipt, $adminId, $reason) {
            $lockedReceipt = OrderReceipt::lockForUpdate()->findOrFail($receipt->id);
            $lockedOrder = $this->lockedOrder($lockedReceipt->order_id);

            $this->assertCustomerAdvanceReceipt($lockedReceipt);
            if ($lockedReceipt->receipt_status !== 'posted') {
                throw new OrderFinancialException('Only a posted receipt can be voided.');
            }

            $this->assertCourierCollectableIsMutable($lockedOrder);
            if (in_array($lockedOrder->order_status, ['delivered', 'refunded'], true)) {
                throw new OrderFinancialException('A receipt cannot be voided after delivery or refund accounting.');
            }

            $notes = trim(implode("\n", array_filter([
                $lockedReceipt->notes,
                $reason ? 'Void reason: '.trim($reason) : null,
            ])));

            $lockedReceipt->forceFill([
                'receipt_status' => 'voided',
                'voided_by_admin_id' => $adminId,
                'voided_at' => now(),
                'notes' => $notes ?: null,
            ])->save();

            OrderPayment::where('order_receipt_id', $lockedReceipt->id)
                ->where('payment_status', 'paid')
                ->update(['payment_status' => 'voided']);

            $this->syncOrderPaymentState($lockedOrder);
            $this->syncCustomerLedger($lockedOrder, $lockedReceipt, $adminId);

            AccountingVoucher::where('source_type', 'customer_advance_receipt')
                ->where('source_id', $lockedReceipt->id)
                ->update([
                    'status' => 'voided',
                    'voided_by_admin_id' => $adminId,
                    'voided_at' => now(),
                    'updated_at' => now(),
                ]);
        });
    }

    public function recordExchange(Order $order, array $data, ?int $adminId): OrderExchange
    {
        return DB::transaction(function () use ($order, $data, $adminId) {
            $lockedOrder = $this->lockedOrder($order->id);
            $existingExchange = OrderExchange::where('idempotency_key', $data['idempotency_key'])->first();
            if ($existingExchange) {
                if ((int) $existingExchange->order_id !== (int) $lockedOrder->id) {
                    throw new OrderFinancialException('Invalid exchange request token.');
                }

                return $existingExchange->load(['items', 'createdByAdmin']);
            }

            if (in_array($lockedOrder->order_status, ['canceled', 'refunded', 'failed'], true)) {
                throw new OrderFinancialException('An exchange cannot be posted for a canceled, failed, or refunded order.');
            }

            $shipmentBooked = $lockedOrder->courierShipment?->hasSuccessfulBooking() ?? false;
            if ($shipmentBooked && $lockedOrder->order_status !== 'delivered') {
                throw new OrderFinancialException('The courier booking already exists. Complete or cancel that workflow before posting an exchange.');
            }

            $orderDetail = OrderDetail::withoutGlobalScopes()
                ->where('order_id', $lockedOrder->id)
                ->lockForUpdate()
                ->findOrFail((int) $data['order_detail_id']);
            $returnQuantity = (int) $data['return_quantity'];
            $previouslyReturned = (int) OrderExchangeItem::where('order_detail_id', $orderDetail->id)
                ->where('direction', 'returned')
                ->whereHas('exchange', fn ($query) => $query->where('exchange_status', 'posted'))
                ->sum('quantity');
            $availableQuantity = max((int) $orderDetail->display_quantity - $previouslyReturned, 0);

            if ($returnQuantity < 1 || $returnQuantity > $availableQuantity) {
                throw new OrderFinancialException('Return quantity exceeds the quantity still available for exchange.');
            }

            $replacementItem = Item::withoutGlobalScopes()
                ->where('store_id', $lockedOrder->store_id)
                ->findOrFail((int) $data['replacement_item_id']);
            $replacementQuantity = (int) $data['replacement_quantity'];

            if ($replacementQuantity < 1) {
                throw new OrderFinancialException('Replacement quantity must be at least one.');
            }

            $returnedUnitPrice = round(max((float) $orderDetail->display_price, 0), 3);
            $replacementUnitPrice = array_key_exists('replacement_unit_price', $data)
                ? round(max((float) $data['replacement_unit_price'], 0), 3)
                : round(max((float) $replacementItem->price, 0), 3);
            $returnedAmount = round($returnedUnitPrice * $returnQuantity, 3);
            $replacementAmount = round($replacementUnitPrice * $replacementQuantity, 3);
            $difference = round($replacementAmount - $returnedAmount, 3);

            $exchange = $lockedOrder->exchanges()->create([
                'idempotency_key' => $data['idempotency_key'],
                'user_id' => $lockedOrder->is_guest ? null : $lockedOrder->user_id,
                'returned_amount' => $returnedAmount,
                'replacement_amount' => $replacementAmount,
                'difference_amount' => $difference,
                'affects_collectable' => ! $shipmentBooked,
                'exchange_status' => 'posted',
                'exchanged_at' => now(),
                'notes' => trim((string) ($data['notes'] ?? '')) ?: null,
                'created_by_admin_id' => $adminId,
            ]);

            $exchange->forceFill([
                'exchange_no' => sprintf('EX-%s-%06d', now()->format('Ymd'), $exchange->id),
            ])->save();

            $exchange->items()->createMany([
                [
                    'direction' => 'returned',
                    'order_detail_id' => $orderDetail->id,
                    'item_id' => $orderDetail->item?->id,
                    'item_name' => $orderDetail->display_item_name ?: 'Order item #'.$orderDetail->id,
                    'variation' => $orderDetail->variation,
                    'quantity' => $returnQuantity,
                    'unit_price' => $returnedUnitPrice,
                    'line_total' => $returnedAmount,
                ],
                [
                    'direction' => 'replacement',
                    'item_id' => $replacementItem->id,
                    'item_name' => $replacementItem->name,
                    'quantity' => $replacementQuantity,
                    'unit_price' => $replacementUnitPrice,
                    'line_total' => $replacementAmount,
                ],
            ]);

            $this->syncOrderPaymentState($lockedOrder);

            return $exchange->fresh(['items', 'createdByAdmin']);
        });
    }

    public function voidExchange(OrderExchange $exchange, ?int $adminId, ?string $reason = null): void
    {
        DB::transaction(function () use ($exchange, $adminId, $reason) {
            $lockedExchange = OrderExchange::lockForUpdate()->findOrFail($exchange->id);
            $lockedOrder = $this->lockedOrder($lockedExchange->order_id);

            if ($lockedExchange->exchange_status !== 'posted') {
                throw new OrderFinancialException('Only a posted exchange can be voided.');
            }

            if ($lockedExchange->affects_collectable) {
                $this->assertCourierCollectableIsMutable($lockedOrder);
            }

            $notes = trim(implode("\n", array_filter([
                $lockedExchange->notes,
                $reason ? 'Void reason: '.trim($reason) : null,
            ])));

            $lockedExchange->forceFill([
                'exchange_status' => 'voided',
                'voided_by_admin_id' => $adminId,
                'voided_at' => now(),
                'notes' => $notes ?: null,
            ])->save();

            $this->syncOrderPaymentState($lockedOrder);
        });
    }

    private function lockedOrder(int $orderId): Order
    {
        $relations = ['payments', 'receipts', 'exchanges'];
        if (Schema::hasTable('order_courier_shipments')) {
            $relations[] = 'courierShipment';
        }

        return Order::with($relations)->lockForUpdate()->findOrFail($orderId);
    }

    private function summary(Order $order): array
    {
        $order->unsetRelation('payments');
        $order->unsetRelation('receipts');
        $order->unsetRelation('exchanges');
        $order->load(['payments', 'receipts', 'exchanges']);

        return $order->financialSummary();
    }

    private function syncOrderPaymentState(Order $order): void
    {
        $summary = $this->summary($order);
        if (Schema::hasColumn('orders', 'partially_paid_amount')) {
            $order->partially_paid_amount = min($summary['paid_amount'], $summary['invoice_total']);
        }
        $order->payment_status = $summary['remaining_due'] <= 0.0005
            ? 'paid'
            : ($summary['paid_amount'] > 0 ? 'partially_paid' : 'unpaid');
        $order->save();
    }

    private function assertOrderCanReceiveAdvance(Order $order, float $allowExistingAmount = 0): void
    {
        if (! $order->canReceiveAdvance()) {
            throw new OrderFinancialException('Advance cannot be received after delivery, cancellation, failure, or refund.');
        }

        if ($this->summary($order)['remaining_due'] + $allowExistingAmount <= 0.0005) {
            throw new OrderFinancialException('This order has no remaining due.');
        }
    }

    private function assertCourierCollectableIsMutable(Order $order): void
    {
        if (Schema::hasTable('order_courier_shipments') && $order->courierShipment?->hasSuccessfulBooking()) {
            throw new OrderFinancialException('Courier booking already exists, so the collectable amount is locked.');
        }
    }

    private function assertCustomerAdvanceReceipt(OrderReceipt $receipt): void
    {
        if ($receipt->account_type !== OrderReceipt::ACCOUNT_TYPE_CUSTOMER_ADVANCE) {
            throw new OrderFinancialException('Only a customer advance receipt can be changed from this screen.');
        }
    }

    private function resolveReceiptAccount(array $data, string $paymentMethod): AccountingAccount
    {
        if (! empty($data['account_id'])) {
            return AccountingAccount::whereKey((int) $data['account_id'])
                ->where('is_active', true)
                ->where('type', 'asset')
                ->lockForUpdate()
                ->firstOrFail();
        }

        return $this->accountingVoucherService->accountForPaymentMethod($paymentMethod);
    }

    private function materializeLegacyPartialPayment(Order $order): void
    {
        $legacyPaid = round(max((float) ($order->partially_paid_amount ?? 0), 0), 3);
        if ($legacyPaid <= 0 || $order->payments()->where('payment_status', 'paid')->exists()) {
            return;
        }

        $payment = new OrderPayment();
        $payment->forceFill([
            'order_id' => $order->id,
            'transaction_ref' => 'legacy-partial-'.$order->id,
            'amount' => min($legacyPaid, (float) $order->order_amount),
            'payment_status' => 'paid',
            'payment_method' => 'legacy_partial_payment',
        ])->save();
    }

    private function resolveReceivedAt(mixed $value): Carbon
    {
        return filled($value) ? Carbon::parse($value)->startOfDay() : now();
    }

    private function syncCustomerLedger(Order $order, OrderReceipt $receipt, ?int $adminId): CustomerLedgerEntry
    {
        $order->unsetRelation('payments');
        $order->unsetRelation('receipts');
        $order->unsetRelation('exchanges');
        $order->load(['payments', 'receipts', 'exchanges']);

        $summary = $order->financialSummary();
        $nonReceiptPaid = round((float) $order->payments
            ->where('payment_status', 'paid')
            ->whereNull('order_receipt_id')
            ->sum('amount'), 3);
        $balance = max(round($summary['adjusted_total'] - $nonReceiptPaid, 3), 0);

        $advanceReceipts = $order->receipts
            ->where('account_type', OrderReceipt::ACCOUNT_TYPE_CUSTOMER_ADVANCE);

        foreach ($advanceReceipts->sortBy([
            ['received_at', 'asc'],
            ['id', 'asc'],
        ]) as $orderReceipt) {
            if ($orderReceipt->receipt_status === 'posted') {
                $balance = max(round($balance - (float) $orderReceipt->amount, 3), 0);
            }

            CustomerLedgerEntry::updateOrCreate(
                ['order_receipt_id' => $orderReceipt->id],
                [
                    'order_id' => $order->id,
                    'user_id' => $order->is_guest ? null : $order->user_id,
                    'entry_type' => 'advance_payment',
                    'debit' => 0,
                    'credit' => $orderReceipt->amount,
                    'balance_after' => $balance,
                    'status' => $orderReceipt->receipt_status,
                    'entry_date' => $orderReceipt->received_at ?: $orderReceipt->created_at ?: now(),
                    'note' => $orderReceipt->notes,
                    'created_by_admin_id' => $orderReceipt->created_by_admin_id ?: $adminId,
                    'voided_by_admin_id' => $orderReceipt->voided_by_admin_id,
                    'voided_at' => $orderReceipt->voided_at,
                ]
            );
        }

        return CustomerLedgerEntry::where('order_receipt_id', $receipt->id)->firstOrFail();
    }

    private function syncAdvanceAccounting(OrderReceipt $receipt, AccountingAccount $account, ?int $adminId): AccountingVoucher
    {
        $liability = $this->accountingVoucherService->accountForKey('customer_advance_liability');
        $voucher = AccountingVoucher::where('source_type', 'customer_advance_receipt')
            ->where('source_id', $receipt->id)
            ->lockForUpdate()
            ->first();

        if (! $voucher) {
            $voucherPayload = [
                'voucher_no' => 'CADV-'.now()->format('ymdHis').'-'.random_int(1000, 9999),
                'source_type' => 'customer_advance_receipt',
                'source_id' => $receipt->id,
                'source_fingerprint' => 'customer_advance_receipt:'.$receipt->id,
                'source_reference' => $receipt->voucher_no,
                'sale_channel' => 'website',
                'status' => 'posted',
                'voucher_date' => $receipt->received_at->toDateString(),
                'total_debit' => $receipt->amount,
                'total_credit' => $receipt->amount,
                'note' => $receipt->notes,
                'created_by_admin_id' => $adminId,
            ];
            if (! Schema::hasColumn('accounting_vouchers', 'source_fingerprint')) {
                unset($voucherPayload['source_fingerprint']);
            }
            $voucher = AccountingVoucher::create($voucherPayload);
        } else {
            $voucher->forceFill([
                'source_reference' => $receipt->voucher_no,
                'status' => 'posted',
                'voucher_date' => $receipt->received_at->toDateString(),
                'total_debit' => $receipt->amount,
                'total_credit' => $receipt->amount,
                'note' => $receipt->notes,
                'voided_by_admin_id' => null,
                'voided_at' => null,
            ])->save();
            $voucher->lines()->delete();
        }

        $partyId = $receipt->user_id;
        $voucher->lines()->createMany([
            [
                'account_id' => $account->id,
                'debit' => $receipt->amount,
                'credit' => 0,
                'party_type' => $partyId ? 'customer' : null,
                'party_id' => $partyId,
                'description' => 'Customer advance received for invoice #'.$receipt->order_id,
            ],
            [
                'account_id' => $liability->id,
                'debit' => 0,
                'credit' => $receipt->amount,
                'party_type' => $partyId ? 'customer' : null,
                'party_id' => $partyId,
                'description' => 'Customer advance liability for invoice #'.$receipt->order_id,
            ],
        ]);

        $receipt->forceFill(['accounting_voucher_id' => $voucher->id])->save();

        return $voucher->fresh('lines.account');
    }
}
