<?php

namespace App\Services\Quote;

use App\Models\CategoryGroup;
use App\Models\Items\Item;
use App\Models\Products\Price;
use App\Models\Quote\QuoteFormula;
use App\Services\Quote\Contracts\TenantFormulaHandler;
use App\Services\Service;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * 수식 평가 서비스
 *
 * 견적 자동산출을 위한 수식 검증 및 평가 엔진
 * MNG FormulaEvaluatorService와 동기화된 BOM 계산 로직 포함
 *
 * 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
 */
class FormulaEvaluatorService extends Service
{
    /**
     * 지원 함수 목록
     */
    public const SUPPORTED_FUNCTIONS = [
        'SUM', 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MIN', 'MAX', 'IF', 'AND', 'OR', 'NOT',
    ];

    private array $variables = [];

    private array $errors = [];

    private array $debugSteps = [];

    private bool $debugMode = false;

    // =========================================================================
    // 기본 수식 평가 메서드
    // =========================================================================

    /**
     * 수식 검증
     */
    public function validateFormula(string $formula): array
    {
        $errors = [];

        // 기본 문법 검증
        if (empty(trim($formula))) {
            return [
                'success' => false,
                'errors' => [__('error.formula_empty')],
            ];
        }

        // 괄호 매칭 검증
        if (! $this->validateParentheses($formula)) {
            $errors[] = __('error.formula_parentheses_mismatch');
        }

        // 변수 추출
        $variables = $this->extractVariables($formula);

        // 지원 함수 검증
        $functions = $this->extractFunctions($formula);

        foreach ($functions as $func) {
            if (! in_array(strtoupper($func), self::SUPPORTED_FUNCTIONS)) {
                $errors[] = __('error.formula_unsupported_function', ['function' => $func]);
            }
        }

        return [
            'success' => empty($errors),
            'errors' => $errors,
            'variables' => $variables,
            'functions' => $functions,
        ];
    }

    /**
     * 수식 평가
     *
     * @param  string  $formula  수식 문자열
     * @param  array  $variables  변수 배열 [변수명 => 값]
     */
    public function evaluate(string $formula, array $variables = []): mixed
    {
        $this->variables = array_merge($this->variables, $variables);
        $this->errors = [];

        try {
            // 변수 치환
            $expression = $this->substituteVariables($formula);

            // 함수 처리
            $expression = $this->processFunctions($expression);

            // 최종 계산
            return $this->calculateExpression($expression);
        } catch (\Exception $e) {
            $this->errors[] = $e->getMessage();

            return null;
        }
    }

    /**
     * 다중 수식 일괄 평가
     *
     * @param  array  $formulas  [변수명 => 수식] 배열
     * @param  array  $inputVariables  입력 변수
     * @return array [변수명 => 결과값]
     */
    public function evaluateMultiple(array $formulas, array $inputVariables = []): array
    {
        $this->variables = $inputVariables;
        $results = [];

        foreach ($formulas as $variable => $formula) {
            $result = $this->evaluate($formula);
            $this->variables[$variable] = $result;
            $results[$variable] = $result;
        }

        return [
            'results' => $results,
            'errors' => $this->errors,
        ];
    }

    /**
     * 범위 기반 값 결정
     *
     * @param  float  $value  검사할 값
     * @param  array  $ranges  범위 배열 [['min' => 0, 'max' => 100, 'result' => 값], ...]
     * @param  mixed  $default  기본값
     */
    public function evaluateRange(float $value, array $ranges, mixed $default = null): mixed
    {
        foreach ($ranges as $range) {
            $min = $range['min'] ?? null;
            $max = $range['max'] ?? null;

            $inRange = true;

            if ($min !== null && $value < $min) {
                $inRange = false;
            }
            if ($max !== null && $value > $max) {
                $inRange = false;
            }

            if ($inRange) {
                $result = $range['result'] ?? $default;

                // result가 수식인 경우 평가
                if (is_string($result) && $this->isFormula($result)) {
                    return $this->evaluate($result);
                }

                return $result;
            }
        }

        return $default;
    }

    /**
     * 매핑 기반 값 결정
     *
     * @param  mixed  $sourceValue  소스 값
     * @param  array  $mappings  매핑 배열 [['source' => 값, 'result' => 결과], ...]
     * @param  mixed  $default  기본값
     */
    public function evaluateMapping(mixed $sourceValue, array $mappings, mixed $default = null): mixed
    {
        foreach ($mappings as $mapping) {
            $source = $mapping['source'] ?? null;
            $result = $mapping['result'] ?? $default;

            if ($sourceValue == $source) {
                // result가 수식인 경우 평가
                if (is_string($result) && $this->isFormula($result)) {
                    return $this->evaluate($result);
                }

                return $result;
            }
        }

        return $default;
    }

    // =========================================================================
    // Private Helper Methods (기본)
    // =========================================================================

    private function validateParentheses(string $formula): bool
    {
        $count = 0;
        foreach (str_split($formula) as $char) {
            if ($char === '(') {
                $count++;
            }
            if ($char === ')') {
                $count--;
            }
            if ($count < 0) {
                return false;
            }
        }

        return $count === 0;
    }

    private function extractVariables(string $formula): array
    {
        preg_match_all('/\b([A-Z][A-Z0-9_]*)\b/', $formula, $matches);
        $variables = array_unique($matches[1] ?? []);

        // 함수명 제외
        return array_values(array_diff($variables, self::SUPPORTED_FUNCTIONS));
    }

    private function extractFunctions(string $formula): array
    {
        preg_match_all('/\b([A-Za-z_]+)\s*\(/', $formula, $matches);

        return array_unique($matches[1] ?? []);
    }

    private function substituteVariables(string $formula): string
    {
        foreach ($this->variables as $var => $value) {
            $formula = preg_replace('/\b'.preg_quote($var, '/').'\b/', (string) $value, $formula);
        }

        return $formula;
    }

    private function processFunctions(string $expression): string
    {
        // ROUND(value, decimals)
        $expression = preg_replace_callback(
            '/ROUND\s*\(\s*([^,]+)\s*,\s*(\d+)\s*\)/i',
            fn ($m) => round((float) $this->calculateExpression($m[1]), (int) $m[2]),
            $expression
        );

        // SUM(a, b, c, ...)
        $expression = preg_replace_callback(
            '/SUM\s*\(([^)]+)\)/i',
            fn ($m) => array_sum(array_map('floatval', explode(',', $m[1]))),
            $expression
        );

        // MIN, MAX
        $expression = preg_replace_callback(
            '/MIN\s*\(([^)]+)\)/i',
            fn ($m) => min(array_map('floatval', explode(',', $m[1]))),
            $expression
        );

        $expression = preg_replace_callback(
            '/MAX\s*\(([^)]+)\)/i',
            fn ($m) => max(array_map('floatval', explode(',', $m[1]))),
            $expression
        );

        // IF(condition, true_val, false_val)
        $expression = preg_replace_callback(
            '/IF\s*\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^)]+)\s*\)/i',
            function ($m) {
                $condition = $this->evaluateCondition($m[1]);

                return $condition ? $this->calculateExpression($m[2]) : $this->calculateExpression($m[3]);
            },
            $expression
        );

        // ABS, CEIL, FLOOR
        $expression = preg_replace_callback(
            '/ABS\s*\(([^)]+)\)/i',
            fn ($m) => abs((float) $this->calculateExpression($m[1])),
            $expression
        );
        $expression = preg_replace_callback(
            '/CEIL\s*\(([^)]+)\)/i',
            fn ($m) => ceil((float) $this->calculateExpression($m[1])),
            $expression
        );
        $expression = preg_replace_callback(
            '/FLOOR\s*\(([^)]+)\)/i',
            fn ($m) => floor((float) $this->calculateExpression($m[1])),
            $expression
        );

        return $expression;
    }

