<?php

namespace App\Services\Quote;

use App\Models\Bidding\Bidding;
use App\Models\Items\Item;
use App\Models\Orders\Order;
use App\Models\Orders\OrderItem;
use App\Models\Orders\OrderNode;
use App\Models\Quote\Quote;
use App\Models\Quote\QuoteItem;
use App\Models\Quote\QuoteRevision;
use App\Models\Tenants\SiteBriefing;
use App\Services\OrderService;
use App\Services\Service;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class QuoteService extends Service
{
    public function __construct(
        private QuoteNumberService $numberService,
        private QuoteCalculationService $calculationService,
        private OrderService $orderService
    ) {}

    /**
     * 견적 목록 조회
     */
    public function index(array $params): LengthAwarePaginator
    {
        $tenantId = $this->tenantId();

        $page = (int) ($params['page'] ?? 1);
        $size = (int) ($params['size'] ?? 20);
        $q = trim((string) ($params['q'] ?? ''));
        $quoteType = $params['quote_type'] ?? null;
        $status = $params['status'] ?? null;
        $productCategory = $params['product_category'] ?? null;
        $clientId = $params['client_id'] ?? null;
        $dateFrom = $params['date_from'] ?? null;
        $dateTo = $params['date_to'] ?? null;
        $sortBy = $params['sort_by'] ?? 'registration_date';
        $sortOrder = $params['sort_order'] ?? 'desc';
        $withItems = filter_var($params['with_items'] ?? false, FILTER_VALIDATE_BOOLEAN);
        $forOrder = filter_var($params['for_order'] ?? false, FILTER_VALIDATE_BOOLEAN);

        $query = Quote::query()->where('tenant_id', $tenantId);

        // 수주 전환용 조회: 아직 수주가 생성되지 않은 견적만
        if ($forOrder) {
            // 1. Quote.order_id가 null인 것 (빠른 체크)
            $query->whereNull('order_id');
            // 2. Orders 테이블에 해당 quote_id가 없는 것 (이중 체크, 인덱스 있음)
            $query->whereDoesntHave('orders');
        }

        // items 포함 (수주 전환용)
        if ($withItems) {
            $query->with(['items', 'client:id,name,contact_person,phone']);
        }

        // 검색어
        if ($q !== '') {
            $query->search($q);
        }

        // 견적 유형 필터
        if ($quoteType) {
            $query->where('quote_type', $quoteType);
        }

        // 상태 필터 (converted는 order_id 기반으로 판별)
        if ($status === Quote::STATUS_CONVERTED) {
            $query->whereNotNull('order_id');
        } elseif ($status) {
            $query->where('status', $status)->whereNull('order_id');
        }

        // 제품 카테고리 필터
        if ($productCategory) {
            $query->where('product_category', $productCategory);
        }

        // 발주처 필터
        if ($clientId) {
            $query->where('client_id', $clientId);
        }

        // 날짜 범위
        $query->dateRange($dateFrom, $dateTo);

        // 정렬
        $allowedSortColumns = ['registration_date', 'quote_number', 'client_name', 'total_amount', 'status', 'created_at'];
        if (in_array($sortBy, $allowedSortColumns)) {
            $query->orderBy($sortBy, $sortOrder === 'asc' ? 'asc' : 'desc');
        } else {
            $query->orderBy('registration_date', 'desc');
        }

        $query->orderBy('id', 'desc');

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

    /**
     * 견적 참조 데이터 조회 (현장명, 부호 목록)
     *
     * 기존 견적/수주에서 사용된 현장명과 부호를 DISTINCT로 조회합니다.
     */
    public function referenceData(): array
    {
        $tenantId = $this->tenantId();

        // 현장명: 견적 테이블에서 DISTINCT
        $siteNames = Quote::where('tenant_id', $tenantId)
            ->whereNotNull('site_name')
            ->where('site_name', '!=', '')
            ->distinct()
            ->orderBy('site_name')
            ->pluck('site_name')
            ->toArray();

        // 부호(개소코드): calculation_inputs JSON 내 items[].code (예: FSS-01, SD-02)
        $locationCodes = collect();

        // calculation_inputs JSON에서 items[].code 추출
        $quotesWithInputs = Quote::where('tenant_id', $tenantId)
            ->whereNotNull('calculation_inputs')
            ->select('calculation_inputs')
            ->get();

        foreach ($quotesWithInputs as $quote) {
            $inputs = is_string($quote->calculation_inputs)
                ? json_decode($quote->calculation_inputs, true)
                : $quote->calculation_inputs;

            if (! is_array($inputs)) {
                continue;
            }

            $items = $inputs['items'] ?? $inputs['locations'] ?? [];
            foreach ($items as $item) {
                $code = $item['code'] ?? null;
                if ($code && trim($code) !== '') {
                    $locationCodes->push(trim($code));
                }
            }
        }

        // 중복 제거, 정렬
        $locationCodes = $locationCodes
            ->unique()
            ->sort()
            ->values()
            ->toArray();

        return [
            'site_names' => $siteNames,
            'location_codes' => $locationCodes,
        ];
    }

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

        $quote = Quote::with(['items', 'revisions', 'client', 'creator', 'updater', 'finalizer', 'siteBriefing.partner'])
            ->where('tenant_id', $tenantId)
            ->find($id);

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

        // 역방향 참조 보정: order_id가 null인데 Order.quote_id로 연결된 수주가 있는 경우
        if (! $quote->order_id) {
            $linkedOrder = Order::where('quote_id', $quote->id)
                ->where('tenant_id', $tenantId)
                ->first();
            if ($linkedOrder) {
                $quote->update(['order_id' => $linkedOrder->id]);
                $quote->refresh()->load(['items', 'revisions', 'client', 'creator', 'updater', 'finalizer', 'siteBriefing.partner']);
            }
        }

        // BOM 자재 데이터 계산 및 추가
        $bomMaterials = $this->calculateBomMaterials($quote);
        if (! empty($bomMaterials)) {
            $quote->setAttribute('bom_materials', $bomMaterials);
        }

        return $quote;
    }

    /**
     * 저장된 calculation_inputs를 기반으로 BOM 원자재(leaf nodes) 목록 조회
     *
     * 세부산출내역과 달리, BOM 트리에서 실제 원자재만 추출합니다:
     * - 세부산출내역: BOM 계산 결과 (수식 기반 산출 품목)
     * - 소요자재내역: BOM 트리 leaf nodes (실제 구매 필요한 원자재)
     */
    private function calculateBomMaterials(Quote $quote): array
    {
        $calculationInputs = $quote->calculation_inputs;

        // calculation_inputs가 없거나 items가 없으면 빈 배열 반환
        if (empty($calculationInputs) || empty($calculationInputs['items'])) {
            return [];
        }

        $tenantId = $this->tenantId();
        $inputItems = $calculationInputs['items'];
        $allMaterials = [];

        foreach ($inputItems as $index => $input) {
            // 완제품 코드 찾기 (productName에 저장됨)
            $finishedGoodsCode = $input['productName'] ?? null;
            if (! $finishedGoodsCode) {
                continue;
            }

            // 주문 수량
            $orderQuantity = (float) ($input['quantity'] ?? 1);

            // BOM 계산을 위한 입력 변수 구성
            $variables = [
                'W0' => (float) ($input['openWidth'] ?? 0),
                'H0' => (float) ($input['openHeight'] ?? 0),
                'QTY' => $orderQuantity,
                'PC' => $input['productCategory'] ?? 'SCREEN',
                'GT' => $input['guideRailType'] ?? 'wall',
                'MP' => $input['motorPower'] ?? 'single',
                'CT' => $input['controller'] ?? 'basic',
                'WS' => (float) ($input['wingSize'] ?? 50),
                'INSP' => (float) ($input['inspectionFee'] ?? 50000),
            ];

            try {
                // BOM 트리에서 원자재(leaf nodes)만 추출
                $leafMaterials = $this->calculationService->formulaEvaluator->getBomLeafMaterials(
                    $finishedGoodsCode,
                    $orderQuantity,
                    $variables,
                    $tenantId
                );

                // 각 자재 항목에 인덱스 정보 추가
                foreach ($leafMaterials as $material) {
                    $allMaterials[] = [
                        'item_index' => $index,
                        'finished_goods_code' => $finishedGoodsCode,
                        'item_id' => $material['item_id'] ?? null,
                        'item_code' => $material['item_code'] ?? '',
                        'item_name' => $material['item_name'] ?? '',
                        'item_type' => $material['item_type'] ?? '',
                        'item_category' => $material['item_category'] ?? '',
                        'specification' => $material['specification'] ?? '',
                        'unit' => $material['unit'] ?? 'EA',
                        'quantity' => $material['quantity'] ?? 0,
                        'unit_price' => $material['unit_price'] ?? 0,
                        'total_price' => $material['total_price'] ?? 0,
                        'process_type' => $material['process_type'] ?? '',
                    ];
                }
            } catch (\Throwable) {
                // BOM 조회 실패 시 해당 품목은 스킵
                continue;
            }
        }

        return $allMaterials;
    }

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

        return DB::transaction(function () use ($data, $tenantId, $userId) {
            // 견적번호 생성
            $quoteNumber = $data['quote_number'] ?? $this->numberService->generate($data['product_category'] ?? 'SCREEN');

            // 금액 계산
            $materialCost = (float) ($data['material_cost'] ?? 0);
            $laborCost = (float) ($data['labor_cost'] ?? 0);
            $installCost = (float) ($data['install_cost'] ?? 0);
            $subtotal = $materialCost + $laborCost + $installCost;
            $discountRate = (float) ($data['discount_rate'] ?? 0);
            // 프론트엔드에서 직접 계산한 할인금액이 있으면 사용, 없으면 subtotal에서 계산
            $discountAmount = isset($data['discount_amount'])
                ? (float) $data['discount_amount']
                : $subtotal * ($discountRate / 100);
            $totalAmount = $subtotal - $discountAmount;

            // 견적 생성
            $quote = Quote::create([
                'tenant_id' => $tenantId,
                'quote_number' => $quoteNumber,
                'registration_date' => $data['registration_date'] ?? now()->toDateString(),
                'receipt_date' => $data['receipt_date'] ?? null,
                'author' => $data['author'] ?? null,
                // 발주처 정보
                'client_id' => $data['client_id'] ?? null,
                'client_name' => $data['client_name'] ?? null,
                'manager' => $data['manager'] ?? null,
                'contact' => $data['contact'] ?? null,
                // 현장 정보
                'site_id' => $data['site_id'] ?? null,
                'site_name' => $data['site_name'] ?? null,
                'site_code' => $data['site_code'] ?? null,
                // 제품 정보
                'product_category' => $data['product_category'] ?? Quote::CATEGORY_SCREEN,
                'product_id' => $data['product_id'] ?? null,
                'product_code' => $data['product_code'] ?? $this->extractProductCodeFromInputs($data),
                'product_name' => $data['product_name'] ?? null,
                // 규격 정보
                'open_size_width' => $data['open_size_width'] ?? null,
                'open_size_height' => $data['open_size_height'] ?? null,
                'quantity' => $data['quantity'] ?? 1,
                'unit_symbol' => $data['unit_symbol'] ?? null,
                'floors' => $data['floors'] ?? null,
                // 금액 정보
                'material_cost' => $materialCost,
                'labor_cost' => $laborCost,
                'install_cost' => $installCost,
                'subtotal' => $subtotal,
                'discount_rate' => $discountRate,
                'discount_amount' => $discountAmount,
                'total_amount' => $data['total_amount'] ?? $totalAmount,
                // 상태 관리
                'status' => Quote::STATUS_DRAFT,
                'current_revision' => 0,
                'is_final' => false,
                // 기타 정보
                'completion_date' => $data['completion_date'] ?? null,
                'remarks' => $data['remarks'] ?? null,
                'memo' => $data['memo'] ?? null,
                'notes' => $data['notes'] ?? null,
                // 자동산출 입력값
                'calculation_inputs' => $data['calculation_inputs'] ?? null,
                // 감사
                'created_by' => $userId,
            ]);

            // 견적 품목 생성
            if (! empty($data['items']) && is_array($data['items'])) {
                $this->createItems($quote, $data['items'], $tenantId);
            }

            return $quote->load(['items', 'client']);
        });
    }

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

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

        if (! $quote->isEditable()) {
            throw new BadRequestHttpException(__('error.quote_not_editable'));
        }

        return DB::transaction(function () use ($quote, $data, $tenantId, $userId) {
            // 수정 이력 생성
            $this->createRevision($quote, $userId);

            // 상태 변경: pending(견적대기) → draft(작성중)
            // 현장설명회에서 자동 생성된 견적을 처음 수정하면 작성중 상태로 변경
            $newStatus = $quote->status;
            if ($quote->status === Quote::STATUS_PENDING) {
                $newStatus = Quote::STATUS_DRAFT;
            }

            // 금액 재계산
            $materialCost = (float) ($data['material_cost'] ?? $quote->material_cost);
            $laborCost = (float) ($data['labor_cost'] ?? $quote->labor_cost);
            $installCost = (float) ($data['install_cost'] ?? $quote->install_cost);
            $subtotal = $materialCost + $laborCost + $installCost;
            $discountRate = (float) ($data['discount_rate'] ?? $quote->discount_rate);
            // 프론트엔드에서 직접 계산한 할인금액이 있으면 사용, 없으면 subtotal에서 계산
            $discountAmount = isset($data['discount_amount'])
                ? (float) $data['discount_amount']
                : $subtotal * ($discountRate / 100);
            $totalAmount = $subtotal - $discountAmount;

            // 업데이트
            $quote->update([
                'status' => $newStatus,
                'receipt_date' => $data['receipt_date'] ?? $quote->receipt_date,
                'author' => $data['author'] ?? $quote->author,
                // 발주처 정보
                'client_id' => $data['client_id'] ?? $quote->client_id,
                'client_name' => $data['client_name'] ?? $quote->client_name,
                'manager' => $data['manager'] ?? $quote->manager,
                'contact' => $data['contact'] ?? $quote->contact,
                // 현장 정보
                'site_id' => $data['site_id'] ?? $quote->site_id,
                'site_name' => $data['site_name'] ?? $quote->site_name,
                'site_code' => $data['site_code'] ?? $quote->site_code,
                // 제품 정보
                'product_category' => $data['product_category'] ?? $quote->product_category,
                'product_id' => $data['product_id'] ?? $quote->product_id,
                'product_code' => $data['product_code'] ?? $this->extractProductCodeFromInputs($data) ?? $quote->product_code,
                'product_name' => $data['product_name'] ?? $quote->product_name,
                // 규격 정보
                'open_size_width' => $data['open_size_width'] ?? $quote->open_size_width,
                'open_size_height' => $data['open_size_height'] ?? $quote->open_size_height,
                'quantity' => $data['quantity'] ?? $quote->quantity,
                'unit_symbol' => $data['unit_symbol'] ?? $quote->unit_symbol,
                'floors' => $data['floors'] ?? $quote->floors,
                // 금액 정보
                'material_cost' => $materialCost,
                'labor_cost' => $laborCost,
                'install_cost' => $installCost,
                'subtotal' => $subtotal,
                'discount_rate' => $discountRate,
                'discount_amount' => $discountAmount,
                'total_amount' => $data['total_amount'] ?? $totalAmount,
                // 기타 정보
                'completion_date' => $data['completion_date'] ?? $quote->completion_date,
                'remarks' => $data['remarks'] ?? $quote->remarks,
                'memo' => $data['memo'] ?? $quote->memo,
                'notes' => $data['notes'] ?? $quote->notes,
                // 자동산출 입력값
                'calculation_inputs' => $data['calculation_inputs'] ?? $quote->calculation_inputs,
                // 견적 옵션 (summary_items, expense_items, price_adjustments, detail_items, price_adjustment_data)
                // 기존 options와 새 options를 병합 (새 데이터가 기존 데이터를 덮어씀)
                'options' => $this->mergeOptions($quote->options, $data['options'] ?? null),
                // 감사
                'updated_by' => $userId,
                'current_revision' => $quote->current_revision + 1,
            ]);

            // 품목 업데이트 (전체 교체)
            if (isset($data['items']) && is_array($data['items'])) {
                $quote->items()->delete();
                $this->createItems($quote, $data['items'], $tenantId);
            }

            $quote->refresh()->load(['items', 'revisions', 'client']);

            // 연결된 수주가 있으면 동기화 (역방향 참조도 포함)
            $syncOrderId = $quote->order_id;
            if (! $syncOrderId) {
                $linkedOrder = Order::where('quote_id', $quote->id)
                    ->where('tenant_id', $tenantId)
                    ->first();
                if ($linkedOrder) {
                    $quote->update(['order_id' => $linkedOrder->id]);
                    $quote->refresh()->load(['items', 'revisions', 'client']);
                    $syncOrderId = $linkedOrder->id;
                }
            }
            if ($syncOrderId) {
                try {
                    $this->orderService->setContext($tenantId, $userId);
                    $this->orderService->syncFromQuote($quote, $quote->current_revision);
                } catch (\Throwable $e) {
                    // 수주 동기화 실패는 로그만 남기고 견적 수정은 성공 처리
                    Log::warning('Failed to sync order from quote', [
                        'quote_id' => $quote->id,
                        'order_id' => $syncOrderId,
                        'error' => $e->getMessage(),
                    ]);
                }
            }

            return $quote;
        });
    }

    /**
     * 견적 삭제 (Soft Delete)
     */
    public function destroy(int $id): bool
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        if (! $quote->isDeletable()) {
            throw new BadRequestHttpException(__('error.quote_not_deletable'));
        }

        $quote->deleted_by = $userId;
        $quote->save();
        $quote->delete();

        return true;
    }

    /**
     * 견적 일괄 삭제
     */
    public function bulkDestroy(array $ids): int
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();
        $deletedCount = 0;

        foreach ($ids as $id) {
            $quote = Quote::where('tenant_id', $tenantId)->find($id);
            if ($quote && $quote->isDeletable()) {
                $quote->deleted_by = $userId;
                $quote->save();
                $quote->delete();
                $deletedCount++;
            }
        }

        return $deletedCount;
    }

    /**
     * 최종 확정
     */
    public function finalize(int $id): Quote
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        if (! $quote->isFinalizable()) {
            throw new BadRequestHttpException(__('error.quote_not_finalizable'));
        }

        // 확정 시 필수 필드 검증 (업체명, 현장명, 담당자, 연락처)
        $missing = [];
        if (empty($quote->client_name)) {
            $missing[] = '업체명';
        }
        if (empty($quote->site_name)) {
            $missing[] = '현장명';
        }
        if (empty($quote->manager)) {
            $missing[] = '담당자';
        }
        if (empty($quote->contact)) {
            $missing[] = '연락처';
        }
        if (! empty($missing)) {
            throw new BadRequestHttpException(
                __('error.quote_finalize_missing_fields', ['fields' => implode(', ', $missing)])
            );
        }

        $quote->update([
            'status' => Quote::STATUS_FINALIZED,
            'is_final' => true,
            'finalized_at' => now(),
            'finalized_by' => $userId,
            'updated_by' => $userId,
        ]);

        return $quote->refresh()->load(['items', 'client', 'finalizer']);
    }

    /**
     * 확정 취소
     */
    public function cancelFinalize(int $id): Quote
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        if ($quote->order_id) {
            throw new BadRequestHttpException(__('error.quote_already_converted'));
        }

        if ($quote->getRawOriginal('status') !== Quote::STATUS_FINALIZED) {
            throw new BadRequestHttpException(__('error.quote_not_finalized'));
        }

        $quote->update([
            'status' => Quote::STATUS_DRAFT,
            'is_final' => false,
            'finalized_at' => null,
            'finalized_by' => null,
            'updated_by' => $userId,
        ]);

        return $quote->refresh()->load(['items', 'client']);
    }

    /**
     * 수주 전환
     */
    public function convertToOrder(int $id): Quote
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        $quote = Quote::where('tenant_id', $tenantId)
            ->with(['items', 'client'])
            ->find($id);

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

        if (! $quote->isConvertible()) {
            throw new BadRequestHttpException(__('error.quote_not_convertible'));
        }

        return DB::transaction(function () use ($quote, $userId, $tenantId) {
            // 수주번호 생성
            $orderNo = $this->generateOrderNumber($tenantId);

            // 수주 마스터 생성
            $order = Order::createFromQuote($quote, $orderNo);
            $order->created_by = $userId;
            $order->save();

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

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

                $node = 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,
                        '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,
                    ],
                    'depth' => 0,
                    'sort_order' => $idx,
                    'created_by' => $userId,
                ]);
                $nodeMap[$idx] = $node;
            }

            // 수주 상세 품목 생성 (노드 연결 포함)
            // 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;

            $serialIndex = 1;
            foreach ($quote->items as $index => $quoteItem) {
                $productMapping = $this->resolveLocationMapping($quoteItem, $productItems);
                $locIdx = $this->resolveLocationIndex($quoteItem, $productItems);

                // sort_order 기반 분배 (formula_source/note 매칭 모두 실패 시)
                if ($locIdx === 0 && $itemsPerLocation > 0) {
                    $locIdx = min(intdiv($index, $itemsPerLocation), $locationCount - 1);
                }

                $productMapping['order_node_id'] = isset($nodeMap[$locIdx]) ? $nodeMap[$locIdx]->id : null;

                $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping);
                $orderItem->created_by = $userId;
                $orderItem->save();
                $serialIndex++;
            }

            // 수주 합계 재계산
            $order->load('items');
            $order->recalculateTotals();
            $order->save();

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

            return $quote->refresh()->load(['items', 'client', 'order']);
        });
    }

    /**
     * 견적 품목에서 개소(층/부호) 매핑 정보 추출
     *
     * 1순위: note 필드 파싱 ("4F FSS-01" → floor_code:"4F", symbol_code:"FSS-01")
     * 2순위: formula_source → calculation_inputs.items[] 매칭
     */
    private function resolveLocationMapping(QuoteItem $quoteItem, array $productItems): array
    {
        $floorCode = null;
        $symbolCode = null;

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

        // 2순위: formula_source → calculation_inputs
        if (empty($floorCode) && empty($symbolCode)) {
            $productIndex = 0;
            $formulaSource = $quoteItem->formula_source ?? '';
            if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
                $productIndex = (int) $matches[1];
            }

            if (isset($productItems[$productIndex])) {
                $floorCode = $productItems[$productIndex]['floor'] ?? null;
                $symbolCode = $productItems[$productIndex]['code'] ?? null;
            } elseif (count($productItems) === 1) {
                $floorCode = $productItems[0]['floor'] ?? null;
                $symbolCode = $productItems[0]['code'] ?? null;
            }
        }

        return ['floor_code' => $floorCode, 'symbol_code' => $symbolCode];
    }

    /**
     * 견적 품목이 속하는 개소(productItems) 인덱스 반환
     *
     * 1순위: formula_source에서 product_N 패턴 추출
     * 2순위: note 파싱 후 productItems에서 floor/code 매칭
     * 매칭 실패 시 0 반환
     */
    private function resolveLocationIndex(QuoteItem $quoteItem, array $productItems): int
    {
        // 1순위: formula_source
        $formulaSource = $quoteItem->formula_source ?? '';
        if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
            return (int) $matches[1];
        }

        // 2순위: note에서 floor/code 매칭
        $note = trim($quoteItem->note ?? '');
        if ($note !== '') {
            $parts = preg_split('/\s+/', $note, 2);
            $floor = $parts[0] ?? '';
            $code = $parts[1] ?? '';
            foreach ($productItems as $idx => $item) {
                if (($item['floor'] ?? '') === $floor && ($item['code'] ?? '') === $code) {
                    return $idx;
                }
            }
        }

        return 0;
    }

    /**
     * calculation_inputs에서 첫 번째 개소의 productCode 추출
     * 다중 개소 시 첫 번째를 대표값으로 사용
     */
    private function extractProductCodeFromInputs(array $data): ?string
    {
        $inputs = $data['calculation_inputs'] ?? null;
        if (! $inputs || ! is_array($inputs)) {
            return null;
        }

        $items = $inputs['items'] ?? [];

        return $items[0]['productCode'] ?? null;
    }

    /**
     * 수주번호 생성
     * 형식: ORD-YYMMDD-NNN (예: ORD-260105-001)
     */
    private function generateOrderNumber(int $tenantId): string
    {
        $dateStr = now()->format('ymd');
        $prefix = "ORD-{$dateStr}-";

        $lastOrder = Order::withTrashed()
            ->where('tenant_id', $tenantId)
            ->where('order_no', 'like', $prefix.'%')
            ->orderBy('order_no', 'desc')
            ->first();

        $sequence = 1;
        if ($lastOrder) {
            $parts = explode('-', $lastOrder->order_no);
            if (count($parts) >= 3) {
                $lastSeq = (int) end($parts);
                $sequence = $lastSeq + 1;
            }
        }

        $seqStr = str_pad((string) $sequence, 3, '0', STR_PAD_LEFT);

        return "{$prefix}{$seqStr}";
    }

    /**
     * 견적 품목 생성
     */
    private function createItems(Quote $quote, array $items, int $tenantId): void
    {
        foreach ($items as $index => $item) {
            $quantity = (float) ($item['calculated_quantity'] ?? $item['base_quantity'] ?? 1);
            $unitPrice = (float) ($item['unit_price'] ?? 0);
            $totalPrice = $quantity * $unitPrice;

            // item_type: 전달된 값 또는 items 테이블에서 조회
            $itemType = $item['item_type'] ?? null;
            if ($itemType === null && isset($item['item_id'])) {
                $itemType = Item::where('id', $item['item_id'])->value('item_type');
            }

            QuoteItem::create([
                'quote_id' => $quote->id,
                'tenant_id' => $tenantId,
                'item_id' => $item['item_id'] ?? null,
                'item_type' => $itemType,
                'item_code' => $item['item_code'] ?? '',
                'item_name' => $item['item_name'] ?? '',
                'specification' => $item['specification'] ?? null,
                'unit' => $item['unit'] ?? 'EA',
                'base_quantity' => $item['base_quantity'] ?? 1,
                'calculated_quantity' => $quantity,
                'unit_price' => $unitPrice,
                'total_price' => $item['total_price'] ?? $totalPrice,
                'formula' => $item['formula'] ?? null,
                'formula_result' => $item['formula_result'] ?? null,
                'formula_source' => $item['formula_source'] ?? null,
                'formula_category' => $item['formula_category'] ?? null,
                'data_source' => $item['data_source'] ?? null,
                'delivery_date' => $item['delivery_date'] ?? null,
                'note' => $item['note'] ?? null,
                'sort_order' => $item['sort_order'] ?? $index,
            ]);
        }
    }

    /**
     * 수정 이력 생성
     */
    private function createRevision(Quote $quote, int $userId): QuoteRevision
    {
        // 현재 견적 데이터 스냅샷
        $previousData = $quote->toArray();
        $previousData['items'] = $quote->items->toArray();

        return QuoteRevision::create([
            'quote_id' => $quote->id,
            'tenant_id' => $quote->tenant_id,
            'revision_number' => $quote->current_revision + 1,
            'revision_date' => now()->toDateString(),
            'revision_by' => $userId,
            'revision_by_name' => auth()->user()?->name ?? 'Unknown',
            'revision_reason' => null, // 별도 입력 받지 않음
            'previous_data' => $previousData,
        ]);
    }

    /**
     * 현장설명회에서 견적 Upsert (생성 또는 업데이트)
     *
     * 참석완료 상태일 때 견적을 자동 생성하거나 업데이트합니다.
     * - 견적이 없으면: 견적대기(pending) 상태로 신규 생성
     * - 견적이 있으면: 현장설명회 정보로 업데이트 (거래처, 현장 등)
     */
    public function upsertFromSiteBriefing(SiteBriefing $siteBriefing): Quote
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 기존 견적 조회
        $existingQuote = Quote::where('tenant_id', $tenantId)
            ->where('site_briefing_id', $siteBriefing->id)
            ->first();

        return DB::transaction(function () use ($siteBriefing, $tenantId, $userId, $existingQuote) {
            if ($existingQuote) {
                // 기존 견적 업데이트 (현장설명회 정보 동기화)
                $existingQuote->update([
                    // 발주처 정보 동기화
                    'client_id' => $siteBriefing->partner_id,
                    'client_name' => $siteBriefing->partner?->name,
                    // 현장 정보 동기화
                    'site_id' => $siteBriefing->site_id,
                    'site_name' => $siteBriefing->title,
                    // 감사
                    'updated_by' => $userId,
                ]);

                return $existingQuote->refresh();
            }

            // 신규 견적 생성
            $quoteNumber = $this->numberService->generate('SCREEN');

            return Quote::create([
                'tenant_id' => $tenantId,
                'quote_type' => Quote::TYPE_CONSTRUCTION,
                'site_briefing_id' => $siteBriefing->id,
                'quote_number' => $quoteNumber,
                'registration_date' => now()->toDateString(),
                // 발주처 정보 (현장설명회에서 복사)
                'client_id' => $siteBriefing->partner_id,
                'client_name' => $siteBriefing->partner?->name,
                // 현장 정보 (현장설명회에서 복사)
                'site_id' => $siteBriefing->site_id,
                'site_name' => $siteBriefing->title,
                // 제품 카테고리 없음 (pending 상태이므로)
                'product_category' => null,
                // 금액 정보 (빈 값)
                'material_cost' => 0,
                'labor_cost' => 0,
                'install_cost' => 0,
                'subtotal' => 0,
                'discount_rate' => 0,
                'discount_amount' => 0,
                'total_amount' => 0,
                // 상태 관리 (견적대기)
                'status' => Quote::STATUS_PENDING,
                'current_revision' => 0,
                'is_final' => false,
                // 비고 (현장설명회 정보 기록)
                'remarks' => "현장설명회 참석완료로 자동생성 (현설번호: {$siteBriefing->briefing_code})",
                // 감사
                'created_by' => $userId,
            ]);
        });
    }

    /**
     * @deprecated Use upsertFromSiteBriefing() instead
     */
    public function createFromSiteBriefing(SiteBriefing $siteBriefing): ?Quote
    {
        return $this->upsertFromSiteBriefing($siteBriefing);
    }

    /**
     * 현장설명회 ID로 연결된 견적 조회
     */
    public function findBySiteBriefingId(int $siteBriefingId): ?Quote
    {
        $tenantId = $this->tenantId();

        return Quote::where('tenant_id', $tenantId)
            ->where('site_briefing_id', $siteBriefingId)
            ->first();
    }

    /**
     * 견적 options 병합
     * 기존 options와 새 options를 병합하여 반환
     */
    private function mergeOptions(?array $existingOptions, ?array $newOptions): ?array
    {
        \Log::info('🔍 [QuoteService::mergeOptions] 시작', [
            'existingOptions_keys' => $existingOptions ? array_keys($existingOptions) : null,
            'newOptions_keys' => $newOptions ? array_keys($newOptions) : null,
            'newOptions_detail_items_count' => isset($newOptions['detail_items']) ? count($newOptions['detail_items']) : 0,
            'newOptions_price_adjustment_data' => isset($newOptions['price_adjustment_data']) ? 'exists' : 'null',
        ]);

        if ($newOptions === null) {
            return $existingOptions;
        }

        if ($existingOptions === null) {
            \Log::info('✅ [QuoteService::mergeOptions] 기존 없음, 새 options 반환', [
                'result_keys' => array_keys($newOptions),
            ]);

            return $newOptions;
        }

        $merged = array_merge($existingOptions, $newOptions);

        \Log::info('✅ [QuoteService::mergeOptions] 병합 완료', [
            'merged_keys' => array_keys($merged),
            'merged_detail_items_count' => isset($merged['detail_items']) ? count($merged['detail_items']) : 0,
        ]);

        return $merged;
    }

    /**
     * 견적을 입찰로 변환
     * 시공 견적(finalized)만 입찰로 변환 가능
     *
     * @param  int  $quoteId  견적 ID
     * @return Bidding 생성된 입찰 정보
     *
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException 견적이 존재하지 않는 경우
     * @throws \RuntimeException 이미 입찰로 변환된 경우 또는 시공 견적이 아닌 경우
     */
    public function convertToBidding(int $quoteId): Bidding
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        // 1. 견적 조회 (시공 견적이어야 함)
        $quote = Quote::where('tenant_id', $tenantId)
            ->where('id', $quoteId)
            ->where('type', Quote::TYPE_CONSTRUCTION) // 시공 견적만
            ->firstOrFail();

        // 2. 이미 입찰로 변환되었는지 확인
        $existingBidding = Bidding::where('tenant_id', $tenantId)
            ->where('quote_id', $quoteId)
            ->first();

        if ($existingBidding) {
            throw new \RuntimeException(__('error.bidding.already_converted'));
        }

        // 3. 입찰번호 생성 (BID-YYYYMMDD-XXXX)
        $today = now()->format('Ymd');
        $lastBidding = Bidding::where('tenant_id', $tenantId)
            ->where('bidding_code', 'like', "BID-{$today}-%")
            ->orderBy('bidding_code', 'desc')
            ->first();

        $sequence = 1;
        if ($lastBidding) {
            $parts = explode('-', $lastBidding->bidding_code);
            $sequence = (int) ($parts[2] ?? 0) + 1;
        }
        $biddingCode = sprintf('BID-%s-%04d', $today, $sequence);

        // 4. 입찰 생성
        $bidding = Bidding::create([
            'tenant_id' => $tenantId,
            'bidding_code' => $biddingCode,
            'quote_id' => $quote->id,
            // 거래처/현장 정보
            'client_id' => $quote->client_id,
            'client_name' => $quote->client_name ?? $quote->client?->name,
            'project_name' => $quote->project_name,
            // 입찰 정보 (초기값)
            'bidding_date' => now()->toDateString(),
            'total_count' => 1,
            'bidding_amount' => $quote->total_amount ?? 0,
            'status' => Bidding::STATUS_WAITING,
            // 입찰자 정보 (현재 사용자)
            'bidder_id' => $userId,
            // VAT 유형
            'vat_type' => $quote->vat_type ?? Bidding::VAT_INCLUDED,
            // 견적 데이터 스냅샷
            'expense_items' => $quote->options['expense_items'] ?? null,
            'estimate_detail_items' => $quote->options['detail_items'] ?? null,
            // 감사 필드
            'created_by' => $userId,
        ]);

        \Log::info('✅ [QuoteService::convertToBidding] 입찰 변환 완료', [
            'quote_id' => $quoteId,
            'bidding_id' => $bidding->id,
            'bidding_code' => $biddingCode,
        ]);

        return $bidding;
    }
}
