<?php

declare(strict_types=1);

namespace App\Business\MasterData;

use App\Business\Inventory\Enums\InventoryBusinessType;
use App\Business\Inventory\Enums\InventoryChangeDirection;
use App\Business\Inventory\Enums\StocktakeDifferenceStatus;
use App\Business\Purchase\Enums\PurchaseOrderStatus;
use App\Business\Purchase\PurchaseOrderOverviewService;
use App\Business\Sales\Enums\SalesOrderStatus;
use App\Business\Sales\SalesOrderOverviewService;
use App\Filament\Resources\DberpInventoryBalances\DberpInventoryBalanceResource;
use App\Filament\Resources\DberpInventoryTransactions\DberpInventoryTransactionResource;
use App\Filament\Resources\DberpProductOperationQueries\DberpProductOperationQueryResource;
use App\Filament\Resources\DberpPurchaseOrders\DberpPurchaseOrderResource;
use App\Filament\Resources\DberpSalesOrders\DberpSalesOrderResource;
use App\Filament\Resources\DberpStocktakeDifferences\DberpStocktakeDifferenceResource;
use App\Models\DberpInventoryBalance;
use App\Models\DberpInventoryTransaction;
use App\Models\DberpProduct;
use App\Models\DberpPurchaseOrderItem;
use App\Models\DberpSalesOrderItem;
use App\Models\DberpStocktakeDifference;
use App\Support\DberpFilamentListPageUrl;
use App\Support\DberpNumberFormatter;
use App\Support\DberpReportSkuPresentation;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * 商品档案查看/编辑页只读库存经营聚合，不修改商品、库存或订单数据。
 */
final class ProductBusinessOverviewService
{
    public const int RELATED_LIST_LIMIT = 10;

    public const int STOCK_SUMMARY_LIMIT = 20;

    public function __construct(
        private readonly PurchaseOrderOverviewService $purchaseOrderOverviewService,
        private readonly SalesOrderOverviewService $salesOrderOverviewService,
    ) {}

    public function loadOverviewRelations(DberpProduct $product): DberpProduct
    {
        return $product->loadMissing(['unit', 'skus']);
    }

    /**
     * @return list<array{label: string, value: string}>
     */
    public function headerSummary(DberpProduct $product): array
    {
        $product = $this->loadOverviewRelations($product);

        $statusLabel = $product->is_enabled
            ? (string) __('dberp.messages.product_status_enabled')
            : (string) __('dberp.messages.product_status_disabled');

        $skuCount = (string) $product->skus()->count();

        $rows = [
            [
                'label' => (string) __('dberp.fields.product_status'),
                'value' => $statusLabel,
            ],
            [
                'label' => (string) __('dberp.fields.product_sku_count'),
                'value' => $skuCount,
            ],
        ];

        if (! (bool) $product->manages_inventory) {
            $rows[] = [
                'label' => (string) __('dberp.fields.total_stock_quantity'),
                'value' => (string) __('dberp.messages.product_overview_not_inventory_managed'),
            ];
            $rows[] = [
                'label' => (string) __('dberp.fields.latest_purchase_price'),
                'value' => $this->formatLatestPurchasePrice($product),
            ];
            $rows[] = [
                'label' => (string) __('dberp.fields.latest_sales_price'),
                'value' => $this->formatLatestSalesPrice($product),
            ];

            return $rows;
        }

        $rows[] = [
            'label' => (string) __('dberp.fields.total_stock_quantity'),
            'value' => DberpNumberFormatter::quantity($this->sumBalanceColumn($product, 'realtime_quantity')),
        ];
        $rows[] = [
            'label' => (string) __('dberp.fields.available_stock'),
            'value' => DberpNumberFormatter::quantity($this->sumBalanceColumn($product, 'available_quantity')),
        ];
        $rows[] = [
            'label' => (string) __('dberp.fields.pending_inbound'),
            'value' => DberpNumberFormatter::quantity($this->pendingInboundQuantitySum($product)),
        ];
        $rows[] = [
            'label' => (string) __('dberp.fields.pending_outbound'),
            'value' => DberpNumberFormatter::quantity($this->pendingOutboundQuantitySum($product)),
        ];
        $rows[] = [
            'label' => (string) __('dberp.fields.latest_purchase_price'),
            'value' => $this->formatLatestPurchasePrice($product),
        ];
        $rows[] = [
            'label' => (string) __('dberp.fields.latest_sales_price'),
            'value' => $this->formatLatestSalesPrice($product),
        ];

        return $rows;
    }