    private function calculateExpression(string $expression): float
    {
        // 안전한 수식 평가 (숫자, 연산자, 괄호만 허용)
        $expression = preg_replace('/[^0-9+\-*\/().%\s]/', '', $expression);

        if (empty(trim($expression))) {
            return 0;
        }

        try {
            // TODO: 프로덕션에서는 symfony/expression-language 등 안전한 라이브러리 사용 권장
            return (float) eval("return {$expression};");
        } catch (\Throwable $e) {
            $this->errors[] = __('error.formula_calculation_error', ['expression' => $expression]);

            return 0;
        }
    }

    private function evaluateCondition(string $condition): bool
    {
        // 비교 연산자 처리
        if (preg_match('/(.+)(>=|<=|>|<|==|!=)(.+)/', $condition, $m)) {
            $left = (float) $this->calculateExpression(trim($m[1]));
            $right = (float) $this->calculateExpression(trim($m[3]));
            $op = $m[2];

            return match ($op) {
                '>=' => $left >= $right,
                '<=' => $left <= $right,
                '>' => $left > $right,
                '<' => $left < $right,
                '==' => $left == $right,
                '!=' => $left != $right,
                default => false,
            };
        }

        return (bool) $this->calculateExpression($condition);
    }

    private function isFormula(string $value): bool
    {
        // 연산자나 함수가 포함되어 있으면 수식으로 판단
        return preg_match('/[+\-*\/()]|[A-Z]+\s*\(/', $value) === 1;
    }

    // =========================================================================
    // 상태 관리 메서드
    // =========================================================================

    /**
     * 에러 목록 반환
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * 현재 변수 상태 반환
     */
    public function getVariables(): array
    {
        return $this->variables;
    }

    /**
     * 변수 설정
     */
    public function setVariables(array $variables): self
    {
        $this->variables = $variables;

        return $this;
    }

    /**
     * 변수 추가
     */
    public function addVariable(string $name, mixed $value): self
    {
        $this->variables[$name] = $value;

        return $this;
    }

    /**
     * 변수 및 에러 초기화
     */
    public function reset(): void
    {
        $this->variables = [];
        $this->errors = [];
        $this->debugSteps = [];
    }

    // =========================================================================
    // DB 기반 수식 실행 (QuoteFormula 모델 사용)
    // =========================================================================

    /**
     * 범위별 수식 평가 (QuoteFormula 기반)
     */
    public function evaluateRangeFormula(QuoteFormula $formula, array $variables = []): mixed
    {
        $conditionVar = $formula->ranges->first()?->condition_variable;
        $value = $variables[$conditionVar] ?? 0;

        foreach ($formula->ranges as $range) {
            if ($range->isInRange($value)) {
                if ($range->result_type === 'formula') {
                    return $this->evaluate($range->result_value, $variables);
                }

                return $range->result_value;
            }
        }

        return null;
    }

    /**
     * 매핑 수식 평가 (QuoteFormula 기반)
     */
    public function evaluateMappingFormula(QuoteFormula $formula, array $variables = []): mixed
    {
        foreach ($formula->mappings as $mapping) {
            $sourceValue = $variables[$mapping->source_variable] ?? null;

            if ($sourceValue == $mapping->source_value) {
                if ($mapping->result_type === 'formula') {
                    return $this->evaluate($mapping->result_value, $variables);
                }

                return $mapping->result_value;
            }
        }

        return null;
    }

    /**
     * 전체 수식 실행 (카테고리 순서대로)
     *
     * @param  Collection  $formulasByCategory  카테고리별 수식 컬렉션
     * @param  array  $inputVariables  입력 변수
     * @return array ['variables' => 결과, 'items' => 품목, 'errors' => 에러]
     */
    public function executeAll(Collection $formulasByCategory, array $inputVariables = []): array
    {
        if (! $this->tenantId()) {
            throw new RuntimeException(__('error.tenant_id_required'));
        }

        $this->variables = $inputVariables;
        $results = [];
        $items = [];

        foreach ($formulasByCategory as $categoryCode => $formulas) {
            foreach ($formulas as $formula) {
                $result = $this->executeFormula($formula);

                if ($formula->output_type === QuoteFormula::OUTPUT_VARIABLE) {
                    $this->variables[$formula->variable] = $result;
                    $results[$formula->variable] = [
                        'name' => $formula->name,
                        'value' => $result,
                        'category' => $formula->category->name,
                        'type' => $formula->type,
                    ];
                } else {
                    // 품목 출력
                    foreach ($formula->items as $item) {
                        $quantity = $this->evaluate($item->quantity_formula);
                        $unitPrice = $item->unit_price_formula
                            ? $this->evaluate($item->unit_price_formula)
                            : $this->getItemPrice($item->item_code);

                        // 품목마스터에서 규격/단위 조회 (우선순위: 품목마스터 > 수식품목)
                        $itemMasterData = $this->getItemSpecAndUnit($item->item_code);
                        $specification = $itemMasterData['specification'] ?? $item->specification;
                        $unit = $itemMasterData['unit'] ?? $item->unit ?? 'EA';

                        $items[] = [
                            'item_code' => $item->item_code,
                            'item_name' => $item->item_name,
                            'specification' => $specification,
                            'unit' => $unit,
                            'quantity' => $quantity,
                            'unit_price' => $unitPrice,
                            'total_price' => $quantity * $unitPrice,
                            'formula_variable' => $formula->variable,
                        ];
                    }
                }
            }
        }

        return [
            'variables' => $results,
            'items' => $items,
            'errors' => $this->errors,
        ];
    }

    /**
     * 단일 수식 실행
     */
    private function executeFormula(QuoteFormula $formula): mixed
    {
        return match ($formula->type) {
            QuoteFormula::TYPE_INPUT => $this->variables[$formula->variable] ??
                ($formula->formula ? $this->evaluate($formula->formula) : null),
            QuoteFormula::TYPE_CALCULATION => $this->evaluate($formula->formula, $this->variables),
            QuoteFormula::TYPE_RANGE => $this->evaluateRangeFormula($formula, $this->variables),
            QuoteFormula::TYPE_MAPPING => $this->evaluateMappingFormula($formula, $this->variables),
            default => null,
        };
    }

    // =========================================================================
    // 디버그 모드 (MNG 시뮬레이터 동기화)
    // =========================================================================

    /**
     * 디버그 모드 활성화
     */
    public function enableDebugMode(bool $enabled = true): self
    {
        $this->debugMode = $enabled;
        if ($enabled) {
            $this->debugSteps = [];
        }

        return $this;
    }

    /**
     * 디버그 단계 기록
     */
    /**
     * FG 코드에서 모델/설치타입/마감타입 파싱
     * 형식: FG-{MODEL}-{INSTALLATION}-{FINISHING} (예: FG-KQTS01-벽면형-SUS)
     * 파싱 실패 시 null 반환 (추후 FG 코드 형식 변경 대비)
     */
    private function parseFgCode(string $code): array
    {
        $parts = explode('-', $code);
        if (count($parts) >= 4 && $parts[0] === 'FG') {
            return [
                'model' => $parts[1],
                'installation' => $parts[2],
                'finishing' => $parts[3],
            ];
        }

        return [];
    }

    private function addDebugStep(int $step, string $name, array $data): void
    {
        if (! $this->debugMode) {
            return;
        }

        $this->debugSteps[] = [
            'step' => $step,
            'name' => $name,
            'timestamp' => microtime(true),
            'data' => $data,
        ];
    }

    /**
     * 디버그 정보 반환
     */
    public function getDebugSteps(): array
    {
        return $this->debugSteps;
    }

    // =========================================================================
    // BOM 기반 계산 (10단계 디버깅 포함) - MNG 동기화
    // =========================================================================

