<?php

namespace App\Services;

use App\Models\Documents\Document;
use App\Models\Documents\DocumentTemplate;
use App\Models\Orders\Order;
use App\Models\Process;
use App\Models\Production\WorkOrder;
use App\Models\Production\WorkOrderAssignee;
use App\Models\Production\WorkOrderBendingDetail;
use App\Models\Production\WorkOrderItem;
use App\Models\Production\WorkOrderMaterialInput;
use App\Models\Production\WorkOrderStepProgress;
use App\Models\Tenants\Shipment;
use App\Models\Tenants\ShipmentItem;
use App\Models\Tenants\StockTransaction;
use App\Services\Audit\AuditLogger;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class WorkOrderService extends Service
{
    private const AUDIT_TARGET = 'work_order';

    public function __construct(
        private readonly AuditLogger $auditLogger,
        private readonly StockService $stockService
    ) {}

    /**
     * 목록 조회 (검색/필터링/페이징)
     */
    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;
        $processId = $params['process_id'] ?? null;
        $processCode = $params['process_code'] ?? null;
        $assigneeId = $params['assignee_id'] ?? null;
        $assignedToMe = isset($params['assigned_to_me']) && $params['assigned_to_me'];
        $workerScreen = isset($params['worker_screen']) && $params['worker_screen'];
        $teamId = $params['team_id'] ?? null;
        $scheduledFrom = $params['scheduled_from'] ?? null;
        $scheduledTo = $params['scheduled_to'] ?? null;

        $query = WorkOrder::query()
            ->where('tenant_id', $tenantId)
            ->with([
                'assignee:id,name',
                'assignees.user:id,name',
                'team:id,name',
                'salesOrder' => fn ($q) => $q->select('id', 'order_no', 'client_id', 'client_name', 'client_contact', 'site_name', 'quantity', 'received_at', 'delivery_date', 'options')->withCount('rootNodes'),
                'salesOrder.client:id,name',
                'process:id,process_name,process_code,department,options',
                'items:id,work_order_id,item_id,item_name,specification,quantity,unit,status,options,sort_order,source_order_item_id',
                'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
                'items.sourceOrderItem.node:id,name,code',
                'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
                'items.materialInputs.stockLot:id,lot_no',
                'items.materialInputs.item:id,code,name,unit',
            ]);

        // 검색어
        if ($q !== '') {
            $query->where(function ($qq) use ($q) {
                $qq->where('work_order_no', 'like', "%{$q}%")
                    ->orWhere('project_name', 'like', "%{$q}%");
            });
        }

        // 상태 필터
        if ($status !== null) {
            $query->where('status', $status);
        }

        // 공정 필터 (process_id)
        // - 'none' 또는 '0': 공정 미지정 (process_id IS NULL)
        // - 숫자: 해당 공정 ID로 필터
        if ($processId !== null) {
            if ($processId === 'none' || $processId === '0' || $processId === 0) {
                $query->whereNull('process_id');
            } else {
                $query->where('process_id', $processId);
            }
        }

        // 공정 코드 필터 (process_code) - 대시보드용
        if ($processCode !== null) {
            $query->whereHas('process', fn ($q) => $q->where('process_code', $processCode));
        }

        // 담당자 필터
        if ($assigneeId !== null) {
            $query->where('assignee_id', $assigneeId);
        }

        // 나에게 배정된 작업만 필터 (주 담당자 또는 공동 담당자)
        if ($assignedToMe) {
            $userId = $this->apiUserId();
            $query->where(function ($q) use ($userId) {
                $q->where('assignee_id', $userId)
                    ->orWhereHas('assignees', fn ($aq) => $aq->where('user_id', $userId));
            });
        }

        // 작업자 화면용 캐스케이드 필터: 개인 배정 → 부서 → 전체
        if ($workerScreen) {
            $userId = $this->apiUserId();

            // 1차: 개인 배정된 작업이 있는지 확인
            $hasPersonal = (clone $query)->where(function ($q) use ($userId) {
                $q->where('assignee_id', $userId)
                    ->orWhereHas('assignees', fn ($aq) => $aq->where('user_id', $userId));
            })->exists();

            if ($hasPersonal) {
                $query->where(function ($q) use ($userId) {
                    $q->where('assignee_id', $userId)
                        ->orWhereHas('assignees', fn ($aq) => $aq->where('user_id', $userId));
                });
            } else {
                // 2차: 사용자 소속 부서의 작업지시 필터
                $departmentIds = DB::table('department_user')
                    ->where('user_id', $userId)
                    ->where('tenant_id', $tenantId)
                    ->pluck('department_id');

                if ($departmentIds->isNotEmpty()) {
                    $query->whereIn('team_id', $departmentIds);
                }
                // 3차: 부서도 없으면 필터 없이 전체 노출
            }
        }

        // 팀 필터
        if ($teamId !== null) {
            $query->where('team_id', $teamId);
        }

        // 예정일 범위
        if ($scheduledFrom !== null) {
            $query->where('scheduled_date', '>=', $scheduledFrom);
        }
        if ($scheduledTo !== null) {
            $query->where('scheduled_date', '<=', $scheduledTo);
        }

        $query->orderByDesc('created_at');

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

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

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

        // 공정별 카운트 (탭 숫자 표시용)
        $byProcess = WorkOrder::where('tenant_id', $tenantId)
            ->select('process_id', DB::raw('count(*) as count'))
            ->groupBy('process_id')
            ->pluck('count', 'process_id')
            ->toArray();

        $total = array_sum($counts);
        $noneCount = $byProcess[''] ?? $byProcess[0] ?? 0;
        // null 키는 빈 문자열로 변환되므로 별도 처리
        $processedByProcess = [];
        foreach ($byProcess as $key => $count) {
            if ($key === '' || $key === 0 || $key === null) {
                $processedByProcess['none'] = $count;
            } else {
                $processedByProcess[(string) $key] = $count;
            }
        }

        return [
            'total' => $total,
            'unassigned' => $counts[WorkOrder::STATUS_UNASSIGNED] ?? 0,
            'pending' => $counts[WorkOrder::STATUS_PENDING] ?? 0,
            'waiting' => $counts[WorkOrder::STATUS_WAITING] ?? 0,
            'in_progress' => $counts[WorkOrder::STATUS_IN_PROGRESS] ?? 0,
            'completed' => $counts[WorkOrder::STATUS_COMPLETED] ?? 0,
            'shipped' => $counts[WorkOrder::STATUS_SHIPPED] ?? 0,
            'by_process' => $processedByProcess,
        ];
    }

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

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'assignee:id,name',
                'assignees.user:id,name',
                'team:id,name',
                'salesOrder' => fn ($q) => $q->select('id', 'order_no', 'site_name', 'client_id', 'client_contact', 'received_at', 'writer_id', 'created_at', 'quantity', 'options')->withCount('rootNodes'),
                'salesOrder.client:id,name',
                'salesOrder.writer:id,name',
                'process:id,process_name,process_code,work_steps,department,options',
                'process.steps' => fn ($q) => $q->where('is_active', true)->orderBy('sort_order'),
                'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
                'items.sourceOrderItem.node:id,name,code',
                'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
                'items.materialInputs.stockLot:id,lot_no',
                'bendingDetail',
                'issues' => fn ($q) => $q->orderByDesc('created_at'),
                'stepProgress.processStep:id,process_id,step_code,step_name,sort_order,needs_inspection,connection_type,completion_type',
            ])
            ->find($id);

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

        return $workOrder;
    }

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

        return DB::transaction(function () use ($data, $tenantId, $userId) {
            // 작업지시번호 자동 생성
            $data['work_order_no'] = $this->generateWorkOrderNo($tenantId);
            $data['tenant_id'] = $tenantId;
            $data['created_by'] = $userId;
            $data['updated_by'] = $userId;

            // 담당자가 있으면 상태를 pending으로
            if (! empty($data['assignee_id'])) {
                $data['status'] = $data['status'] ?? WorkOrder::STATUS_PENDING;
            }

            $items = $data['items'] ?? [];
            $bendingDetail = $data['bending_detail'] ?? null;
            $salesOrderId = $data['sales_order_id'] ?? null;
            unset($data['items'], $data['bending_detail']);

            $workOrder = WorkOrder::create($data);

            // process 관계 로드 (isBending 체크용)
            $workOrder->load('process:id,process_name,process_code');

            // 품목 저장: 직접 전달된 품목이 없고 수주 ID가 있으면 수주에서 복사
            if (empty($items) && $salesOrderId) {
                $salesOrder = \App\Models\Orders\Order::with(['items.node', 'rootNodes'])->find($salesOrderId);
                if ($salesOrder && $salesOrder->items->isNotEmpty()) {
                    // order_node_id가 null인 품목용 fallback: 수주의 root node를 인덱스로 매핑
                    $rootNodes = $salesOrder->rootNodes;
                    $rootNodeCount = $rootNodes->count();

                    foreach ($salesOrder->items as $index => $orderItem) {
                        // 수주 품목 + 노드에서 options 조합
                        // 1순위: 품목에 직접 연결된 node, 2순위: root node fallback
                        $nodeOptions = $orderItem->node?->options ?? [];
                        if (empty($nodeOptions) && $rootNodeCount > 0) {
                            // root node가 1개면 모든 품목이 해당 node, 여러 개면 인덱스 기반 분배
                            $fallbackNode = $rootNodeCount === 1
                                ? $rootNodes->first()
                                : $rootNodes->values()->get($index % $rootNodeCount);
                            $nodeOptions = $fallbackNode?->options ?? [];
                        }
                        $options = 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' => $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null,
                            'height' => $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null,
                            'cutting_info' => $nodeOptions['cutting_info'] ?? null,
                            'slat_info' => $nodeOptions['slat_info'] ?? null,
                            'bending_info' => $nodeOptions['bending_info'] ?? null,
                            'wip_info' => $nodeOptions['wip_info'] ?? null,
                        ], fn ($v) => $v !== null);

                        $workOrder->items()->create([
                            'tenant_id' => $tenantId,
                            'source_order_item_id' => $orderItem->id, // 원본 수주 품목 추적용
                            'item_id' => $orderItem->item_id,
                            'item_name' => $orderItem->item_name,
                            'specification' => $orderItem->specification,
                            'quantity' => $orderItem->quantity,
                            'unit' => $orderItem->unit,
                            'sort_order' => $index,
                            'options' => ! empty($options) ? $options : null,
                        ]);
                    }
                }
            } else {
                // 직접 전달된 품목 저장
                foreach ($items as $index => $item) {
                    $item['tenant_id'] = $tenantId;
                    $item['sort_order'] = $index;
                    $workOrder->items()->create($item);
                }
            }

            // 벤딩 상세 저장 (벤딩 공정인 경우)
            if ($workOrder->isBending() && $bendingDetail) {
                $bendingDetail['tenant_id'] = $tenantId;
                $workOrder->bendingDetail()->create($bendingDetail);
            }

            // 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrder->id,
                'created',
                null,
                $workOrder->toArray()
            );

            return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'process:id,process_name,process_code', 'items', 'bendingDetail']);
        });
    }

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

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with('process:id,process_name,process_code')
            ->find($id);
        if (! $workOrder) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        $beforeData = $workOrder->toArray();

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

            $items = $data['items'] ?? null;
            $bendingDetail = $data['bending_detail'] ?? null;
            $assigneeIds = $data['assignee_ids'] ?? null;
            unset($data['items'], $data['bending_detail'], $data['assignee_ids'], $data['work_order_no']); // 번호 변경 불가

            // 품목 수정 시 기존 품목 기록
            $oldItems = null;
            if ($items !== null) {
                $oldItems = $workOrder->items()->get()->toArray();
            }

            // 담당자 수정 시 기존 담당자 기록
            $oldAssignees = null;
            if ($assigneeIds !== null) {
                $oldAssignees = $workOrder->assignees()->pluck('user_id')->toArray();
            }

            $workOrder->update($data);

            // 담당자 처리 (assignee_ids 배열)
            if ($assigneeIds !== null) {
                $assigneeIds = array_unique(array_filter($assigneeIds));

                // 기존 담당자 삭제 후 새로 추가
                $workOrder->assignees()->delete();

                foreach ($assigneeIds as $index => $assigneeId) {
                    WorkOrderAssignee::create([
                        'tenant_id' => $workOrder->tenant_id,
                        'work_order_id' => $workOrder->id,
                        'user_id' => $assigneeId,
                        'is_primary' => $index === 0, // 첫 번째가 주 담당자
                    ]);
                }

                // 주 담당자는 work_orders 테이블에도 설정 (하위 호환)
                $primaryAssigneeId = $assigneeIds[0] ?? null;
                $workOrder->assignee_id = $primaryAssigneeId;
                $workOrder->save();

                // 담당자 수정 감사 로그
                $this->auditLogger->log(
                    $workOrder->tenant_id,
                    self::AUDIT_TARGET,
                    $workOrder->id,
                    'assignees_updated',
                    ['assignee_ids' => $oldAssignees],
                    ['assignee_ids' => $assigneeIds]
                );
            }

            // 품목 부분 수정 (ID 기반 upsert/delete)
            if ($items !== null) {
                $existingIds = $workOrder->items()->pluck('id')->toArray();
                $incomingIds = [];

                foreach ($items as $index => $item) {
                    $itemData = [
                        'tenant_id' => $workOrder->tenant_id,
                        'item_name' => $item['item_name'] ?? null,
                        'specification' => $item['specification'] ?? null,
                        'quantity' => $item['quantity'] ?? 1,
                        'unit' => $item['unit'] ?? null,
                        'sort_order' => $index,
                    ];

                    if (isset($item['id']) && $item['id']) {
                        // ID가 있으면 업데이트
                        $existingItem = $workOrder->items()->find($item['id']);
                        if ($existingItem) {
                            $existingItem->update($itemData);
                            $incomingIds[] = (int) $item['id'];
                        }
                    } else {
                        // ID가 없으면 신규 생성
                        $newItem = $workOrder->items()->create($itemData);
                        $incomingIds[] = $newItem->id;
                    }
                }

                // 요청에 없는 기존 품목 삭제
                $toDelete = array_diff($existingIds, $incomingIds);
                if (! empty($toDelete)) {
                    $workOrder->items()->whereIn('id', $toDelete)->delete();
                }

                // 품목 수정 감사 로그
                $this->auditLogger->log(
                    $workOrder->tenant_id,
                    self::AUDIT_TARGET,
                    $workOrder->id,
                    'items_updated',
                    ['items' => $oldItems],
                    ['items' => $workOrder->items()->get()->toArray()]
                );
            }

            // 벤딩 상세 업데이트 (벤딩 공정인 경우에만)
            if ($bendingDetail !== null && $workOrder->isBending()) {
                $bendingDetail['tenant_id'] = $workOrder->tenant_id;
                $workOrder->bendingDetail()->updateOrCreate(
                    ['work_order_id' => $workOrder->id],
                    $bendingDetail
                );
            }

            // 수정 감사 로그
            $this->auditLogger->log(
                $workOrder->tenant_id,
                self::AUDIT_TARGET,
                $workOrder->id,
                'updated',
                $beforeData,
                $workOrder->fresh()->toArray()
            );

            return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'process:id,process_name,process_code', 'items', 'bendingDetail']);
        });
    }

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

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

        // 진행 중이거나 완료된 작업은 삭제 불가
        if (in_array($workOrder->status, [
            WorkOrder::STATUS_IN_PROGRESS,
            WorkOrder::STATUS_COMPLETED,
            WorkOrder::STATUS_SHIPPED,
        ])) {
            throw new BadRequestHttpException(__('error.work_order.cannot_delete_in_progress'));
        }

        $beforeData = $workOrder->toArray();
        $workOrder->delete();

        // 삭제 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrder->id,
            'deleted',
            $beforeData,
            null
        );

        return 'success';
    }

    /**
     * 상태 변경
     *
     * @param  int  $id  작업지시 ID
     * @param  string  $status  변경할 상태
     * @param  array|null  $resultData  완료 시 결과 데이터 (선택)
     */
    public function updateStatus(int $id, string $status, ?array $resultData = null)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        // 상태 유효성 검증
        if (! in_array($status, WorkOrder::STATUSES)) {
            throw new BadRequestHttpException(__('error.invalid_status'));
        }

        // Fast-track 완료 체크: pending/waiting에서 completed로 직접 전환 허용
        // 작업자 화면의 "전량완료" 버튼 지원
        $isFastTrackCompletion = $status === WorkOrder::STATUS_COMPLETED &&
            in_array($workOrder->status, [WorkOrder::STATUS_PENDING, WorkOrder::STATUS_WAITING]);

        // 일반 상태 전이 규칙 검증 (fast-track이 아닌 경우)
        if (! $isFastTrackCompletion && ! $workOrder->canTransitionTo($status)) {
            $allowed = implode(', ', $workOrder->getAllowedTransitions());
            throw new BadRequestHttpException(
                __('error.work_order.invalid_transition', [
                    'from' => $workOrder->status,
                    'to' => $status,
                    'allowed' => $allowed,
                ])
            );
        }

        return DB::transaction(function () use ($workOrder, $status, $resultData, $tenantId, $userId) {
            $oldStatus = $workOrder->status;
            $workOrder->status = $status;
            $workOrder->updated_by = $userId;

            // 상태에 따른 타임스탬프 업데이트
            switch ($status) {
                case WorkOrder::STATUS_IN_PROGRESS:
                    $workOrder->started_at = $workOrder->started_at ?? now();
                    break;
                case WorkOrder::STATUS_COMPLETED:
                    // Fast-track 완료의 경우 started_at도 설정 (중간 상태 생략)
                    $workOrder->started_at = $workOrder->started_at ?? now();
                    $workOrder->completed_at = now();
                    // 모든 품목에 결과 데이터 저장
                    $this->saveItemResults($workOrder, $resultData, $userId);
                    break;
                case WorkOrder::STATUS_SHIPPED:
                    $workOrder->shipped_at = now();
                    break;
            }

            $workOrder->save();

            // 상태 변경 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrder->id,
                'status_changed',
                ['status' => $oldStatus],
                ['status' => $status]
            );

            // 연결된 수주(Order) 상태 동기화
            $this->syncOrderStatus($workOrder, $tenantId);

            // 작업완료 시: 수주 연동 → 출하 생성 / 선생산 → 재고 입고
            if ($status === WorkOrder::STATUS_COMPLETED) {
                if ($workOrder->sales_order_id) {
                    $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId);
                } else {
                    $this->stockInFromProduction($workOrder);
                }
            }

            return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'process:id,process_name,process_code']);
        });
    }

    /**
     * 선생산 작업지시 완료 시 완성품을 재고로 입고
     *
     * 수주 없는 작업지시(sales_order_id = null)가 완료되면
     * 각 품목의 양품 수량을 재고 시스템에 입고 처리합니다.
     */
    private function stockInFromProduction(WorkOrder $workOrder): void
    {
        $workOrder->loadMissing('items.item');

        foreach ($workOrder->items as $woItem) {
            if ($this->shouldStockIn($woItem)) {
                $resultData = $woItem->options['result'] ?? [];
                $goodQty = (float) ($resultData['good_qty'] ?? $woItem->quantity);
                $lotNo = $resultData['lot_no'] ?? '';

                if ($goodQty > 0 && $lotNo) {
                    $this->stockService->increaseFromProduction(
                        $workOrder, $woItem, $goodQty, $lotNo
                    );
                }
            }
        }
    }

    /**
     * 품목이 생산입고 대상인지 판단
     *
     * items.options의 production_source와 lot_managed 속성으로 판단.
     */
    private function shouldStockIn(WorkOrderItem $woItem): bool
    {
        $item = $woItem->item;
        if (! $item) {
            return false;
        }

        $options = $item->options ?? [];

        return ($options['production_source'] ?? null) === 'self_produced'
            && ($options['lot_managed'] ?? false) === true;
    }

    /**
     * 작업지시 완료 시 자동 출하 생성
     *
     * 작업지시가 완료(completed) 상태가 되면 출하(Shipment)를 자동 생성하여 출하관리로 넘깁니다.
     * 발주처/배송 정보는 출하에 복사하지 않고, 수주(Order)를 참조합니다.
     * (Shipment 모델의 accessor 메서드로 수주 정보 참조)
     */
    private function createShipmentFromWorkOrder(WorkOrder $workOrder, int $tenantId, int $userId): ?Shipment
    {
        // 이미 이 작업지시에 연결된 출하가 있으면 스킵
        $existingShipment = Shipment::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrder->id)
            ->first();

        if ($existingShipment) {
            return $existingShipment;
        }

        // 출하번호 자동 생성
        $shipmentNo = Shipment::generateShipmentNo($tenantId);

        // 출하 생성 데이터
        // 발주처/배송 정보는 수주(Order)를 참조하므로 여기서 복사하지 않음
        $shipmentData = [
            'tenant_id' => $tenantId,
            'shipment_no' => $shipmentNo,
            'work_order_id' => $workOrder->id,
            'order_id' => $workOrder->sales_order_id,
            'scheduled_date' => now()->toDateString(), // 오늘 날짜로 출하 예정
            'status' => 'scheduled', // 예정 상태로 생성
            'priority' => 'normal',
            'delivery_method' => 'pickup', // 기본값
            'can_ship' => true, // 생산 완료 후 생성되므로 출하가능
            'created_by' => $userId,
            'updated_by' => $userId,
        ];

        $shipment = Shipment::create($shipmentData);

        // 작업지시 품목을 출하 품목으로 복사
        $this->copyWorkOrderItemsToShipment($workOrder, $shipment, $tenantId);

        // 자동 출하 생성 감사 로그
        $this->auditLogger->log(
            $tenantId,
            'shipment',
            $shipment->id,
            'auto_created_from_work_order',
            null,
            [
                'work_order_id' => $workOrder->id,
                'shipment_no' => $shipmentNo,
                'items_count' => $shipment->items()->count(),
            ]
        );

        return $shipment;
    }

    /**
     * 작업지시 품목을 출하 품목으로 복사
     *
     * 작업지시 품목(work_order_items)의 정보를 출하 품목(shipment_items)으로 복사합니다.
     * 작업지시 품목이 없으면 수주 품목(order_items)을 대체 사용합니다.
     * LOT 번호는 작업지시 품목의 결과 데이터에서 가져옵니다.
     * 층/부호(floor_unit)는 원본 수주품목(order_items)에서 가져옵니다.
     */
    private function copyWorkOrderItemsToShipment(WorkOrder $workOrder, Shipment $shipment, int $tenantId): void
    {
        $workOrderItems = $workOrder->items()->get();

        // 작업지시 품목이 있으면 사용
        if ($workOrderItems->isNotEmpty()) {
            foreach ($workOrderItems as $index => $woItem) {
                // 작업지시 품목의 결과 데이터에서 LOT 번호 추출
                $result = $woItem->options['result'] ?? [];
                $lotNo = $result['lot_no'] ?? null;

                // 원본 수주품목에서 층/부호 정보 조회
                $floorUnit = $this->getFloorUnitFromOrderItem($woItem->source_order_item_id, $tenantId);

                // 출하 품목 생성
                ShipmentItem::create([
                    'tenant_id' => $tenantId,
                    'shipment_id' => $shipment->id,
                    'seq' => $index + 1,
                    'item_code' => $woItem->item_id ? "ITEM-{$woItem->item_id}" : null,
                    'item_name' => $woItem->item_name,
                    'floor_unit' => $floorUnit,
                    'specification' => $woItem->specification,
                    'quantity' => $result['good_qty'] ?? $woItem->quantity, // 양품 수량 우선
                    'unit' => $woItem->unit,
                    'lot_no' => $lotNo,
                    'remarks' => null,
                ]);
            }

            return;
        }

        // 작업지시 품목이 없으면 수주 품목에서 복사 (Fallback)
        if ($workOrder->salesOrder) {
            $orderItems = $workOrder->salesOrder->items()->get();

            foreach ($orderItems as $index => $orderItem) {
                // 수주품목에서 층/부호 정보 조회
                $floorUnit = $this->getFloorUnitFromOrderItem($orderItem->id, $tenantId);

                // 출하 품목 생성
                ShipmentItem::create([
                    'tenant_id' => $tenantId,
                    'shipment_id' => $shipment->id,
                    'seq' => $index + 1,
                    'item_code' => $orderItem->item_id ? "ITEM-{$orderItem->item_id}" : null,
                    'item_name' => $orderItem->item_name,
                    'floor_unit' => $floorUnit,
                    'specification' => $orderItem->specification,
                    'quantity' => $orderItem->quantity,
                    'unit' => $orderItem->unit,
                    'lot_no' => null, // 수주 품목에는 LOT 번호 없음
                    'remarks' => null,
                ]);
            }
        }
    }

    /**
     * 수주품목에서 층/부호 정보 조회
     *
     * floor_code와 symbol_code를 조합하여 floor_unit 형식으로 반환합니다.
     * 예: floor_code='3층', symbol_code='A호' → '3층/A호'
     */
    private function getFloorUnitFromOrderItem(?int $orderItemId, int $tenantId): ?string
    {
        if (! $orderItemId) {
            return null;
        }

        $orderItem = \App\Models\Orders\OrderItem::where('tenant_id', $tenantId)
            ->find($orderItemId);

        if (! $orderItem) {
            return null;
        }

        $parts = array_filter([
            $orderItem->floor_code,
            $orderItem->symbol_code,
        ]);

        return ! empty($parts) ? implode('/', $parts) : null;
    }

    /**
     * 작업지시 상태 변경 시 연결된 수주(Order) 상태 동기화
     *
     * 매핑 규칙:
     * - WorkOrder::STATUS_IN_PROGRESS → Order::STATUS_IN_PRODUCTION (생산중)
     * - WorkOrder::STATUS_COMPLETED → Order::STATUS_PRODUCED (생산완료)
     * - WorkOrder::STATUS_SHIPPED → Order::STATUS_SHIPPED (출하완료)
     */
    private function syncOrderStatus(WorkOrder $workOrder, int $tenantId): void
    {
        // 수주 연결이 없으면 스킵
        if (! $workOrder->sales_order_id) {
            return;
        }

        $order = Order::where('tenant_id', $tenantId)->find($workOrder->sales_order_id);
        if (! $order) {
            return;
        }

        // 작업지시 상태 → 수주 상태 매핑
        $statusMap = [
            WorkOrder::STATUS_IN_PROGRESS => Order::STATUS_IN_PRODUCTION,
            WorkOrder::STATUS_COMPLETED => Order::STATUS_PRODUCED,
            WorkOrder::STATUS_SHIPPED => Order::STATUS_SHIPPED,
        ];

        $newOrderStatus = $statusMap[$workOrder->status] ?? null;

        // 매핑되는 상태가 없거나 이미 동일한 상태면 스킵
        if (! $newOrderStatus || $order->status_code === $newOrderStatus) {
            return;
        }

        $oldOrderStatus = $order->status_code;
        $order->status_code = $newOrderStatus;
        $order->updated_by = $this->apiUserId();
        $order->save();

        // 수주 상태 동기화 감사 로그
        $this->auditLogger->log(
            $tenantId,
            'order',
            $order->id,
            'status_synced_from_work_order',
            ['status_code' => $oldOrderStatus, 'work_order_id' => $workOrder->id],
            ['status_code' => $newOrderStatus, 'work_order_id' => $workOrder->id]
        );
    }

    /**
     * 작업지시 품목에 결과 데이터 저장
     */
    private function saveItemResults(WorkOrder $workOrder, ?array $resultData, int $userId): void
    {
        $items = $workOrder->items;
        $lotNo = $this->generateLotNo($workOrder);

        foreach ($items as $item) {
            $itemResult = [
                'completed_at' => now()->toDateTimeString(),
                'good_qty' => $item->quantity, // 기본값: 지시수량 전체가 양품
                'defect_qty' => 0,
                'defect_rate' => 0,
                'lot_no' => $lotNo,
                'is_inspected' => false,
                'is_packaged' => false,
                'worker_id' => $userId,
                'memo' => null,
            ];

            // 개별 품목 결과 데이터가 있으면 병합
            if ($resultData && isset($resultData['items'][$item->id])) {
                $itemResult = array_merge($itemResult, $resultData['items'][$item->id]);
                // 불량률 재계산
                $totalQty = ($itemResult['good_qty'] ?? 0) + ($itemResult['defect_qty'] ?? 0);
                $itemResult['defect_rate'] = $totalQty > 0
                    ? round(($itemResult['defect_qty'] / $totalQty) * 100, 2)
                    : 0;
            }

            // 품목 상태도 완료로 변경
            $item->status = WorkOrderItem::STATUS_COMPLETED;
            $options = $item->options ?? [];
            $options['result'] = $itemResult;
            $item->options = $options;
            $item->save();
        }
    }

    /**
     * LOT 번호 생성
     */
    private function generateLotNo(WorkOrder $workOrder): string
    {
        $date = now()->format('ymd');
        $prefix = 'KD-SA';

        // 오늘 날짜의 마지막 LOT 번호 조회
        $lastLotNo = WorkOrderItem::where('tenant_id', $workOrder->tenant_id)
            ->whereNotNull('options->result->lot_no')
            ->where('options->result->lot_no', 'like', "{$prefix}-{$date}-%")
            ->orderByDesc('id')
            ->value('options->result->lot_no');

        if ($lastLotNo) {
            // 마지막 번호에서 시퀀스 추출 후 증가
            $parts = explode('-', $lastLotNo);
            $seq = (int) end($parts) + 1;
        } else {
            $seq = 1;
        }

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

    /**
     * 담당자 배정 (다중 담당자 지원)
     *
     * @param  int  $id  작업지시 ID
     * @param  array  $data  배정 데이터 (assignee_ids: int[], team_id?: int)
     */
    public function assign(int $id, array $data)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        return DB::transaction(function () use ($workOrder, $data, $tenantId, $userId) {
            // 이전 상태 기록
            $beforeAssignees = $workOrder->assignees()->pluck('user_id')->toArray();
            $beforePrimaryAssignee = $workOrder->assignee_id;
            $beforeTeam = $workOrder->team_id;

            // 담당자 ID 배열 처리 (단일 값도 배열로 변환)
            $assigneeIds = $data['assignee_ids'] ?? [];
            if (isset($data['assignee_id']) && ! empty($data['assignee_id'])) {
                // 하위 호환: 단일 assignee_id도 지원
                $assigneeIds = is_array($data['assignee_id']) ? $data['assignee_id'] : [$data['assignee_id']];
            }
            $assigneeIds = array_unique(array_filter($assigneeIds));

            // 기존 담당자 삭제 후 새로 추가
            $workOrder->assignees()->delete();

            foreach ($assigneeIds as $index => $assigneeId) {
                WorkOrderAssignee::create([
                    'tenant_id' => $tenantId,
                    'work_order_id' => $workOrder->id,
                    'user_id' => $assigneeId,
                    'is_primary' => $index === 0, // 첫 번째가 주 담당자
                ]);
            }

            // 주 담당자는 work_orders 테이블에도 설정 (하위 호환)
            $primaryAssigneeId = $assigneeIds[0] ?? null;
            $workOrder->assignee_id = $primaryAssigneeId;
            $workOrder->team_id = $data['team_id'] ?? $workOrder->team_id;
            $workOrder->updated_by = $userId;

            // 미배정이었으면 대기로 변경
            if ($workOrder->status === WorkOrder::STATUS_UNASSIGNED && $primaryAssigneeId) {
                $workOrder->status = WorkOrder::STATUS_PENDING;
            }

            $workOrder->save();

            // 배정 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrder->id,
                'assigned',
                [
                    'assignee_id' => $beforePrimaryAssignee,
                    'assignee_ids' => $beforeAssignees,
                    'team_id' => $beforeTeam,
                ],
                [
                    'assignee_id' => $workOrder->assignee_id,
                    'assignee_ids' => $assigneeIds,
                    'team_id' => $workOrder->team_id,
                ]
            );

            return $workOrder->load(['assignee:id,name', 'assignees.user:id,name', 'team:id,name', 'process:id,process_name,process_code']);
        });
    }

    /**
     * 벤딩 항목 토글
     */
    public function toggleBendingField(int $id, string $field)
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with('process:id,process_name,process_code')
            ->find($id);
        if (! $workOrder) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        if (! $workOrder->isBending()) {
            throw new BadRequestHttpException(__('error.work_order.not_bending_process'));
        }

        $detail = $workOrder->bendingDetail;
        if (! $detail) {
            $detail = $workOrder->bendingDetail()->create([
                'tenant_id' => $workOrder->tenant_id,
            ]);
        }

        if (! in_array($field, WorkOrderBendingDetail::PROCESS_FIELDS)) {
            throw new BadRequestHttpException(__('error.invalid_field'));
        }

        $beforeValue = $detail->{$field};
        $detail->toggleField($field);

        // 벤딩 토글 감사 로그
        $this->auditLogger->log(
            $workOrder->tenant_id,
            self::AUDIT_TARGET,
            $workOrder->id,
            'bending_toggled',
            [$field => $beforeValue],
            [$field => $detail->{$field}]
        );

        return $detail;
    }

    /**
     * 이슈 추가
     */
    public function addIssue(int $workOrderId, array $data)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $data['tenant_id'] = $tenantId;
        $data['reported_by'] = $userId;

        $issue = $workOrder->issues()->create($data);

        // 이슈 추가 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            'issue_added',
            null,
            ['issue_id' => $issue->id, 'title' => $issue->title, 'priority' => $issue->priority]
        );

        return $issue;
    }

    /**
     * 이슈 해결
     */
    public function resolveIssue(int $workOrderId, int $issueId)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $issue = $workOrder->issues()->find($issueId);
        if (! $issue) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        $issue->resolve($userId);

        // 이슈 해결 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            'issue_resolved',
            ['issue_id' => $issueId, 'status' => 'open'],
            ['issue_id' => $issueId, 'status' => 'resolved', 'resolved_by' => $userId]
        );

        return $issue;
    }

    /**
     * 작업지시번호 자동 생성
     */
    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 updateItemStatus(int $workOrderId, int $itemId, string $status)
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $item = $workOrder->items()->find($itemId);
        if (! $item) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        // 상태 유효성 검증
        if (! in_array($status, WorkOrderItem::STATUSES)) {
            throw new BadRequestHttpException(__('error.invalid_status'));
        }

        $beforeStatus = $item->status;
        $item->status = $status;
        $item->save();

        // 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            'item_status_changed',
            ['item_id' => $itemId, 'status' => $beforeStatus],
            ['item_id' => $itemId, 'status' => $status]
        );

        // 작업지시 상태 자동 연동
        $workOrderStatusChanged = $this->syncWorkOrderStatusFromItems($workOrder);

        // 품목과 함께 작업지시 상태도 반환
        return [
            'item' => $item,
            'work_order_status' => $workOrder->fresh()->status,
            'work_order_status_changed' => $workOrderStatusChanged,
        ];
    }

    /**
     * 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 로트별 재고)
     *
     * 작업지시 품목의 BOM 자재별로 StockLot(입고 로트)를 FIFO 순서로 반환합니다.
     * 동일 자재가 여러 작업지시 품목에 걸쳐 있으면 필요수량을 합산하고 로트는 중복 없이 반환합니다.
     *
     * @param  int  $workOrderId  작업지시 ID
     * @return array 자재 목록 (로트 단위)
     */
    public function getMaterials(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with(['items.item'])
            ->find($workOrderId);

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

        // ── Step 1: dynamic_bom 대상 item_id 일괄 수집 (N+1 방지) ──
        $allDynamicItemIds = [];
        foreach ($workOrder->items as $woItem) {
            $options = is_string($woItem->options) ? json_decode($woItem->options, true) : ($woItem->options ?? []);
            $dynamicBom = $options['dynamic_bom'] ?? null;
            if ($dynamicBom && is_array($dynamicBom)) {
                $allDynamicItemIds = array_merge($allDynamicItemIds, array_column($dynamicBom, 'child_item_id'));
            }
        }

        // 배치 조회 (dynamic_bom 품목)
        $dynamicItems = [];
        if (! empty($allDynamicItemIds)) {
            $dynamicItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
                ->whereIn('id', array_unique($allDynamicItemIds))
                ->get()
                ->keyBy('id');
        }

        // ── Step 2: 유니크 자재 목록 수집 ──
        // 키: dynamic_bom → "{item_id}_{woItem_id}", 기존 BOM → "{item_id}"
        $uniqueMaterials = [];

        foreach ($workOrder->items as $woItem) {
            $options = is_string($woItem->options) ? json_decode($woItem->options, true) : ($woItem->options ?? []);
            $dynamicBom = $options['dynamic_bom'] ?? null;

            // dynamic_bom 우선 — 있으면 BOM 무시
            if ($dynamicBom && is_array($dynamicBom)) {
                foreach ($dynamicBom as $bomEntry) {
                    $childItemId = $bomEntry['child_item_id'] ?? null;
                    if (! $childItemId || ! isset($dynamicItems[$childItemId])) {
                        continue;
                    }

                    // 합산 키: (item_id, work_order_item_id) 쌍
                    $key = $childItemId.'_'.$woItem->id;
                    // dynamic_bom.qty는 아이템 수량이 곱해져 있으므로 나눠서 개소당 수량 산출
                    $bomQty = (float) ($bomEntry['qty'] ?? 1);
                    $woItemQty = max(1, (float) ($woItem->quantity ?? 1));
                    $perNodeQty = $bomQty / $woItemQty;

                    if (isset($uniqueMaterials[$key])) {
                        $uniqueMaterials[$key]['required_qty'] += $perNodeQty;
                    } else {
                        $uniqueMaterials[$key] = [
                            'item' => $dynamicItems[$childItemId],
                            'bom_qty' => $perNodeQty,
                            'required_qty' => $perNodeQty,
                            'work_order_item_id' => $woItem->id,
                            'lot_prefix' => $bomEntry['lot_prefix'] ?? null,
                            'part_type' => $bomEntry['part_type'] ?? null,
                            'category' => $bomEntry['category'] ?? null,
                        ];
                    }
                }

                continue; // dynamic_bom이 있으면 기존 BOM fallback 건너뜀
            }

            // 기존 BOM 로직 (하위 호환)
            $materialItems = [];

            if ($woItem->item_id) {
                $item = \App\Models\Items\Item::where('tenant_id', $tenantId)
                    ->find($woItem->item_id);

                if ($item && ! empty($item->bom)) {
                    foreach ($item->bom as $bomItem) {
                        $childItemId = $bomItem['child_item_id'] ?? null;
                        $bomQty = (float) ($bomItem['qty'] ?? 1);

                        if (! $childItemId) {
                            continue;
                        }

                        $childItem = \App\Models\Items\Item::where('tenant_id', $tenantId)
                            ->find($childItemId);

                        if (! $childItem) {
                            continue;
                        }

                        $materialItems[] = [
                            'item' => $childItem,
                            'bom_qty' => $bomQty,
                            'required_qty' => $bomQty * ($woItem->quantity ?? 1),
                        ];
                    }
                }
            }

            // BOM이 없으면 품목 자체를 자재로 사용
            if (empty($materialItems) && $woItem->item_id && $woItem->item) {
                $materialItems[] = [
                    'item' => $woItem->item,
                    'bom_qty' => 1,
                    'required_qty' => $woItem->quantity ?? 1,
                ];
            }

            // 기존 방식: item_id 기준 합산
            foreach ($materialItems as $matInfo) {
                $itemId = $matInfo['item']->id;
                if (isset($uniqueMaterials[$itemId])) {
                    $uniqueMaterials[$itemId]['required_qty'] += $matInfo['required_qty'];
                } else {
                    $uniqueMaterials[$itemId] = $matInfo;
                }
            }
        }

        // ── Step 3: 유니크 자재별로 StockLot 조회 ──
        // 배치 조회를 위해 전체 item_id 수집
        $allItemIds = [];
        foreach ($uniqueMaterials as $matInfo) {
            $allItemIds[] = $matInfo['item']->id;
        }
        $allItemIds = array_unique($allItemIds);

        // Stock 배치 조회 (N+1 방지)
        $stockMap = [];
        if (! empty($allItemIds)) {
            $stocks = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
                ->whereIn('item_id', $allItemIds)
                ->get();

            foreach ($stocks as $stock) {
                $stockMap[$stock->item_id] = $stock;
            }
        }

        // StockLot 배치 조회 (N+1 방지)
        $lotsByStockId = [];
        $stockIds = array_map(fn ($s) => $s->id, $stockMap);
        if (! empty($stockIds)) {
            $allLots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
                ->whereIn('stock_id', $stockIds)
                ->where('status', 'available')
                ->where('available_qty', '>', 0)
                ->orderBy('fifo_order', 'asc')
                ->get();

            foreach ($allLots as $lot) {
                $lotsByStockId[$lot->stock_id][] = $lot;
            }
        }

        $materials = [];
        $rank = 1;

        foreach ($uniqueMaterials as $matInfo) {
            $materialItem = $matInfo['item'];
            $stock = $stockMap[$materialItem->id] ?? null;
            $lotsFound = false;

            // 공통 필드 (dynamic_bom 추가 필드 포함)
            $extraFields = [];
            if (isset($matInfo['work_order_item_id'])) {
                $extraFields = [
                    'work_order_item_id' => $matInfo['work_order_item_id'],
                    'lot_prefix' => $matInfo['lot_prefix'],
                    'part_type' => $matInfo['part_type'],
                    'category' => $matInfo['category'],
                ];
            }

            if ($stock) {
                $lots = $lotsByStockId[$stock->id] ?? [];

                foreach ($lots as $lot) {
                    $lotsFound = true;
                    $materials[] = array_merge([
                        'stock_lot_id' => $lot->id,
                        'item_id' => $materialItem->id,
                        'lot_no' => $lot->lot_no,
                        'material_code' => $materialItem->code,
                        'material_name' => $materialItem->name,
                        'specification' => $materialItem->specification,
                        'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
                        'bom_qty' => $matInfo['bom_qty'],
                        'required_qty' => $matInfo['required_qty'],
                        'lot_qty' => (float) $lot->qty,
                        'lot_available_qty' => (float) $lot->available_qty,
                        'lot_reserved_qty' => (float) $lot->reserved_qty,
                        'receipt_date' => $lot->receipt_date,
                        'supplier' => $lot->supplier,
                        'fifo_rank' => $rank++,
                    ], $extraFields);
                }
            }

            // 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시)
            if (! $lotsFound) {
                $materials[] = array_merge([
                    'stock_lot_id' => null,
                    'item_id' => $materialItem->id,
                    'lot_no' => null,
                    'material_code' => $materialItem->code,
                    'material_name' => $materialItem->name,
                    'specification' => $materialItem->specification,
                    'unit' => $materialItem->unit ?? 'EA',
                    'bom_qty' => $matInfo['bom_qty'],
                    'required_qty' => $matInfo['required_qty'],
                    'lot_qty' => 0,
                    'lot_available_qty' => 0,
                    'lot_reserved_qty' => 0,
                    'receipt_date' => null,
                    'supplier' => null,
                    'fifo_rank' => $rank++,
                ], $extraFields);
            }
        }

        return $materials;
    }

    /**
     * 자재 투입 등록 (로트 지정 차감)
     *
     * 사용자가 선택한 로트별로 지정 수량을 차감합니다.
     *
     * @param  int  $workOrderId  작업지시 ID
     * @param  array  $inputs  투입 목록 [['stock_lot_id' => int, 'qty' => float], ...]
     * @return array 투입 결과
     *
     * @throws \Exception 재고 부족 시
     */
    public function registerMaterialInput(int $workOrderId, array $inputs): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        // work_order_item_id가 있는 항목은 registerMaterialInputForItem()으로 위임
        $groupedByItem = [];
        $noItemInputs = [];

        foreach ($inputs as $input) {
            $woItemId = $input['work_order_item_id'] ?? null;
            if ($woItemId) {
                $groupedByItem[$woItemId][] = $input;
            } else {
                $noItemInputs[] = $input;
            }
        }

        // work_order_item_id가 있는 항목 → 개소별 투입으로 위임
        $delegatedResults = [];
        foreach ($groupedByItem as $woItemId => $itemInputs) {
            $delegatedResults[] = $this->registerMaterialInputForItem($workOrderId, $woItemId, $itemInputs);
        }

        // work_order_item_id가 없는 항목 → 기존 방식 + WorkOrderMaterialInput 레코드 생성
        if (empty($noItemInputs)) {
            // 전부 위임된 경우
            $totalCount = array_sum(array_column($delegatedResults, 'material_count'));
            $allResults = array_merge(...array_map(fn ($r) => $r['input_results'], $delegatedResults));

            return [
                'work_order_id' => $workOrderId,
                'material_count' => $totalCount,
                'input_results' => $allResults,
                'input_at' => now()->toDateTimeString(),
            ];
        }

        // fallback: 첫 번째 work_order_item_id로 매핑
        $fallbackWoItemId = WorkOrderItem::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->orderBy('id')
            ->value('id');

        return DB::transaction(function () use ($noItemInputs, $tenantId, $userId, $workOrderId, $fallbackWoItemId, $delegatedResults) {
            $stockService = app(StockService::class);
            $inputResults = [];

            foreach ($noItemInputs as $input) {
                $stockLotId = $input['stock_lot_id'] ?? null;
                $qty = (float) ($input['qty'] ?? 0);

                if (! $stockLotId || $qty <= 0) {
                    continue;
                }

                // 특정 로트에서 재고 차감
                $result = $stockService->decreaseFromLot(
                    stockLotId: $stockLotId,
                    qty: $qty,
                    reason: 'work_order_input',
                    referenceId: $workOrderId
                );

                // WorkOrderMaterialInput 레코드 생성 (이력 통일)
                $lot = \App\Models\Tenants\StockLot::with('stock')->find($stockLotId);
                $lotItemId = $lot?->stock?->item_id;

                WorkOrderMaterialInput::create([
                    'tenant_id' => $tenantId,
                    'work_order_id' => $workOrderId,
                    'work_order_item_id' => $fallbackWoItemId,
                    'stock_lot_id' => $stockLotId,
                    'item_id' => $lotItemId ?? 0,
                    'qty' => $qty,
                    'input_by' => $userId,
                    'input_at' => now(),
                ]);

                $inputResults[] = [
                    'stock_lot_id' => $stockLotId,
                    'qty' => $qty,
                    'status' => 'success',
                    'deducted_lot' => $result,
                ];
            }

            // 자재 투입 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrderId,
                'material_input',
                null,
                [
                    'inputs' => $noItemInputs,
                    'input_results' => $inputResults,
                    'input_by' => $userId,
                    'input_at' => now()->toDateTimeString(),
                ]
            );

            // 위임된 결과와 합산
            $allResults = $inputResults;
            foreach ($delegatedResults as $dr) {
                $allResults = array_merge($allResults, $dr['input_results']);
            }

            return [
                'work_order_id' => $workOrderId,
                'material_count' => count($allResults),
                'input_results' => $allResults,
                'input_at' => now()->toDateTimeString(),
            ];
        });
    }

    /**
     * 품목 상태 기반으로 작업지시 상태 자동 동기화
     *
     * 규칙:
     * - 품목 중 하나라도 in_progress → 작업지시 in_progress (pending에서도 자동 전환)
     * - 모든 품목이 completed → 작업지시 completed
     * - 모든 품목이 waiting → 작업지시 waiting (단, waiting 이상인 경우만)
     * - 미배정(unassigned) 상태에서는 동기화하지 않음
     *
     * @return bool 상태 변경 여부
     */
    private function syncWorkOrderStatusFromItems(WorkOrder $workOrder): bool
    {
        // 품목이 없으면 동기화하지 않음
        $items = $workOrder->items()->get();
        if ($items->isEmpty()) {
            return false;
        }

        // 미배정(unassigned) 상태에서는 동기화하지 않음 (배정 없이 작업 시작 불가)
        if ($workOrder->status === WorkOrder::STATUS_UNASSIGNED) {
            return false;
        }

        // 품목 상태 집계
        $statusCounts = $items->groupBy('status')->map->count();
        $totalItems = $items->count();

        $waitingCount = $statusCounts->get(WorkOrderItem::STATUS_WAITING, 0);
        $inProgressCount = $statusCounts->get(WorkOrderItem::STATUS_IN_PROGRESS, 0);
        $completedCount = $statusCounts->get(WorkOrderItem::STATUS_COMPLETED, 0);

        // 새 상태 결정
        $newStatus = null;
        if ($inProgressCount > 0) {
            // 하나라도 진행중이면 작업지시도 진행중
            $newStatus = WorkOrder::STATUS_IN_PROGRESS;
        } elseif ($completedCount === $totalItems) {
            // 모두 완료면 작업지시도 완료
            $newStatus = WorkOrder::STATUS_COMPLETED;
        } elseif ($waitingCount === $totalItems) {
            // 모두 대기면 작업지시도 대기
            $newStatus = WorkOrder::STATUS_WAITING;
        }

        // 상태가 변경되어야 하고, 현재와 다른 경우에만 업데이트
        if ($newStatus && $newStatus !== $workOrder->status) {
            $oldStatus = $workOrder->status;
            $workOrder->status = $newStatus;

            // 상태에 따른 타임스탬프 업데이트
            if ($newStatus === WorkOrder::STATUS_IN_PROGRESS && ! $workOrder->started_at) {
                $workOrder->started_at = now();
            } elseif ($newStatus === WorkOrder::STATUS_COMPLETED) {
                $workOrder->completed_at = now();
            }

            $workOrder->save();

            // 상태 변경 감사 로그
            $this->auditLogger->log(
                $workOrder->tenant_id,
                self::AUDIT_TARGET,
                $workOrder->id,
                'status_synced_from_items',
                ['status' => $oldStatus],
                ['status' => $newStatus]
            );

            // 완료 시 수주 상태 동기화 및 자동 출하 생성
            if ($newStatus === WorkOrder::STATUS_COMPLETED) {
                $this->syncOrderStatus($workOrder, $workOrder->tenant_id);
                $this->createShipmentFromWorkOrder($workOrder, $workOrder->tenant_id, $this->apiUserId());
            }

            return true;
        }

        return false;
    }

    // ──────────────────────────────────────────────────────────────
    // 공정 단계 진행 관리
    // ──────────────────────────────────────────────────────────────

    /**
     * 작업지시의 공정 단계 진행 현황 조회
     *
     * process_steps 마스터 기준으로 진행 레코드를 자동 생성(없으면)하고 반환
     */
    public function getStepProgress(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'process.steps' => fn ($q) => $q->where('is_active', true)->orderBy('sort_order'),
                'items',
            ])
            ->find($workOrderId);

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

        $processSteps = $workOrder->process?->steps ?? collect();
        if ($processSteps->isEmpty()) {
            return [];
        }

        $items = $workOrder->items;
        $result = [];

        if ($items->isNotEmpty()) {
            // 개소(item)별 진행 레코드 생성/조회
            $existingProgress = WorkOrderStepProgress::where('work_order_id', $workOrderId)
                ->whereNotNull('work_order_item_id')
                ->get()
                ->groupBy('work_order_item_id');

            foreach ($items as $item) {
                $itemProgress = ($existingProgress->get($item->id) ?? collect())->keyBy('process_step_id');

                foreach ($processSteps as $step) {
                    if ($itemProgress->has($step->id)) {
                        $progress = $itemProgress->get($step->id);
                    } else {
                        $progress = WorkOrderStepProgress::create([
                            'tenant_id' => $tenantId,
                            'work_order_id' => $workOrderId,
                            'process_step_id' => $step->id,
                            'work_order_item_id' => $item->id,
                            'status' => WorkOrderStepProgress::STATUS_WAITING,
                        ]);
                    }

                    $result[] = [
                        'id' => $progress->id,
                        'process_step_id' => $step->id,
                        'work_order_item_id' => $item->id,
                        'step_code' => $step->step_code,
                        'step_name' => $step->step_name,
                        'sort_order' => $step->sort_order,
                        'needs_inspection' => $step->needs_inspection,
                        'connection_type' => $step->connection_type,
                        'completion_type' => $step->completion_type,
                        'status' => $progress->status,
                        'is_completed' => $progress->isCompleted(),
                        'completed_at' => $progress->completed_at?->toDateTimeString(),
                        'completed_by' => $progress->completed_by,
                    ];
                }
            }
        } else {
            // items 없으면 작업지시 전체 레벨 (기존 동작)
            $existingProgress = WorkOrderStepProgress::where('work_order_id', $workOrderId)
                ->whereNull('work_order_item_id')
                ->get()
                ->keyBy('process_step_id');

            foreach ($processSteps as $step) {
                if ($existingProgress->has($step->id)) {
                    $progress = $existingProgress->get($step->id);
                } else {
                    $progress = WorkOrderStepProgress::create([
                        'tenant_id' => $tenantId,
                        'work_order_id' => $workOrderId,
                        'process_step_id' => $step->id,
                        'work_order_item_id' => null,
                        'status' => WorkOrderStepProgress::STATUS_WAITING,
                    ]);
                }

                $result[] = [
                    'id' => $progress->id,
                    'process_step_id' => $step->id,
                    'work_order_item_id' => null,
                    'step_code' => $step->step_code,
                    'step_name' => $step->step_name,
                    'sort_order' => $step->sort_order,
                    'needs_inspection' => $step->needs_inspection,
                    'connection_type' => $step->connection_type,
                    'completion_type' => $step->completion_type,
                    'status' => $progress->status,
                    'is_completed' => $progress->isCompleted(),
                    'completed_at' => $progress->completed_at?->toDateTimeString(),
                    'completed_by' => $progress->completed_by,
                ];
            }
        }

        return $result;
    }

    /**
     * 공정 단계 완료 토글
     */
    public function toggleStepProgress(int $workOrderId, int $progressId): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $progress = WorkOrderStepProgress::where('id', $progressId)
            ->where('work_order_id', $workOrderId)
            ->first();

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

        $before = ['status' => $progress->status];
        $progress->toggle($userId);
        $after = ['status' => $progress->status];

        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            'step_progress_toggled',
            $before,
            $after
        );

        return [
            'id' => $progress->id,
            'status' => $progress->status,
            'is_completed' => $progress->isCompleted(),
            'completed_at' => $progress->completed_at?->toDateTimeString(),
            'completed_by' => $progress->completed_by,
        ];
    }

    /**
     * 자재 투입 이력 조회
     */
    public function getMaterialInputHistory(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

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

        // audit_logs에서 material_input 액션 이력 조회
        $logs = DB::table('audit_logs')
            ->where('tenant_id', $tenantId)
            ->where('target_type', self::AUDIT_TARGET)
            ->where('target_id', $workOrderId)
            ->where('action', 'material_input')
            ->orderByDesc('created_at')
            ->get();

        return $logs->map(function ($log) {
            $after = json_decode($log->after_data ?? '{}', true);

            return [
                'id' => $log->id,
                'materials' => $after['input_results'] ?? [],
                'created_at' => $log->created_at,
                'actor_id' => $log->actor_id,
            ];
        })->toArray();
    }

    /**
     * 작업지시에 투입된 자재 LOT 번호 조회 (stock_transactions 기반)
     *
     * stock_transactions에서 reference_type='work_order_input'인 거래를 조회하여
     * 중복 없는 LOT 번호 목록을 반환합니다.
     */
    public function getMaterialInputLots(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

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

        $transactions = DB::table('stock_transactions')
            ->where('tenant_id', $tenantId)
            ->where('reference_type', StockTransaction::REASON_WORK_ORDER_INPUT)
            ->where('reference_id', $workOrderId)
            ->orderBy('created_at')
            ->get(['id', 'lot_no', 'item_code', 'item_name', 'qty', 'stock_lot_id', 'created_at']);

        // 품목코드별 그룹핑 (작업일지에서 item_code → lot_no 매핑에 사용)
        $itemMap = [];
        foreach ($transactions as $tx) {
            $itemCode = $tx->item_code;
            if (! isset($itemMap[$itemCode])) {
                $itemMap[$itemCode] = [
                    'item_code' => $itemCode,
                    'lot_no' => $tx->lot_no,
                    'item_name' => $tx->item_name,
                    'total_qty' => 0,
                    'input_count' => 0,
                    'first_input_at' => $tx->created_at,
                ];
            }
            $itemMap[$itemCode]['total_qty'] += abs((float) $tx->qty);
            $itemMap[$itemCode]['input_count']++;
        }

        return array_values($itemMap);
    }

    // ──────────────────────────────────────────────────────────────
    // 중간검사 관련
    // ──────────────────────────────────────────────────────────────

    /**
     * 품목별 중간검사 데이터 저장
     */
    public function storeItemInspection(int $workOrderId, int $itemId, array $data): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $item = $workOrder->items()->find($itemId);
        if (! $item) {
            throw new NotFoundHttpException(__('error.not_found'));
        }

        $beforeData = $item->getInspectionData();

        $inspectionData = $data['inspection_data'];
        $inspectionData['process_type'] = $data['process_type'];
        $inspectionData['inspected_at'] = now()->toDateTimeString();
        $inspectionData['inspected_by'] = $userId;

        $item->setInspectionData($inspectionData);
        $item->save();

        // 절곡 공정: 수주 단위 검사 → 동일 작업지시의 모든 item에 검사 데이터 복제
        $processType = $data['process_type'] ?? '';
        if (in_array($processType, ['bending', 'bending_wip'])) {
            $otherItems = $workOrder->items()->where('id', '!=', $itemId)->get();
            foreach ($otherItems as $otherItem) {
                $otherItem->setInspectionData($inspectionData);
                $otherItem->save();
            }
        }

        // 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            'item_inspection_saved',
            ['item_id' => $itemId, 'inspection_data' => $beforeData],
            ['item_id' => $itemId, 'inspection_data' => $inspectionData]
        );

        return [
            'item_id' => $item->id,
            'inspection_data' => $item->getInspectionData(),
        ];
    }

    /**
     * 작업지시의 전체 품목 검사 데이터 조회
     */
    public function getInspectionData(int $workOrderId, array $params = []): array
    {
        $tenantId = $this->tenantId();

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

        $query = $workOrder->items()->ordered();

        // 공정 유형 필터
        if (! empty($params['process_type'])) {
            $query->where('options->inspection_data->process_type', $params['process_type']);
        }

        $items = $query->get();

        $inspectionMap = [];
        foreach ($items as $item) {
            $inspectionData = $item->getInspectionData();
            if ($inspectionData) {
                $inspectionMap[$item->id] = [
                    'item_id' => $item->id,
                    'item_name' => $item->item_name,
                    'specification' => $item->specification,
                    'quantity' => $item->quantity,
                    'sort_order' => $item->sort_order,
                    'options' => $item->options,
                    'inspection_data' => $inspectionData,
                ];
            }
        }

        return [
            'work_order_id' => $workOrderId,
            'items' => array_values($inspectionMap),
            'total' => count($inspectionMap),
        ];
    }

    // ──────────────────────────────────────────────────────────────
    // 검사 설정 (inspection-config)
    // ──────────────────────────────────────────────────────────────

    /**
     * 절곡 검사 기준 간격 프로파일 (5130 레거시 기준 S1/S2/S3 마감유형별)
     *
     * S1: KSS01 계열 (KQTS01 포함)
     * S2: KSS02 계열 (EGI 마감 포함)
     * S3: KWE01/KSE01 + SUS 별도마감
     *
     * 향후 DB 테이블 또는 테넌트 설정으로 이관 가능
     */
    private const BENDING_GAP_PROFILES = [
        'S1' => [
            'guide_rail_wall' => [
                'name' => '가이드레일(벽면형)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '80'],
                    ['point' => '(3)', 'design_value' => '45'],
                    ['point' => '(4)', 'design_value' => '40'],
                ],
            ],
            'guide_rail_side' => [
                'name' => '가이드레일(측면형)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '70'],
                    ['point' => '(3)', 'design_value' => '45'],
                    ['point' => '(4)', 'design_value' => '35'],
                    ['point' => '(5)', 'design_value' => '95'],
                    ['point' => '(6)', 'design_value' => '90'],
                ],
            ],
            'bottom_bar' => [
                'name' => '하단마감재',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '60'],
                ],
            ],
        ],
        'S2' => [
            'guide_rail_wall' => [
                'name' => '가이드레일(벽면형)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '80'],
                    ['point' => '(3)', 'design_value' => '45'],
                ],
            ],
            'guide_rail_side' => [
                'name' => '가이드레일(측면형)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '70'],
                    ['point' => '(3)', 'design_value' => '45'],
                    ['point' => '(4)', 'design_value' => '35'],
                    ['point' => '(5)', 'design_value' => '95'],
                ],
            ],
            'bottom_bar' => [
                'name' => '하단마감재',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '60'],
                ],
            ],
        ],
        'S3' => [
            'guide_rail_wall' => [
                'name' => '가이드레일(벽면형·별도마감)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '80'],
                    ['point' => '(3)', 'design_value' => '45'],
                    ['point' => '(4)', 'design_value' => '40'],
                    ['point' => '(5)', 'design_value' => '34'],
                ],
            ],
            'guide_rail_side' => [
                'name' => '가이드레일(측면형·별도마감)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '30'],
                    ['point' => '(2)', 'design_value' => '70'],
                    ['point' => '(3)', 'design_value' => '80'],
                    ['point' => '(4)', 'design_value' => '45'],
                    ['point' => '(5)', 'design_value' => '40'],
                    ['point' => '(6)', 'design_value' => '34'],
                    ['point' => '(7)', 'design_value' => '74'],
                ],
            ],
            'bottom_bar' => [
                'name' => '하단마감재(별도마감)',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '60'],
                    ['point' => '(2)', 'design_value' => '64'],
                ],
            ],
        ],
        'common' => [
            'case_box' => [
                'name' => '케이스',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '550'],
                    ['point' => '(2)', 'design_value' => '50'],
                    ['point' => '(3)', 'design_value' => '385'],
                    ['point' => '(4)', 'design_value' => '50'],
                    ['point' => '(5)', 'design_value' => '410'],
                ],
            ],
            'smoke_w50' => [
                'name' => '연기차단재 W50',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '50'],
                    ['point' => '(2)', 'design_value' => '12'],
                ],
            ],
            'smoke_w80' => [
                'name' => '연기차단재 W80',
                'gap_points' => [
                    ['point' => '(1)', 'design_value' => '80'],
                    ['point' => '(2)', 'design_value' => '12'],
                ],
            ],
        ],
    ];

    /**
     * 작업지시의 검사 설정 조회 (공정 자동 판별 + 구성품 목록)
     *
     * 절곡 공정: bending_info 기반으로 검사 대상 구성품 + 간격 기준치 반환
     * 기타 공정: items 빈 배열 (스크린/슬랫은 별도 구성품 없음)
     */
    public function getInspectionConfig(int $workOrderId): array
    {
        $workOrder = WorkOrder::where('tenant_id', $this->tenantId())
            ->with(['process', 'items' => fn ($q) => $q->orderBy('sort_order')])
            ->findOrFail($workOrderId);

        $process = $workOrder->process;
        $processType = $this->resolveInspectionProcessType($process);

        $firstItem = $workOrder->items->first();
        $productCode = $firstItem?->options['product_code'] ?? null;
        $templateId = $process?->document_template_id;

        $items = [];
        $finishingType = null;
        if ($processType === 'bending') {
            $finishingType = $this->resolveFinishingType($productCode);
            $items = $this->buildBendingInspectionItems($firstItem);
        }

        return [
            'work_order_id' => $workOrder->id,
            'process_type' => $processType,
            'product_code' => $productCode,
            'finishing_type' => $finishingType,
            'template_id' => $templateId,
            'items' => $items,
        ];
    }

    /**
     * 공정명 → 검사 공정 타입 변환
     */
    private function resolveInspectionProcessType(?Process $process): string
    {
        if (! $process) {
            return 'unknown';
        }

        return match ($process->process_name) {
            '스크린' => 'screen',
            '슬랫' => 'slat',
            '절곡' => 'bending',
            default => strtolower($process->process_code ?? 'unknown'),
        };
    }

    /**
     * 제품코드에서 마감유형(S1/S2/S3) 결정 (5130 레거시 기준)
     *
     * KSS01, KQTS01 → S1
     * KSS02 (및 EGI 마감) → S2
     * KWE01/KSE01 + SUS → S3
     */
    private function resolveFinishingType(?string $productCode): string
    {
        if (! $productCode) {
            return 'S1';
        }

        // FG-{model}-{type}-{material} 형식에서 모델코드와 재질 추출
        $parts = explode('-', $productCode);
        $modelCode = $parts[1] ?? '';
        $material = $parts[3] ?? '';

        // SUS 재질 + KWE/KSE 모델 → S3 (별도마감)
        if (stripos($material, 'SUS') !== false && (str_starts_with($modelCode, 'KWE') || str_starts_with($modelCode, 'KSE'))) {
            return 'S3';
        }

        return match (true) {
            str_starts_with($modelCode, 'KSS01'), str_starts_with($modelCode, 'KQTS') => 'S1',
            str_starts_with($modelCode, 'KSS02') => 'S2',
            str_starts_with($modelCode, 'KWE'), str_starts_with($modelCode, 'KSE') => 'S2', // EGI마감 = S2
            default => 'S2', // 기본값: S2 (5130 기준 EGI와 동일)
        };
    }

    /**
     * 절곡 bending_info에서 검사 대상 구성품 + 간격 기준치 빌드
     * 마감유형(S1/S2/S3)에 따라 gap_points가 달라짐
     */
    private function buildBendingInspectionItems(?WorkOrderItem $firstItem): array
    {
        if (! $firstItem) {
            return [];
        }

        $productCode = $firstItem->options['product_code'] ?? null;
        $finishingType = $this->resolveFinishingType($productCode);
        $typeProfiles = self::BENDING_GAP_PROFILES[$finishingType] ?? self::BENDING_GAP_PROFILES['S1'];
        $commonProfiles = self::BENDING_GAP_PROFILES['common'];

        $bendingInfo = $firstItem->options['bending_info'] ?? null;
        $items = [];

        // 가이드레일 벽면 (벽면형 또는 혼합형)
        $guideRail = $bendingInfo['guideRail'] ?? null;
        $hasWall = ! $bendingInfo || ($guideRail && ($guideRail['wall'] ?? false));
        $hasSide = $guideRail && ($guideRail['side'] ?? false);

        if ($hasWall) {
            $profile = $typeProfiles['guide_rail_wall'];
            $items[] = [
                'id' => 'guide_rail_wall',
                'name' => $profile['name'],
                'gap_points' => $profile['gap_points'],
            ];
        }

        if ($hasSide) {
            $profile = $typeProfiles['guide_rail_side'];
            $items[] = [
                'id' => 'guide_rail_side',
                'name' => $profile['name'],
                'gap_points' => $profile['gap_points'],
            ];
        }

        // 하단마감재 (항상 포함, 마감유형별 gap_points 다름)
        $profile = $typeProfiles['bottom_bar'];
        $items[] = [
            'id' => 'bottom_bar',
            'name' => $profile['name'],
            'gap_points' => $profile['gap_points'],
        ];

        // 케이스 (항상 포함, 공통)
        $profile = $commonProfiles['case_box'];
        $items[] = [
            'id' => 'case_box',
            'name' => $profile['name'],
            'gap_points' => $profile['gap_points'],
        ];

        // 연기차단재 W50 (항상 포함, 공통)
        $profile = $commonProfiles['smoke_w50'];
        $items[] = [
            'id' => 'smoke_w50',
            'name' => $profile['name'],
            'gap_points' => $profile['gap_points'],
        ];

        // 연기차단재 W80 (항상 포함, 공통)
        $profile = $commonProfiles['smoke_w80'];
        $items[] = [
            'id' => 'smoke_w80',
            'name' => $profile['name'],
            'gap_points' => $profile['gap_points'],
        ];

        return $items;
    }

    // ──────────────────────────────────────────────────────────────
    // 검사 문서 템플릿 연동
    // ──────────────────────────────────────────────────────────────

    /**
     * 작업지시의 검사용 문서 템플릿 조회
     *
     * work_order → process → steps(needs_inspection=true) → documentTemplate 로드
     * 모든 검사 단계의 템플릿을 반환 (다중 검사 단계 지원)
     */
    public function getInspectionTemplate(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'process.documentTemplate' => fn ($q) => $q->with([
                    'approvalLines',
                    'basicFields',
                    'sections.items',
                    'columns',
                    'sectionFields',
                ]),
                'salesOrder:id,order_no,client_name,site_name',
                'items:id,work_order_id,item_name,specification,quantity,unit,sort_order',
            ])
            ->find($workOrderId);

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

        $process = $workOrder->process;
        $docTemplate = $process?->documentTemplate;

        if (! $docTemplate) {
            return [
                'work_order_id' => $workOrderId,
                'has_template' => false,
                'templates' => [],
                'template' => null,
                'work_order_info' => $this->buildWorkOrderInfo($workOrder),
            ];
        }

        $documentService = app(DocumentService::class);
        $formattedTemplate = $documentService->formatTemplateForReact($docTemplate);

        return [
            'work_order_id' => $workOrderId,
            'has_template' => true,
            'templates' => [[
                'template_id' => $docTemplate->id,
                'template_name' => $docTemplate->name,
                'template' => $formattedTemplate,
            ]],
            'template' => $formattedTemplate,
            'work_order_info' => $this->buildWorkOrderInfo($workOrder),
        ];
    }

    /**
     * 작업지시 검사 문서 resolve (기존 문서 조회 또는 생성 정보 반환)
     *
     * step_id 기반으로 해당 검사 단계의 템플릿과 기존 문서를 조회.
     * 기존 DRAFT/REJECTED 문서가 있으면 반환, 없으면 template만 반환.
     */
    public function resolveInspectionDocument(int $workOrderId, array $params = []): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'process.documentTemplate' => fn ($q) => $q->with([
                    'approvalLines',
                    'basicFields',
                    'sections.items',
                    'columns',
                    'sectionFields',
                ]),
                'salesOrder:id,order_no,client_name,site_name',
                'items:id,work_order_id,item_name,specification,quantity,unit,sort_order',
            ])
            ->find($workOrderId);

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

        $process = $workOrder->process;
        $templateId = $process?->document_template_id;
        $docTemplate = $process?->documentTemplate;

        if (! $templateId || ! $docTemplate) {
            throw new BadRequestHttpException(__('error.work_order.no_inspection_template'));
        }

        $documentService = app(DocumentService::class);
        $formattedTemplate = $documentService->formatTemplateForReact($docTemplate);

        // 기존 문서 조회 (work_order + template, 수정 가능한 상태)
        $existingDocument = Document::query()
            ->where('tenant_id', $tenantId)
            ->where('template_id', $templateId)
            ->where('linkable_type', 'work_order')
            ->where('linkable_id', $workOrderId)
            ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
            ->with(['data', 'attachments.file', 'approvals.user:id,name'])
            ->latest()
            ->first();

        return [
            'work_order_id' => $workOrderId,
            'template_id' => $templateId,
            'template' => $formattedTemplate,
            'existing_document' => $existingDocument,
            'work_order_info' => $this->buildWorkOrderInfo($workOrder),
        ];
    }

    /**
     * 검사 완료 시 Document + DocumentData 생성
     *
     * 공정(Process) 레벨의 document_template_id를 사용.
     * 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create.
     */
    public function createInspectionDocument(int $workOrderId, array $inspectionData): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with(['process', 'items' => fn ($q) => $q->orderBy('sort_order')])
            ->find($workOrderId);

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

        // 공정 레벨의 중간검사 양식 사용
        $process = $workOrder->process;
        $templateId = $process?->document_template_id;

        if (! $templateId) {
            throw new BadRequestHttpException(__('error.work_order.no_inspection_template'));
        }

        return DB::transaction(function () use ($workOrder, $workOrderId, $tenantId, $templateId, $inspectionData) {
            // 동시 생성 방지: 동일 작업지시에 대한 락
            $workOrder->lockForUpdate();

            $documentService = app(DocumentService::class);

            // 기존 DRAFT/REJECTED 문서가 있으면 update
            $existingDocument = Document::query()
                ->where('tenant_id', $tenantId)
                ->where('template_id', $templateId)
                ->where('linkable_type', 'work_order')
                ->where('linkable_id', $workOrderId)
                ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
                ->latest()
                ->first();

            // ★ 원본 기반 동기화: work_order_items.options.inspection_data에서 전체 수집
            $rawItems = [];
            foreach ($workOrder->items as $item) {
                $inspData = $item->getInspectionData();
                if ($inspData) {
                    $rawItems[] = $inspData;
                }
            }

            $documentDataRecords = $this->transformInspectionDataToDocumentRecords($rawItems, $templateId);

            // 기존 문서의 기본필드(bf_*) 보존
            if ($existingDocument) {
                $existingBasicFields = $existingDocument->data()
                    ->whereNull('section_id')
                    ->where('field_key', 'LIKE', 'bf_%')
                    ->get()
                    ->map(fn ($d) => [
                        'section_id' => null,
                        'column_id' => null,
                        'row_index' => $d->row_index,
                        'field_key' => $d->field_key,
                        'field_value' => $d->field_value,
                    ])
                    ->toArray();

                $document = $documentService->update($existingDocument->id, [
                    'title' => $inspectionData['title'] ?? $existingDocument->title,
                    'data' => array_merge($existingBasicFields, $documentDataRecords),
                ]);

                $action = 'inspection_document_updated';
            } else {
                $documentData = [
                    'template_id' => $templateId,
                    'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}",
                    'linkable_type' => 'work_order',
                    'linkable_id' => $workOrderId,
                    'data' => $documentDataRecords,
                    'approvers' => $inspectionData['approvers'] ?? [],
                ];

                $document = $documentService->create($documentData);
                $action = 'inspection_document_created';
            }

            // 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrderId,
                $action,
                null,
                ['document_id' => $document->id, 'document_no' => $document->document_no]
            );

            return [
                'document_id' => $document->id,
                'document_no' => $document->document_no,
                'status' => $document->status,
                'is_new' => $action === 'inspection_document_created',
            ];
        });
    }

    /**
     * 프론트 InspectionData를 정규화된 document_data 레코드로 변환
     *
     * 정규화 형식 (NEW):
     *   [{ section_id, column_id, row_index, field_key, field_value }]
     *   field_key: 'value', 'n1', 'n2', 'n1_ok', 'n1_ng', 'overall_result', 'remark', 'row_judgment'
     *
     * 레거시 형식 (WorkerScreen):
     *   [{ templateValues: { section_X_item_Y: "ok"|number }, judgment, nonConformingContent }]
     */
    private function transformInspectionDataToDocumentRecords(array $rawItems, int $templateId): array
    {
        if (empty($rawItems)) {
            return [];
        }

        // 정규화 형식 감지: 첫 번째 요소에 field_key가 있으면 새 형식
        if (isset($rawItems[0]['field_key'])) {
            return array_map(fn (array $item) => [
                'section_id' => $item['section_id'] ?? null,
                'column_id' => $item['column_id'] ?? null,
                'row_index' => $item['row_index'] ?? 0,
                'field_key' => $item['field_key'],
                'field_value' => $item['field_value'] ?? null,
            ], $rawItems);
        }

        // 절곡 products 배열 감지 → bending 전용 EAV 레코드 생성
        $productsItem = collect($rawItems)->first(fn ($item) => isset($item['products']) && is_array($item['products']));
        if ($productsItem) {
            return $this->transformBendingProductsToRecords($productsItem, $templateId);
        }

        // 레거시 형식: templateValues/values 기반 → 정규화 변환
        return $this->normalizeOldFormatRecords($rawItems, $templateId);
    }

    /**
     * 절곡 products 배열 → bending 전용 EAV 레코드 변환
     *
     * InspectionInputModal이 저장하는 products 형식:
     * [{ id, bendingStatus: '양호'|'불량', lengthMeasured, widthMeasured, gapPoints: [{point, designValue, measured}] }]
     *
     * 프론트엔드 TemplateInspectionContent가 기대하는 EAV field_key 형식:
     * b{productIdx}_ok / b{productIdx}_ng, b{productIdx}_n1, b{productIdx}_p{pointIdx}_n1
     */
    private function transformBendingProductsToRecords(array $item, int $templateId): array
    {
        $template = DocumentTemplate::with(['columns'])->find($templateId);
        if (! $template) {
            return [];
        }

        // 컬럼 식별 (column_type + sort_order 기반)
        $checkCol = $template->columns->firstWhere('column_type', 'check');
        $complexCols = $template->columns->where('column_type', 'complex')->sortBy('sort_order')->values();
        // complex 컬럼 순서: 길이(0), 너비(1), 간격(2)
        $lengthCol = $complexCols->get(0);
        $widthCol = $complexCols->get(1);
        $gapCol = $complexCols->get(2);

        $records = [];
        $products = $item['products'];

        foreach ($products as $productIdx => $product) {
            // 절곡상태 → check column
            if ($checkCol) {
                if (($product['bendingStatus'] ?? null) === '양호') {
                    $records[] = [
                        'section_id' => null, 'column_id' => $checkCol->id,
                        'row_index' => $productIdx, 'field_key' => "b{$productIdx}_ok", 'field_value' => 'OK',
                    ];
                } elseif (($product['bendingStatus'] ?? null) === '불량') {
                    $records[] = [
                        'section_id' => null, 'column_id' => $checkCol->id,
                        'row_index' => $productIdx, 'field_key' => "b{$productIdx}_ng", 'field_value' => 'NG',
                    ];
                }
            }

            // 길이 → first complex column
            if ($lengthCol && ! empty($product['lengthMeasured'])) {
                $records[] = [
                    'section_id' => null, 'column_id' => $lengthCol->id,
                    'row_index' => $productIdx, 'field_key' => "b{$productIdx}_n1", 'field_value' => (string) $product['lengthMeasured'],
                ];
            }

            // 너비 → second complex column
            if ($widthCol && ! empty($product['widthMeasured'])) {
                $records[] = [
                    'section_id' => null, 'column_id' => $widthCol->id,
                    'row_index' => $productIdx, 'field_key' => "b{$productIdx}_n1", 'field_value' => (string) $product['widthMeasured'],
                ];
            }

            // 간격 포인트 → third complex column (gap)
            if ($gapCol && ! empty($product['gapPoints'])) {
                foreach ($product['gapPoints'] as $pointIdx => $gp) {
                    if (! empty($gp['measured'])) {
                        $records[] = [
                            'section_id' => null, 'column_id' => $gapCol->id,
                            'row_index' => $productIdx, 'field_key' => "b{$productIdx}_p{$pointIdx}_n1", 'field_value' => (string) $gp['measured'],
                        ];
                    }
                }
            }
        }

        // 전체 판정
        if (isset($item['judgment'])) {
            $records[] = [
                'section_id' => null, 'column_id' => null,
                'row_index' => 0, 'field_key' => 'overall_result', 'field_value' => (string) $item['judgment'],
            ];
        }

        // 부적합 내용
        if (! empty($item['nonConformingContent'])) {
            $records[] = [
                'section_id' => null, 'column_id' => null,
                'row_index' => 0, 'field_key' => 'remark', 'field_value' => (string) $item['nonConformingContent'],
            ];
        }

        return $records;
    }

    /**
     * 레거시 형식(section_X_item_Y 키)을 정규화 레코드로 변환
     */
    private function normalizeOldFormatRecords(array $rawItems, int $templateId): array
    {
        $template = DocumentTemplate::with(['sections.items', 'columns'])->find($templateId);
        if (! $template) {
            return [];
        }

        // sectionItem.id → { section_id, column_id, measurement_type } 매핑
        $itemMap = $this->buildItemColumnMap($template);

        $records = [];
        foreach ($rawItems as $rowIdx => $item) {
            $values = $item['values'] ?? $item['templateValues'] ?? [];

            foreach ($values as $key => $cellValue) {
                // section_{sectionId}_item_{itemId} 또는 item_{itemId} 형식 파싱
                if (! preg_match('/^(?:section_(\d+)_)?item_(\d+)$/', $key, $m)) {
                    continue;
                }

                $sectionId = $m[1] ? (int) $m[1] : null;
                $itemId = (int) $m[2];
                $info = $itemMap[$itemId] ?? null;
                $columnId = $info['column_id'] ?? null;
                $sectionId = $sectionId ?? ($info['section_id'] ?? null);

                $expanded = $this->expandCellValue($cellValue, $info['measurement_type'] ?? '');
                foreach ($expanded as $rec) {
                    $records[] = [
                        'section_id' => $sectionId,
                        'column_id' => $columnId,
                        'row_index' => $rowIdx,
                        'field_key' => $rec['field_key'],
                        'field_value' => $rec['field_value'],
                    ];
                }
            }

            // 행 판정
            $judgment = $item['judgment'] ?? null;
            if ($judgment !== null) {
                $records[] = [
                    'section_id' => null,
                    'column_id' => null,
                    'row_index' => $rowIdx,
                    'field_key' => 'row_judgment',
                    'field_value' => (string) $judgment,
                ];
            }

            // 부적합 내용
            if (! empty($item['nonConformingContent'])) {
                $records[] = [
                    'section_id' => null,
                    'column_id' => null,
                    'row_index' => $rowIdx,
                    'field_key' => 'remark',
                    'field_value' => (string) $item['nonConformingContent'],
                ];
            }
        }

        return $records;
    }

    /**
     * 템플릿 구조에서 sectionItem → (section_id, column_id) 매핑 구축
     */
    private function buildItemColumnMap(DocumentTemplate $template): array
    {
        $map = [];
        foreach ($template->sections as $section) {
            foreach ($section->items as $item) {
                $itemLabel = $this->normalizeInspectionLabel($item->getFieldValue('item') ?? '');
                $columnId = null;

                foreach ($template->columns as $col) {
                    $colLabel = $this->normalizeInspectionLabel($col->label);
                    if ($itemLabel && $colLabel === $itemLabel) {
                        $columnId = $col->id;

                        break;
                    }
                }

                $map[$item->id] = [
                    'section_id' => $section->id,
                    'column_id' => $columnId,
                    'measurement_type' => $item->getFieldValue('measurement_type') ?? '',
                ];
            }
        }

        return $map;
    }

    /**
     * CellValue를 개별 field_key/field_value 레코드로 확장
     */
    private function expandCellValue(mixed $cellValue, string $measurementType): array
    {
        if ($cellValue === null) {
            return [];
        }

        // 단순 문자열/숫자 → value 레코드
        if (is_string($cellValue) || is_numeric($cellValue)) {
            return [['field_key' => 'value', 'field_value' => (string) $cellValue]];
        }

        if (! is_array($cellValue)) {
            return [['field_key' => 'value', 'field_value' => (string) $cellValue]];
        }

        $records = [];

        // measurements 배열: 복합 컬럼 데이터
        if (isset($cellValue['measurements']) && is_array($cellValue['measurements'])) {
            foreach ($cellValue['measurements'] as $n => $val) {
                $nNum = $n + 1;
                if ($measurementType === 'checkbox') {
                    $lower = strtolower($val ?? '');
                    $records[] = ['field_key' => "n{$nNum}_ok", 'field_value' => $lower === 'ok' ? 'OK' : ''];
                    $records[] = ['field_key' => "n{$nNum}_ng", 'field_value' => $lower === 'ng' ? 'NG' : ''];
                } else {
                    $records[] = ['field_key' => "n{$nNum}", 'field_value' => (string) ($val ?? '')];
                }
            }
        }

        // value 필드: 단일 값
        if (isset($cellValue['value'])) {
            $records[] = ['field_key' => 'value', 'field_value' => (string) $cellValue['value']];
        }

        // text 필드: 텍스트 값
        if (isset($cellValue['text'])) {
            $records[] = ['field_key' => 'value', 'field_value' => (string) $cellValue['text']];
        }

        // 아무 필드도 매칭 안 되면 JSON으로 저장
        return $records ?: [['field_key' => 'value', 'field_value' => json_encode($cellValue)]];
    }

    /**
     * 라벨 정규화 (매칭용)
     */
    private function normalizeInspectionLabel(string $label): string
    {
        $label = trim($label);
        // ①②③ 등 번호 접두사 제거
        $label = preg_replace('/^[①②③④⑤⑥⑦⑧⑨⑩]+/', '', $label);

        return mb_strtolower(trim($label));
    }

    /**
     * 작업지시 기본정보 빌드 (검사 문서 렌더링용)
     */
    private function buildWorkOrderInfo(WorkOrder $workOrder): array
    {
        return [
            'id' => $workOrder->id,
            'work_order_no' => $workOrder->work_order_no,
            'project_name' => $workOrder->project_name,
            'status' => $workOrder->status,
            'scheduled_date' => $workOrder->scheduled_date,
            'sales_order' => $workOrder->salesOrder ? [
                'order_no' => $workOrder->salesOrder->order_no,
                'client_name' => $workOrder->salesOrder->client_name,
                'site_name' => $workOrder->salesOrder->site_name,
            ] : null,
            'items' => $workOrder->items?->map(fn ($item) => [
                'id' => $item->id,
                'item_name' => $item->item_name,
                'specification' => $item->specification,
                'quantity' => $item->quantity,
                'unit' => $item->unit,
            ])->toArray() ?? [],
        ];
    }

    /**
     * 작업지시 검사 성적서용 데이터 조회 (전체 품목 + 검사 데이터 + 주문 정보)
     */
    public function getInspectionReport(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with(['salesOrder', 'items' => function ($q) {
                $q->ordered()->with('sourceOrderItem');
            }])
            ->find($workOrderId);

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

        // 개소(order_node_id)별 그룹핑 — WorkerScreen과 동일한 구조
        $grouped = $workOrder->items->groupBy(
            fn ($item) => $item->sourceOrderItem?->order_node_id ?? 'unassigned'
        );

        $nodeIds = $grouped->keys()->filter(fn ($k) => $k !== 'unassigned')->values()->all();
        $nodes = ! empty($nodeIds)
            ? \App\Models\Orders\OrderNode::whereIn('id', $nodeIds)->get()->keyBy('id')
            : collect();

        $nodeGroups = [];
        foreach ($grouped as $nodeId => $groupItems) {
            $node = $nodeId !== 'unassigned' ? $nodes->get($nodeId) : null;
            $nodeOpts = $node?->options ?? [];

            $firstItem = $groupItems->first();
            $soi = $firstItem->sourceOrderItem;
            $floorCode = $soi?->floor_code ?? '-';
            $symbolCode = $soi?->symbol_code ?? '-';
            $floorLabel = collect([$floorCode, $symbolCode])
                ->filter(fn ($v) => $v && $v !== '-')->join('/');

            $nodeGroups[] = [
                'node_id' => $nodeId !== 'unassigned' ? (int) $nodeId : null,
                'node_name' => $floorLabel ?: ($node?->name ?? '미지정'),
                'floor' => $nodeOpts['floor'] ?? $floorCode,
                'code' => $nodeOpts['symbol'] ?? $symbolCode,
                'width' => $nodeOpts['width'] ?? 0,
                'height' => $nodeOpts['height'] ?? 0,
                'total_quantity' => $groupItems->sum('quantity'),
                'options' => $nodeOpts,
                'items' => $groupItems->map(fn ($item) => [
                    'id' => $item->id,
                    'item_name' => $item->item_name,
                    'specification' => $item->specification,
                    'quantity' => $item->quantity,
                    'sort_order' => $item->sort_order,
                    'status' => $item->status,
                    'options' => $item->options,
                    'inspection_data' => $item->getInspectionData(),
                ])->values()->all(),
            ];
        }

        // 플랫 아이템 목록 (summary 계산용)
        $items = $workOrder->items->map(fn ($item) => [
            'id' => $item->id,
            'item_name' => $item->item_name,
            'specification' => $item->specification,
            'quantity' => $item->quantity,
            'sort_order' => $item->sort_order,
            'status' => $item->status,
            'options' => $item->options,
            'inspection_data' => $item->getInspectionData(),
        ]);

        return [
            'work_order' => [
                'id' => $workOrder->id,
                'order_no' => $workOrder->order_no,
                'status' => $workOrder->status,
                'planned_date' => $workOrder->planned_date,
                'due_date' => $workOrder->due_date,
            ],
            'order' => $workOrder->salesOrder ? [
                'id' => $workOrder->salesOrder->id,
                'order_no' => $workOrder->salesOrder->order_no,
                'client_name' => $workOrder->salesOrder->client_name ?? null,
                'site_name' => $workOrder->salesOrder->site_name ?? null,
                'order_date' => $workOrder->salesOrder->order_date ?? null,
            ] : null,
            'node_groups' => $nodeGroups,
            'items' => $items,
            'summary' => [
                'total_items' => $items->count(),
                'inspected_items' => $items->filter(fn ($i) => $i['inspection_data'] !== null)->count(),
                'passed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'pass')->count(),
                'failed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'fail')->count(),
            ],
        ];
    }

    // ──────────────────────────────────────────────────────────────
    // 작업일지 (Work Log)
    // ──────────────────────────────────────────────────────────────

    /**
     * 작업일지 양식 템플릿 조회
     *
     * 공정(Process)의 work_log_template_id 기반으로 작업일지 양식을 조회하고
     * 기본필드에 작업지시 정보를 자동 매핑하여 반환
     */
    public function getWorkLogTemplate(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'process.workLogTemplateRelation' => fn ($q) => $q->with([
                    'approvalLines',
                    'basicFields',
                    'columns',
                ]),
                'salesOrder:id,order_no,client_name,site_name,delivery_date',
                'items:id,work_order_id,item_name,specification,quantity,unit,sort_order,status',
            ])
            ->find($workOrderId);

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

        $process = $workOrder->process;
        $docTemplate = $process?->workLogTemplateRelation;

        if (! $docTemplate) {
            return [
                'work_order_id' => $workOrderId,
                'has_template' => false,
                'template' => null,
                'work_order_info' => $this->buildWorkOrderInfo($workOrder),
            ];
        }

        $documentService = app(DocumentService::class);
        $formattedTemplate = $documentService->formatTemplateForReact($docTemplate);

        // 기본필드 자동 매핑 (발주처, 현장명, LOT NO 등)
        $autoValues = $this->buildWorkLogAutoValues($workOrder);

        return [
            'work_order_id' => $workOrderId,
            'has_template' => true,
            'template' => $formattedTemplate,
            'auto_values' => $autoValues,
            'work_order_info' => $this->buildWorkOrderInfo($workOrder),
            'work_stats' => $this->calculateWorkStats($workOrder),
        ];
    }

    /**
     * 작업일지 조회 (기존 문서가 있으면 데이터 포함)
     */
    public function getWorkLog(int $workOrderId): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with([
                'process.workLogTemplateRelation' => fn ($q) => $q->with([
                    'approvalLines',
                    'basicFields',
                    'columns',
                ]),
                'salesOrder:id,order_no,client_name,site_name,delivery_date',
                'items:id,work_order_id,item_name,specification,quantity,unit,sort_order,status',
            ])
            ->find($workOrderId);

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

        $process = $workOrder->process;
        $templateId = $process?->work_log_template_id;

        // 기존 작업일지 문서 조회
        $document = null;
        if ($templateId) {
            $document = Document::query()
                ->where('tenant_id', $tenantId)
                ->where('template_id', $templateId)
                ->where('linkable_type', 'work_order')
                ->where('linkable_id', $workOrderId)
                ->with(['approvals.user:id,name', 'data'])
                ->latest()
                ->first();
        }

        $docTemplate = $process?->workLogTemplateRelation;
        $formattedTemplate = null;
        if ($docTemplate) {
            $documentService = app(DocumentService::class);
            $formattedTemplate = $documentService->formatTemplateForReact($docTemplate);
        }

        return [
            'work_order_id' => $workOrderId,
            'has_template' => $docTemplate !== null,
            'template' => $formattedTemplate,
            'document' => $document ? [
                'id' => $document->id,
                'document_no' => $document->document_no,
                'status' => $document->status,
                'submitted_at' => $document->submitted_at,
                'completed_at' => $document->completed_at,
                'approvals' => $document->approvals->map(fn ($a) => [
                    'id' => $a->id,
                    'step' => $a->step,
                    'role' => $a->role,
                    'status' => $a->status,
                    'user' => $a->user ? ['id' => $a->user->id, 'name' => $a->user->name] : null,
                    'comment' => $a->comment,
                    'acted_at' => $a->acted_at,
                ])->toArray(),
                'data' => $document->data->map(fn ($d) => [
                    'field_key' => $d->field_key,
                    'field_value' => $d->field_value,
                    'section_id' => $d->section_id,
                    'column_id' => $d->column_id,
                    'row_index' => $d->row_index,
                ])->toArray(),
            ] : null,
            'auto_values' => $this->buildWorkLogAutoValues($workOrder),
            'work_order_info' => $this->buildWorkOrderInfo($workOrder),
            'work_stats' => $this->calculateWorkStats($workOrder),
        ];
    }

    /**
     * 작업일지 생성/수정 (Document 기반)
     *
     * 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create
     */
    public function createWorkLog(int $workOrderId, array $workLogData): array
    {
        $tenantId = $this->tenantId();

        $workOrder = WorkOrder::where('tenant_id', $tenantId)
            ->with(['process', 'salesOrder:id,order_no,client_name,site_name', 'items'])
            ->find($workOrderId);

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

        $process = $workOrder->process;
        $templateId = $process?->work_log_template_id;

        if (! $templateId) {
            throw new BadRequestHttpException(__('error.work_order.no_work_log_template'));
        }

        // 템플릿의 기본필드 로드 (bf_{id} 형식으로 저장하기 위해)
        $template = DocumentTemplate::with('basicFields')->find($templateId);

        $documentService = app(DocumentService::class);

        // 기존 DRAFT/REJECTED 문서 확인
        $existingDocument = Document::query()
            ->where('tenant_id', $tenantId)
            ->where('template_id', $templateId)
            ->where('linkable_type', 'work_order')
            ->where('linkable_id', $workOrderId)
            ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
            ->latest()
            ->first();

        // 작업일지 데이터를 document_data 레코드로 변환
        $documentDataRecords = $this->transformWorkLogDataToRecords($workLogData, $workOrder, $template);

        if ($existingDocument) {
            $document = $documentService->update($existingDocument->id, [
                'title' => $workLogData['title'] ?? $existingDocument->title,
                'data' => $documentDataRecords,
            ]);
            $action = 'work_log_updated';
        } else {
            $document = $documentService->create([
                'template_id' => $templateId,
                'title' => $workLogData['title'] ?? "작업일지 - {$workOrder->work_order_no}",
                'linkable_type' => 'work_order',
                'linkable_id' => $workOrderId,
                'data' => $documentDataRecords,
                'approvers' => $workLogData['approvers'] ?? [],
            ]);
            $action = 'work_log_created';
        }

        // 감사 로그
        $this->auditLogger->log(
            $tenantId,
            self::AUDIT_TARGET,
            $workOrderId,
            $action,
            null,
            ['document_id' => $document->id, 'document_no' => $document->document_no]
        );

        return [
            'document_id' => $document->id,
            'document_no' => $document->document_no,
            'status' => $document->status,
            'is_new' => $action === 'work_log_created',
        ];
    }

    /**
     * 작업일지 기본필드 자동 매핑값 생성
     */
    private function buildWorkLogAutoValues(WorkOrder $workOrder): array
    {
        $salesOrder = $workOrder->salesOrder;

        // 수주일: received_at (date 또는 datetime)
        $receivedAt = $salesOrder?->received_at;
        $orderDate = $receivedAt ? substr((string) $receivedAt, 0, 10) : '';

        // 납기일/출고예정일
        $deliveryDate = $salesOrder?->delivery_date;
        $deliveryStr = $deliveryDate ? substr((string) $deliveryDate, 0, 10) : '';

        // 제품 LOT NO = 수주번호 (order_no)
        $orderNo = $salesOrder?->order_no ?? '';

        return [
            '발주처' => $salesOrder?->client_name ?? '',
            '현장명' => $salesOrder?->site_name ?? '',
            '작업일자' => now()->format('Y-m-d'),
            'LOT NO' => $orderNo,
            '납기일' => $deliveryStr,
            '작업지시번호' => $workOrder->work_order_no ?? '',
            '수주일' => $orderDate,
            '수주처' => $salesOrder?->client_name ?? '',
            '담당자' => '',
            '연락처' => '',
            '제품 LOT NO' => $orderNo,
            '생산담당자' => '',
            '출고예정일' => $deliveryStr,
        ];
    }

    /**
     * 작업 통계 계산
     */
    private function calculateWorkStats(WorkOrder $workOrder): array
    {
        $items = $workOrder->items;

        if (! $items || $items->isEmpty()) {
            return [
                'order_qty' => 0,
                'completed_qty' => 0,
                'in_progress_qty' => 0,
                'waiting_qty' => 0,
                'progress' => 0,
            ];
        }

        $total = $items->count();
        $completed = $items->where('status', 'completed')->count();
        $inProgress = $items->where('status', 'in_progress')->count();
        $waiting = $total - $completed - $inProgress;

        return [
            'order_qty' => $total,
            'completed_qty' => $completed,
            'in_progress_qty' => $inProgress,
            'waiting_qty' => $waiting,
            'progress' => $total > 0 ? round(($completed / $total) * 100, 1) : 0,
        ];
    }

    /**
     * 작업일지 데이터를 document_data 레코드로 변환
     *
     * mng show.blade.php 호환 형식:
     *   기본필드: field_key = 'bf_{basicField->id}' (template basicFields 기반)
     *   통계/비고: field_key = 'stats_*', 'remarks'
     *
     * auto_values로 기본필드 자동 채움, basic_data로 수동 override 가능
     */
    private function transformWorkLogDataToRecords(array $workLogData, WorkOrder $workOrder, ?DocumentTemplate $template): array
    {
        $records = [];

        // 1. 기본필드: bf_{id} 형식으로 저장 (mng show.blade.php 호환)
        if ($template && $template->basicFields) {
            $autoValues = $this->buildWorkLogAutoValues($workOrder);
            $manualData = $workLogData['basic_data'] ?? [];

            foreach ($template->basicFields as $field) {
                // 수동 입력 우선, 없으면 auto_values에서 라벨로 매칭
                $value = $manualData[$field->label]
                    ?? $manualData[$field->field_key ?? '']
                    ?? $autoValues[$field->label]
                    ?? $field->default_value
                    ?? '';

                if ($value !== '') {
                    $records[] = [
                        'field_key' => "bf_{$field->id}",
                        'field_value' => (string) $value,
                    ];
                }
            }
        }

        // 2. 작업 통계 (자동 계산)
        $stats = $this->calculateWorkStats($workOrder);
        foreach ($stats as $key => $value) {
            $records[] = [
                'field_key' => "stats_{$key}",
                'field_value' => (string) $value,
            ];
        }

        // 3. 특이사항
        if (isset($workLogData['remarks'])) {
            $records[] = [
                'field_key' => 'remarks',
                'field_value' => $workLogData['remarks'],
            ];
        }

        return $records;
    }

    // ──────────────────────────────────────────────────────────────
    // 개소별 자재 투입
    // ──────────────────────────────────────────────────────────────

    /**
     * 개소별 BOM 기반 필요 자재 + 재고 LOT 조회
     */
    public function getMaterialsForItem(int $workOrderId, int $itemId): array
    {
        $tenantId = $this->tenantId();

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

        $woItem = WorkOrderItem::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->with('item')
            ->find($itemId);

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

        // 해당 개소의 BOM 기반 자재 추출
        $materialItems = [];

        // ① dynamic_bom 우선 체크 (절곡 등 동적 BOM 사용 공정)
        $options = is_string($woItem->options) ? json_decode($woItem->options, true) : ($woItem->options ?? []);
        $dynamicBom = $options['dynamic_bom'] ?? null;

        if ($dynamicBom && is_array($dynamicBom)) {
            // dynamic_bom child_item_id 배치 조회 (N+1 방지)
            $childItemIds = array_filter(array_column($dynamicBom, 'child_item_id'));
            $childItems = [];
            if (! empty($childItemIds)) {
                $childItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
                    ->whereIn('id', array_unique($childItemIds))
                    ->get()
                    ->keyBy('id');
            }

            foreach ($dynamicBom as $bomEntry) {
                $childItemId = $bomEntry['child_item_id'] ?? null;
                if (! $childItemId || ! isset($childItems[$childItemId])) {
                    continue;
                }

                // LOT 관리 대상이 아닌 품목은 자재투입 목록에서 제외
                $childOptions = $childItems[$childItemId]->options ?? [];
                if (isset($childOptions['lot_managed']) && $childOptions['lot_managed'] === false) {
                    continue;
                }

                // dynamic_bom.qty는 아이템 수량이 곱해져 있으므로 나눠서 개소당 수량 산출
                // (작업일지 bendingInfo와 동일한 수량)
                $bomQty = (float) ($bomEntry['qty'] ?? 1);
                $woItemQty = max(1, (float) ($woItem->quantity ?? 1));
                $perNodeQty = $bomQty / $woItemQty;
                $materialItems[] = [
                    'item' => $childItems[$childItemId],
                    'bom_qty' => $perNodeQty,
                    'required_qty' => $perNodeQty,
                    'lot_prefix' => $bomEntry['lot_prefix'] ?? null,
                    'part_type' => $bomEntry['part_type'] ?? null,
                    'category' => $bomEntry['category'] ?? null,
                ];
            }
        }

        // ② dynamic_bom이 없으면 정적 BOM fallback
        if (empty($materialItems) && $woItem->item_id) {
            $item = \App\Models\Items\Item::where('tenant_id', $tenantId)
                ->find($woItem->item_id);

            if ($item && ! empty($item->bom)) {
                foreach ($item->bom as $bomItem) {
                    $childItemId = $bomItem['child_item_id'] ?? null;
                    $bomQty = (float) ($bomItem['qty'] ?? 1);

                    if (! $childItemId) {
                        continue;
                    }

                    $childItem = \App\Models\Items\Item::where('tenant_id', $tenantId)
                        ->find($childItemId);

                    if (! $childItem) {
                        continue;
                    }

                    // LOT 관리 대상이 아닌 품목은 자재투입 목록에서 제외
                    $childOptions = $childItem->options ?? [];
                    if (isset($childOptions['lot_managed']) && $childOptions['lot_managed'] === false) {
                        continue;
                    }

                    $materialItems[] = [
                        'item' => $childItem,
                        'bom_qty' => $bomQty,
                        'required_qty' => $bomQty * ($woItem->quantity ?? 1),
                    ];
                }
            }
        }

        // BOM이 없으면 품목 자체를 자재로 사용
        if (empty($materialItems) && $woItem->item_id && $woItem->item) {
            $materialItems[] = [
                'item' => $woItem->item,
                'bom_qty' => 1,
                'required_qty' => $woItem->quantity ?? 1,
            ];
        }

        // 이미 투입된 수량 조회 (item_id별 SUM)
        $inputtedQties = WorkOrderMaterialInput::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->where('work_order_item_id', $itemId)
            ->selectRaw('item_id, SUM(qty) as total_qty')
            ->groupBy('item_id')
            ->pluck('total_qty', 'item_id');

        // LOT별 기투입 수량 조회 (stock_lot_id + bom_group_key별 SUM)
        $lotInputtedRaw = WorkOrderMaterialInput::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->where('work_order_item_id', $itemId)
            ->whereNotNull('stock_lot_id')
            ->selectRaw('stock_lot_id, bom_group_key, SUM(qty) as total_qty')
            ->groupBy('stock_lot_id', 'bom_group_key')
            ->get();

        // bom_group_key 포함 복합키 매핑 + stock_lot_id 단순 매핑 (하위호환)
        $lotInputtedByGroup = [];
        $lotInputtedByLot = [];
        foreach ($lotInputtedRaw as $row) {
            $lotId = $row->stock_lot_id;
            $groupKey = $row->bom_group_key;
            $qty = (float) $row->total_qty;

            if ($groupKey) {
                $compositeKey = $lotId.'_'.$groupKey;
                $lotInputtedByGroup[$compositeKey] = ($lotInputtedByGroup[$compositeKey] ?? 0) + $qty;
            }
            $lotInputtedByLot[$lotId] = ($lotInputtedByLot[$lotId] ?? 0) + $qty;
        }

        // 자재별 LOT 조회
        $materials = [];
        $rank = 1;

        foreach ($materialItems as $bomIdx => $matInfo) {
            $materialItem = $matInfo['item'];
            $alreadyInputted = (float) ($inputtedQties[$materialItem->id] ?? 0);
            $remainingRequired = max(0, $matInfo['required_qty'] - $alreadyInputted);

            // BOM 엔트리별 고유 그룹키 (같은 item_id라도 category+partType이 다르면 별도 그룹)
            $bomGroupKey = $materialItem->id
                .'_'.($matInfo['category'] ?? '')
                .'_'.($matInfo['part_type'] ?? '');

            $stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
                ->where('item_id', $materialItem->id)
                ->first();

            $lotsFound = false;

            if ($stock) {
                $lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
                    ->where('stock_id', $stock->id)
                    ->where('status', 'available')
                    ->where('available_qty', '>', 0)
                    ->orderBy('fifo_order', 'asc')
                    ->get();

                foreach ($lots as $lot) {
                    $lotsFound = true;
                    $materials[] = [
                        'stock_lot_id' => $lot->id,
                        'item_id' => $materialItem->id,
                        'bom_group_key' => $bomGroupKey,
                        'lot_no' => $lot->lot_no,
                        'material_code' => $materialItem->code,
                        'material_name' => $materialItem->name,
                        'specification' => $materialItem->specification,
                        'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
                        'bom_qty' => $matInfo['bom_qty'],
                        'required_qty' => $matInfo['required_qty'],
                        'already_inputted' => $alreadyInputted,
                        'remaining_required_qty' => $remainingRequired,
                        'lot_inputted_qty' => (float) ($lotInputtedByGroup[$lot->id.'_'.$bomGroupKey] ?? $lotInputtedByLot[$lot->id] ?? 0),
                        'lot_qty' => (float) $lot->qty,
                        'lot_available_qty' => (float) $lot->available_qty,
                        'lot_reserved_qty' => (float) $lot->reserved_qty,
                        'receipt_date' => $lot->receipt_date,
                        'supplier' => $lot->supplier,
                        'fifo_rank' => $rank++,
                        'lot_prefix' => $matInfo['lot_prefix'] ?? null,
                        'part_type' => $matInfo['part_type'] ?? null,
                        'category' => $matInfo['category'] ?? null,
                    ];
                }
            }

            if (! $lotsFound) {
                $materials[] = [
                    'stock_lot_id' => null,
                    'item_id' => $materialItem->id,
                    'bom_group_key' => $bomGroupKey,
                    'lot_no' => null,
                    'material_code' => $materialItem->code,
                    'material_name' => $materialItem->name,
                    'specification' => $materialItem->specification,
                    'unit' => $materialItem->unit ?? 'EA',
                    'bom_qty' => $matInfo['bom_qty'],
                    'required_qty' => $matInfo['required_qty'],
                    'already_inputted' => $alreadyInputted,
                    'remaining_required_qty' => $remainingRequired,
                    'lot_inputted_qty' => 0,
                    'lot_qty' => 0,
                    'lot_available_qty' => 0,
                    'lot_reserved_qty' => 0,
                    'receipt_date' => null,
                    'supplier' => null,
                    'fifo_rank' => $rank++,
                    'lot_prefix' => $matInfo['lot_prefix'] ?? null,
                    'part_type' => $matInfo['part_type'] ?? null,
                    'category' => $matInfo['category'] ?? null,
                ];
            }
        }

        return $materials;
    }

    /**
     * 개소별 자재 투입 등록
     *
     * @param  bool  $replace  true면 기존 투입 이력을 삭제(재고 복원) 후 새로 등록
     */
    public function registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs, bool $replace = false): array
    {
        $tenantId = $this->tenantId();
        $userId = $this->apiUserId();

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

        $woItem = WorkOrderItem::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->find($itemId);

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

        return DB::transaction(function () use ($inputs, $tenantId, $userId, $workOrderId, $itemId, $replace) {
            $stockService = app(StockService::class);
            $inputResults = [];

            // replace 모드: 기존 투입 이력 삭제 + 재고 복원
            if ($replace) {
                $existingInputs = WorkOrderMaterialInput::where('tenant_id', $tenantId)
                    ->where('work_order_id', $workOrderId)
                    ->where('work_order_item_id', $itemId)
                    ->get();

                foreach ($existingInputs as $existing) {
                    $stockService->increaseToLot(
                        stockLotId: $existing->stock_lot_id,
                        qty: (float) $existing->qty,
                        reason: 'work_order_input_replace',
                        referenceId: $workOrderId
                    );
                    $existing->delete();
                }
            }

            foreach ($inputs as $input) {
                $stockLotId = $input['stock_lot_id'] ?? null;
                $qty = (float) ($input['qty'] ?? 0);
                $bomGroupKey = $input['bom_group_key'] ?? null;

                if (! $stockLotId || $qty <= 0) {
                    continue;
                }

                // 기존 재고 차감 로직 재사용
                $result = $stockService->decreaseFromLot(
                    stockLotId: $stockLotId,
                    qty: $qty,
                    reason: 'work_order_input',
                    referenceId: $workOrderId
                );

                // 로트의 품목 ID 조회 (Eager Loading으로 N+1 방지)
                $lot = \App\Models\Tenants\StockLot::with('stock')->find($stockLotId);
                $lotItemId = $lot?->stock?->item_id;

                // 개소별 매핑 레코드 생성
                WorkOrderMaterialInput::create([
                    'tenant_id' => $tenantId,
                    'work_order_id' => $workOrderId,
                    'work_order_item_id' => $itemId,
                    'stock_lot_id' => $stockLotId,
                    'item_id' => $lotItemId ?? 0,
                    'bom_group_key' => $bomGroupKey,
                    'qty' => $qty,
                    'input_by' => $userId,
                    'input_at' => now(),
                ]);

                $inputResults[] = [
                    'stock_lot_id' => $stockLotId,
                    'qty' => $qty,
                    'status' => 'success',
                    'deducted_lot' => $result,
                ];
            }

            // 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrderId,
                'material_input_for_item',
                null,
                [
                    'work_order_item_id' => $itemId,
                    'inputs' => $inputs,
                    'input_results' => $inputResults,
                    'input_by' => $userId,
                    'input_at' => now()->toDateTimeString(),
                ]
            );

            return [
                'work_order_id' => $workOrderId,
                'work_order_item_id' => $itemId,
                'material_count' => count($inputResults),
                'input_results' => $inputResults,
                'input_at' => now()->toDateTimeString(),
            ];
        });
    }

    /**
     * 개소별 자재 투입 이력 조회
     */
    public function getMaterialInputsForItem(int $workOrderId, int $itemId): array
    {
        $tenantId = $this->tenantId();

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

        $woItem = WorkOrderItem::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->find($itemId);

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

        $inputs = WorkOrderMaterialInput::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->where('work_order_item_id', $itemId)
            ->with(['stockLot', 'item', 'inputBy'])
            ->orderBy('input_at', 'desc')
            ->get();

        return $inputs->map(function ($input) {
            return [
                'id' => $input->id,
                'stock_lot_id' => $input->stock_lot_id,
                'lot_no' => $input->stockLot?->lot_no,
                'item_id' => $input->item_id,
                'material_code' => $input->item?->code,
                'material_name' => $input->item?->name,
                'qty' => (float) $input->qty,
                'unit' => $input->item?->unit ?? 'EA',
                'input_by' => $input->input_by,
                'input_by_name' => $input->inputBy?->name,
                'input_at' => $input->input_at?->toDateTimeString(),
            ];
        })->toArray();
    }

    /**
     * 개소별 자재 투입 삭제 (재고 복원)
     */
    public function deleteMaterialInput(int $workOrderId, int $inputId): void
    {
        $tenantId = $this->tenantId();

        $input = WorkOrderMaterialInput::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->find($inputId);

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

        DB::transaction(function () use ($input, $tenantId, $workOrderId) {
            // 재고 복원
            $stockService = app(StockService::class);
            $stockService->increaseToLot(
                stockLotId: $input->stock_lot_id,
                qty: (float) $input->qty,
                reason: 'work_order_input_cancel',
                referenceId: $workOrderId
            );

            // 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrderId,
                'material_input_deleted',
                [
                    'input_id' => $input->id,
                    'stock_lot_id' => $input->stock_lot_id,
                    'qty' => (float) $input->qty,
                    'work_order_item_id' => $input->work_order_item_id,
                ],
                null
            );

            $input->delete();
        });
    }

    /**
     * 개소별 자재 투입 수량 수정 (재고 차이 반영)
     */
    public function updateMaterialInput(int $workOrderId, int $inputId, float $newQty): array
    {
        $tenantId = $this->tenantId();

        $input = WorkOrderMaterialInput::where('tenant_id', $tenantId)
            ->where('work_order_id', $workOrderId)
            ->find($inputId);

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

        return DB::transaction(function () use ($input, $newQty, $tenantId, $workOrderId) {
            $oldQty = (float) $input->qty;
            $diff = $newQty - $oldQty;

            if (abs($diff) < 0.001) {
                return ['id' => $input->id, 'qty' => $oldQty, 'changed' => false];
            }

            $stockService = app(StockService::class);

            if ($diff > 0) {
                // 수량 증가 → 추가 차감
                $stockService->decreaseFromLot(
                    stockLotId: $input->stock_lot_id,
                    qty: $diff,
                    reason: 'work_order_input',
                    referenceId: $workOrderId
                );
            } else {
                // 수량 감소 → 차이만큼 복원
                $stockService->increaseToLot(
                    stockLotId: $input->stock_lot_id,
                    qty: abs($diff),
                    reason: 'work_order_input_adjust',
                    referenceId: $workOrderId
                );
            }

            // 감사 로그
            $this->auditLogger->log(
                $tenantId,
                self::AUDIT_TARGET,
                $workOrderId,
                'material_input_updated',
                ['input_id' => $input->id, 'qty' => $oldQty],
                ['input_id' => $input->id, 'qty' => $newQty]
            );

            $input->qty = $newQty;
            $input->save();

            return ['id' => $input->id, 'qty' => $newQty, 'changed' => true];
        });
    }
}