    /**
     * @return list<array{
     *     spec_summary: string,
     *     warehouse: string,
     *     current_stock: string,
     *     available_stock: string,
     *     safety_stock: string,
     *     latest_inbound: string,
     *     latest_outbound: string,
     * }>
     */
    public function skuWarehouseStockSummaries(DberpProduct $product): array
    {
        if (! (bool) $product->manages_inventory) {
            return [];
        }

        $balances = DberpInventoryBalance::query()
            ->with(['productSku', 'warehouse'])
            ->where('product_id', $product->getKey())
            ->orderByDesc('realtime_quantity')
            ->orderBy('warehouse_id')
            ->orderBy('product_sku_id')
            ->limit(self::STOCK_SUMMARY_LIMIT)
            ->get();

        if ($balances->isEmpty()) {
            return [];
        }

        $movementDates = $this->latestMovementDatesBySkuWarehouse((int) $product->getKey(), $balances);

        $rows = [];

        foreach ($balances as $balance) {
            if (! $balance instanceof DberpInventoryBalance) {
                continue;
            }

            $skuId = (int) ($balance->product_sku_id ?? 0);
            $warehouseId = (int) $balance->warehouse_id;
            $key = $skuId.'|'.$warehouseId;

            $rows[] = [
                'spec_summary' => DberpReportSkuPresentation::inventoryBalanceSpecSummary(
                    $balance->product_sku_id,
                    $balance->productSku?->spec_text,
                    $balance->productSku?->sku_code,
                ),
                'warehouse' => (string) ($balance->warehouse?->name ?? __('dberp.messages.not_available')),
                'current_stock' => DberpNumberFormatter::quantity($balance->realtime_quantity),
                'available_stock' => DberpNumberFormatter::quantity($balance->available_quantity),
                'safety_stock' => DberpNumberFormatter::quantity($balance->safety_stock),
                'latest_inbound' => $movementDates[$key]['in'] ?? (string) __('dberp.messages.not_available'),
                'latest_outbound' => $movementDates[$key]['out'] ?? (string) __('dberp.messages.not_available'),
            ];
        }

        return $rows;
    }

    /**
     * @return list<array{
     *     code: string,
     *     business_time: string,
     *     partner: string,
     *     quantity: string,
     *     price: string,
     *     status: string,
     *     url: ?string,
     * }>
     */
    public function recentPurchaseLineSummaries(DberpProduct $product): array
    {
        if (! DberpPurchaseOrderResource::canViewAny()) {
            return [];
        }

        $canLink = DberpPurchaseOrderResource::canViewAny();

        $items = DberpPurchaseOrderItem::query()
            ->with(['order.supplier'])
            ->where('dberp_purchase_order_items.product_id', $product->getKey())
            ->join('dberp_purchase_orders as purchase_orders', 'purchase_orders.id', '=', 'dberp_purchase_order_items.order_id')
            ->whereNot('purchase_orders.status', PurchaseOrderStatus::Voided->value)
            ->orderByDesc('purchase_orders.business_time')
            ->orderByDesc('dberp_purchase_order_items.id')
            ->select('dberp_purchase_order_items.*')
            ->limit(self::RELATED_LIST_LIMIT)
            ->get();

        $rows = [];

        foreach ($items as $item) {
            if (! $item instanceof DberpPurchaseOrderItem) {
                continue;
            }

            $order = $item->order;
            $status = $order !== null
                ? (PurchaseOrderStatus::tryFrom((string) $order->status)?->label() ?? (string) __('dberp.messages.not_available'))
                : (string) __('dberp.messages.not_available');

            $rows[] = [
                'code' => (string) ($order?->code ?? $item->product_code ?? __('dberp.messages.not_available')),
                'business_time' => $order?->business_time?->format('Y-m-d H:i') ?? (string) __('dberp.messages.not_available'),
                'partner' => (string) ($order?->supplier?->name ?? __('dberp.messages.not_available')),
                'quantity' => DberpNumberFormatter::quantity($item->quantity),
                'price' => DberpNumberFormatter::price($item->unit_price),
                'status' => $status,
                'url' => $canLink && $order !== null
                    ? DberpPurchaseOrderResource::getUrl('edit', ['record' => $order->getKey()])
                    : null,
            ];
        }

        return $rows;
    }