    /**
     * BOM 계산 (10단계 디버깅 포함)
     *
     * MNG 시뮬레이터와 동일한 10단계 계산 과정:
     * 1. 입력값 수집 (W0, H0)
     * 2. 완제품 선택
     * 3. 변수 계산 (W1, H1, M, K)
     * 4. BOM 전개
     * 5. 단가 출처 결정
     * 6. 수량 수식 평가
     * 7. 단가 계산 (카테고리 기반)
     * 8. 공정별 그룹화
     * 9. 소계 계산
     * 10. 최종 합계
     */
    public function calculateBomWithDebug(
        string $finishedGoodsCode,
        array $inputVariables,
        ?int $tenantId = null
    ): array {
        $this->enableDebugMode(true);
        $tenantId = $tenantId ?? $this->tenantId();

        if (! $tenantId) {
            return [
                'success' => false,
                'error' => __('error.tenant_id_required'),
                'debug_steps' => $this->debugSteps,
            ];
        }

        // 테넌트 전용 핸들러 분기 (Zero Config: class_exists 자동 발견)
        $tenantHandler = FormulaHandlerFactory::make($tenantId);
        if ($tenantHandler !== null) {
            return $this->calculateTenantBom($tenantHandler, $finishedGoodsCode, $inputVariables, $tenantId);
        }

        // Step 1: 입력값 수집 (React 동기화 변수 포함)
        $this->addDebugStep(1, '입력값수집', [
            'W0' => $inputVariables['W0'] ?? null,
            'H0' => $inputVariables['H0'] ?? null,
            'QTY' => $inputVariables['QTY'] ?? 1,
            'PC' => $inputVariables['PC'] ?? '',
            'GT' => $inputVariables['GT'] ?? 'wall',
            'MP' => $inputVariables['MP'] ?? 'single',
            'CT' => $inputVariables['CT'] ?? 'basic',
            'WS' => $inputVariables['WS'] ?? 50,
            'INSP' => $inputVariables['INSP'] ?? 50000,
            'finished_goods' => $finishedGoodsCode,
        ]);

        // Step 2: 완제품 조회 (마진값 결정을 위해 먼저 조회)
        $finishedGoods = $this->getItemDetails($finishedGoodsCode, $tenantId);

        if (! $finishedGoods) {
            $this->addDebugStep(2, '완제품선택', [
                'code' => $finishedGoodsCode,
                'error' => '완제품을 찾을 수 없습니다.',
            ]);

            return [
                'success' => false,
                'error' => __('error.finished_goods_not_found', ['code' => $finishedGoodsCode]),
                'debug_steps' => $this->debugSteps,
            ];
        }

        $this->addDebugStep(2, '완제품선택', [
            'code' => $finishedGoods['code'],
            'name' => $finishedGoods['name'],
            'item_category' => $finishedGoods['item_category'] ?? 'N/A',
            'has_bom' => $finishedGoods['has_bom'],
            'bom_count' => count($finishedGoods['bom'] ?? []),
        ]);

        // Step 3: 변수 계산 (제품 카테고리에 따라 마진값 결정)
        $W0 = $inputVariables['W0'] ?? 0;
        $H0 = $inputVariables['H0'] ?? 0;
        $productCategory = $finishedGoods['item_category'] ?? 'SCREEN';

        // 제품 카테고리에 따른 마진값 결정
        if (strtoupper($productCategory) === 'STEEL') {
            $marginW = 110; // 철재 마진
            $marginH = 350;
        } else {
            $marginW = 160; // 스크린 기본 마진
            $marginH = 350;
        }

        $W1 = $W0 + $marginW; // 마진 포함 폭
        $H1 = $H0 + $marginH; // 마진 포함 높이
        $M = ($W1 * $H1) / 1000000; // 면적 (㎡)

        // 제품 카테고리에 따른 중량(K) 계산
        if (strtoupper($productCategory) === 'STEEL') {
            $K = $M * 25; // 철재 중량
        } else {
            $K = $M * 2 + ($W0 / 1000) * 14.17; // 스크린 중량
        }

        $calculatedVariables = array_merge($inputVariables, [
            'W0' => $W0,
            'H0' => $H0,
            'W1' => $W1,
            'H1' => $H1,
            'W' => $W1, // 수식용 별칭
            'H' => $H1, // 수식용 별칭
            'M' => $M,
            'K' => $K,
            'PC' => $productCategory,
        ]);

        $this->addDebugStep(3, '변수계산', array_merge($calculatedVariables, [
            'margin_type' => strtoupper($productCategory) === 'STEEL' ? '철재(W+110)' : '스크린(W+140)',
        ]));

        // Step 4: BOM 전개
        $bomItems = $this->expandBomWithFormulas($finishedGoods, $calculatedVariables, $tenantId);

        $this->addDebugStep(4, 'BOM전개', [
            'total_items' => count($bomItems),
            'item_codes' => array_column($bomItems, 'item_code'),
        ]);

        // Step 5-7: 각 품목별 단가 계산
        $calculatedItems = [];
        foreach ($bomItems as $bomItem) {
            // Step 6: 수량 수식 평가
            $quantityFormula = $bomItem['quantity_formula'] ?? '1';
            $quantity = $this->evaluateQuantityFormula($quantityFormula, $calculatedVariables);

            $this->addDebugStep(6, '수량계산', [
                'item_code' => $bomItem['item_code'],
                'formula' => $quantityFormula,
                'result' => $quantity,
            ]);

            // Step 5 & 7: 단가 출처 및 계산
            $basePrice = $this->getItemPrice($bomItem['item_code']);
            $itemCategory = $bomItem['item_category'] ?? $this->getItemCategory($bomItem['item_code'], $tenantId);

            $priceResult = $this->calculateCategoryPrice(
                $itemCategory,
                $basePrice,
                $calculatedVariables,
                $tenantId
            );

            // 면적/중량 기반: final_price에 이미 면적/중량이 곱해져 있음
            // 수량 기반: quantity × unit_price
            $categoryGroup = $priceResult['category_group'];
            if ($categoryGroup === 'area_based' || $categoryGroup === 'weight_based') {
                // 면적/중량 기반: final_price = base_price × M or K (이미 계산됨)
                $totalPrice = $priceResult['final_price'];
                $displayQuantity = $priceResult['multiplier']; // 표시용 수량 = 면적 또는 중량
            } else {
                // 수량 기반: total = quantity × unit_price
                $totalPrice = $quantity * $priceResult['final_price'];
                $displayQuantity = $quantity;
            }

            $this->addDebugStep(7, '금액계산', [
                'item_code' => $bomItem['item_code'],
                'quantity' => $displayQuantity,
                'unit_price' => $basePrice,
                'total_price' => $totalPrice,
                'calculation_note' => $priceResult['calculation_note'],
            ]);

            $calculatedItems[] = [
                'item_code' => $bomItem['item_code'],
                'item_name' => $bomItem['item_name'],
                'item_category' => $itemCategory,
                'specification' => $bomItem['specification'] ?? null,
                'unit' => $bomItem['unit'] ?? 'EA',
                'quantity' => $displayQuantity,
                'quantity_formula' => $quantityFormula,
                'base_price' => $basePrice,
                'multiplier' => $priceResult['multiplier'],
                'unit_price' => $basePrice,
                'total_price' => $totalPrice,
                'calculation_note' => $priceResult['calculation_note'],
                'category_group' => $priceResult['category_group'],
            ];
        }

        // Step 8: 공정별 그룹화 및 items에 process_group 추가
        $groupedItems = $this->groupItemsByProcess($calculatedItems, $tenantId);

        // items에 process_group 필드 추가 (프론트엔드에서 분류에 사용)
        $calculatedItems = $this->addProcessGroupToItems($calculatedItems, $groupedItems);

        // Step 9: 소계 계산
        $subtotals = [];
        foreach ($groupedItems as $processType => $group) {
            if (! is_array($group) || ! isset($group['items'])) {
                continue;
            }
            $subtotals[$processType] = [
                'name' => $group['name'] ?? $processType,
                'count' => count($group['items']),
                'subtotal' => $group['subtotal'] ?? 0,
            ];
        }

        $this->addDebugStep(9, '소계계산', $subtotals);

        // Step 10: 최종 합계
        $grandTotal = array_sum(array_column($calculatedItems, 'total_price'));

        $this->addDebugStep(10, '최종합계', [
            'item_count' => count($calculatedItems),
            'grand_total' => $grandTotal,
            'formatted' => number_format($grandTotal).'원',
        ]);

        return [
            'success' => true,
            'finished_goods' => $finishedGoods,
            'variables' => $calculatedVariables,
            'items' => $calculatedItems,
            'grouped_items' => $groupedItems,
            'subtotals' => $subtotals,
            'grand_total' => $grandTotal,
            'debug_steps' => $this->debugSteps,
        ];
    }

