<?php

declare(strict_types=1);

namespace App\Business\Sales;

use App\Business\Inventory\Enums\InventoryBusinessType;
use App\Business\Inventory\Enums\InventoryDocumentStatus;
use App\Business\Inventory\Exceptions\InsufficientStockException;
use App\Business\Inventory\InventoryLedgerLineTrace;
use App\Business\Inventory\InventoryLedgerService;
use App\Business\Inventory\InventoryRefinementService;
use App\Business\Inventory\InventoryRuleService;
use App\Business\Product\DberpProductUnitConversionService;
use App\Business\Product\InventoryPostingProductSkuResolver;
use App\Business\Settlement\ReceivableService;
use App\Models\DberpInventoryBalance;
use App\Models\DberpProductSku;
use App\Models\DberpSalesReturnOrder;
use App\Models\DberpSalesReturnOrderItem;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class SalesReturnService
{
    public function __construct(
        private readonly InventoryLedgerService $ledgerService,
        private readonly InventoryRefinementService $refinementService,
        private readonly InventoryRuleService $ruleService,
        private readonly ReceivableService $receivableService,
        private readonly DberpAuditService $auditService,
    ) {}

    public function apply(DberpSalesReturnOrder $order): void
    {
        DB::transaction(function () use ($order): void {
            $locked = DberpSalesReturnOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
            $locked->loadMissing('salesOrder', 'salesOutboundOrder', 'warehouse');

            if ($locked->status !== InventoryDocumentStatus::Draft->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.inventory_doc_not_draft'),
                ]);
            }

            $this->validateOrderReference($locked);

            $items = $locked->items()->with(['product', 'salesOrderItem.productSku', 'salesOutboundItem.productSku'])->orderBy('sort_order')->orderBy('id')->get();

            if ($items->isEmpty()) {
                throw ValidationException::withMessages([
                    'items' => __('dberp.errors.sales_return_no_items'),
                ]);
            }

            foreach ($items as $item) {
                if ($locked->sales_order_id !== null && (int) $item->salesOrderItem->order_id !== (int) $locked->sales_order_id) {
                    throw ValidationException::withMessages([
                        'sales_order_item_id' => __('dberp.errors.sales_return_item_order_mismatch'),
                    ]);
                }

                if ($item->salesOutboundItem !== null && $locked->sales_outbound_order_id !== null && (int) $item->salesOutboundItem->order_id !== (int) $locked->sales_outbound_order_id) {
                    throw ValidationException::withMessages([
                        'sales_outbound_item_id' => __('dberp.errors.sales_return_item_outbound_mismatch'),
                    ]);
                }

                if ((int) $item->salesOrderItem->product_id !== (int) $item->product_id) {
                    throw ValidationException::withMessages([
                        'product_id' => __('dberp.errors.sales_item_product_mismatch'),
                    ]);
                }

                if ($item->salesOutboundItem !== null && (int) $item->salesOutboundItem->product_id !== (int) $item->product_id) {
                    throw ValidationException::withMessages([
                        'product_id' => __('dberp.errors.sales_item_product_mismatch'),
                    ]);
                }

                if ($item->salesOutboundItem !== null
                    && (int) $item->salesOutboundItem->product_sku_id !== (int) $item->salesOrderItem->product_sku_id) {
                    throw ValidationException::withMessages([
                        'product_sku_id' => __('dberp.errors.sales_item_sku_mismatch'),
                    ]);
                }

                $ledgerQty = (string) $item->base_return_quantity;
                $lineTrace = InventoryLedgerLineTrace::fromDocumentLine($item, 'return_quantity');

                if (bccomp($ledgerQty, '0', 4) !== 1) {
                    throw ValidationException::withMessages([
                        'return_quantity' => __('dberp.errors.sales_return_quantity_invalid'),
                    ]);
                }

                if ($item->salesOutboundItem !== null) {
                    $outLine = $item->salesOutboundItem;
                    $returnableBase = bcsub(
                        (string) ($outLine->base_ship_quantity ?? '0'),
                        (string) ($outLine->base_returned_quantity ?? '0'),
                        4
                    );

                    if (bccomp($ledgerQty, $returnableBase, 4) === 1) {
                        $this->auditService->log(
                            'dberp.sales_return.quantity_overflow_blocked',
                            $locked,
                            null,
                            [
                                'sales_order_item_id' => $item->sales_order_item_id,
                                'sales_outbound_item_id' => $item->sales_outbound_item_id,
                                'product_id' => $item->product_id,
                                'returnable_base' => $returnableBase,
                                'return_quantity' => (string) $item->return_quantity,
                                'base_return_quantity' => (string) $item->base_return_quantity,
                            ],
                        );

                        throw ValidationException::withMessages([
                            'return_quantity' => __('dberp.errors.sales_return_quantity_exceeds_returnable'),
                        ]);
                    }
                } else {
                    $returnable = (float) $item->salesOrderItem->shipped_quantity - (float) $item->salesOrderItem->returned_quantity;

                    if ((float) $item->return_quantity > $returnable) {
                        $this->auditService->log(
                            'dberp.sales_return.quantity_overflow_blocked',
                            $locked,
                            null,
                            [
                                'sales_order_item_id' => $item->sales_order_item_id,
                                'sales_outbound_item_id' => $item->sales_outbound_item_id,
                                'product_id' => $item->product_id,
                                'returnable' => number_format($returnable, 4, '.', ''),
                                'return_quantity' => (string) $item->return_quantity,
                            ],
                        );

                        throw ValidationException::withMessages([
                            'return_quantity' => __('dberp.errors.sales_return_quantity_exceeds_returnable'),
                        ]);
                    }
                }

                $returnApplyKey = sprintf('sales_return:%d:confirm:%d', (int) $locked->getKey(), (int) $item->getKey());

                $sku = $this->resolveProductSkuForSalesReturnItem($item);

                $post = $this->ledgerService->inbound(
                    productSku: $sku,
                    warehouse: $locked->warehouse,
                    quantity: $ledgerQty,
                    businessType: InventoryBusinessType::SalesReturn,
                    businessNumber: (string) $locked->code,
                    sourceType: $locked->getMorphClass(),
                    sourceId: (int) $locked->getKey(),
                    remark: $item->remark ?? $locked->remark,
                    businessTime: $locked->business_time ?? now(),
                    idempotencyKey: $returnApplyKey,
                    lineTrace: $lineTrace,
                );

                if (! $post->wasIdempotentReplay) {
                    $detail = $this->buildRefinementDetail($item);

                    $this->refinementService->assertDocumentLineSerialNumbersMatchQuantity(
                        $sku->product,
                        $ledgerQty,
                        $detail['serial_numbers'] ?? [],
                    );

                    $this->refinementService->rollbackOutbound(
                        product: $sku->product,
                        warehouse: $locked->warehouse,
                        quantity: $ledgerQty,
                        businessType: InventoryBusinessType::SalesReturn,
                        businessNumber: (string) $locked->code,
                        sourceType: $locked->getMorphClass(),
                        sourceId: (int) $locked->getKey(),
                        remark: $item->remark ?? $locked->remark,
                        businessTime: $locked->business_time ?? now(),
                        detail: $detail,
                    );

                    $item->forceFill([
                        'source_batch_id' => $detail['source_batch_id'] ?? $item->source_batch_id,
                        'batch_no' => $detail['batch_no'] ?? $item->batch_no,
                        'batch_allocations' => $detail['batch_allocations'] ?? [],
                        'serial_numbers' => $detail['serial_numbers'] ?? [],
                    ])->save();

                    $soLine = $item->salesOrderItem;
                    $deltaSo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                        (string) $item->base_return_quantity,
                        $soLine,
                    );

                    $soLine->forceFill([
                        'returned_quantity' => bcadd((string) $soLine->returned_quantity, $deltaSo, 4),
                    ])->save();

                    if ($item->salesOutboundItem !== null) {
                        $outLine = $item->salesOutboundItem;
                        $deltaOut = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                            (string) $item->base_return_quantity,
                            $outLine,
                        );

                        $outLine->forceFill([
                            'returned_quantity' => bcadd((string) $outLine->returned_quantity, $deltaOut, 4),
                            'base_returned_quantity' => bcadd((string) ($outLine->base_returned_quantity ?? '0'), (string) $item->base_return_quantity, 4),
                        ])->save();
                    }
                }
            }

            $this->refreshTotals($locked);

            $before = ['status' => $locked->status, 'effective_at' => $locked->effective_at];

            $this->auditService->logSemanticSave(
                'dberp.sales_return.applied',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => InventoryDocumentStatus::Effective->value,
                        'effective_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'effective_at' => $locked->effective_at],
            );

            if ($locked->salesOrder !== null) {
                $this->receivableService->refreshForSalesOrder(
                    order: $locked->salesOrder,
                    changeType: 'sales_return_applied',
                    remark: $locked->remark,
                    source: $locked,
                    documentNumber: (string) $locked->code,
                    businessTime: $locked->business_time,
                );
            }
        });
    }

    public function void(DberpSalesReturnOrder $order): void
    {
        try {
            DB::transaction(function () use ($order): void {
                $locked = DberpSalesReturnOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('warehouse');

                if ($locked->status !== InventoryDocumentStatus::Effective->value) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.inventory_doc_not_effective'),
                    ]);
                }

                if (! $this->ruleService->allowsEffectiveInventoryVoid()) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.inventory_void_policy_blocked'),
                    ]);
                }

                $rollbackInventoryEffects = $this->ruleService->allowVoidRollback();

                if ($rollbackInventoryEffects) {
                    $items = $locked->items()->with(['product', 'salesOrderItem.productSku', 'salesOutboundItem.productSku'])->orderBy('sort_order')->orderBy('id')->get();

                    $preflightVoidLines = [];

                    foreach ($items as $item) {
                        $ledgerQty = (string) $item->base_return_quantity;

                        if (bccomp($ledgerQty, '0', 4) !== 1) {
                            continue;
                        }

                        $sku = $this->resolveProductSkuForSalesReturnItem($item);

                        $preflightVoidLines[] = [
                            'sku' => $sku,
                            'warehouse' => $locked->warehouse,
                            'quantity' => $ledgerQty,
                        ];
                    }

                    if ($preflightVoidLines !== []) {
                        $this->ledgerService->assertNoOutboundShortagesPreflight(
                            $preflightVoidLines,
                            InventoryBusinessType::SalesReturnVoid,
                            (string) $locked->code,
                        );
                    }

                    foreach ($items as $item) {
                        $soLine = $item->salesOrderItem;
                        $ledgerQty = (string) $item->base_return_quantity;
                        $lineTrace = InventoryLedgerLineTrace::fromDocumentLine($item, 'return_quantity');

                        $ledgerReplayed = false;

                        if (bccomp($ledgerQty, '0', 4) === 1) {
                            $sku = $this->resolveProductSkuForSalesReturnItem($item);

                            $returnVoidKey = sprintf('sales_return:%d:void:%d', (int) $locked->getKey(), (int) $item->getKey());

                            $post = $this->ledgerService->outbound(
                                productSku: $sku,
                                warehouse: $locked->warehouse,
                                quantity: $ledgerQty,
                                businessType: InventoryBusinessType::SalesReturnVoid,
                                businessNumber: (string) $locked->code,
                                sourceType: $locked->getMorphClass(),
                                sourceId: (int) $locked->getKey(),
                                remark: $item->remark ?? $locked->remark,
                                businessTime: now(),
                                idempotencyKey: $returnVoidKey,
                                lineTrace: $lineTrace,
                            );

                            $ledgerReplayed = $post->wasIdempotentReplay;

                            if (! $post->wasIdempotentReplay) {
                                $this->refinementService->applyOutbound(
                                    product: $sku->product,
                                    warehouse: $locked->warehouse,
                                    quantity: $ledgerQty,
                                    businessType: InventoryBusinessType::SalesReturnVoid,
                                    businessNumber: (string) $locked->code,
                                    sourceType: $locked->getMorphClass(),
                                    sourceId: (int) $locked->getKey(),
                                    remark: $item->remark ?? $locked->remark,
                                    businessTime: now(),
                                    detail: $this->buildRefinementDetail($item),
                                );
                            }
                        }

                        if (! $ledgerReplayed) {
                            $deltaSo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                                (string) $item->base_return_quantity,
                                $soLine,
                            );

                            $soLine->forceFill([
                                'returned_quantity' => bcsub((string) $soLine->returned_quantity, $deltaSo, 4),
                            ])->save();

                            if ($item->salesOutboundItem !== null) {
                                $outLine = $item->salesOutboundItem;
                                $deltaOut = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                                    (string) $item->base_return_quantity,
                                    $outLine,
                                );

                                $outLine->forceFill([
                                    'returned_quantity' => bcsub((string) $outLine->returned_quantity, $deltaOut, 4),
                                    'base_returned_quantity' => bcsub((string) ($outLine->base_returned_quantity ?? '0'), (string) $item->base_return_quantity, 4),
                                ])->save();
                            }
                        }
                    }

                    if ($locked->salesOrder !== null) {
                        $this->receivableService->refreshForSalesOrder(
                            order: $locked->salesOrder,
                            changeType: 'sales_return_voided',
                            remark: $locked->remark,
                            source: $locked,
                            documentNumber: (string) $locked->code,
                            businessTime: now(),
                        );
                    }
                }

                $before = ['status' => $locked->status, 'voided_at' => $locked->voided_at];

                $this->auditService->logSemanticSave(
                    'dberp.sales_return.voided',
                    $locked,
                    function () use ($locked): void {
                        $locked->forceFill([
                            'status' => InventoryDocumentStatus::Voided->value,
                            'voided_at' => now(),
                        ])->save();
                    },
                    $before,
                    ['status' => $locked->status, 'voided_at' => $locked->voided_at],
                );
            });
        } catch (InsufficientStockException $exception) {
            $this->auditInsufficientStock($exception);

            throw $exception->validationException();
        }
    }

    public function refreshTotals(DberpSalesReturnOrder $order): void
    {
        $totals = $order->items()->selectRaw('COALESCE(SUM(return_quantity), 0) as total_return_quantity, COALESCE(SUM(return_amount), 0) as total_amount')->first();

        DberpAuditTrailGuard::withoutAuditTrail(function () use ($order, $totals): void {
            $order->forceFill([
                'total_return_quantity' => (string) ($totals?->total_return_quantity ?? '0.0000'),
                'total_amount' => (string) ($totals?->total_amount ?? '0.0000'),
            ])->save();
        });
    }

    private function validateOrderReference(DberpSalesReturnOrder $order): void
    {
        if ($order->sales_order_id === null && $order->sales_outbound_order_id === null) {
            throw ValidationException::withMessages([
                'sales_order_id' => __('dberp.errors.sales_return_reference_required'),
            ]);
        }

        if ($order->sales_outbound_order_id !== null && $order->salesOutboundOrder !== null) {
            $outboundOrder = $order->salesOutboundOrder;

            if ($order->sales_order_id !== null && (int) $outboundOrder->sales_order_id !== (int) $order->sales_order_id) {
                throw ValidationException::withMessages([
                    'sales_outbound_order_id' => __('dberp.errors.sales_return_reference_mismatch'),
                ]);
            }

            if ((int) $outboundOrder->customer_id !== (int) $order->customer_id) {
                throw ValidationException::withMessages([
                    'customer_id' => __('dberp.errors.sales_customer_mismatch'),
                ]);
            }
        }

        if ($order->salesOrder !== null && (int) $order->salesOrder->customer_id !== (int) $order->customer_id) {
            throw ValidationException::withMessages([
                'customer_id' => __('dberp.errors.sales_customer_mismatch'),
            ]);
        }
    }

    private function auditInsufficientStock(InsufficientStockException $exception): void
    {
        $balanceQuery = DberpInventoryBalance::query()->where('warehouse_id', $exception->warehouseId);

        if ($exception->productSkuId !== null) {
            $balanceQuery->where('product_sku_id', $exception->productSkuId);
        } else {
            $balanceQuery->where('product_id', $exception->productId);
        }

        $balance = $balanceQuery->first();

        $this->auditService->log(
            'dberp.inventory.insufficient_stock_blocked',
            $balance,
            null,
            $exception->auditContext(),
        );
    }

    private function resolveProductSkuForSalesReturnItem(DberpSalesReturnOrderItem $item): DberpProductSku
    {
        $product = $item->product;

        if ($item->product_sku_id !== null && (int) $item->product_sku_id > 0) {
            $sku = InventoryPostingProductSkuResolver::resolveExplicitSkuForProduct($product, (int) $item->product_sku_id);
            $this->assertSalesReturnLineSkuMatchesUpstreamSources($item, (int) $sku->getKey());

            return $sku;
        }

        $outboundItem = $item->salesOutboundItem;
        if ($outboundItem !== null && $outboundItem->productSku instanceof DberpProductSku) {
            return $outboundItem->productSku;
        }

        if ($item->salesOrderItem->productSku instanceof DberpProductSku) {
            return $item->salesOrderItem->productSku;
        }

        return InventoryPostingProductSkuResolver::resolveSkuForPosting($product, null);
    }

    private function assertSalesReturnLineSkuMatchesUpstreamSources(DberpSalesReturnOrderItem $item, int $resolvedSkuId): void
    {
        $expected = null;
        if ($item->salesOutboundItem !== null && $item->salesOutboundItem->product_sku_id !== null && (int) $item->salesOutboundItem->product_sku_id > 0) {
            $expected = (int) $item->salesOutboundItem->product_sku_id;
        } elseif ($item->salesOrderItem->product_sku_id !== null && (int) $item->salesOrderItem->product_sku_id > 0) {
            $expected = (int) $item->salesOrderItem->product_sku_id;
        }

        if ($expected !== null && $expected !== $resolvedSkuId) {
            throw ValidationException::withMessages([
                'items' => (string) __('dberp.errors.sales_return_line_spec_upstream_mismatch'),
            ]);
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function buildRefinementDetail($item): array
    {
        $sourceAllocations = $item->batch_allocations ?: $item->salesOutboundItem?->batch_allocations ?: [];

        $serialNumbers = $item->serial_numbers ?: [];

        if ($serialNumbers === [] && $item->salesOutboundItem !== null) {
            $sourceSerialNumbers = $item->salesOutboundItem->serial_numbers ?? [];

            if (count($sourceSerialNumbers) === (int) (float) $item->return_quantity) {
                $serialNumbers = $sourceSerialNumbers;
            }
        }

        return [
            'source_batch_id' => $item->source_batch_id ?? $item->salesOutboundItem?->source_batch_id,
            'batch_no' => $item->batch_no ?? $item->salesOutboundItem?->batch_no,
            'production_date' => $item->production_date?->format('Y-m-d') ?? $item->salesOutboundItem?->production_date?->format('Y-m-d'),
            'expiry_date' => $item->expiry_date?->format('Y-m-d') ?? $item->salesOutboundItem?->expiry_date?->format('Y-m-d'),
            'shelf_life_days' => (int) ($item->shelf_life_days ?: $item->salesOutboundItem?->shelf_life_days ?: 0),
            'batch_allocations' => $this->trimAllocations($sourceAllocations, (float) $item->return_quantity),
            'serial_numbers' => $serialNumbers,
        ];
    }

    /**
     * @param  array<int, array<string, mixed>>  $allocations
     * @return array<int, array<string, mixed>>
     */
    private function trimAllocations(array $allocations, float $requestedQuantity): array
    {
        $remaining = $requestedQuantity;
        $resolved = [];

        foreach ($allocations as $allocation) {
            if ($remaining <= 0) {
                break;
            }

            $availableQuantity = (float) ($allocation['quantity'] ?? 0);
            $quantity = min($availableQuantity, $remaining);

            if ($quantity <= 0) {
                continue;
            }

            $allocation['quantity'] = number_format($quantity, 4, '.', '');
            $resolved[] = $allocation;
            $remaining -= $quantity;
        }

        return $resolved;
    }
}