    /**
     * @return list<array{
     *     code: string,
     *     business_time: string,
     *     partner: string,
     *     quantity: string,
     *     price: string,
     *     status: string,
     *     url: ?string,
     * }>
     */
    public function recentSalesLineSummaries(DberpProduct $product): array
    {
        if (! DberpSalesOrderResource::canViewAny()) {
            return [];
        }

        $canLink = DberpSalesOrderResource::canViewAny();

        $items = DberpSalesOrderItem::query()
            ->with(['order.customer'])
            ->where('dberp_sales_order_items.product_id', $product->getKey())
            ->join('dberp_sales_orders as sales_orders', 'sales_orders.id', '=', 'dberp_sales_order_items.order_id')
            ->whereNot('sales_orders.status', SalesOrderStatus::Voided->value)
            ->orderByDesc('sales_orders.business_time')
            ->orderByDesc('dberp_sales_order_items.id')
            ->select('dberp_sales_order_items.*')
            ->limit(self::RELATED_LIST_LIMIT)
            ->get();

        $rows = [];

        foreach ($items as $item) {
            if (! $item instanceof DberpSalesOrderItem) {
                continue;
            }

            $order = $item->order;
            $status = $order !== null
                ? (SalesOrderStatus::tryFrom((string) $order->status)?->label() ?? (string) __('dberp.messages.not_available'))
                : (string) __('dberp.messages.not_available');

            $rows[] = [
                'code' => (string) ($order?->code ?? $item->product_code ?? __('dberp.messages.not_available')),
                'business_time' => $order?->business_time?->format('Y-m-d H:i') ?? (string) __('dberp.messages.not_available'),
                'partner' => (string) ($order?->customer?->name ?? __('dberp.messages.not_available')),
                'quantity' => DberpNumberFormatter::quantity($item->quantity),
                'price' => DberpNumberFormatter::price($item->unit_price),
                'status' => $status,
                'url' => $canLink && $order !== null
                    ? DberpSalesOrderResource::getUrl('edit', ['record' => $order->getKey()])
                    : null,
            ];
        }

        return $rows;
    }

    /**
     * @return list<array{
     *     code: string,
     *     business_time: string,
     *     partner: string,
     *     quantity: string,
     *     price: string,
     *     status: string,
     *     url: ?string,
     * }>
     */
    public function recentInventoryMovementSummaries(DberpProduct $product): array
    {
        if (! DberpInventoryTransactionResource::canViewAny()) {
            return [];
        }

        $transactions = DberpInventoryTransaction::query()
            ->with(['warehouse'])
            ->where('product_id', $product->getKey())
            ->orderByDesc('business_time')
            ->orderByDesc('id')
            ->limit(self::RELATED_LIST_LIMIT)
            ->get();

        $rows = [];

        foreach ($transactions as $transaction) {
            if (! $transaction instanceof DberpInventoryTransaction) {
                continue;
            }

            $businessType = InventoryBusinessType::tryFrom((string) $transaction->business_type);
            $direction = InventoryChangeDirection::tryFrom((string) $transaction->direction);

            $statusParts = array_filter([
                $businessType?->label(),
                $direction?->label(),
            ]);

            $rows[] = [
                'code' => (string) ($transaction->business_number !== '' ? $transaction->business_number : __('dberp.messages.not_available')),
                'business_time' => $transaction->business_time?->format('Y-m-d H:i') ?? (string) __('dberp.messages.not_available'),
                'partner' => (string) ($transaction->warehouse?->name ?? __('dberp.messages.not_available')),
                'quantity' => DberpNumberFormatter::quantity($transaction->change_quantity),
                'price' => '—',
                'status' => $statusParts !== []
                    ? implode(' / ', $statusParts)
                    : (string) __('dberp.messages.not_available'),
                'url' => $this->inventoryTransactionIndexUrl($product),
            ];
        }

        return $rows;
    }