    /**
     * 카테고리 기반 단가 계산
     *
     * CategoryGroup을 사용하여 면적/중량/수량 기반 단가를 계산합니다.
     * - 면적기반: 기본단가 × M (면적)
     * - 중량기반: 기본단가 × K (중량)
     * - 수량기반: 기본단가 × 1
     */
    public function calculateCategoryPrice(
        string $itemCategory,
        float $basePrice,
        array $variables,
        ?int $tenantId = null
    ): array {
        $tenantId = $tenantId ?? $this->tenantId();

        if (! $tenantId) {
            return [
                'final_price' => $basePrice,
                'calculation_note' => '테넌트 미설정',
                'multiplier' => 1.0,
                'category_group' => null,
            ];
        }

        // 카테고리 그룹 조회
        $categoryGroup = CategoryGroup::findByItemCategory($tenantId, $itemCategory);

        if (! $categoryGroup) {
            $this->addDebugStep(5, '단가출처', [
                'item_category' => $itemCategory,
                'base_price' => $basePrice,
                'category_group' => null,
                'note' => '카테고리 그룹 미등록 - 수량단가 적용',
            ]);

            return [
                'final_price' => $basePrice,
                'calculation_note' => '수량단가 (그룹 미등록)',
                'multiplier' => 1.0,
                'category_group' => null,
            ];
        }

        // CategoryGroup 모델의 calculatePrice 메서드 사용
        $result = $categoryGroup->calculatePrice($basePrice, $variables);
        $result['category_group'] = $categoryGroup->code;

        $this->addDebugStep(5, '단가출처', [
            'item_category' => $itemCategory,
            'base_price' => $basePrice,
            'category_group' => $categoryGroup->code,
            'multiplier_variable' => $categoryGroup->multiplier_variable,
            'multiplier_value' => $result['multiplier'],
            'final_price' => $result['final_price'],
        ]);

        return $result;
    }

    /**
     * 품목 카테고리 그룹화 (동적 카테고리 시스템)
     *
     * 품목을 item_category 필드 기준으로 그룹화합니다.
     * 카테고리 정보는 categories 테이블에서 code_group='item_category'로 조회합니다.
     *
     * 카테고리 구조:
     * - BODY: 본체
     * - BENDING 하위 카테고리:
     *   - BENDING_GUIDE: 절곡품 - 가이드레일
     *   - BENDING_CASE: 절곡품 - 케이스
     *   - BENDING_BOTTOM: 절곡품 - 하단마감재
     * - MOTOR_CTRL: 모터 & 제어기
     * - ACCESSORY: 부자재
     */
    public function groupItemsByProcess(array $items, ?int $tenantId = null): array
    {
        $tenantId = $tenantId ?? $this->tenantId();

        if (! $tenantId) {
            return ['ungrouped' => ['name' => '그룹없음', 'items' => $items, 'subtotal' => 0]];
        }

        // 품목 코드로 item_category 일괄 조회
        $itemCodes = array_unique(array_column($items, 'item_code'));

        if (empty($itemCodes)) {
            return ['ungrouped' => ['name' => '그룹없음', 'items' => $items, 'subtotal' => 0]];
        }

        // 품목별 item_category 조회
        $itemCategories = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->whereIn('code', $itemCodes)
            ->whereNull('deleted_at')
            ->pluck('item_category', 'code')
            ->toArray();

        // 카테고리 트리 조회 (item_category 코드 그룹)
        $categoryTree = $this->getItemCategoryTree($tenantId);

        // 카테고리 코드 → 정보 매핑 생성 (탭 구조에 맞게)
        $categoryMapping = $this->buildCategoryMapping($categoryTree);

        // 그룹별 분류를 위한 빈 구조 생성
        $grouped = [];
        foreach ($categoryMapping as $code => $info) {
            $grouped[$code] = ['name' => $info['name'], 'items' => [], 'subtotal' => 0];
        }
        // 기타 그룹 추가
        $grouped['OTHER'] = ['name' => '기타', 'items' => [], 'subtotal' => 0];

        foreach ($items as $item) {
            $categoryCode = $itemCategories[$item['item_code']] ?? 'OTHER';

            // 매핑에 없는 카테고리는 기타로 분류
            if (! isset($grouped[$categoryCode])) {
                $categoryCode = 'OTHER';
            }

            $grouped[$categoryCode]['items'][] = $item;
            $grouped[$categoryCode]['subtotal'] += $item['total_price'] ?? 0;
        }

        // 빈 그룹 제거
        $grouped = array_filter($grouped, fn ($g) => ! empty($g['items']));

        $this->addDebugStep(8, '카테고리그룹화', [
            'total_items' => count($items),
            'groups' => array_map(fn ($g) => [
                'name' => $g['name'],
                'count' => count($g['items']),
                'subtotal' => $g['subtotal'],
            ], $grouped),
        ]);

        return $grouped;
    }

    /**
     * item_category 카테고리 트리 조회
     *
     * categories 테이블에서 code_group='item_category'인 카테고리를 트리 구조로 조회
     */
    private function getItemCategoryTree(int $tenantId): array
    {
        $categories = DB::table('categories')
            ->where('tenant_id', $tenantId)
            ->where('code_group', 'item_category')
            ->where('is_active', 1)
            ->whereNull('deleted_at')
            ->orderBy('parent_id')
            ->orderBy('sort_order')
            ->orderBy('id')
            ->get(['id', 'parent_id', 'code', 'name'])
            ->toArray();

        // 트리 구조로 변환
        $categoryIds = array_column($categories, 'id');
        $rootIds = array_filter($categories, fn ($c) => $c->parent_id === null || ! in_array($c->parent_id, $categoryIds));

        $byParent = [];
        foreach ($categories as $c) {
            $parentKey = in_array($c->id, array_column($rootIds, 'id')) ? 0 : ($c->parent_id ?? 0);
            $byParent[$parentKey][] = $c;
        }

        $buildTree = function ($parentId) use (&$buildTree, &$byParent) {
            $nodes = $byParent[$parentId] ?? [];

            return array_map(function ($n) use ($buildTree) {
                return [
                    'id' => $n->id,
                    'code' => $n->code,
                    'name' => $n->name,
                    'children' => $buildTree($n->id),
                ];
            }, $nodes);
        };

        return $buildTree(0);
    }

