<?php

namespace App\Services;

use App\Models\Documents\Document;
use App\Models\Documents\DocumentApproval;
use App\Models\Documents\DocumentData;
use App\Models\Items\Item;
use App\Models\Orders\Order;
use App\Models\Orders\OrderHistory;
use App\Models\Orders\OrderNode;
use App\Models\Production\WorkOrder;
use App\Models\Production\WorkOrderMaterialInput;
use App\Models\Quote\Quote;
use App\Models\Tenants\Sale;
use App\Services\Production\BendingInfoBuilder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class OrderService extends Service
{
    public function __construct(
        private NumberingService $numberingService
    ) {}

    /**
     * 상세 조회용 공통 relations (show와 동일한 구조 보장)
     */
    private function loadDetailRelations(Order $order): Order
    {
        return $order->load([
            'client:id,name,contact_person,phone,email,manager_name',
            'items' => fn ($q) => $q->orderBy('sort_order'),
            'rootNodes' => fn ($q) => $q->withRecursiveChildren(),
            'quote:id,quote_number,site_name,calculation_inputs',
        ]);
    }

    /**
     * 목록 조회 (검색/필터링/페이징)
     */
    public function index(array $params)
    {
        $tenantId = $this->tenantId();

        $page = (int) ($params['page'] ?? 1);
        $size = (int) ($params['size'] ?? 20);
        $q = trim((string) ($params['q'] ?? ''));
        $status = $params['status'] ?? null;
        $orderType = $params['order_type'] ?? null;
        $clientId = $params['client_id'] ?? null;
        $dateFrom = $params['date_from'] ?? null;
        $dateTo = $params['date_to'] ?? null;
        $forWorkOrder = filter_var($params['for_work_order'] ?? false, FILTER_VALIDATE_BOOLEAN);

        $query = Order::query()
            ->where('tenant_id', $tenantId)
            ->with(['client:id,name,manager_name', 'items', 'quote:id,quote_number', 'rootNodes:id,order_id,name,options'])
            ->withSum('rootNodes', 'quantity');

        // 작업지시 생성 가능한 수주만 필터링
        if ($forWorkOrder) {
            // 1. DRAFT(등록) 상태만 (생산지시 전)
            $query->where('status_code', Order::STATUS_DRAFT);

            // 2. 작업지시가 아직 없는 수주만
            $query->whereDoesntHave('workOrders');
        }

        // 검색어 (수주번호, 현장명, 거래처명)
        if ($q !== '') {
            $query->where(function ($qq) use ($q) {
                $qq->where('order_no', 'like', "%{$q}%")
                    ->orWhere('site_name', 'like', "%{$q}%")
                    ->orWhere('client_name', 'like', "%{$q}%");
            });
        }

        // 상태 필터 (for_work_order와 함께 사용시 무시)
        if ($status !== null && ! $forWorkOrder) {
            $query->where('status_code', $status);
        }

        // 주문유형 필터 (ORDER/PURCHASE)
        if ($orderType !== null) {
            $query->where('order_type_code', $orderType);
        }

        // 거래처 필터
        if ($clientId !== null) {
            $query->where('client_id', $clientId);
        }

        // 날짜 범위 (수주일 기준)
        if ($dateFrom !== null) {
            $query->where('received_at', '>=', $dateFrom);
        }
        if ($dateTo !== null) {
            $query->where('received_at', '<=', $dateTo);
        }

        $query->orderByDesc('created_at');

        return $query->paginate($size, ['*'], 'page', $page);
    }

    /**
     * 통계 조회
     */
    public function stats(): array
    {
        $tenantId = $this->tenantId();

        $counts = Order::where('tenant_id', $tenantId)
            ->select('status_code', DB::raw('count(*) as count'))
            ->groupBy('status_code')
            ->pluck('count', 'status_code')
            ->toArray();

        $amounts = Order::where('tenant_id', $tenantId)
            ->select('status_code', DB::raw('sum(total_amount) as total'))
            ->groupBy('status_code')
            ->pluck('total', 'status_code')
            ->toArray();

        return [
            'total' => array_sum($counts),
            'draft' => $counts[Order::STATUS_DRAFT] ?? 0,
            'confirmed' => $counts[Order::STATUS_CONFIRMED] ?? 0,
            'in_progress' => $counts[Order::STATUS_IN_PROGRESS] ?? 0,
            'completed' => $counts[Order::STATUS_COMPLETED] ?? 0,
            'cancelled' => $counts[Order::STATUS_CANCELLED] ?? 0,
            'total_amount' => array_sum($amounts),
            'confirmed_amount' => ($amounts[Order::STATUS_CONFIRMED] ?? 0) + ($amounts[Order::STATUS_IN_PROGRESS] ?? 0),
        ];
    }

    /**
     * 단건 조회
     */
    public function show(int $id)
    {
        $tenantId = $this->tenantId();

        $order = Order::where('tenant_id', $tenantId)->find($id);

        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        return $this->loadDetailRelations($order);
    }

    /**
     * 생성
     */
    public function store(array $data)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        return DB::transaction(function () use ($data, $tenantId, $userId) {
            // 수주번호 자동 생성
            $pairCode = $data['pair_code'] ?? null;
            unset($data['pair_code']);
            $data['order_no'] = $this->generateOrderNo($tenantId, $pairCode);
            $data['tenant_id'] = $tenantId;
            $data['created_by'] = $userId;
            $data['updated_by'] = $userId;

            // 기본 상태 설정
            $data['status_code'] = $data['status_code'] ?? Order::STATUS_DRAFT;
            $data['order_type_code'] = $data['order_type_code'] ?? Order::TYPE_ORDER;

            $items = $data['items'] ?? [];
            unset($data['items']);

            $order = Order::create($data);

            // quote_id가 있으면 OrderNode 생성 (개소별 사이즈 정보)
            $nodeMap = [];
            $productItems = [];
            if ($order->quote_id) {
                $quote = Quote::withoutGlobalScopes()->find($order->quote_id);
                if ($quote) {
                    $ci = $quote->calculation_inputs ?? [];
                    $productItems = $ci['items'] ?? [];
                    $bomResults = $ci['bomResults'] ?? [];

                    foreach ($productItems as $idx => $locItem) {
                        $bomResult = $bomResults[$idx] ?? null;
                        $grandTotal = $bomResult['grand_total'] ?? 0;
                        $bomVars = $bomResult['variables'] ?? [];
                        $qty = (int) ($locItem['quantity'] ?? 1);
                        $floor = $locItem['floor'] ?? '';
                        $symbol = $locItem['code'] ?? '';

                        $nodeMap[$idx] = OrderNode::create([
                            'tenant_id' => $tenantId,
                            'order_id' => $order->id,
                            'parent_id' => null,
                            'node_type' => 'location',
                            'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
                            'name' => trim("{$floor} {$symbol}") ?: '개소 '.($idx + 1),
                            'status_code' => OrderNode::STATUS_PENDING,
                            'quantity' => $qty,
                            'unit_price' => $grandTotal,
                            'total_price' => $grandTotal * $qty,
                            'options' => [
                                'floor' => $floor,
                                'symbol' => $symbol,
                                'product_code' => $locItem['productCode'] ?? null,
                                'product_name' => $locItem['productName'] ?? null,
                                'open_width' => $locItem['openWidth'] ?? null,
                                'open_height' => $locItem['openHeight'] ?? null,
                                'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
                                'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
                                'bom_result' => $bomResult,
                            ],
                            'depth' => 0,
                            'sort_order' => $idx,
                            'created_by' => $userId,
                        ]);
                    }
                }
            }

            // 품목 저장
            // sort_order 기반 분배 준비
            $locationCount = count($productItems);
            $itemsPerLocation = ($locationCount > 1)
                ? intdiv(count($items), $locationCount)
                : 0;

            // floor/code 조합이 개소별로 고유한지 확인 (모두 동일하면 매칭 무의미)
            $uniqueLocations = collect($productItems)
                ->map(fn ($p) => ($p['floor'] ?? '').'-'.($p['code'] ?? ''))
                ->unique()
                ->count();
            $canMatchByFloorCode = $uniqueLocations > 1;

            foreach ($items as $index => $item) {
                $item['tenant_id'] = $tenantId;
                $item['serial_no'] = $index + 1; // 1부터 시작하는 순번
                $item['sort_order'] = $index;
                $this->calculateItemAmounts($item);

                // item_id가 없고 item_code가 있으면 item_code로 조회하여 보완
                if (empty($item['item_id']) && ! empty($item['item_code'])) {
                    $foundItem = Item::withoutGlobalScopes()
                        ->where('tenant_id', $tenantId)
                        ->where('code', $item['item_code'])
                        ->first();
                    if ($foundItem) {
                        $item['item_id'] = $foundItem->id;
                    }
                }

                // 노드 매칭 (개소 분배)
                if (! empty($nodeMap) && ! empty($productItems)) {
                    $locIdx = 0;
                    $matched = false;

                    // 1순위: floor_code/symbol_code로 매칭 (개소별 고유값이 있는 경우만)
                    if ($canMatchByFloorCode) {
                        $floorCode = $item['floor_code'] ?? null;
                        $symbolCode = $item['symbol_code'] ?? null;
                        if ($floorCode && $symbolCode) {
                            foreach ($productItems as $pidx => $pItem) {
                                if (($pItem['floor'] ?? '') === $floorCode && ($pItem['code'] ?? '') === $symbolCode) {
                                    $locIdx = $pidx;
                                    $matched = true;
                                    break;
                                }
                            }
                        }
                    }

                    // 2순위: sort_order 기반 균등 분배
                    if (! $matched && $itemsPerLocation > 0) {
                        $locIdx = min(intdiv($index, $itemsPerLocation), $locationCount - 1);
                    }

                    $item['order_node_id'] = $nodeMap[$locIdx]->id ?? null;
                }

                $order->items()->create($item);
            }

            // 합계 재계산
            $order->refresh();
            $order->recalculateTotals()->save();

            return $this->loadDetailRelations($order);
        });
    }

    /**
     * 수정
     */
    public function update(int $id, array $data)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        $order = Order::where('tenant_id', $tenantId)->find($id);
        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 완료/취소 상태에서는 수정 불가
        if (in_array($order->status_code, [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED])) {
            throw new BadRequestHttpException(__('error.order.cannot_update_completed'));
        }

        return DB::transaction(function () use ($order, $data, $tenantId, $userId) {
            $data['updated_by'] = $userId;

            $items = $data['items'] ?? null;
            unset($data['items'], $data['order_no']); // 번호 변경 불가

            $order->update($data);

            // 품목 교체 (있는 경우)
            if ($items !== null) {
                // 기존 품목의 floor_code/symbol_code 매핑 저장 (item_name + specification → floor_code/symbol_code)
                $existingMappings = [];
                foreach ($order->items as $existingItem) {
                    $key = ($existingItem->item_name ?? '').'|'.($existingItem->specification ?? '');
                    $existingMappings[$key] = [
                        'floor_code' => $existingItem->floor_code,
                        'symbol_code' => $existingItem->symbol_code,
                    ];
                }

                $order->items()->delete();
                foreach ($items as $index => $item) {
                    $item['tenant_id'] = $tenantId;
                    $item['serial_no'] = $index + 1; // 1부터 시작하는 순번
                    $item['sort_order'] = $index;

                    // floor_code/symbol_code 보존: 프론트엔드에서 전달되지 않으면 기존 값 사용
                    if (empty($item['floor_code']) || empty($item['symbol_code'])) {
                        $key = ($item['item_name'] ?? '').'|'.($item['specification'] ?? '');
                        if (isset($existingMappings[$key])) {
                            $item['floor_code'] = $item['floor_code'] ?? $existingMappings[$key]['floor_code'];
                            $item['symbol_code'] = $item['symbol_code'] ?? $existingMappings[$key]['symbol_code'];
                        }
                    }

                    $this->calculateItemAmounts($item);
                    $order->items()->create($item);
                }

                // 합계 재계산
                $order->refresh();
                $order->recalculateTotals()->save();
            }

            return $this->loadDetailRelations($order);
        });
    }

    /**
     * 삭제
     */
    public function destroy(int $id)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        $order = Order::where('tenant_id', $tenantId)->find($id);
        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 진행 중이거나 완료된 수주는 삭제 불가
        if (in_array($order->status_code, [
            Order::STATUS_IN_PROGRESS,
            Order::STATUS_IN_PRODUCTION,
            Order::STATUS_PRODUCED,
            Order::STATUS_SHIPPING,
            Order::STATUS_SHIPPED,
            Order::STATUS_COMPLETED,
        ])) {
            throw new BadRequestHttpException(__('error.order.cannot_delete_in_progress'));
        }

        // 작업지시가 존재하면 삭제 불가
        if ($order->workOrders()->exists()) {
            throw new BadRequestHttpException(__('error.order.cannot_delete_has_work_orders'));
        }

        // 출하 정보가 존재하면 삭제 불가
        if ($order->shipments()->exists()) {
            throw new BadRequestHttpException(__('error.order.cannot_delete_has_shipments'));
        }

        return DB::transaction(function () use ($order, $userId) {
            // 0. 연결된 견적의 수주 연결 해제 (order_id → null, status → finalized)
            if ($order->quote_id) {
                Quote::withoutGlobalScopes()
                    ->where('id', $order->quote_id)
                    ->where('order_id', $order->id)
                    ->update([
                        'order_id' => null,
                        'status' => Quote::STATUS_FINALIZED,
                    ]);
            }

            // 1. order_item_components soft delete
            foreach ($order->items as $item) {
                $item->components()->update(['deleted_by' => $userId]);
                $item->components()->delete();
            }

            // 2. order_items soft delete
            $order->items()->update(['deleted_by' => $userId]);
            $order->items()->delete();

            // 3. order_nodes soft delete
            $order->nodes()->update(['deleted_by' => $userId]);
            $order->nodes()->delete();

            // 4. order 마스터 soft delete
            $order->deleted_by = $userId;
            $order->save();
            $order->delete();

            // order_histories, order_versions는 감사 기록이므로 보존

            return 'success';
        });
    }

    /**
     * 일괄 삭제
     */
    public function bulkDestroy(array $ids, bool $force = false): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // force=true는 운영 환경에서 차단
        if ($force && app()->environment('production')) {
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException(__('error.forbidden'));
        }

        $orders = Order::where('tenant_id', $tenantId)
            ->whereIn('id', $ids)
            ->get();

        $deletedCount = 0;
        $skippedIds = [];

        return DB::transaction(function () use ($orders, $force, $userId, $tenantId, &$deletedCount, &$skippedIds) {
            foreach ($orders as $order) {
                if ($force) {
                    // force=true (개발환경 완전삭제): 모든 상태 허용, 연관 데이터 모두 삭제
                    $this->forceDeleteWorkOrders($order, $tenantId);
                } else {
                    // 일반 삭제: 상태/작업지시/출하 검증
                    if (! in_array($order->status_code, [
                        Order::STATUS_DRAFT,
                        Order::STATUS_CONFIRMED,
                        Order::STATUS_CANCELLED,
                    ])) {
                        $skippedIds[] = $order->id;

                        continue;
                    }

                    if ($order->workOrders()->exists()) {
                        $skippedIds[] = $order->id;

                        continue;
                    }

                    if ($order->shipments()->exists()) {
                        $skippedIds[] = $order->id;

                        continue;
                    }
                }

                // 견적 연결 해제
                if ($order->quote_id) {
                    Quote::withoutGlobalScopes()
                        ->where('id', $order->quote_id)
                        ->where('order_id', $order->id)
                        ->update([
                            'order_id' => null,
                            'status' => Quote::STATUS_FINALIZED,
                        ]);
                }

                if ($force) {
                    // hard delete: 컴포넌트 → 품목 → 노드 → 마스터
                    foreach ($order->items as $item) {
                        $item->components()->forceDelete();
                    }
                    $order->items()->forceDelete();
                    $order->nodes()->forceDelete();
                    $order->forceDelete();
                } else {
                    // soft delete: 기존 destroy() 로직과 동일
                    foreach ($order->items as $item) {
                        $item->components()->update(['deleted_by' => $userId]);
                        $item->components()->delete();
                    }
                    $order->items()->update(['deleted_by' => $userId]);
                    $order->items()->delete();
                    $order->nodes()->update(['deleted_by' => $userId]);
                    $order->nodes()->delete();
                    $order->deleted_by = $userId;
                    $order->save();
                    $order->delete();
                }

                $deletedCount++;
            }

            return [
                'deleted_count' => $deletedCount,
                'skipped_count' => count($skippedIds),
                'skipped_ids' => $skippedIds,
            ];
        });
    }

    /**
     * 작업지시 및 연관 데이터 강제 삭제 (개발환경 완전삭제용)
     */
    private function forceDeleteWorkOrders(Order $order, int $tenantId): void
    {
        $workOrderIds = WorkOrder::where('tenant_id', $tenantId)
            ->where('sales_order_id', $order->id)
            ->pluck('id')
            ->toArray();

        if (empty($workOrderIds)) {
            return;
        }

        // 1. 자재 투입 재고 복구 + 삭제
        $materialInputs = WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->get();
        if ($materialInputs->isNotEmpty()) {
            $stockService = app(StockService::class);
            foreach ($materialInputs as $input) {
                try {
                    $stockService->increaseToLot(
                        stockLotId: $input->stock_lot_id,
                        qty: (float) $input->qty,
                        reason: 'work_order_input_cancel',
                        referenceId: $input->work_order_id
                    );
                } catch (\Exception $e) {
                    Log::warning('완전삭제: 재고 복원 실패', [
                        'input_id' => $input->id,
                        'stock_lot_id' => $input->stock_lot_id,
                        'error' => $e->getMessage(),
                    ]);
                }
            }
            WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->delete();
        }

        // 2. 문서 삭제
        $documentIds = Document::where('linkable_type', 'work_order')
            ->whereIn('linkable_id', $workOrderIds)
            ->pluck('id')
            ->toArray();

        if (! empty($documentIds)) {
            DocumentData::whereIn('document_id', $documentIds)->delete();
            DocumentApproval::whereIn('document_id', $documentIds)->delete();
            Document::whereIn('id', $documentIds)->forceDelete();
        }

        // 3. 출하 참조 해제
        DB::table('shipments')
            ->whereIn('work_order_id', $workOrderIds)
            ->update(['work_order_id' => null]);

        // 4. 부속 데이터 삭제
        DB::table('work_order_step_progress')->whereIn('work_order_id', $workOrderIds)->delete();
        DB::table('work_order_assignees')->whereIn('work_order_id', $workOrderIds)->delete();
        DB::table('work_order_bending_details')->whereIn('work_order_id', $workOrderIds)->delete();
        DB::table('work_order_issues')->whereIn('work_order_id', $workOrderIds)->delete();
        DB::table('work_results')->whereIn('work_order_id', $workOrderIds)->delete();

        // 5. 작업지시 품목 → 작업지시 삭제
        DB::table('work_order_items')->whereIn('work_order_id', $workOrderIds)->delete();
        WorkOrder::whereIn('id', $workOrderIds)->forceDelete();
    }

    /**
     * 상태 변경
     */
    public function updateStatus(int $id, string $status)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        $order = Order::where('tenant_id', $tenantId)->find($id);
        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 상태 유효성 검증
        $validStatuses = [
            Order::STATUS_DRAFT,
            Order::STATUS_CONFIRMED,
            Order::STATUS_IN_PROGRESS,
            Order::STATUS_COMPLETED,
            Order::STATUS_CANCELLED,
        ];

        if (! in_array($status, $validStatuses)) {
            throw new BadRequestHttpException(__('error.invalid_status'));
        }

        // 상태 전환 규칙 검증
        $this->validateStatusTransition($order->status_code, $status);

        return DB::transaction(function () use ($order, $status, $userId) {
            $createdSale = null;
            $previousStatus = $order->status_code;

            // 수주확정 시 매출 자동 생성 (sales_recognition = on_order_confirm인 경우)
            if ($status === Order::STATUS_CONFIRMED && $order->shouldCreateSaleOnConfirm()) {
                $createdSale = $this->createSaleFromOrder($order, $userId);
                $order->sale_id = $createdSale->id;
            }

            // 🆕 수주확정 시 재고 예약
            if ($status === Order::STATUS_CONFIRMED && $previousStatus !== Order::STATUS_CONFIRMED) {
                $order->load('items');
                app(StockService::class)->reserveForOrder($order->items, $order->id);
            }

            // 🆕 수주취소 시 재고 예약 해제
            if ($status === Order::STATUS_CANCELLED && $previousStatus === Order::STATUS_CONFIRMED) {
                $order->load('items');
                app(StockService::class)->releaseReservationForOrder($order->items, $order->id);
            }

            $order->status_code = $status;
            $order->updated_by = $userId;
            $order->save();

            $result = $this->loadDetailRelations($order);

            // 매출이 생성된 경우 응답에 포함
            if ($createdSale) {
                $result->setAttribute('created_sale', $createdSale);
            }

            return $result;
        });
    }

    /**
     * 수주에서 매출 생성
     */
    private function createSaleFromOrder(Order $order, int $userId): Sale
    {
        $saleNumber = $this->generateSaleNumber($order->tenant_id);

        $sale = Sale::createFromOrder($order, $saleNumber);
        $sale->created_by = $userId;
        $sale->save();

        return $sale;
    }

    /**
     * 매출번호 자동 생성
     */
    private function generateSaleNumber(int $tenantId): string
    {
        $prefix = 'SAL';
        $yearMonth = now()->format('Ym');

        // 해당 월 기준 마지막 번호 조회
        $lastNo = Sale::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->where('sale_number', 'like', "{$prefix}-{$yearMonth}-%")
            ->orderByDesc('sale_number')
            ->value('sale_number');

        if ($lastNo) {
            $seq = (int) substr($lastNo, -4) + 1;
        } else {
            $seq = 1;
        }

        return sprintf('%s-%s-%04d', $prefix, $yearMonth, $seq);
    }

    /**
     * 상태 전환 규칙 검증
     */
    private function validateStatusTransition(string $from, string $to): void
    {
        $allowedTransitions = [
            Order::STATUS_DRAFT => [Order::STATUS_CONFIRMED, Order::STATUS_CANCELLED],
            Order::STATUS_CONFIRMED => [Order::STATUS_IN_PROGRESS, Order::STATUS_CANCELLED],
            Order::STATUS_IN_PROGRESS => [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED],
            Order::STATUS_COMPLETED => [], // 완료 상태에서는 변경 불가
            Order::STATUS_CANCELLED => [Order::STATUS_DRAFT], // 취소에서 임시저장으로만 복구 가능
        ];

        if (! in_array($to, $allowedTransitions[$from] ?? [])) {
            throw new BadRequestHttpException(__('error.order.invalid_status_transition'));
        }
    }

    /**
     * 품목 금액 계산
     */
    private function calculateItemAmounts(array &$item): void
    {
        $quantity = (float) ($item['quantity'] ?? 0);
        $unitPrice = (float) ($item['unit_price'] ?? 0);

        $item['supply_amount'] = $quantity * $unitPrice;
        $item['tax_amount'] = round($item['supply_amount'] * 0.1, 2);
        $item['total_amount'] = $item['supply_amount'] + $item['tax_amount'];
    }

    /**
     * 수주번호 자동 생성
     *
     * 채번규칙이 있으면 NumberingService 사용 (KD-{pairCode}-{YYMMDD}-{NN}),
     * 없으면 레거시 로직 (ORD{YYYYMMDD}{NNNN})
     */
    private function generateOrderNo(int $tenantId, ?string $pairCode = null): string
    {
        $this->numberingService->setContext($tenantId, $this->apiUserId());

        $number = $this->numberingService->generate('order', [
            'pair_code' => $pairCode ?? 'SS',
        ]);

        if ($number !== null) {
            return $number;
        }

        return $this->generateOrderNoLegacy($tenantId);
    }

    /**
     * 레거시 수주번호 생성 (ORD{YYYYMMDD}{NNNN})
     */
    private function generateOrderNoLegacy(int $tenantId): string
    {
        $prefix = 'ORD';
        $date = now()->format('Ymd');

        // 오늘 날짜 기준 마지막 번호 조회
        $lastNo = Order::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->where('order_no', 'like', "{$prefix}{$date}%")
            ->orderByDesc('order_no')
            ->value('order_no');

        if ($lastNo) {
            $seq = (int) substr($lastNo, -4) + 1;
        } else {
            $seq = 1;
        }

        return sprintf('%s%s%04d', $prefix, $date, $seq);
    }

    /**
     * 견적에서 수주 생성
     */
    public function createFromQuote(int $quoteId, array $data = [])
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 견적 조회
        $quote = Quote::where('tenant_id', $tenantId)
            ->with(['items', 'client'])
            ->find($quoteId);

        if (! $quote) {
            throw new NotFoundHttpException(__('error.quote.not_found'));
        }

        // 이미 수주가 생성된 견적인지 확인
        $existingOrder = Order::where('tenant_id', $tenantId)
            ->where('quote_id', $quoteId)
            ->first();

        if ($existingOrder) {
            throw new BadRequestHttpException(__('error.order.already_created_from_quote'));
        }

        return DB::transaction(function () use ($quote, $data, $tenantId, $userId) {
            // 수주번호 생성
            $pairCode = $data['pair_code'] ?? null;
            $orderNo = $this->generateOrderNo($tenantId, $pairCode);

            // Order 모델의 createFromQuote 사용
            $order = Order::createFromQuote($quote, $orderNo);
            $order->created_by = $userId;
            $order->updated_by = $userId;

            // 추가 데이터 병합 (납품일, 메모 등)
            if (! empty($data['delivery_date'])) {
                $order->delivery_date = $data['delivery_date'];
            }
            if (! empty($data['memo'])) {
                $order->memo = $data['memo'];
            }

            $order->save();

            // calculation_inputs에서 제품 정보 추출
            $calculationInputs = $quote->calculation_inputs ?? [];
            $productItems = $calculationInputs['items'] ?? [];
            $bomResults = $calculationInputs['bomResults'] ?? [];

            // OrderNode 생성 (개소별)
            $nodeMap = [];
            foreach ($productItems as $idx => $locItem) {
                $bomResult = $bomResults[$idx] ?? null;
                $bomVars = $bomResult['variables'] ?? [];
                $grandTotal = $bomResult['grand_total'] ?? 0;
                $qty = (int) ($locItem['quantity'] ?? 1);
                $floor = $locItem['floor'] ?? '';
                $symbol = $locItem['code'] ?? '';

                $nodeMap[$idx] = OrderNode::create([
                    'tenant_id' => $tenantId,
                    'order_id' => $order->id,
                    'parent_id' => null,
                    'node_type' => 'location',
                    'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
                    'name' => trim("{$floor} {$symbol}") ?: '개소 '.($idx + 1),
                    'status_code' => OrderNode::STATUS_PENDING,
                    'quantity' => $qty,
                    'unit_price' => $grandTotal,
                    'total_price' => $grandTotal * $qty,
                    'options' => [
                        'floor' => $floor,
                        'symbol' => $symbol,
                        'product_code' => $locItem['productCode'] ?? null,
                        'product_name' => $locItem['productName'] ?? null,
                        'open_width' => $locItem['openWidth'] ?? null,
                        'open_height' => $locItem['openHeight'] ?? null,
                        'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
                        'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
                        'guide_rail_type' => $locItem['guideRailType'] ?? null,
                        'motor_power' => $locItem['motorPower'] ?? null,
                        'controller' => $locItem['controller'] ?? null,
                        'wing_size' => $locItem['wingSize'] ?? null,
                        'inspection_fee' => $locItem['inspectionFee'] ?? null,
                        'bom_result' => $bomResult,
                        'slat_info' => $this->extractSlatInfoFromBom($bomResult, $locItem),
                    ],
                    'depth' => 0,
                    'sort_order' => $idx,
                    'created_by' => $userId,
                ]);
            }

            // 견적 품목을 수주 품목으로 변환 (노드 연결 포함)
            // formula_source 없는 레거시 데이터용: sort_order 기반 분배 준비
            $locationCount = count($productItems);
            $hasFormulaSource = $quote->items->contains(fn ($item) => ! empty($item->formula_source));
            $itemsPerLocation = (! $hasFormulaSource && $locationCount > 1)
                ? intdiv($quote->items->count(), $locationCount)
                : 0;

            foreach ($quote->items as $index => $quoteItem) {
                $floorCode = null;
                $symbolCode = null;
                $locIdx = 0;

                // 1순위: formula_source에서 인덱스 추출
                $formulaSource = $quoteItem->formula_source ?? '';
                if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
                    $locIdx = (int) $matches[1];
                }

                // 2순위: sort_order 기반 분배 (formula_source 없는 레거시 데이터)
                if ($locIdx === 0 && $itemsPerLocation > 0) {
                    $locIdx = min(intdiv($index, $itemsPerLocation), $locationCount - 1);
                }

                // calculation_inputs에서 floor/code 가져오기
                if (isset($productItems[$locIdx])) {
                    $floorCode = $productItems[$locIdx]['floor'] ?? null;
                    $symbolCode = $productItems[$locIdx]['code'] ?? null;
                } elseif (count($productItems) === 1) {
                    $floorCode = $productItems[0]['floor'] ?? null;
                    $symbolCode = $productItems[0]['code'] ?? null;
                }

                // calculation_inputs에서 못 찾은 경우 note에서 파싱 시도
                if (empty($floorCode) && empty($symbolCode)) {
                    $note = trim($quoteItem->note ?? '');
                    if ($note !== '' && $note !== '-' && $note !== '- -') {
                        $parts = preg_split('/\s+/', $note, 2);
                        $floorCode = ($parts[0] ?? null) !== '-' ? ($parts[0] ?? null) : null;
                        $symbolCode = ($parts[1] ?? null) !== '-' ? ($parts[1] ?? null) : null;
                    }
                }

                // note 파싱으로 locIdx 결정 (formula_source 없는 경우)
                if ($locIdx === 0 && ! $itemsPerLocation && ! empty($floorCode) && ! empty($symbolCode)) {
                    foreach ($productItems as $pidx => $pItem) {
                        if (($pItem['floor'] ?? '') === $floorCode && ($pItem['code'] ?? '') === $symbolCode) {
                            $locIdx = $pidx;
                            break;
                        }
                    }
                }

                $order->items()->create([
                    'tenant_id' => $tenantId,
                    'order_node_id' => $nodeMap[$locIdx]->id ?? null,
                    'serial_no' => $index + 1,
                    'item_id' => $quoteItem->item_id,
                    'item_code' => $quoteItem->item_code,
                    'item_name' => $quoteItem->item_name,
                    'specification' => $quoteItem->specification,
                    'floor_code' => $floorCode,
                    'symbol_code' => $symbolCode,
                    'quantity' => $quoteItem->calculated_quantity,
                    'unit' => $quoteItem->unit,
                    'unit_price' => $quoteItem->unit_price,
                    'supply_amount' => $quoteItem->total_price,
                    'tax_amount' => round($quoteItem->total_price * 0.1, 2),
                    'total_amount' => round($quoteItem->total_price * 1.1, 2),
                    'note' => $quoteItem->formula_category,
                    'sort_order' => $index,
                ]);
            }

            // 합계 재계산
            $order->refresh();
            $order->recalculateTotals()->save();

            // 견적에 수주 연결 (status는 accessor가 자동으로 'converted' 반환)
            $quote->update([
                'order_id' => $order->id,
                'updated_by' => $userId,
            ]);

            return $this->loadDetailRelations($order);
        });
    }

    /**
     * 견적 변경사항을 수주에 동기화
     *
     * 견적이 수정되면 연결된 수주도 함께 업데이트하고 히스토리를 생성합니다.
     *
     * @param  Quote  $quote  수정된 견적
     * @param  int  $revision  견적 수정 차수
     * @return Order|null 업데이트된 수주 또는 null (연결된 수주가 없는 경우)
     */
    public function syncFromQuote(Quote $quote, int $revision): ?Order
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 연결된 수주 확인
        $order = Order::where('tenant_id', $tenantId)
            ->where('quote_id', $quote->id)
            ->first();

        if (! $order) {
            return null;
        }

        // 생산 진행 이상의 상태면 동기화 불가 (DRAFT, CONFIRMED, IN_PROGRESS만 허용)
        $allowedStatuses = [
            Order::STATUS_DRAFT,
            Order::STATUS_CONFIRMED,
            Order::STATUS_IN_PROGRESS,
        ];
        if (! in_array($order->status_code, $allowedStatuses)) {
            throw new BadRequestHttpException(__('error.order.cannot_sync_after_production'));
        }

        return DB::transaction(function () use ($order, $quote, $tenantId, $userId, $revision) {
            // 변경 전 데이터 스냅샷 (히스토리용)
            $beforeData = [
                'site_name' => $order->site_name,
                'client_name' => $order->client_name,
                'total_amount' => $order->total_amount,
                'items_count' => $order->items()->count(),
            ];

            // 수주 기본 정보 업데이트
            $order->update([
                'site_name' => $quote->site_name,
                'client_id' => $quote->client_id,
                'client_name' => $quote->client_name,
                'discount_rate' => $quote->discount_rate ?? 0,
                'discount_amount' => $quote->discount_amount ?? 0,
                'total_amount' => $quote->total_amount,
                'updated_by' => $userId,
            ]);

            // 기존 품목 및 노드 삭제 후 새로 생성
            $order->items()->delete();
            $order->nodes()->delete();

            // calculation_inputs에서 제품 정보 추출
            $calculationInputs = $quote->calculation_inputs ?? [];
            $productItems = $calculationInputs['items'] ?? [];
            $bomResults = $calculationInputs['bomResults'] ?? [];

            // OrderNode 생성 (개소별)
            $nodeMap = [];
            foreach ($productItems as $idx => $locItem) {
                $bomResult = $bomResults[$idx] ?? null;
                $bomVars = $bomResult['variables'] ?? [];
                $grandTotal = $bomResult['grand_total'] ?? 0;
                $qty = (int) ($locItem['quantity'] ?? 1);
                $floor = $locItem['floor'] ?? '';
                $symbol = $locItem['code'] ?? '';

                $nodeMap[$idx] = OrderNode::create([
                    'tenant_id' => $tenantId,
                    'order_id' => $order->id,
                    'parent_id' => null,
                    'node_type' => 'location',
                    'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
                    'name' => trim("{$floor} {$symbol}") ?: '개소 '.($idx + 1),
                    'status_code' => OrderNode::STATUS_PENDING,
                    'quantity' => $qty,
                    'unit_price' => $grandTotal,
                    'total_price' => $grandTotal * $qty,
                    'options' => [
                        'floor' => $floor,
                        'symbol' => $symbol,
                        'product_code' => $locItem['productCode'] ?? null,
                        'product_name' => $locItem['productName'] ?? null,
                        'open_width' => $locItem['openWidth'] ?? null,
                        'open_height' => $locItem['openHeight'] ?? null,
                        'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
                        'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
                        'guide_rail_type' => $locItem['guideRailType'] ?? null,
                        'motor_power' => $locItem['motorPower'] ?? null,
                        'controller' => $locItem['controller'] ?? null,
                        'wing_size' => $locItem['wingSize'] ?? null,
                        'inspection_fee' => $locItem['inspectionFee'] ?? null,
                        'bom_result' => $bomResult,
                        'slat_info' => $this->extractSlatInfoFromBom($bomResult, $locItem),
                    ],
                    'depth' => 0,
                    'sort_order' => $idx,
                    'created_by' => $userId,
                ]);
            }

            // 견적 품목을 수주 품목으로 변환 (노드 연결 포함)
            // formula_source 없는 레거시 데이터용: sort_order 기반 분배 준비
            $locationCount = count($productItems);
            $hasFormulaSource = $quote->items->contains(fn ($item) => ! empty($item->formula_source));
            $itemsPerLocation = (! $hasFormulaSource && $locationCount > 1)
                ? intdiv($quote->items->count(), $locationCount)
                : 0;

            foreach ($quote->items as $index => $quoteItem) {
                $floorCode = null;
                $symbolCode = null;
                $locIdx = 0;

                // 1순위: formula_source에서 인덱스 추출
                $formulaSource = $quoteItem->formula_source ?? '';
                if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
                    $locIdx = (int) $matches[1];
                }

                // 2순위: sort_order 기반 분배 (formula_source 없는 레거시 데이터)
                if ($locIdx === 0 && $itemsPerLocation > 0) {
                    $locIdx = min(intdiv($index, $itemsPerLocation), $locationCount - 1);
                }

                // note에서 floor/code 파싱
                $note = trim($quoteItem->note ?? '');
                if ($note !== '' && $note !== '-' && $note !== '- -') {
                    $parts = preg_split('/\s+/', $note, 2);
                    $floorCode = ($parts[0] ?? null) !== '-' ? ($parts[0] ?? null) : null;
                    $symbolCode = ($parts[1] ?? null) !== '-' ? ($parts[1] ?? null) : null;
                }

                // 3순위: note에서 파싱 실패 시 calculation_inputs에서 가져오기
                if (empty($floorCode) && empty($symbolCode)) {
                    if (isset($productItems[$locIdx])) {
                        $floorCode = $productItems[$locIdx]['floor'] ?? null;
                        $symbolCode = $productItems[$locIdx]['code'] ?? null;
                    } elseif (count($productItems) === 1) {
                        $floorCode = $productItems[0]['floor'] ?? null;
                        $symbolCode = $productItems[0]['code'] ?? null;
                    }
                }

                // note 파싱으로 locIdx 결정 (formula_source 없는 경우)
                if ($locIdx === 0 && ! $itemsPerLocation && ! empty($floorCode) && ! empty($symbolCode)) {
                    foreach ($productItems as $pidx => $pItem) {
                        if (($pItem['floor'] ?? '') === ($floorCode ?? '') && ($pItem['code'] ?? '') === ($symbolCode ?? '')) {
                            $locIdx = $pidx;
                            break;
                        }
                    }
                }

                $order->items()->create([
                    'tenant_id' => $tenantId,
                    'order_node_id' => $nodeMap[$locIdx]->id ?? null,
                    'serial_no' => $index + 1,
                    'item_id' => $quoteItem->item_id,
                    'item_code' => $quoteItem->item_code,
                    'item_name' => $quoteItem->item_name,
                    'specification' => $quoteItem->specification,
                    'floor_code' => $floorCode,
                    'symbol_code' => $symbolCode,
                    'quantity' => $quoteItem->calculated_quantity,
                    'unit' => $quoteItem->unit,
                    'unit_price' => $quoteItem->unit_price,
                    'supply_amount' => $quoteItem->total_price,
                    'tax_amount' => round($quoteItem->total_price * 0.1, 2),
                    'total_amount' => round($quoteItem->total_price * 1.1, 2),
                    'note' => $quoteItem->formula_category,
                    'sort_order' => $index,
                ]);
            }

            // 합계 재계산
            $order->refresh();
            $order->recalculateTotals()->save();

            // 변경 후 데이터 스냅샷
            $afterData = [
                'site_name' => $order->site_name,
                'client_name' => $order->client_name,
                'total_amount' => $order->total_amount,
                'items_count' => $order->items()->count(),
            ];

            // 히스토리 생성
            OrderHistory::create([
                'tenant_id' => $tenantId,
                'order_id' => $order->id,
                'history_type' => 'quote_updated',
                'content' => json_encode([
                    'message' => "견적 {$revision}차 수정으로 수주 정보가 업데이트되었습니다.",
                    'quote_id' => $quote->id,
                    'quote_number' => $quote->quote_number,
                    'revision' => $revision,
                    'before' => $beforeData,
                    'after' => $afterData,
                ], JSON_UNESCAPED_UNICODE),
                'created_by' => $userId,
            ]);

            return $this->loadDetailRelations($order);
        });
    }

    /**
     * 생산지시 생성 (공정별 작업지시 다중 생성)
     */
    public function createProductionOrder(int $orderId, array $data)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 수주 + 노드 조회
        $order = Order::where('tenant_id', $tenantId)
            ->with(['items', 'rootNodes'])
            ->find($orderId);

        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 상태 확인 (CONFIRMED 상태에서만 생산지시 가능)
        if ($order->status_code !== Order::STATUS_CONFIRMED) {
            throw new BadRequestHttpException(__('error.order.must_be_confirmed_for_production'));
        }

        // 이미 생산지시가 존재하는지 확인
        $existingWorkOrder = WorkOrder::where('tenant_id', $tenantId)
            ->where('sales_order_id', $orderId)
            ->first();

        if ($existingWorkOrder) {
            throw new BadRequestHttpException(__('error.order.production_order_already_exists'));
        }

        // order_nodes의 BOM 결과를 기반으로 공정별 자동 분류
        $bomItemIds = [];
        $nodesBomMap = []; // node_id => [item_name => bom_item]

        foreach ($order->rootNodes as $node) {
            $bomResult = $node->options['bom_result'] ?? [];
            $bomItems = $bomResult['items'] ?? [];

            foreach ($bomItems as $bomItem) {
                if (! empty($bomItem['item_id'])) {
                    $bomItemIds[] = $bomItem['item_id'];
                    $nodesBomMap[$node->id][$bomItem['item_name']] = $bomItem;
                }
            }
        }

        $bomItemIds = array_unique($bomItemIds);

        // process_items 테이블에서 item_id → process_id 매핑 조회
        $itemProcessMap = [];
        if (! empty($bomItemIds)) {
            $processItems = DB::table('process_items as pi')
                ->join('processes as p', 'pi.process_id', '=', 'p.id')
                ->where('p.tenant_id', $tenantId)
                ->whereIn('pi.item_id', $bomItemIds)
                ->where('pi.is_active', true)
                ->select('pi.item_id', 'pi.process_id')
                ->get();

            foreach ($processItems as $pi) {
                $itemProcessMap[$pi->item_id] = $pi->process_id;
            }
        }

        // item_code → item_id 매핑 구축 (fallback용)
        $codeToIdMap = [];
        if (! empty($bomItemIds)) {
            $codeToIdRows = DB::table('items')
                ->where('tenant_id', $tenantId)
                ->whereIn('id', $bomItemIds)
                ->whereNull('deleted_at')
                ->select('id', 'code')
                ->get();
            foreach ($codeToIdRows as $row) {
                $codeToIdMap[$row->code] = $row->id;
            }
        }

        // order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용)
        $itemsByProcess = [];
        foreach ($order->items as $orderItem) {
            $processId = null;

            // 1. order_item의 item_id가 있으면 직접 매핑
            if ($orderItem->item_id && isset($itemProcessMap[$orderItem->item_id])) {
                $processId = $itemProcessMap[$orderItem->item_id];
            }
            // 2. item_id가 없으면 노드의 BOM에서 item_name으로 찾기
            elseif ($orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) {
                $nodeBom = $nodesBomMap[$orderItem->order_node_id];
                $bomItem = $nodeBom[$orderItem->item_name] ?? null;
                if ($bomItem && ! empty($bomItem['item_id']) && isset($itemProcessMap[$bomItem['item_id']])) {
                    $processId = $itemProcessMap[$bomItem['item_id']];
                }
            }

            // 3. fallback: item_code로 items 마스터 조회 → process_items 매핑
            if ($processId === null && $orderItem->item_code) {
                $resolvedId = $codeToIdMap[$orderItem->item_code] ?? null;
                if (! $resolvedId) {
                    $resolvedId = DB::table('items')
                        ->where('tenant_id', $tenantId)
                        ->where('code', $orderItem->item_code)
                        ->whereNull('deleted_at')
                        ->value('id');
                    if ($resolvedId) {
                        $codeToIdMap[$orderItem->item_code] = $resolvedId;
                    }
                }
                if ($resolvedId && isset($itemProcessMap[$resolvedId])) {
                    $processId = $itemProcessMap[$resolvedId];
                } elseif ($resolvedId) {
                    // process_items에서도 조회
                    $pi = DB::table('process_items')
                        ->where('item_id', $resolvedId)
                        ->where('is_active', true)
                        ->value('process_id');
                    if ($pi) {
                        $processId = $pi;
                        $itemProcessMap[$resolvedId] = $pi;
                    }
                }
            }

            $key = $processId ?? 'none';

            if (! isset($itemsByProcess[$key])) {
                $itemsByProcess[$key] = [
                    'process_id' => $processId,
                    'items' => [],
                ];
            }
            $itemsByProcess[$key]['items'][] = $orderItem;
        }

        return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap) {
            $workOrders = [];

            // 담당자 ID 배열 처리 (assignee_ids 우선, fallback으로 assignee_id)
            $assigneeIds = $data['assignee_ids'] ?? [];
            if (empty($assigneeIds) && ! empty($data['assignee_id'])) {
                $assigneeIds = [$data['assignee_id']];
            }
            $assigneeIds = array_unique(array_filter($assigneeIds));
            $primaryAssigneeId = $assigneeIds[0] ?? null;

            foreach ($itemsByProcess as $key => $group) {
                $processId = $group['process_id'];
                $items = $group['items'];

                // 작업지시번호 생성
                $workOrderNo = $this->generateWorkOrderNo($tenantId);

                // 절곡 공정이면 bending_info 자동 생성
                $workOrderOptions = null;
                if ($processId) {
                    // 이 작업지시에 포함되는 노드 ID만 추출
                    $nodeIds = collect($items)
                        ->pluck('order_node_id')
                        ->filter()
                        ->unique()
                        ->values()
                        ->all();

                    $buildResult = app(BendingInfoBuilder::class)->build($order, $processId, $nodeIds ?: null);
                    if ($buildResult) {
                        $workOrderOptions = ['bending_info' => $buildResult['bending_info']];
                    }
                }

                // 작업지시 생성
                $workOrder = WorkOrder::create([
                    'tenant_id' => $tenantId,
                    'work_order_no' => $workOrderNo,
                    'sales_order_id' => $order->id,
                    'project_name' => $order->site_name ?? $order->client_name,
                    'process_id' => $processId,
                    'status' => ! empty($assigneeIds) ? WorkOrder::STATUS_PENDING : WorkOrder::STATUS_UNASSIGNED,
                    'assignee_id' => $primaryAssigneeId,
                    'team_id' => $data['team_id'] ?? null,
                    'scheduled_date' => $data['scheduled_date'] ?? $order->delivery_date,
                    'memo' => $data['memo'] ?? null,
                    'options' => $workOrderOptions,
                    'is_active' => true,
                    'created_by' => $userId,
                    'updated_by' => $userId,
                ]);

                // 다중 담당자 저장 (work_order_assignees)
                foreach ($assigneeIds as $index => $assigneeId) {
                    $workOrder->assignees()->create([
                        'tenant_id' => $tenantId,
                        'user_id' => $assigneeId,
                        'is_primary' => $index === 0,
                    ]);
                }

                // work_order_items에 아이템 추가
                $sortOrder = 1;
                foreach ($items as $orderItem) {
                    // item_id 결정: order_item에 있으면 사용, 없으면 item_code로 조회, 최후에 BOM에서 가져오기
                    $itemId = $orderItem->item_id;
                    if (! $itemId && $orderItem->item_code) {
                        $foundItem = Item::withoutGlobalScopes()
                            ->where('tenant_id', $tenantId)
                            ->where('code', $orderItem->item_code)
                            ->first();
                        $itemId = $foundItem?->id;
                    }
                    if (! $itemId && $orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) {
                        $bomItem = $nodesBomMap[$orderItem->order_node_id][$orderItem->item_name] ?? null;
                        $itemId = $bomItem['item_id'] ?? null;
                    }

                    // 수주 품목의 노드에서 options(사이즈 등) 조합
                    $nodeOptions = [];
                    if ($orderItem->order_node_id) {
                        $node = $order->rootNodes->firstWhere('id', $orderItem->order_node_id);
                        $nodeOptions = $node ? ($node->options ?? []) : [];
                    }
                    // slat_info: nodeOptions에 있으면 사용, 없으면 bom_result에서 추출 (하위 호환)
                    $slatInfo = $nodeOptions['slat_info'] ?? null;
                    if (! $slatInfo && isset($nodeOptions['bom_result'])) {
                        $slatInfo = $this->extractSlatInfoFromBom($nodeOptions['bom_result']);
                    }

                    // slat_info의 joint_bar가 0이면 레거시 공식으로 자동 계산
                    $woWidth = $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null;
                    if ($slatInfo && ($slatInfo['joint_bar'] ?? 0) <= 0 && $woWidth > 0) {
                        $qty = (int) $orderItem->quantity;
                        $slatInfo['joint_bar'] = (2 + (int) floor(((float) $woWidth - 500) / 1000)) * $qty;
                    }

                    $woHeight = $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null;

                    $woItemOptions = array_filter([
                        'floor' => $orderItem->floor_code,
                        'code' => $orderItem->symbol_code,
                        'product_code' => ! empty($nodeOptions['product_code']) ? $nodeOptions['product_code'] : null,
                        'product_name' => ! empty($nodeOptions['product_name']) ? $nodeOptions['product_name'] : null,
                        'width' => $woWidth,
                        'height' => $woHeight,
                        'cutting_info' => $nodeOptions['cutting_info'] ?? null,
                        'slat_info' => $slatInfo,
                        'bending_info' => $nodeOptions['bending_info'] ?? null,
                        'wip_info' => $nodeOptions['wip_info'] ?? null,
                    ], fn ($v) => $v !== null);

                    // 절곡 공정: 개소별 dynamic_bom 생성
                    if (! empty($buildResult['context']) && $woWidth && $woHeight) {
                        $dynamicBom = app(BendingInfoBuilder::class)->buildDynamicBomForItem(
                            $buildResult['context'],
                            (int) $woWidth,
                            (int) $woHeight,
                            (int) ($orderItem->quantity ?? 1),
                            $tenantId,
                        );
                        if (! empty($dynamicBom)) {
                            $woItemOptions['dynamic_bom'] = $dynamicBom;
                        }
                    }

                    DB::table('work_order_items')->insert([
                        'tenant_id' => $tenantId,
                        'work_order_id' => $workOrder->id,
                        'source_order_item_id' => $orderItem->id,
                        'item_id' => $itemId,
                        'item_name' => $orderItem->item_name,
                        'specification' => $orderItem->specification,
                        'quantity' => $orderItem->quantity,
                        'unit' => $orderItem->unit,
                        'sort_order' => $sortOrder++,
                        'status' => 'pending',
                        'options' => ! empty($woItemOptions) ? json_encode($woItemOptions) : null,
                        'created_at' => now(),
                        'updated_at' => now(),
                    ]);
                }

                $workOrders[] = $workOrder->load(['assignee:id,name', 'team:id,name', 'process:id,process_name,process_code']);
            }

            // 수주 상태를 IN_PROGRESS로 변경
            $order->status_code = Order::STATUS_IN_PROGRESS;
            $order->updated_by = $userId;
            $order->save();

            return [
                'work_orders' => $workOrders,
                'work_order' => $workOrders[0] ?? null, // 하위 호환성
                'order' => $this->loadDetailRelations($order),
            ];
        });
    }

    /**
     * BOM 결과에서 슬랫 공정 정보 추출
     *
     * 조인트바 수량: BOM items에서 '조인트바' 항목의 quantity, 없으면 inputVariables의 joint_bar_qty
     * 방화유리 수량: 프론트 입력값(glass_qty) 우선, 없으면 QTY(개소당 수량) 사용
     */
    private function extractSlatInfoFromBom(?array $bomResult, array $locItem = []): ?array
    {
        if (! $bomResult) {
            return null;
        }

        $bomVars = $bomResult['variables'] ?? [];
        $bomItems = $bomResult['items'] ?? [];
        $productType = $bomVars['product_type'] ?? 'screen';

        // 스크린 전용 제품은 슬랫 정보 불필요
        if ($productType === 'screen') {
            return null;
        }

        // 조인트바 수량: BOM items에서 추출, 없으면 입력 변수에서
        $jointBarQty = (int) ($bomVars['joint_bar_qty'] ?? 0);
        if ($jointBarQty === 0) {
            foreach ($bomItems as $item) {
                if (str_contains($item['item_name'] ?? '', '조인트바')) {
                    $jointBarQty = (int) ($item['quantity'] ?? 0);
                    break;
                }
            }
        }

        // 프론트 미전달 시 레거시 5130 자동 계산 (Slat_updateCo76)
        // col76 = (2 + floor((제작가로 - 500) / 1000)) * 셔터수량
        if ($jointBarQty <= 0) {
            $width = (float) ($bomVars['W0'] ?? $locItem['width'] ?? 0);
            $quantity = (int) ($bomVars['QTY'] ?? $locItem['quantity'] ?? 1);
            if ($width > 0) {
                $jointBarQty = (2 + (int) floor(($width - 500) / 1000)) * $quantity;
            }
        }

        // 방화유리 수량: 투시창 선택 시에만 유효 (프론트 입력값 또는 견적 BOM 변수)
        // 레거시: col4_quartz='투시창'일 때만 col14에 수량 표시
        $glassQty = (int) ($locItem['glass_qty'] ?? $bomVars['glass_qty'] ?? 0);

        return [
            'joint_bar' => $jointBarQty,
            'glass_qty' => $glassQty,
        ];
    }

    /**
     * 작업지시번호 자동 생성
     */
    private function generateWorkOrderNo(int $tenantId): string
    {
        $prefix = 'WO';
        $date = now()->format('Ymd');

        $lastNo = WorkOrder::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->where('work_order_no', 'like', "{$prefix}{$date}%")
            ->orderByDesc('work_order_no')
            ->value('work_order_no');

        if ($lastNo) {
            $seq = (int) substr($lastNo, -4) + 1;
        } else {
            $seq = 1;
        }

        return sprintf('%s%s%04d', $prefix, $date, $seq);
    }

    /**
     * 수주확정 되돌리기 (수주등록 상태로 변경)
     */
    public function revertOrderConfirmation(int $orderId): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 수주 조회
        $order = Order::where('tenant_id', $tenantId)
            ->find($orderId);

        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 수주확정 상태에서만 되돌리기 가능
        if ($order->status_code !== Order::STATUS_CONFIRMED) {
            throw new BadRequestHttpException(__('error.order.cannot_revert_not_confirmed'));
        }

        return DB::transaction(function () use ($order, $tenantId, $userId) {
            $deletedSaleId = null;

            // 수주확정 시 생성된 매출이 있으면 삭제
            if ($order->sale_id) {
                $sale = Sale::where('tenant_id', $tenantId)
                    ->where('id', $order->sale_id)
                    ->first();

                if ($sale) {
                    // 수주확정 시 생성된 매출만 삭제 (draft 상태이고 order_confirm 타입)
                    if ($sale->source_type === Sale::SOURCE_ORDER_CONFIRM
                        && $sale->status === 'draft') {
                        $deletedSaleId = $sale->id;
                        $sale->deleted_by = $userId;
                        $sale->save();
                        $sale->delete();
                    }
                }

                // 수주의 매출 연결 해지
                $order->sale_id = null;
            }

            // 상태 변경
            $previousStatus = $order->status_code;
            $order->status_code = Order::STATUS_DRAFT;
            $order->updated_by = $userId;
            $order->save();

            return [
                'order' => $this->loadDetailRelations($order),
                'previous_status' => $previousStatus,
                'deleted_sale_id' => $deletedSaleId,
            ];
        });
    }

    /**
     * 생산지시 되돌리기
     *
     * force=true: 개발 모드 - 모든 데이터 hard delete + 재고 복구 (운영환경 차단)
     * force=false: 운영 모드 - 작업지시 취소 상태 변경 + 재고 역분개 (데이터 보존)
     */
    public function revertProductionOrder(int $orderId, bool $force = false, ?string $reason = null): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // force=true는 운영 환경에서 차단
        if ($force && app()->environment('production')) {
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException(__('error.forbidden'));
        }

        // 운영 모드에서는 reason 필수
        if (! $force && empty($reason)) {
            throw new BadRequestHttpException(__('error.order.cancel_reason_required'));
        }

        // 수주 조회
        $order = Order::where('tenant_id', $tenantId)
            ->find($orderId);

        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 완료된 수주는 되돌리기 불가
        if ($order->status_code === Order::STATUS_COMPLETED) {
            throw new BadRequestHttpException(__('error.order.cannot_revert_completed'));
        }

        if ($force) {
            return $this->revertProductionOrderForce($order, $tenantId, $userId);
        }

        return $this->revertProductionOrderCancel($order, $tenantId, $userId, $reason);
    }

    /**
     * 생산지시 되돌리기 - 개발 모드 (hard delete)
     */
    private function revertProductionOrderForce(Order $order, int $tenantId, int $userId): array
    {
        return DB::transaction(function () use ($order, $tenantId, $userId) {
            $workOrderIds = WorkOrder::where('tenant_id', $tenantId)
                ->where('sales_order_id', $order->id)
                ->pluck('id')
                ->toArray();

            $deletedCounts = [
                'work_results' => 0,
                'work_order_items' => 0,
                'work_orders' => 0,
                'material_inputs' => 0,
                'documents' => 0,
                'step_progress' => 0,
                'assignees' => 0,
                'bending_details' => 0,
                'issues' => 0,
            ];

            if (count($workOrderIds) > 0) {
                // 1. 자재 투입 재고 복구 + 삭제
                $materialInputs = WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->get();
                if ($materialInputs->isNotEmpty()) {
                    $stockService = app(StockService::class);
                    foreach ($materialInputs as $input) {
                        try {
                            $stockService->increaseToLot(
                                stockLotId: $input->stock_lot_id,
                                qty: (float) $input->qty,
                                reason: 'work_order_input_cancel',
                                referenceId: $input->work_order_id
                            );
                        } catch (\Exception $e) {
                            Log::warning('생산지시 되돌리기: 재고 복원 실패', [
                                'input_id' => $input->id,
                                'stock_lot_id' => $input->stock_lot_id,
                                'error' => $e->getMessage(),
                            ]);
                        }
                    }
                    $deletedCounts['material_inputs'] = WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->delete();
                }

                // 2. 문서 삭제 (검사 성적서, 작업일지 등)
                $documentIds = Document::where('linkable_type', 'work_order')
                    ->whereIn('linkable_id', $workOrderIds)
                    ->pluck('id')
                    ->toArray();

                if (count($documentIds) > 0) {
                    DocumentData::whereIn('document_id', $documentIds)->delete();
                    DocumentApproval::whereIn('document_id', $documentIds)->delete();
                    $deletedCounts['documents'] = Document::whereIn('id', $documentIds)->forceDelete();
                }

                // 3. 출하 정보에서 작업지시 참조 해제 (출하 자체는 보존)
                DB::table('shipments')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->update(['work_order_id' => null]);

                // 4. 작업지시 부속 데이터 삭제
                $deletedCounts['step_progress'] = DB::table('work_order_step_progress')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                $deletedCounts['assignees'] = DB::table('work_order_assignees')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                $deletedCounts['bending_details'] = DB::table('work_order_bending_details')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                $deletedCounts['issues'] = DB::table('work_order_issues')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                // 5. 작업결과 삭제
                $deletedCounts['work_results'] = DB::table('work_results')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                // 6. 작업지시 품목 삭제
                $deletedCounts['work_order_items'] = DB::table('work_order_items')
                    ->whereIn('work_order_id', $workOrderIds)
                    ->delete();

                // 7. 작업지시 삭제
                $deletedCounts['work_orders'] = WorkOrder::whereIn('id', $workOrderIds)->delete();
            }

            // 8. 수주 상태를 CONFIRMED로 되돌리기
            $previousStatus = $order->status_code;
            $order->status_code = Order::STATUS_CONFIRMED;
            $order->updated_by = $userId;
            $order->save();

            return [
                'order' => $this->loadDetailRelations($order),
                'deleted_counts' => $deletedCounts,
                'previous_status' => $previousStatus,
            ];
        });
    }

    /**
     * 생산지시 되돌리기 - 운영 모드 (취소 상태 변경, 데이터 보존)
     */
    private function revertProductionOrderCancel(Order $order, int $tenantId, int $userId, string $reason): array
    {
        return DB::transaction(function () use ($order, $tenantId, $userId, $reason) {
            $workOrders = WorkOrder::where('tenant_id', $tenantId)
                ->where('sales_order_id', $order->id)
                ->get();

            $cancelledCount = 0;
            $skippedIds = [];

            $stockService = app(StockService::class);

            foreach ($workOrders as $workOrder) {
                // completed/shipped 상태는 취소 거부
                if (in_array($workOrder->status, [WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED])) {
                    $skippedIds[] = $workOrder->id;

                    continue;
                }

                // 작업지시 상태를 cancelled로 변경
                $workOrder->status = WorkOrder::STATUS_CANCELLED;
                $workOrder->updated_by = $userId;

                // options에 취소 정보 기록
                $options = $workOrder->options ?? [];
                $options['cancelled_at'] = now()->toIso8601String();
                $options['cancelled_by'] = $userId;
                $options['cancel_reason'] = $reason;
                $workOrder->options = $options;
                $workOrder->save();

                // 자재 투입분 재고 역분개
                $materialInputs = WorkOrderMaterialInput::where('work_order_id', $workOrder->id)->get();
                foreach ($materialInputs as $input) {
                    try {
                        $stockService->increaseToLot(
                            stockLotId: $input->stock_lot_id,
                            qty: (float) $input->qty,
                            reason: 'work_order_cancel',
                            referenceId: $workOrder->id
                        );
                    } catch (\Exception $e) {
                        Log::warning('생산지시 취소: 재고 복원 실패', [
                            'input_id' => $input->id,
                            'stock_lot_id' => $input->stock_lot_id,
                            'error' => $e->getMessage(),
                        ]);
                    }
                }

                $cancelledCount++;
            }

            // 수주 상태를 CONFIRMED로 복원
            $previousStatus = $order->status_code;
            $order->status_code = Order::STATUS_CONFIRMED;
            $order->updated_by = $userId;
            $order->save();

            return [
                'order' => $this->loadDetailRelations($order),
                'cancelled_count' => $cancelledCount,
                'skipped_count' => count($skippedIds),
                'skipped_ids' => $skippedIds,
                'previous_status' => $previousStatus,
            ];
        });
    }

    /**
     * 수주의 절곡 BOM 품목별 재고 현황 조회
     *
     * order_items에서 item_category='BENDING'인 품목을 추출하고
     * 각 품목의 재고 가용량/부족량을 반환합니다.
     */
    public function checkBendingStockForOrder(int $orderId): array
    {
        $tenantId = $this->tenantId();

        $order = Order::where('tenant_id', $tenantId)
            ->with(['items'])
            ->find($orderId);

        if (! $order) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // order_items에서 item_id가 있는 품목의 ID 수집 + 수량 합산
        $itemQtyMap = []; // item_id => total_qty
        foreach ($order->items as $orderItem) {
            $itemId = $orderItem->item_id;
            if (! $itemId) {
                continue;
            }
            $qty = (float) ($orderItem->quantity ?? 0);
            if ($qty <= 0) {
                continue;
            }
            $itemQtyMap[$itemId] = ($itemQtyMap[$itemId] ?? 0) + $qty;
        }

        if (empty($itemQtyMap)) {
            return [];
        }

        // items 테이블에서 item_category = 'BENDING'인 것만 필터
        $bendingItems = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->whereIn('id', array_keys($itemQtyMap))
            ->where('item_category', 'BENDING')
            ->whereNull('deleted_at')
            ->select('id', 'code', 'name', 'unit')
            ->get();

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

        $stockService = app(StockService::class);
        $result = [];

        foreach ($bendingItems as $item) {
            $neededQty = $itemQtyMap[$item->id];
            $stockInfo = $stockService->getAvailableStock($item->id);

            $availableQty = $stockInfo ? (float) $stockInfo['available_qty'] : 0;
            $reservedQty = $stockInfo ? (float) $stockInfo['reserved_qty'] : 0;
            $stockQty = $stockInfo ? (float) $stockInfo['stock_qty'] : 0;
            $shortfallQty = max(0, $neededQty - $availableQty);

            $result[] = [
                'item_id' => $item->id,
                'item_code' => $item->code,
                'item_name' => $item->name,
                'unit' => $item->unit,
                'needed_qty' => $neededQty,
                'stock_qty' => $stockQty,
                'reserved_qty' => $reservedQty,
                'available_qty' => $availableQty,
                'shortfall_qty' => $shortfallQty,
                'status' => $shortfallQty > 0 ? 'insufficient' : 'sufficient',
            ];
        }

        return $result;
    }
}