    /**
     * @return list<array{
     *     code: string,
     *     business_time: string,
     *     partner: string,
     *     quantity: string,
     *     price: string,
     *     status: string,
     *     url: ?string,
     * }>
     */
    public function recentStocktakeDifferenceSummaries(DberpProduct $product): array
    {
        if (! DberpStocktakeDifferenceResource::canViewAny()) {
            return [];
        }

        $canLink = DberpStocktakeDifferenceResource::canViewAny();

        $differences = DberpStocktakeDifference::query()
            ->with(['stocktakeOrder', 'warehouse'])
            ->where('product_id', $product->getKey())
            ->orderByDesc('handled_at')
            ->orderByDesc('id')
            ->limit(self::RELATED_LIST_LIMIT)
            ->get();

        $rows = [];

        foreach ($differences as $difference) {
            if (! $difference instanceof DberpStocktakeDifference) {
                continue;
            }

            $rows[] = [
                'code' => (string) ($difference->stocktakeOrder?->code ?? __('dberp.messages.not_available')),
                'business_time' => $difference->handled_at?->format('Y-m-d H:i')
                    ?? (string) __('dberp.messages.not_available'),
                'partner' => (string) ($difference->warehouse?->name ?? __('dberp.messages.not_available')),
                'quantity' => DberpNumberFormatter::quantity($difference->difference_quantity),
                'price' => '—',
                'status' => StocktakeDifferenceStatus::tryFrom((string) $difference->status)?->label()
                    ?? (string) __('dberp.messages.not_available'),
                'url' => $canLink
                    ? DberpStocktakeDifferenceResource::getUrl('edit', ['record' => $difference->getKey()])
                    : null,
            ];
        }

        return $rows;
    }

    public function productOperationQueryUrl(DberpProduct $product): ?string
    {
        if (! DberpProductOperationQueryResource::canViewAny()) {
            return null;
        }

        return DberpFilamentListPageUrl::append(
            DberpProductOperationQueryResource::getUrl('index'),
            [
                'product_id' => ['value' => (int) $product->getKey()],
            ],
        );
    }

    public function inventoryBalanceIndexUrl(DberpProduct $product): ?string
    {
        if (! DberpInventoryBalanceResource::canViewAny()) {
            return null;
        }

        return DberpFilamentListPageUrl::append(
            DberpInventoryBalanceResource::getUrl('index'),
            [
                'product_id' => ['value' => (int) $product->getKey()],
            ],
        );
    }

    public function inventoryTransactionIndexUrl(DberpProduct $product): ?string
    {
        if (! DberpInventoryTransactionResource::canViewAny()) {
            return null;
        }

        return DberpFilamentListPageUrl::append(
            DberpInventoryTransactionResource::getUrl('index'),
            [
                'product_id' => ['value' => (int) $product->getKey()],
            ],
        );
    }

    public function countStockBalanceRows(DberpProduct $product): int
    {
        return DberpInventoryBalance::query()
            ->where('product_id', $product->getKey())
            ->count();
    }