    /**
     * 카테고리 트리를 탭 구조에 맞게 매핑 생성
     *
     * BENDING 카테고리의 경우 하위 카테고리를 개별 탭으로,
     * 나머지는 그대로 1depth 탭으로 매핑
     */
    private function buildCategoryMapping(array $categoryTree): array
    {
        $mapping = [];

        foreach ($categoryTree as $category) {
            if ($category['code'] === 'BENDING' && ! empty($category['children'])) {
                // BENDING: 하위 카테고리를 개별 탭으로
                foreach ($category['children'] as $subCategory) {
                    $mapping[$subCategory['code']] = [
                        'name' => '절곡품 - '.$subCategory['name'],
                        'parentCode' => 'BENDING',
                    ];
                }
            } else {
                // 나머지: 1depth 탭
                $mapping[$category['code']] = [
                    'name' => $category['name'],
                    'parentCode' => null,
                ];
            }
        }

        return $mapping;
    }

    /**
     * items 배열에 카테고리 정보 필드 추가
     *
     * groupedItems에서 각 아이템의 소속 그룹을 찾아 카테고리 관련 필드를 추가합니다.
     * 프론트엔드에서 탭별 분류에 사용됩니다.
     *
     * 추가되는 필드:
     * - process_group: 그룹명 (레거시 호환)
     * - process_group_key: 그룹키 (레거시 호환)
     * - category_code: 동적 카테고리 코드 (신규 시스템)
     */
    private function addProcessGroupToItems(array $items, array $groupedItems): array
    {
        // 각 그룹의 아이템 코드 → 그룹정보 매핑 생성
        $itemCodeToGroup = [];
        foreach ($groupedItems as $groupKey => $group) {
            if (! isset($group['items']) || ! is_array($group['items'])) {
                continue;
            }
            foreach ($group['items'] as $groupItem) {
                $itemCodeToGroup[$groupItem['item_code']] = [
                    'key' => $groupKey,
                    'name' => $group['name'] ?? $groupKey,
                ];
            }
        }

        // items 배열에 카테고리 정보 추가
        return array_map(function ($item) use ($itemCodeToGroup) {
            $groupInfo = $itemCodeToGroup[$item['item_code']] ?? ['key' => 'OTHER', 'name' => '기타'];
            $item['process_group'] = $groupInfo['name'];
            $item['process_group_key'] = $groupInfo['key'];  // 레거시 호환
            $item['category_code'] = $groupInfo['key'];       // 신규 동적 카테고리 시스템

            return $item;
        }, $items);
    }

    // =========================================================================
    // 품목 조회 메서드
    // =========================================================================

    /**
     * 품목 단가 조회
     *
     * @param  string  $itemCode  품목 코드
     * @param  int|null  $tenantIdOverride  테넌트 ID (외부 호출 시 사용)
     */
    public function getItemPrice(string $itemCode, ?int $tenantIdOverride = null): float
    {
        $tenantId = $tenantIdOverride ?? $this->tenantId();

        if (! $tenantId) {
            $this->errors[] = __('error.tenant_id_required');

            return 0;
        }

        // 1. Price 모델에서 조회
        $price = Price::getSalesPriceByItemCode($tenantId, $itemCode);

        if ($price > 0) {
            return $price;
        }

        // 2. Fallback: items.attributes.salesPrice에서 조회
        $item = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->where('code', $itemCode)
            ->whereNull('deleted_at')
            ->first();

        if ($item && ! empty($item->attributes)) {
            $attributes = json_decode($item->attributes, true);

            return (float) ($attributes['salesPrice'] ?? 0);
        }

        return 0;
    }

    /**
     * 품목 상세 정보 조회 (BOM 트리 포함)
     */
    public function getItemDetails(string $itemCode, ?int $tenantId = null): ?array
    {
        $tenantId = $tenantId ?? $this->tenantId();

        if (! $tenantId) {
            return null;
        }

        $item = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->where('code', $itemCode)
            ->whereNull('deleted_at')
            ->first();

        if (! $item) {
            return null;
        }

        return [
            'id' => $item->id,
            'code' => $item->code,
            'name' => $item->name,
            'item_type' => $item->item_type,
            'item_type_label' => $this->getItemTypeLabel($item->item_type),
            'item_category' => $item->item_category,
            'process_type' => $item->process_type,
            'unit' => $item->unit,
            'description' => $item->description,
            'attributes' => json_decode($item->attributes ?? '{}', true),
            'bom' => $this->getBomTree($tenantId, $item->id, json_decode($item->bom ?? '[]', true)),
            'has_bom' => ! empty($item->bom) && $item->bom !== '[]',
        ];
    }

    /**
     * BOM 트리 재귀적으로 조회
     */
    private function getBomTree(int $tenantId, int $parentItemId, array $bomData, int $depth = 0): array
    {
        // 무한 루프 방지
        if ($depth > 10 || empty($bomData)) {
            return [];
        }

        $children = [];
        $childIds = array_column($bomData, 'child_item_id');

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

        // 자식 품목들 일괄 조회
        $childItems = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->whereIn('id', $childIds)
            ->whereNull('deleted_at')
            ->get()
            ->keyBy('id');

        foreach ($bomData as $bomItem) {
            $childItemId = $bomItem['child_item_id'] ?? null;
            $quantity = $bomItem['quantity'] ?? 1;

            if (! $childItemId) {
                continue;
            }

            $childItem = $childItems->get($childItemId);

            if (! $childItem) {
                continue;
            }

            $childBomData = json_decode($childItem->bom ?? '[]', true);

            $children[] = [
                'id' => $childItem->id,
                'code' => $childItem->code,
                'name' => $childItem->name,
                'item_type' => $childItem->item_type,
                'item_type_label' => $this->getItemTypeLabel($childItem->item_type),
                'unit' => $childItem->unit,
                'quantity' => (float) $quantity,
                'description' => $childItem->description,
                'has_bom' => ! empty($childBomData),
                'children' => $this->getBomTree($tenantId, $childItem->id, $childBomData, $depth + 1),
            ];
        }

        return $children;
    }

    /**
     * 품목 유형 라벨
     */
    public function getItemTypeLabel(string $itemType): string
    {
        return match ($itemType) {
            'FG' => '완제품',
            'PT' => '부품',
            'SM' => '부자재',
            'RM' => '원자재',
            'CS' => '소모품',
            default => $itemType,
        };
    }

    /**
     * 품목의 item_category 조회
     */
    private function getItemCategory(string $itemCode, int $tenantId): string
    {
        $category = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->where('code', $itemCode)
            ->whereNull('deleted_at')
            ->value('item_category');

        return $category ?? '기타';
    }

    /**
     * 품목마스터에서 규격(specification)과 단위(unit) 조회
     *
     * @param  string  $itemCode  품목 코드
     * @return array ['specification' => string|null, 'unit' => string]
     */
    private function getItemSpecAndUnit(string $itemCode): array
    {
        $tenantId = $this->tenantId();

        if (! $tenantId) {
            return ['specification' => null, 'unit' => 'EA'];
        }

        // Items 테이블에서 기본 정보 조회
        $item = Item::where('tenant_id', $tenantId)
            ->where('code', $itemCode)
            ->whereNull('deleted_at')
            ->with('details')
            ->first();

        if (! $item) {
            return ['specification' => null, 'unit' => 'EA'];
        }

        // specification은 ItemDetail에서, unit은 Item에서 가져옴
        $specification = $item->details?->specification ?? null;
        $unit = $item->unit ?? 'EA';

        return [
            'specification' => $specification,
            'unit' => $unit,
        ];
    }

