package com.erp.service;

import com.erp.common.BaseService;
import com.erp.util.DecimalUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductionService extends BaseService {

    private final CommonService commonService;
    private final JdbcTemplate jdbcTemplate;

    // ─── 수주 데이터 조회 (품목별 그룹핑) ───

    public List<Map<String, Object>> getOrderSummary(Map<String, Object> params) {
        String companyCode = (String) params.get("company_code");
        String itemCode = (String) params.get("item_code");
        String itemName = (String) params.get("item_name");
        boolean excludePlanned = "true".equals(String.valueOf(params.getOrDefault("exclude_planned", "false")));

        List<String> conditions = new ArrayList<>();
        List<Object> args = new ArrayList<>();
        conditions.add("so.company_code = ?");
        args.add(companyCode);

        if (itemCode != null && !itemCode.isEmpty()) {
            conditions.add("so.part_code ILIKE ?");
            args.add("%" + itemCode + "%");
        }
        if (itemName != null && !itemName.isEmpty()) {
            conditions.add("so.part_name ILIKE ?");
            args.add("%" + itemName + "%");
        }

        String whereClause = String.join(" AND ", conditions);

        String query = "WITH order_summary AS (" +
            "  SELECT so.part_code AS item_code, COALESCE(so.part_name, so.part_code) AS item_name," +
            "    SUM(COALESCE(so.order_qty::numeric, 0)) AS total_order_qty," +
            "    SUM(COALESCE(so.ship_qty::numeric, 0)) AS total_ship_qty," +
            "    SUM(COALESCE(so.balance_qty::numeric, 0)) AS total_balance_qty," +
            "    COUNT(*) AS order_count, MIN(so.due_date) AS earliest_due_date" +
            "  FROM sales_order_mng so WHERE " + whereClause +
            "  GROUP BY so.part_code, so.part_name" +
            ")," +
            "stock_info AS (" +
            "  SELECT item_code," +
            "    SUM(COALESCE(current_qty::numeric, 0)) AS current_stock," +
            "    MAX(COALESCE(safety_qty::numeric, 0)) AS safety_stock" +
            "  FROM inventory_stock WHERE company_code = ? GROUP BY item_code" +
            ")," +
            "plan_info AS (" +
            "  SELECT item_code," +
            "    SUM(CASE WHEN status = 'planned' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS existing_plan_qty," +
            "    SUM(CASE WHEN status = 'in_progress' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS in_progress_qty" +
            "  FROM production_plan_mng WHERE company_code = ?" +
            "    AND COALESCE(product_type, '완제품') = '완제품'" +
            "    AND status NOT IN ('completed', 'cancelled')" +
            "  GROUP BY item_code" +
            ") " +
            "SELECT os.item_code, os.item_name, os.total_order_qty, os.total_ship_qty, os.total_balance_qty," +
            "  os.order_count, os.earliest_due_date," +
            "  COALESCE(si.current_stock, 0) AS current_stock," +
            "  COALESCE(si.safety_stock, 0) AS safety_stock," +
            "  COALESCE(pi.existing_plan_qty, 0) AS existing_plan_qty," +
            "  COALESCE(pi.in_progress_qty, 0) AS in_progress_qty," +
            "  GREATEST(os.total_balance_qty + COALESCE(si.safety_stock, 0) - COALESCE(si.current_stock, 0)" +
            "    - COALESCE(pi.existing_plan_qty, 0) - COALESCE(pi.in_progress_qty, 0), 0) AS required_plan_qty" +
            " FROM order_summary os" +
            " LEFT JOIN stock_info si ON os.item_code = si.item_code" +
            " LEFT JOIN plan_info pi ON os.item_code = pi.item_code" +
            (excludePlanned ? " WHERE COALESCE(pi.existing_plan_qty, 0) = 0" : "") +
            " ORDER BY os.item_code";

        args.add(companyCode);
        args.add(companyCode);

        List<Map<String, Object>> summaryRows = jdbcTemplate.queryForList(query, args.toArray());

        // 상세 수주 데이터 조회
        List<String> detailConditions = new ArrayList<>();
        List<Object> detailArgs = new ArrayList<>();
        detailConditions.add("company_code = ?");
        detailArgs.add(companyCode);
        if (itemCode != null && !itemCode.isEmpty()) {
            detailConditions.add("part_code ILIKE ?");
            detailArgs.add("%" + itemCode + "%");
        }
        if (itemName != null && !itemName.isEmpty()) {
            detailConditions.add("part_name ILIKE ?");
            detailArgs.add("%" + itemName + "%");
        }
        String detailWhere = String.join(" AND ", detailConditions);
        String detailQuery = "SELECT id, order_no, part_code, part_name," +
            "  COALESCE(order_qty::numeric, 0) AS order_qty," +
            "  COALESCE(ship_qty::numeric, 0) AS ship_qty," +
            "  COALESCE(balance_qty::numeric, 0) AS balance_qty," +
            "  due_date, status, partner_id, manager_name" +
            " FROM sales_order_mng WHERE " + detailWhere +
            " ORDER BY part_code, due_date";

        List<Map<String, Object>> detailRows = jdbcTemplate.queryForList(detailQuery, detailArgs.toArray());

        Map<String, List<Map<String, Object>>> ordersByItem = new LinkedHashMap<>();
        for (Map<String, Object> row : detailRows) {
            String key = row.get("part_code") != null ? String.valueOf(row.get("part_code")) : "__null__";
            ordersByItem.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }

        List<Map<String, Object>> result = new ArrayList<>();
        for (Map<String, Object> group : summaryRows) {
            Map<String, Object> item = new LinkedHashMap<>(group);
            String key = group.get("item_code") != null ? String.valueOf(group.get("item_code")) : "__null__";
            item.put("orders", ordersByItem.getOrDefault(key, new ArrayList<>()));
            result.add(item);
        }

        log.info("수주 데이터 조회: companyCode={}, groupCount={}", companyCode, result.size());
        return result;
    }

    // ─── 안전재고 부족분 조회 ───

    public List<Map<String, Object>> getStockShortage(String companyCode) {
        String query = "SELECT ist.item_code, ii.item_name," +
            "  COALESCE(ist.current_qty::numeric, 0) AS current_qty," +
            "  COALESCE(ist.safety_qty::numeric, 0) AS safety_qty," +
            "  (COALESCE(ist.current_qty::numeric, 0) - COALESCE(ist.safety_qty::numeric, 0)) AS shortage_qty," +
            "  GREATEST(COALESCE(ist.safety_qty::numeric, 0) * 2 - COALESCE(ist.current_qty::numeric, 0), 0) AS recommended_qty," +
            "  ist.last_in_date" +
            " FROM inventory_stock ist" +
            " LEFT JOIN item_info ii ON ist.item_code = ii.id AND ist.company_code = ii.company_code" +
            " WHERE ist.company_code = ?" +
            "   AND COALESCE(ist.current_qty::numeric, 0) < COALESCE(ist.safety_qty::numeric, 0)" +
            " ORDER BY shortage_qty ASC";
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(query, companyCode);
        log.info("안전재고 부족분 조회: companyCode={}, count={}", companyCode, rows.size());
        return rows;
    }

    // ─── 생산계획 단건 조회 ───

    public Map<String, Object> getPlanById(String companyCode, Long planId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE id = ? AND company_code = ?",
            planId, companyCode
        );
        return rows.isEmpty() ? null : rows.get(0);
    }

    // ─── 생산계획 수정 ───

    @Transactional
    public Map<String, Object> updatePlan(String companyCode, Long planId, Map<String, Object> data, String updatedBy) {
        List<String> allowedFields = Arrays.asList(
            "plan_qty", "start_date", "end_date", "due_date",
            "equipment_id", "equipment_code", "equipment_name",
            "manager_name", "work_shift", "priority", "remarks", "status",
            "item_code", "item_name", "product_type", "order_no"
        );

        List<String> setClauses = new ArrayList<>();
        List<Object> args = new ArrayList<>();

        for (String field : allowedFields) {
            if (data.containsKey(field)) {
                setClauses.add(field + " = ?");
                args.add(data.get(field));
            }
        }

        if (setClauses.isEmpty()) {
            throw new IllegalArgumentException("수정할 필드가 없습니다");
        }

        setClauses.add("updated_date = NOW()");
        setClauses.add("updated_by = ?");
        args.add(updatedBy);
        args.add(planId);
        args.add(companyCode);

        String sql = "UPDATE production_plan_mng SET " + String.join(", ", setClauses) +
            " WHERE id = ? AND company_code = ?";

        int affected = jdbcTemplate.update(sql, args.toArray());
        if (affected == 0) {
            throw new IllegalArgumentException("생산계획을 찾을 수 없거나 권한이 없습니다");
        }

        log.info("생산계획 수정: companyCode={}, planId={}", companyCode, planId);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE id = ? AND company_code = ?",
            planId, companyCode
        );
        return rows.isEmpty() ? null : rows.get(0);
    }

    // ─── 생산계획 삭제 ───

    @Transactional
    public Map<String, Object> deletePlan(String companyCode, Long planId) {
        int affected = jdbcTemplate.update(
            "DELETE FROM production_plan_mng WHERE id = ? AND company_code = ?",
            planId, companyCode
        );
        if (affected == 0) {
            throw new IllegalArgumentException("생산계획을 찾을 수 없거나 권한이 없습니다");
        }
        log.info("생산계획 삭제: companyCode={}, planId={}", companyCode, planId);
        Map<String, Object> result = new HashMap<>();
        result.put("id", planId);
        return result;
    }

    // ─── 자동 스케줄 미리보기 ───

    public Map<String, Object> previewSchedule(String companyCode, List<Map<String, Object>> items, Map<String, Object> options) {
        String productType = (String) options.getOrDefault("product_type", "완제품");
        int safetyLeadTime = toInt(options.getOrDefault("safety_lead_time", 1));
        boolean recalculateUnstarted = Boolean.TRUE.equals(options.get("recalculate_unstarted")) ||
            "true".equals(String.valueOf(options.getOrDefault("recalculate_unstarted", "false")));

        List<Map<String, Object>> previews = new ArrayList<>();
        List<Map<String, Object>> deletedSchedules = new ArrayList<>();
        List<Map<String, Object>> keptSchedules = new ArrayList<>();

        for (Map<String, Object> item : items) {
            String itemCode = (String) item.get("item_code");

            if (recalculateUnstarted) {
                List<Map<String, Object>> deleteRows = jdbcTemplate.queryForList(
                    "SELECT id, plan_no, item_code, item_name, plan_qty, start_date, end_date, status" +
                    " FROM production_plan_mng" +
                    " WHERE company_code = ? AND item_code = ?" +
                    "   AND COALESCE(product_type, '완제품') = ? AND status = 'planned'",
                    companyCode, itemCode, productType
                );
                deletedSchedules.addAll(deleteRows);

                List<Map<String, Object>> keptRows = jdbcTemplate.queryForList(
                    "SELECT id, plan_no, item_code, item_name, plan_qty, start_date, end_date, status, completed_qty" +
                    " FROM production_plan_mng" +
                    " WHERE company_code = ? AND item_code = ?" +
                    "   AND COALESCE(product_type, '완제품') = ?" +
                    "   AND status NOT IN ('planned', 'completed', 'cancelled')",
                    companyCode, itemCode, productType
                );
                keptSchedules.addAll(keptRows);
            }

            BigDecimal dailyCapacity = DecimalUtils.toBigDecimal(item.getOrDefault("daily_capacity", 800));
            BigDecimal requiredQty = DecimalUtils.toBigDecimal(item.get("required_qty"));
            if (requiredQty.compareTo(BigDecimal.ZERO) <= 0) continue;

            int productionDays = requiredQty.divide(dailyCapacity, 0, RoundingMode.CEILING).intValue();

            LocalDate dueDate = toLocalDate(item.get("earliest_due_date"));
            LocalDate endDate = dueDate.minusDays(safetyLeadTime);
            LocalDate startDate = endDate.minusDays(productionDays);

            LocalDate today = LocalDate.now();
            if (startDate.isBefore(today)) {
                startDate = today;
                endDate = startDate.plusDays(productionDays);
            }

            List<Map<String, Object>> orderCountRows = jdbcTemplate.queryForList(
                "SELECT COUNT(*) AS cnt FROM sales_order_mng" +
                " WHERE company_code = ? AND part_code = ? AND part_code IS NOT NULL",
                companyCode, itemCode
            );
            long orderCount = orderCountRows.isEmpty() ? 0 : toLong(orderCountRows.get(0).get("cnt"));

            Map<String, Object> preview = new LinkedHashMap<>();
            preview.put("item_code", itemCode);
            preview.put("item_name", item.get("item_name"));
            preview.put("required_qty", requiredQty);
            preview.put("daily_capacity", dailyCapacity);
            preview.put("hourly_capacity", toDouble(item.getOrDefault("hourly_capacity", 100)));
            preview.put("production_days", productionDays);
            preview.put("start_date", startDate.toString());
            preview.put("end_date", endDate.toString());
            preview.put("due_date", item.get("earliest_due_date"));
            preview.put("order_count", orderCount);
            preview.put("status", "planned");
            previews.add(preview);
        }

        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("total", previews.size() + keptSchedules.size());
        summary.put("new_count", previews.size());
        summary.put("kept_count", keptSchedules.size());
        summary.put("deleted_count", deletedSchedules.size());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("summary", summary);
        result.put("previews", previews);
        result.put("deleted_schedules", deletedSchedules);
        result.put("kept_schedules", keptSchedules);
        log.info("자동 스케줄 미리보기: companyCode={}, summary={}", companyCode, summary);
        return result;
    }

    // ─── 자동 스케줄 생성 ───

    @Transactional
    public Map<String, Object> generateSchedule(String companyCode, List<Map<String, Object>> items, Map<String, Object> options, String createdBy) {
        String productType = (String) options.getOrDefault("product_type", "완제품");
        int safetyLeadTime = toInt(options.getOrDefault("safety_lead_time", 1));
        boolean recalculateUnstarted = Boolean.TRUE.equals(options.get("recalculate_unstarted")) ||
            "true".equals(String.valueOf(options.getOrDefault("recalculate_unstarted", "false")));

        int deletedCount = 0;
        int keptCount = 0;
        List<Map<String, Object>> newSchedules = new ArrayList<>();
        String todayStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy_m_mdd"));

        for (Map<String, Object> item : items) {
            String itemCode = (String) item.get("item_code");

            if (recalculateUnstarted) {
                int deleted = jdbcTemplate.update(
                    "DELETE FROM production_plan_mng" +
                    " WHERE company_code = ? AND item_code = ?" +
                    "   AND COALESCE(product_type, '완제품') = ? AND status = 'planned'",
                    companyCode, itemCode, productType
                );
                deletedCount += deleted;

                List<Map<String, Object>> keptRows = jdbcTemplate.queryForList(
                    "SELECT COUNT(*) AS cnt FROM production_plan_mng" +
                    " WHERE company_code = ? AND item_code = ?" +
                    "   AND COALESCE(product_type, '완제품') = ?" +
                    "   AND status NOT IN ('planned', 'completed', 'cancelled')",
                    companyCode, itemCode, productType
                );
                keptCount += (int) toLong(keptRows.get(0).get("cnt"));
            }

            BigDecimal dailyCapacity = DecimalUtils.toBigDecimal(item.getOrDefault("daily_capacity", 800));
            BigDecimal requiredQty = DecimalUtils.toBigDecimal(item.get("required_qty"));
            if (requiredQty.compareTo(BigDecimal.ZERO) <= 0) continue;

            int productionDays = requiredQty.divide(dailyCapacity, 0, RoundingMode.CEILING).intValue();

            LocalDate dueDate = toLocalDate(item.get("earliest_due_date"));
            LocalDate endDate = dueDate.minusDays(safetyLeadTime);
            LocalDate startDate = endDate.minusDays(productionDays);

            LocalDate today = LocalDate.now();
            if (startDate.isBefore(today)) {
                startDate = today;
                endDate = startDate.plusDays(productionDays);
            }

            List<Map<String, Object>> planNoRows = jdbcTemplate.queryForList(
                "SELECT COUNT(*) + 1 AS next_no FROM production_plan_mng" +
                " WHERE company_code = ? AND plan_no LIKE ?",
                companyCode, "PP-" + todayStr + "-%"
            );
            int nextNo = (int) toLong(planNoRows.get(0).get("next_no"));
            String planNo = String.format("PP-%s-%04d", todayStr, nextNo);

            List<Map<String, Object>> inserted = jdbcTemplate.queryForList(
                "INSERT INTO production_plan_mng (" +
                "  company_code, plan_no, plan_date, item_code, item_name," +
                "  product_type, plan_qty, start_date, end_date, due_date," +
                "  status, priority, hourly_capacity, daily_capacity, lead_time," +
                "  created_by, created_date, updated_date" +
                ") VALUES (?, ?, CURRENT_DATE, ?, ?, ?, ?, ?, ?, ?, 'planned', 'normal', ?, ?, ?, ?, NOW(), NOW())" +
                " RETURNING *",
                companyCode, planNo, itemCode, item.get("item_name"),
                productType, requiredQty,
                startDate.toString(), endDate.toString(), item.get("earliest_due_date"),
                toDouble(item.getOrDefault("hourly_capacity", 100)),
                dailyCapacity,
                toInt(item.getOrDefault("lead_time", 1)),
                createdBy
            );
            if (!inserted.isEmpty()) newSchedules.add(inserted.get(0));
        }

        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("total", newSchedules.size() + keptCount);
        summary.put("new_count", newSchedules.size());
        summary.put("kept_count", keptCount);
        summary.put("deleted_count", deletedCount);

        log.info("자동 스케줄 생성 완료: companyCode={}, summary={}", companyCode, summary);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("summary", summary);
        result.put("schedules", newSchedules);
        return result;
    }

    // ─── 스케줄 병합 ───

    @Transactional
    public Map<String, Object> mergeSchedules(String companyCode, List<Long> scheduleIds, String productType, String mergedBy) {
        String placeholders = scheduleIds.stream().map(i -> "?").collect(Collectors.joining(", "));
        List<Object> args = new ArrayList<>();
        args.add(companyCode);
        args.addAll(scheduleIds);

        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE company_code = ? AND id IN (" + placeholders + ") ORDER BY start_date",
            args.toArray()
        );

        if (rows.size() != scheduleIds.size()) {
            throw new IllegalArgumentException("일부 스케줄을 찾을 수 없습니다");
        }

        Set<String> itemCodeSet = new HashSet<>();
        for (Map<String, Object> r : rows) {
            if (r.get("item_code") != null) itemCodeSet.add(String.valueOf(r.get("item_code")));
        }
        if (itemCodeSet.size() > 1) {
            throw new IllegalArgumentException("동일 품목의 스케줄만 병합할 수 있습니다");
        }

        BigDecimal totalQty = BigDecimal.ZERO;
        String earliestStart = null;
        String latestEnd = null;
        String earliestDue = null;
        Set<String> orderNosSet = new LinkedHashSet<>();

        for (Map<String, Object> r : rows) {
            totalQty = totalQty.add(DecimalUtils.toBigDecimal(r.get("plan_qty")));
            String startStr = toDateStr(r.get("start_date"));
            String endStr = toDateStr(r.get("end_date"));
            String dueStr = toDateStr(r.get("due_date"));
            if (earliestStart == null || (startStr != null && startStr.compareTo(earliestStart) < 0)) earliestStart = startStr;
            if (latestEnd == null || (endStr != null && endStr.compareTo(latestEnd) > 0)) latestEnd = endStr;
            if (dueStr != null && (earliestDue == null || dueStr.compareTo(earliestDue) < 0)) earliestDue = dueStr;
            if (r.get("order_no") != null) orderNosSet.add(String.valueOf(r.get("order_no")));
        }
        String orderNos = orderNosSet.isEmpty() ? null : String.join(", ", orderNosSet);

        List<Object> deleteArgs = new ArrayList<>();
        deleteArgs.add(companyCode);
        deleteArgs.addAll(scheduleIds);
        jdbcTemplate.update(
            "DELETE FROM production_plan_mng WHERE company_code = ? AND id IN (" + placeholders + ")",
            deleteArgs.toArray()
        );

        List<Map<String, Object>> planNoRows = jdbcTemplate.queryForList(
            "SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no" +
            " FROM production_plan_mng WHERE company_code = ?",
            companyCode
        );
        int nextNo = (int) toLong(planNoRows.get(0).get("next_no"));
        String planNo = String.format("PP-%06d", nextNo);

        List<Map<String, Object>> inserted = jdbcTemplate.queryForList(
            "INSERT INTO production_plan_mng (" +
            "  company_code, plan_no, plan_date, item_code, item_name," +
            "  product_type, plan_qty, start_date, end_date, due_date," +
            "  status, order_no, created_by, created_date, updated_date" +
            ") VALUES (?, ?, CURRENT_DATE, ?, ?, ?, ?, ?, ?, ?, 'planned', ?, ?, NOW(), NOW())" +
            " RETURNING *",
            companyCode, planNo, rows.get(0).get("item_code"), rows.get(0).get("item_name"),
            productType, totalQty,
            earliestStart, latestEnd, earliestDue,
            orderNos, mergedBy
        );

        log.info("스케줄 병합 완료: companyCode={}, mergedFrom={}", companyCode, scheduleIds);
        return inserted.isEmpty() ? new HashMap<>() : inserted.get(0);
    }

    // ─── 반제품 계획 미리보기 ───

    public Map<String, Object> previewSemiSchedule(String companyCode, List<Long> planIds, Map<String, Object> options) {
        boolean considerStock = Boolean.TRUE.equals(options.get("consider_stock")) ||
            "true".equals(String.valueOf(options.getOrDefault("consider_stock", "false")));

        String placeholders = planIds.stream().map(i -> "?").collect(Collectors.joining(", "));
        List<Object> args = new ArrayList<>();
        args.add(companyCode);
        args.addAll(planIds);

        List<Map<String, Object>> plans = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE company_code = ? AND id IN (" + placeholders + ") AND product_type = '완제품'",
            args.toArray()
        );

        List<Map<String, Object>> previews = new ArrayList<>();
        List<Map<String, Object>> existingSemiPlans = new ArrayList<>();

        for (Map<String, Object> plan : plans) {
            Long planId = toLongObj(plan.get("id"));
            List<Map<String, Object>> existing = jdbcTemplate.queryForList(
                "SELECT * FROM production_plan_mng WHERE company_code = ? AND parent_plan_id = ? AND product_type = '반제품'",
                companyCode, planId
            );
            existingSemiPlans.addAll(existing);

            String itemCode = (String) plan.get("item_code");
            List<Map<String, Object>> bomItems = getBomChildItems(companyCode, itemCode);

            for (Map<String, Object> bomItem : bomItems) {
                BigDecimal planQty = DecimalUtils.toBigDecimal(plan.get("plan_qty"));
                BigDecimal bomQty = DecimalUtils.toBigDecimal(bomItem.getOrDefault("bom_qty", 1));
                BigDecimal requiredQty = planQty.multiply(bomQty);

                if (considerStock) {
                    String childCode = bomItem.get("child_item_code") != null ?
                        String.valueOf(bomItem.get("child_item_code")) : String.valueOf(bomItem.get("child_item_id"));
                    List<Map<String, Object>> stockRows = jdbcTemplate.queryForList(
                        "SELECT COALESCE(SUM(CAST(current_qty AS numeric)), 0) AS stock FROM inventory_stock" +
                        " WHERE company_code = ? AND item_code = ?",
                        companyCode, childCode
                    );
                    BigDecimal stock = stockRows.isEmpty() ? BigDecimal.ZERO : DecimalUtils.toBigDecimal(stockRows.get(0).get("stock"));
                    requiredQty = requiredQty.subtract(stock).max(BigDecimal.ZERO);
                }

                if (requiredQty.compareTo(BigDecimal.ZERO) <= 0) continue;

                String semiDueDateStr = toDateStr(plan.get("start_date"));
                LocalDate semiStartDate = LocalDate.parse(semiDueDateStr).minusDays(toInt(plan.getOrDefault("lead_time", 1)));

                String childCode = bomItem.get("child_item_code") != null ?
                    String.valueOf(bomItem.get("child_item_code")) : String.valueOf(bomItem.get("child_item_id"));
                String childName = bomItem.get("child_item_name") != null ?
                    String.valueOf(bomItem.get("child_item_name")) : String.valueOf(bomItem.get("child_item_id"));

                Map<String, Object> preview = new LinkedHashMap<>();
                preview.put("parent_plan_id", planId);
                preview.put("parent_plan_no", plan.get("plan_no"));
                preview.put("parent_item_name", plan.get("item_name"));
                preview.put("item_code", childCode);
                preview.put("item_name", childName);
                preview.put("plan_qty", requiredQty);
                preview.put("bom_qty", bomQty);
                preview.put("start_date", semiStartDate.toString());
                preview.put("end_date", semiDueDateStr);
                preview.put("due_date", semiDueDateStr);
                preview.put("product_type", "반제품");
                preview.put("status", "planned");
                previews.add(preview);
            }
        }

        List<Map<String, Object>> deletedSchedules = existingSemiPlans.stream()
            .filter(s -> "planned".equals(s.get("status"))).collect(Collectors.toList());
        List<Map<String, Object>> keptSchedules = existingSemiPlans.stream()
            .filter(s -> !"planned".equals(s.get("status")) && !"completed".equals(s.get("status"))).collect(Collectors.toList());

        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("total", previews.size() + keptSchedules.size());
        summary.put("new_count", previews.size());
        summary.put("deleted_count", deletedSchedules.size());
        summary.put("kept_count", keptSchedules.size());
        summary.put("parent_count", plans.size());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("summary", summary);
        result.put("previews", previews);
        result.put("deleted_schedules", deletedSchedules);
        result.put("kept_schedules", keptSchedules);
        return result;
    }

    // ─── 반제품 계획 생성 ───

    @Transactional
    public Map<String, Object> generateSemiSchedule(String companyCode, List<Long> planIds, Map<String, Object> options, String createdBy) {
        boolean considerStock = Boolean.TRUE.equals(options.get("consider_stock")) ||
            "true".equals(String.valueOf(options.getOrDefault("consider_stock", "false")));

        String placeholders = planIds.stream().map(i -> "?").collect(Collectors.joining(", "));
        List<Object> args = new ArrayList<>();
        args.add(companyCode);
        args.addAll(planIds);

        List<Map<String, Object>> plans = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE company_code = ? AND id IN (" + placeholders + ") AND product_type = '완제품'",
            args.toArray()
        );

        for (Map<String, Object> plan : plans) {
            jdbcTemplate.update(
                "DELETE FROM production_plan_mng WHERE company_code = ? AND parent_plan_id = ? AND product_type = '반제품' AND status = 'planned'",
                companyCode, toLongObj(plan.get("id"))
            );
        }

        List<Map<String, Object>> newSemiPlans = new ArrayList<>();
        String todayStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy_m_mdd"));

        for (Map<String, Object> plan : plans) {
            String itemCode = (String) plan.get("item_code");
            List<Map<String, Object>> bomItems = getBomChildItems(companyCode, itemCode);

            for (Map<String, Object> bomItem : bomItems) {
                BigDecimal planQty = DecimalUtils.toBigDecimal(plan.get("plan_qty"));
                BigDecimal bomQty = DecimalUtils.toBigDecimal(bomItem.getOrDefault("bom_qty", 1));
                BigDecimal requiredQty = planQty.multiply(bomQty);

                if (considerStock) {
                    String childCode = bomItem.get("child_item_code") != null ?
                        String.valueOf(bomItem.get("child_item_code")) : String.valueOf(bomItem.get("child_item_id"));
                    List<Map<String, Object>> stockRows = jdbcTemplate.queryForList(
                        "SELECT COALESCE(SUM(CAST(current_qty AS numeric)), 0) AS stock FROM inventory_stock" +
                        " WHERE company_code = ? AND item_code = ?",
                        companyCode, childCode
                    );
                    BigDecimal stock = stockRows.isEmpty() ? BigDecimal.ZERO : DecimalUtils.toBigDecimal(stockRows.get(0).get("stock"));
                    requiredQty = requiredQty.subtract(stock).max(BigDecimal.ZERO);
                }

                if (requiredQty.compareTo(BigDecimal.ZERO) <= 0) continue;

                String semiDueDateStr = toDateStr(plan.get("start_date"));
                LocalDate semiStartDate = LocalDate.parse(semiDueDateStr).minusDays(toInt(plan.getOrDefault("lead_time", 1)));

                List<Map<String, Object>> planNoRows = jdbcTemplate.queryForList(
                    "SELECT COUNT(*) + 1 AS next_no FROM production_plan_mng WHERE company_code = ? AND plan_no LIKE ?",
                    companyCode, "PP-" + todayStr + "-S%"
                );
                int nextNo = (int) toLong(planNoRows.get(0).get("next_no"));
                String planNo = String.format("PP-%s-S%03d", todayStr, nextNo);

                String childCode = bomItem.get("child_item_code") != null ?
                    String.valueOf(bomItem.get("child_item_code")) : String.valueOf(bomItem.get("child_item_id"));
                String childName = bomItem.get("child_item_name") != null ?
                    String.valueOf(bomItem.get("child_item_name")) : String.valueOf(bomItem.get("child_item_id"));

                List<Map<String, Object>> inserted = jdbcTemplate.queryForList(
                    "INSERT INTO production_plan_mng (" +
                    "  company_code, plan_no, plan_date, item_code, item_name," +
                    "  product_type, plan_qty, start_date, end_date, due_date," +
                    "  status, parent_plan_id, created_by, created_date, updated_date" +
                    ") VALUES (?, ?, CURRENT_DATE, ?, ?, '반제품', ?, ?, ?, ?, 'planned', ?, ?, NOW(), NOW())" +
                    " RETURNING *",
                    companyCode, planNo, childCode, childName,
                    requiredQty,
                    semiStartDate.toString(), semiDueDateStr, semiDueDateStr,
                    toLongObj(plan.get("id")), createdBy
                );
                if (!inserted.isEmpty()) newSemiPlans.add(inserted.get(0));
            }
        }

        log.info("반제품 계획 생성 완료: companyCode={}, parentPlanIds={}, count={}", companyCode, planIds, newSemiPlans.size());
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("count", newSemiPlans.size());
        result.put("schedules", newSemiPlans);
        return result;
    }

    // ─── 스케줄 분할 ───

    @Transactional
    public Map<String, Object> splitSchedule(String companyCode, Long planId, double splitQty, String splitBy) {
        List<Map<String, Object>> planRows = jdbcTemplate.queryForList(
            "SELECT * FROM production_plan_mng WHERE id = ? AND company_code = ?",
            planId, companyCode
        );
        if (planRows.isEmpty()) {
            throw new IllegalArgumentException("생산계획을 찾을 수 없습니다");
        }

        Map<String, Object> plan = planRows.get(0);
        BigDecimal originalQty = DecimalUtils.toBigDecimal(plan.get("plan_qty"));
        BigDecimal bdSplitQty = BigDecimal.valueOf(splitQty);

        if (bdSplitQty.compareTo(originalQty) >= 0 || splitQty <= 0) {
            throw new IllegalArgumentException("분할 수량은 0보다 크고 원래 수량보다 작아야 합니다");
        }

        jdbcTemplate.update(
            "UPDATE production_plan_mng SET plan_qty = ?, updated_date = NOW(), updated_by = ? WHERE id = ? AND company_code = ?",
            originalQty.subtract(bdSplitQty), splitBy, planId, companyCode
        );

        List<Map<String, Object>> planNoRows = jdbcTemplate.queryForList(
            "SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no" +
            " FROM production_plan_mng WHERE company_code = ?",
            companyCode
        );
        int nextNo = (int) toLong(planNoRows.get(0).get("next_no"));
        String planNo = String.format("PP-%06d", nextNo);

        List<Map<String, Object>> inserted = jdbcTemplate.queryForList(
            "INSERT INTO production_plan_mng (" +
            "  company_code, plan_no, plan_date, item_code, item_name," +
            "  product_type, plan_qty, start_date, end_date, due_date," +
            "  status, priority, equipment_id, equipment_code, equipment_name," +
            "  order_no, parent_plan_id, created_by, created_date, updated_date" +
            ") VALUES (?, ?, CURRENT_DATE, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())" +
            " RETURNING *",
            companyCode, planNo, plan.get("item_code"), plan.get("item_name"),
            plan.get("product_type"), splitQty,
            plan.get("start_date"), plan.get("end_date"), plan.get("due_date"),
            plan.get("status"), plan.get("priority"),
            plan.get("equipment_id"), plan.get("equipment_code"), plan.get("equipment_name"),
            plan.get("order_no"), plan.get("parent_plan_id"),
            splitBy
        );

        log.info("스케줄 분할 완료: companyCode={}, planId={}, splitQty={}", companyCode, planId, splitQty);
        Map<String, Object> originalInfo = new HashMap<>();
        originalInfo.put("id", planId);
        originalInfo.put("plan_qty", originalQty.subtract(bdSplitQty));

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("original", originalInfo);
        result.put("split", inserted.isEmpty() ? null : inserted.get(0));
        return result;
    }

    // ─── BOM 자식 품목 조회 (공통) ───

    private List<Map<String, Object>> getBomChildItems(String companyCode, String itemCode) {
        return jdbcTemplate.queryForList(
            "SELECT bd.child_item_id, ii.item_name AS child_item_name, ii.item_number AS child_item_code," +
            "  bd.quantity AS bom_qty, bd.unit" +
            " FROM bom b" +
            " JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code" +
            " LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code" +
            " WHERE b.company_code = ? AND b.item_code = ? AND COALESCE(b.status, 'active') = 'active'",
            companyCode, itemCode
        );
    }

    // ─── 유틸리티 ───

    private double toDouble(Object val) {
        if (val == null) return 0.0;
        if (val instanceof Number) return ((Number) val).doubleValue();
        try { return Double.parseDouble(val.toString()); } catch (Exception e) { return 0.0; }
    }

    private int toInt(Object val) {
        if (val == null) return 0;
        if (val instanceof Number) return ((Number) val).intValue();
        try { return Integer.parseInt(val.toString()); } catch (Exception e) { return 0; }
    }

    private long toLong(Object val) {
        if (val == null) return 0L;
        if (val instanceof Number) return ((Number) val).longValue();
        try { return Long.parseLong(val.toString()); } catch (Exception e) { return 0L; }
    }

    private Long toLongObj(Object val) {
        if (val == null) return null;
        if (val instanceof Number) return ((Number) val).longValue();
        try { return Long.parseLong(val.toString()); } catch (Exception e) { return null; }
    }

    private LocalDate toLocalDate(Object val) {
        if (val == null) return LocalDate.now();
        String s = val.toString();
        if (s.contains("T")) s = s.split("T")[0];
        try { return LocalDate.parse(s); } catch (Exception e) { return LocalDate.now(); }
    }

    private String toDateStr(Object val) {
        if (val == null) return LocalDate.now().toString();
        String s = val.toString();
        if (s.contains("T")) return s.split("T")[0];
        if (s.contains(" ")) return s.split(" ")[0];
        return s;
    }
}