    /**
     * @return array{
     *     header: list<array{label: string, value: string}>,
     *     stock_rows: list<array<string, string>>,
     *     purchase_lines: list<array<string, mixed>>,
     *     sales_lines: list<array<string, mixed>>,
     *     inventory_movements: list<array<string, mixed>>,
     *     stocktake_differences: list<array<string, mixed>>,
     *     product_operation_query_url: ?string,
     *     inventory_balance_url: ?string,
     *     inventory_transaction_url: ?string,
     * }
     */
    public function viewDataForProduct(?DberpProduct $product): array
    {
        if (! $product instanceof DberpProduct) {
            return [
                'header' => [],
                'stock_rows' => [],
                'purchase_lines' => [],
                'sales_lines' => [],
                'inventory_movements' => [],
                'stocktake_differences' => [],
                'product_operation_query_url' => null,
                'inventory_balance_url' => null,
                'inventory_transaction_url' => null,
            ];
        }

        return [
            'header' => $this->headerSummary($product),
            'stock_rows' => $this->skuWarehouseStockSummaries($product),
            'purchase_lines' => $this->recentPurchaseLineSummaries($product),
            'sales_lines' => $this->recentSalesLineSummaries($product),
            'inventory_movements' => $this->recentInventoryMovementSummaries($product),
            'stocktake_differences' => $this->recentStocktakeDifferenceSummaries($product),
            'product_operation_query_url' => $this->productOperationQueryUrl($product),
            'inventory_balance_url' => $this->inventoryBalanceIndexUrl($product),
            'inventory_transaction_url' => $this->inventoryTransactionIndexUrl($product),
        ];
    }

    private function sumBalanceColumn(DberpProduct $product, string $column): string
    {
        $sum = DberpInventoryBalance::query()
            ->where('product_id', $product->getKey())
            ->sum($column);

        return is_numeric($sum) ? (string) $sum : '0';
    }

    private function pendingInboundQuantitySum(DberpProduct $product): string
    {
        $items = DberpPurchaseOrderItem::query()
            ->where('product_id', $product->getKey())
            ->whereHas('order', fn (Builder $query): Builder => $query->whereIn('status', $this->pendingInboundOrderStatuses()))
            ->get(['id', 'quantity', 'received_quantity', 'remaining_arrival_quantity']);

        $total = '0.0000';

        foreach ($items as $item) {
            if (! $item instanceof DberpPurchaseOrderItem) {
                continue;
            }

            $remaining = $this->purchaseOrderOverviewService->displayRemainingArrivalQuantity($item);
            if (bccomp($remaining, '0', 4) === 1) {
                $total = bcadd($total, $remaining, 4);
            }
        }

        return $total;
    }

    private function pendingOutboundQuantitySum(DberpProduct $product): string
    {
        $items = DberpSalesOrderItem::query()
            ->where('product_id', $product->getKey())
            ->whereHas('order', fn (Builder $query): Builder => $query->whereIn('status', $this->pendingOutboundOrderStatuses()))
            ->get(['id', 'quantity', 'shipped_quantity']);

        $total = '0.0000';

        foreach ($items as $item) {
            if (! $item instanceof DberpSalesOrderItem) {
                continue;
            }

            $pending = $this->salesOrderOverviewService->pendingShipQuantity($item);
            if (bccomp($pending, '0', 4) === 1) {
                $total = bcadd($total, $pending, 4);
            }
        }

        return $total;
    }

    /**
     * @return list<string>
     */
    private function pendingInboundOrderStatuses(): array
    {
        return [
            PurchaseOrderStatus::Confirmed->value,
            PurchaseOrderStatus::PartiallyReceived->value,
        ];
    }

    /**
     * @return list<string>
     */
    private function pendingOutboundOrderStatuses(): array
    {
        return [
            SalesOrderStatus::Confirmed->value,
            SalesOrderStatus::PartiallyShipped->value,
        ];
    }

    private function formatLatestPurchasePrice(DberpProduct $product): string
    {
        $line = $this->latestPurchaseOrderItem($product);

        if ($line instanceof DberpPurchaseOrderItem) {
            return DberpNumberFormatter::price($line->unit_price)
                .' ('.(string) __('dberp.hints.product_overview_price_from_recent_purchase').')';
        }

        return DberpNumberFormatter::price($product->cost_price)
            .' ('.(string) __('dberp.hints.product_overview_price_from_master').')';
    }