    /**
     * BOM 전개 (수량 수식 포함)
     */
    private function expandBomWithFormulas(array $finishedGoods, array $variables, int $tenantId): array
    {
        $bomItems = [];

        // items 테이블에서 완제품의 bom 필드 조회
        $item = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->where('code', $finishedGoods['code'])
            ->whereNull('deleted_at')
            ->first();

        if (! $item || empty($item->bom)) {
            return $bomItems;
        }

        $bomData = json_decode($item->bom, true);

        if (! is_array($bomData)) {
            return $bomItems;
        }

        // BOM 데이터 형식: child_item_id 기반 또는 코드 기반 (Design 형식: childItemCode)
        foreach ($bomData as $bomEntry) {
            $childItemId = $bomEntry['child_item_id'] ?? null;
            $childItemCode = $bomEntry['item_code'] ?? $bomEntry['childItemCode'] ?? null;
            $quantityFormula = $bomEntry['quantityFormula'] ?? $bomEntry['quantity'] ?? '1';

            if ($childItemId) {
                // ID 기반 조회 (item_details 조인하여 specification 포함)
                $childItem = DB::table('items')
                    ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id')
                    ->where('items.tenant_id', $tenantId)
                    ->where('items.id', $childItemId)
                    ->whereNull('items.deleted_at')
                    ->select('items.*', 'item_details.specification')
                    ->first();
            } elseif ($childItemCode) {
                // 코드 기반 조회 (item_details 조인하여 specification 포함)
                $childItem = DB::table('items')
                    ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id')
                    ->where('items.tenant_id', $tenantId)
                    ->where('items.code', $childItemCode)
                    ->whereNull('items.deleted_at')
                    ->select('items.*', 'item_details.specification')
                    ->first();
            } else {
                continue;
            }

            if ($childItem) {
                $bomItems[] = [
                    'item_code' => $childItem->code,
                    'item_name' => $childItem->name,
                    'item_category' => $childItem->item_category,
                    'process_type' => $childItem->process_type,
                    'quantity_formula' => (string) $quantityFormula,
                    'unit' => $childItem->unit,
                    'specification' => $childItem->specification,
                ];

                // 재귀적 BOM 전개 (반제품인 경우)
                if (in_array($childItem->item_type, ['SF', 'PT'])) {
                    $childDetails = $this->getItemDetails($childItem->code, $tenantId);
                    if ($childDetails && $childDetails['has_bom']) {
                        $childBomItems = $this->expandBomWithFormulas($childDetails, $variables, $tenantId);
                        $bomItems = array_merge($bomItems, $childBomItems);
                    }
                }
            }
        }

        return $bomItems;
    }

    /**
     * 수량 수식 평가
     */
    private function evaluateQuantityFormula(string $formula, array $variables): float
    {
        // 빈 수식은 기본값 1 반환
        if (empty(trim($formula))) {
            return 1.0;
        }

        // 숫자만 있으면 바로 반환
        if (is_numeric($formula)) {
            return (float) $formula;
        }

        // 변수 치환
        $expression = $formula;
        foreach ($variables as $var => $value) {
            $expression = preg_replace('/\b'.preg_quote($var, '/').'\b/', (string) $value, $expression);
        }

        // 계산
        try {
            return $this->calculateExpression($expression);
        } catch (\Throwable $e) {
            $this->errors[] = __('error.quantity_formula_error', ['formula' => $formula, 'error' => $e->getMessage()]);

            return 1;
        }
    }

    // =========================================================================
    // BOM 원자재(Leaf Node) 조회 - 소요자재내역용
    // =========================================================================

    /**
     * BOM 트리에서 원자재(leaf nodes)만 추출
     *
     * 완제품의 BOM을 재귀적으로 탐색하여 실제 구매해야 할 원자재 목록을 반환합니다.
     * - Leaf node: BOM이 없는 품목 또는 item_type이 RM(원자재), SM(부자재), CS(소모품)인 품목
     * - 수량은 상위 구조의 수량을 누적하여 계산
     *
     * @param  string  $finishedGoodsCode  완제품 코드
     * @param  float  $orderQuantity  주문 수량 (QTY)
     * @param  array  $variables  변수 배열 (W0, H0 등)
     * @param  int|null  $tenantId  테넌트 ID
     * @return array 원자재 목록 (leaf nodes)
     */
    public function getBomLeafMaterials(
        string $finishedGoodsCode,
        float $orderQuantity,
        array $variables,
        ?int $tenantId = null
    ): array {
        $tenantId = $tenantId ?? $this->tenantId();

        if (! $tenantId) {
            return [];
        }

        // 완제품 조회
        $finishedGoods = DB::table('items')
            ->where('tenant_id', $tenantId)
            ->where('code', $finishedGoodsCode)
            ->whereNull('deleted_at')
            ->first();

        if (! $finishedGoods || empty($finishedGoods->bom)) {
            return [];
        }

        $bomData = json_decode($finishedGoods->bom, true);

        if (! is_array($bomData) || empty($bomData)) {
            return [];
        }

        // 재귀적으로 leaf nodes 수집
        $leafMaterials = [];
        $this->collectLeafMaterials(
            $bomData,
            $tenantId,
            $orderQuantity,
            $variables,
            $leafMaterials
        );

        // 동일 품목 코드 병합 (수량 합산)
        return $this->mergeLeafMaterials($leafMaterials, $tenantId);
    }

    /**
     * 재귀적으로 BOM 트리를 탐색하여 leaf materials 수집
     *
     * @param  array  $bomData  BOM 데이터 배열
     * @param  int  $tenantId  테넌트 ID
     * @param  float  $parentQuantity  상위 품목 수량 (누적)
     * @param  array  $variables  변수 배열
     * @param  array  &$leafMaterials  결과 배열 (참조)
     * @param  int  $depth  재귀 깊이 (무한루프 방지)
     */
    private function collectLeafMaterials(
        array $bomData,
        int $tenantId,
        float $parentQuantity,
        array $variables,
        array &$leafMaterials,
        int $depth = 0
    ): void {
        // 무한 루프 방지
        if ($depth > 10) {
            return;
        }

        foreach ($bomData as $bomEntry) {
            $childItemId = $bomEntry['child_item_id'] ?? null;
            $childItemCode = $bomEntry['item_code'] ?? $bomEntry['childItemCode'] ?? null;
            $quantityFormula = $bomEntry['quantityFormula'] ?? $bomEntry['quantity'] ?? '1';

            // 수량 계산
            $itemQuantity = $this->evaluateQuantityFormula((string) $quantityFormula, $variables);
            $totalQuantity = $parentQuantity * $itemQuantity;

            // 자식 품목 조회
            $childItem = null;
            if ($childItemId) {
                $childItem = DB::table('items')
                    ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id')
                    ->where('items.tenant_id', $tenantId)
                    ->where('items.id', $childItemId)
                    ->whereNull('items.deleted_at')
                    ->select('items.*', 'item_details.specification')
                    ->first();
            } elseif ($childItemCode) {
                $childItem = DB::table('items')
                    ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id')
                    ->where('items.tenant_id', $tenantId)
                    ->where('items.code', $childItemCode)
                    ->whereNull('items.deleted_at')
                    ->select('items.*', 'item_details.specification')
                    ->first();
            }

            if (! $childItem) {
                continue;
            }

            // 자식의 BOM 확인
            $childBomData = json_decode($childItem->bom ?? '[]', true);
            $hasChildBom = ! empty($childBomData) && is_array($childBomData);

            // Leaf node 판단 조건:
            // 1. BOM이 없는 품목
            // 2. 또는 item_type이 원자재(RM), 부자재(SM), 소모품(CS)인 품목
            $isLeafType = in_array($childItem->item_type, ['RM', 'SM', 'CS']);

            if (! $hasChildBom || $isLeafType) {
                // Leaf node - 결과에 추가
                $leafMaterials[] = [
                    'item_id' => $childItem->id,
                    'item_code' => $childItem->code,
                    'item_name' => $childItem->name,
                    'item_type' => $childItem->item_type,
                    'item_category' => $childItem->item_category,
                    'specification' => $childItem->specification,
                    'unit' => $childItem->unit ?? 'EA',
                    'quantity' => $totalQuantity,
                    'process_type' => $childItem->process_type,
                ];
            } else {
                // 중간 노드 (부품/반제품) - 재귀 탐색
                $this->collectLeafMaterials(
                    $childBomData,
                    $tenantId,
                    $totalQuantity,
                    $variables,
                    $leafMaterials,
                    $depth + 1
                );
            }
        }
    }

    /**
     * 동일 품목 코드의 leaf materials 병합 (수량 합산)
     *
     * @param  array  $leafMaterials  원자재 목록
     * @param  int  $tenantId  테넌트 ID
     * @return array 병합된 원자재 목록
     */
    private function mergeLeafMaterials(array $leafMaterials, int $tenantId): array
    {
        $merged = [];

        foreach ($leafMaterials as $material) {
            $code = $material['item_code'];

            if (isset($merged[$code])) {
                // 동일 품목 - 수량 합산
                $merged[$code]['quantity'] += $material['quantity'];
            } else {
                // 새 품목 추가
                $merged[$code] = $material;
            }
        }

        // 단가 조회 및 금액 계산
        $result = [];
        foreach ($merged as $material) {
            $unitPrice = $this->getItemPrice($material['item_code']);
            $totalPrice = $material['quantity'] * $unitPrice;

            $result[] = array_merge($material, [
                'unit_price' => $unitPrice,
                'total_price' => round($totalPrice, 2),
            ]);
        }

        return array_values($result);
    }

    // =========================================================================
    // 경동기업 전용 계산 (tenant_id = 287)
    // =========================================================================

    /**
     * 테넌트 전용 핸들러 기반 BOM 계산
     *
     * FormulaHandlerFactory가 발견한 TenantFormulaHandler 구현체를 사용.
     * 핸들러가 제공하는 calculateMotorCapacity, calculateBracketSize,
     * calculateDynamicItems 메서드로 BOM을 계산한다.
     *
     * @param  TenantFormulaHandler  $handler  테넌트 전용 핸들러
     * @param  string  $finishedGoodsCode  완제품 코드
     * @param  array  $inputVariables  입력 변수 (W0, H0, QTY 등)
     * @param  int  $tenantId  테넌트 ID
     * @return array 계산 결과
     */
    private function calculateTenantBom(
        TenantFormulaHandler $handler,
        string $finishedGoodsCode,
        array $inputVariables,
        int $tenantId
    ): array {
        $handlerClass = get_class($handler);
        $this->addDebugStep(0, '테넌트전용계산', [
            'tenant_id' => $tenantId,
            'handler' => class_basename($handlerClass),
            'handler_class' => $handlerClass,
            'finished_goods' => $finishedGoodsCode,
        ]);

        // Step 1: 입력값 수집
        $W0 = (float) ($inputVariables['W0'] ?? 0);
        $H0 = (float) ($inputVariables['H0'] ?? 0);
        $QTY = (int) ($inputVariables['QTY'] ?? 1);
        $bracketInch = $inputVariables['bracket_inch'] ?? '5';
        // product_type: 프론트 입력값 우선, 없으면 FG item_category에서 자동 추론
        $finishedGoods = $this->getItemDetails($finishedGoodsCode, $tenantId);
        $itemCategory = $finishedGoods['item_category'] ?? null;
        $productType = $inputVariables['product_type'] ?? match (true) {
            $itemCategory === '철재' => 'steel',
            str_contains($itemCategory ?? '', '슬랫') => 'slat',
            default => 'screen',
        };

        $this->addDebugStep(1, '입력값수집', [
            'formulas' => [
                ['var' => 'W0', 'desc' => '개구부 폭', 'value' => $W0, 'unit' => 'mm'],
                ['var' => 'H0', 'desc' => '개구부 높이', 'value' => $H0, 'unit' => 'mm'],
                ['var' => 'QTY', 'desc' => '수량', 'value' => $QTY, 'unit' => 'EA'],
                ['var' => 'bracket_inch', 'desc' => '브라켓 인치', 'value' => $bracketInch, 'unit' => '인치'],
                ['var' => 'product_type', 'desc' => '제품 타입', 'value' => $productType, 'unit' => ''],
                ['var' => 'product_model', 'desc' => '모델코드', 'value' => $inputVariables['product_model'] ?? 'KSS01', 'unit' => ''],
                ['var' => 'finishing_type', 'desc' => '마감타입', 'value' => $inputVariables['finishing_type'] ?? 'SUS', 'unit' => ''],
                ['var' => 'installation_type', 'desc' => '설치타입', 'value' => $inputVariables['installation_type'] ?? '벽면형', 'unit' => ''],
            ],
        ]);

        // Step 2: 완제품 정보 활용 (Step 1에서 이미 조회됨)
        if ($finishedGoods) {
            $this->addDebugStep(2, '완제품선택', [
                'code' => $finishedGoods['code'],
                'name' => $finishedGoods['name'],
                'item_category' => $finishedGoods['item_category'] ?? 'N/A',
            ]);
        } else {
            // 테넌트 전용: 완제품 미등록 상태에서도 견적 계산 진행
            $finishedGoods = [
                'code' => $finishedGoodsCode,
                'name' => $finishedGoodsCode,
                'item_category' => 'estimate',
            ];
            $this->addDebugStep(2, '완제품선택', [
                'code' => $finishedGoodsCode,
                'note' => '테넌트 전용 계산 - 완제품 미등록 상태로 진행',
            ]);
        }

        // 핸들러는 파라미터로 이미 전달됨 (FormulaHandlerFactory가 생성)

        // Step 3: 경동 전용 변수 계산 (제품타입별 면적/중량 공식)
        // 스크린: +160, 철재: +110 (5130 레거시 기준)
        $W1 = $W0 + ($productType === 'steel' ? 110 : 160);
        $H1 = $H0 + 350;

        if ($productType === 'slat') {
            // 슬랫: W0 × (H0 + 50) / 1M
            $area = ($W0 * ($H0 + 50)) / 1000000;
            $weight = $area * 25;
            $areaFormula = '(W0 × (H0 + 50)) / 1,000,000';
            $areaCalc = "({$W0} × ({$H0} + 50)) / 1,000,000";
            $weightFormula = 'AREA × 25';
            $weightCalc = "{$area} × 25";
        } elseif ($productType === 'steel') {
            // 철재: W1 × H1 / 1M, 중량 = 면적 × 25 (레거시 Slat_updateCol12and13 동일)
            $area = ($W1 * $H1) / 1000000;
            $weight = $area * 25;
            $areaFormula = '(W1 × H1) / 1,000,000';
            $areaCalc = "({$W1} × {$H1}) / 1,000,000";
            $weightFormula = 'AREA × 25';
            $weightCalc = "{$area} × 25";
        } else {
            // 스크린: W1 × (H1 + 550) / 1M
            $area = ($W1 * ($H1 + 550)) / 1000000;
            $weight = $area * 2 + ($W0 / 1000) * 14.17;
            $areaFormula = '(W1 × (H1 + 550)) / 1,000,000';
            $areaCalc = "({$W1} × ({$H1} + 550)) / 1,000,000";
            $weightFormula = 'AREA × 2 + (W0 / 1000) × 14.17';
            $weightCalc = "{$area} × 2 + ({$W0} / 1000) × 14.17";
        }

        // 모터 용량 결정 (입력값 우선, 없으면 자동계산)
        $motorCapacity = $inputVariables['MOTOR_CAPACITY']
            ?? $inputVariables['motor_capacity']
            ?? $handler->calculateMotorCapacity($productType, $weight, $bracketInch);

        // 브라켓 크기 결정 (입력값 우선, 없으면 자동계산)
        $bracketSize = $inputVariables['BRACKET_SIZE']
            ?? $inputVariables['bracket_size']
            ?? $handler->calculateBracketSize($weight, $bracketInch);

        // 핸들러가 필요한 키 보장
        // 우선순위: 입력값 > FG 코드 파싱 > 기본값
        $fgParsed = $this->parseFgCode($finishedGoodsCode);
        $productModel = $inputVariables['product_model'] ?? $fgParsed['model'] ?? 'KSS01';
        $finishingType = $inputVariables['finishing_type'] ?? $fgParsed['finishing'] ?? 'SUS';

        // 가이드레일 설치타입: 입력값 > FG파싱 > GT매핑 > 기본값
        $installationType = $inputVariables['installation_type'] ?? $fgParsed['installation'] ?? match ($inputVariables['GT'] ?? 'wall') {
            'floor' => '측면형',
            'mixed' => '혼합형',
            default => '벽면형',
        };

        // 모터 전압: 프론트 MP(single/three) → motor_voltage(220V/380V) 매핑
        $motorVoltage = $inputVariables['motor_voltage'] ?? match ($inputVariables['MP'] ?? 'single') {
            'three' => '380V',
            default => '220V',
        };

        $calculatedVariables = array_merge($inputVariables, [
            'W0' => $W0,
            'H0' => $H0,
            'QTY' => $QTY,
            'W1' => $W1,
            'H1' => $H1,
            'AREA' => round($area, 4),
            'WEIGHT' => round($weight, 2),
            'MOTOR_CAPACITY' => $motorCapacity,
            'BRACKET_SIZE' => $bracketSize,
            'bracket_inch' => $bracketInch,
            'product_type' => $productType,
            'product_model' => $productModel,
            'finishing_type' => $finishingType,
            'installation_type' => $installationType,
            'motor_voltage' => $motorVoltage,
        ]);

        $this->addDebugStep(3, '변수계산', [
            'formulas' => [
                [
                    'var' => 'W1',
                    'desc' => '제작 폭',
                    'formula' => $productType === 'steel' ? 'W0 + 110' : 'W0 + 160',
                    'calculation' => $productType === 'steel' ? "{$W0} + 110" : "{$W0} + 160",
                    'result' => $W1,
                    'unit' => 'mm',
                ],
                [
                    'var' => 'H1',
                    'desc' => '제작 높이',
                    'formula' => 'H0 + 350',
                    'calculation' => "{$H0} + 350",
                    'result' => $H1,
                    'unit' => 'mm',
                ],
                [
                    'var' => 'AREA',
                    'desc' => '면적',
                    'formula' => $areaFormula,
                    'calculation' => $areaCalc,
                    'result' => round($area, 4),
                    'unit' => '㎡',
                ],
                [
                    'var' => 'WEIGHT',
                    'desc' => '중량',
                    'formula' => $weightFormula,
                    'calculation' => $weightCalc,
                    'result' => round($weight, 2),
                    'unit' => 'kg',
                ],
                [
                    'var' => 'MOTOR_CAPACITY',
                    'desc' => '모터 용량',
                    'formula' => '중량/브라켓 기준표 조회',
                    'calculation' => "WEIGHT({$weight}) + INCH({$bracketInch}) → 조회",
                    'result' => $motorCapacity,
                    'unit' => '',
                ],
                [
                    'var' => 'BRACKET_SIZE',
                    'desc' => '브라켓 크기',
                    'formula' => '중량 기준표 조회',
                    'calculation' => "WEIGHT({$weight}) → 조회",
                    'result' => $bracketSize,
                    'unit' => '인치',
                ],
            ],
        ]);

        // Step 4-7: 동적 항목 계산 (TenantFormulaHandler 사용)
        $dynamicItems = $handler->calculateDynamicItems($calculatedVariables);

        $this->addDebugStep(4, 'BOM전개', [
            'total_items' => count($dynamicItems),
            'item_categories' => array_unique(array_column($dynamicItems, 'category')),
            'items' => array_map(fn ($item) => [
                'name' => $item['item_name'],
                'category' => $item['category'],
            ], $dynamicItems),
        ]);

        // Step 5-7: 단가 계산 (각 항목별)
        $calculatedItems = [];
        $itemFormulas = [];

        foreach ($dynamicItems as $item) {
            $itemFormulas[] = [
                'item' => $item['item_name'],
                'qty_formula' => $item['quantity_formula'] ?? '고정값',
                'qty_result' => $item['quantity'],
                'unit_price' => $item['unit_price'],
                'price_formula' => '수량 × 단가',
                'price_calc' => "{$item['quantity']} × {$item['unit_price']}",
                'total' => $item['total_price'],
            ];

            $calculatedItems[] = [
                'item_id' => $item['item_id'] ?? null,
                'item_code' => $item['item_code'] ?? '',
                'item_name' => $item['item_name'],
                'item_category' => $item['category'],
                'specification' => $item['specification'] ?? '',
                'unit' => $item['unit'],
                'quantity' => $item['quantity'],
                'quantity_formula' => $item['quantity_formula'] ?? '',
                'unit_price' => $item['unit_price'],
                'total_price' => $item['total_price'],
                'category_group' => $item['category'],
                'process_group' => $item['category'],
                'calculation_note' => '테넌트 전용 계산',
            ];
        }

        $this->addDebugStep(6, '수량계산', [
            'formulas' => $itemFormulas,
        ]);

        $this->addDebugStep(7, '금액계산', [
            'formulas' => $itemFormulas,
        ]);

        // Step 8: 카테고리별 그룹화
        $groupedItems = [];
        foreach ($calculatedItems as $item) {
            $category = $item['category_group'];
            if (! isset($groupedItems[$category])) {
                $groupedItems[$category] = [
                    'name' => $this->getTenantCategoryName($category),
                    'items' => [],
                    'subtotal' => 0,
                ];
            }
            $groupedItems[$category]['items'][] = $item;
            $groupedItems[$category]['subtotal'] += $item['total_price'];
        }

        $this->addDebugStep(8, '카테고리그룹화', [
            'groups' => array_map(fn ($g) => [
                'name' => $g['name'],
                'count' => count($g['items']),
                'subtotal' => $g['subtotal'],
            ], $groupedItems),
        ]);

        // Step 9: 소계 계산
        $subtotals = [];
        $subtotalFormulas = [];
        foreach ($groupedItems as $category => $group) {
            $subtotals[$category] = [
                'name' => $group['name'],
                'count' => count($group['items']),
                'subtotal' => $group['subtotal'],
            ];
            $subtotalFormulas[] = [
                'category' => $group['name'],
                'formula' => implode(' + ', array_map(fn ($i) => $i['item_name'], $group['items'])),
                'result' => $group['subtotal'],
            ];
        }

        $this->addDebugStep(9, '소계계산', [
            'formulas' => $subtotalFormulas,
            'subtotals' => $subtotals,
        ]);

        // Step 10: 최종 합계
        $grandTotal = array_sum(array_column($calculatedItems, 'total_price'));
        $subtotalValues = array_column($subtotals, 'subtotal');

        $this->addDebugStep(10, '최종합계', [
            'formula' => implode(' + ', array_column($subtotals, 'name')),
            'calculation' => implode(' + ', array_map(fn ($v) => number_format($v), $subtotalValues)),
            'result' => $grandTotal,
            'formatted' => number_format($grandTotal).'원',
        ]);

        return [
            'success' => true,
            'finished_goods' => $finishedGoods,
            'variables' => $calculatedVariables,
            'items' => $calculatedItems,
            'grouped_items' => $groupedItems,
            'subtotals' => $subtotals,
            'grand_total' => $grandTotal,
            'debug_steps' => $this->debugSteps,
            'calculation_type' => 'tenant_handler',
        ];
    }

    /**
     * 테넌트 핸들러용 카테고리명 반환
     */
    private function getTenantCategoryName(string $category): string
    {
        return match ($category) {
            'material' => '주자재',
            'motor' => '모터',
            'controller' => '제어기',
            'steel' => '절곡품',
            'parts' => '부자재',
            default => $category,
        };
    }
}