    private function formatLatestSalesPrice(DberpProduct $product): string
    {
        $line = $this->latestSalesOrderItem($product);

        if ($line instanceof DberpSalesOrderItem) {
            return DberpNumberFormatter::price($line->unit_price)
                .' ('.(string) __('dberp.hints.product_overview_price_from_recent_sales').')';
        }

        return DberpNumberFormatter::price($product->sales_price)
            .' ('.(string) __('dberp.hints.product_overview_price_from_master').')';
    }

    private function latestPurchaseOrderItem(DberpProduct $product): ?DberpPurchaseOrderItem
    {
        $item = DberpPurchaseOrderItem::query()
            ->where('dberp_purchase_order_items.product_id', $product->getKey())
            ->join('dberp_purchase_orders as purchase_orders', 'purchase_orders.id', '=', 'dberp_purchase_order_items.order_id')
            ->whereNot('purchase_orders.status', PurchaseOrderStatus::Voided->value)
            ->whereNot('purchase_orders.status', PurchaseOrderStatus::Draft->value)
            ->orderByDesc('purchase_orders.business_time')
            ->orderByDesc('dberp_purchase_order_items.id')
            ->select('dberp_purchase_order_items.*')
            ->first();

        return $item instanceof DberpPurchaseOrderItem ? $item : null;
    }

    private function latestSalesOrderItem(DberpProduct $product): ?DberpSalesOrderItem
    {
        $item = DberpSalesOrderItem::query()
            ->where('dberp_sales_order_items.product_id', $product->getKey())
            ->join('dberp_sales_orders as sales_orders', 'sales_orders.id', '=', 'dberp_sales_order_items.order_id')
            ->whereNot('sales_orders.status', SalesOrderStatus::Voided->value)
            ->whereNot('sales_orders.status', SalesOrderStatus::Draft->value)
            ->orderByDesc('sales_orders.business_time')
            ->orderByDesc('dberp_sales_order_items.id')
            ->select('dberp_sales_order_items.*')
            ->first();

        return $item instanceof DberpSalesOrderItem ? $item : null;
    }

    /**
     * @param  Collection<int, DberpInventoryBalance>  $balances
     * @return array<string, array{in: string, out: string}>
     */
    private function latestMovementDatesBySkuWarehouse(int $productId, Collection $balances): array
    {
        $warehouseIds = $balances->pluck('warehouse_id')->unique()->filter()->values()->all();

        if ($warehouseIds === []) {
            return [];
        }

        $inboundRows = DberpInventoryTransaction::query()
            ->select([
                'product_sku_id',
                'warehouse_id',
                DB::raw('MAX(business_time) as last_time'),
            ])
            ->where('product_id', $productId)
            ->whereIn('warehouse_id', $warehouseIds)
            ->where('direction', InventoryChangeDirection::In->value)
            ->groupBy('product_sku_id', 'warehouse_id')
            ->get();

        $outboundRows = DberpInventoryTransaction::query()
            ->select([
                'product_sku_id',
                'warehouse_id',
                DB::raw('MAX(business_time) as last_time'),
            ])
            ->where('product_id', $productId)
            ->whereIn('warehouse_id', $warehouseIds)
            ->where('direction', InventoryChangeDirection::Out->value)
            ->groupBy('product_sku_id', 'warehouse_id')
            ->get();

        $map = [];

        foreach ($inboundRows as $row) {
            $skuId = (int) ($row->product_sku_id ?? 0);
            $warehouseId = (int) $row->warehouse_id;
            $key = $skuId.'|'.$warehouseId;
            $map[$key]['in'] = $row->last_time !== null
                ? (string) Carbon::parse($row->last_time)->format('Y-m-d H:i')
                : (string) __('dberp.messages.not_available');
        }

        foreach ($outboundRows as $row) {
            $skuId = (int) ($row->product_sku_id ?? 0);
            $warehouseId = (int) $row->warehouse_id;
            $key = $skuId.'|'.$warehouseId;
            $map[$key]['out'] = $row->last_time !== null
                ? (string) Carbon::parse($row->last_time)->format('Y-m-d H:i')
                : (string) __('dberp.messages.not_available');
        }

        return $map;
    }
}
