package com.erp.service;

import com.erp.common.BaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Timestamp;
import java.util.*;
import java.util.regex.Pattern;

/**
 * PopAction 동작 실행 서비스.
 * Node.js popActionRoutes.ts POST /execute-action 완전 포팅.
 * 동적 테이블/컬럼 SQL은 JdbcTemplate으로 처리.
 */
@Service
@Slf4j
public class PopActionService extends BaseService {

    @Autowired private JdbcTemplate jdbcTemplate;
    @Autowired private NumberingRuleService numberingRuleService;

    private static final Pattern SAFE_IDENTIFIER = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");

    // ── 예외 클래스 ────────────────────────────────────────────────────────────
    public static class PreConditionFailException extends RuntimeException {
        public PreConditionFailException(String message) { super(message); }
    }

    // ──────────────────────────────────────────────────────────────────────────
    // 메인 엔트리포인트
    // ──────────────────────────────────────────────────────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> executeAction(Map<String, Object> body, String companyCode, String userId) {
        String action = str(body.get("action"), "");
        List<Map<String, Object>> tasks = (List<Map<String, Object>>) body.get("tasks");
        Map<String, Object> data = body.get("data") != null ? (Map<String, Object>) body.get("data") : new HashMap<>();
        Map<String, Object> mappings = body.get("mappings") != null ? (Map<String, Object>) body.get("mappings") : new HashMap<>();
        List<Map<String, Object>> statusChanges = body.get("status_changes") != null
                ? (List<Map<String, Object>>) body.get("status_changes") : new ArrayList<>();
        Map<String, Object> cartChanges = (Map<String, Object>) body.get("cart_changes");

        List<Map<String, Object>> items = data.get("items") != null
                ? (List<Map<String, Object>>) data.get("items") : new ArrayList<>();
        Map<String, Object> fieldValues = data.get("field_values") != null
                ? (Map<String, Object>) data.get("field_values") : new HashMap<>();

        Map<String, Object> cardMapping = mappings.get("card_list") != null
                ? (Map<String, Object>) mappings.get("card_list") : null;
        Map<String, Object> fieldMapping = mappings.get("field") != null
                ? (Map<String, Object>) mappings.get("field") : null;

        int processedCount = 0;
        int insertedCount = 0;
        int deletedCount = 0;
        List<Map<String, Object>> generatedCodes = new ArrayList<>();

        log.info("[pop/execute-action] 요청: action={}, items={}, tasks={}",
                action.isEmpty() ? "task-list" : action, items.size(),
                tasks != null ? tasks.size() : 0);

        if (tasks != null && !tasks.isEmpty()) {
            // v2: tasks 배열 기반 처리
            for (Map<String, Object> task : tasks) {
                String type = str(task.get("type"), "");
                switch (type) {
                    case "data-save":
                        insertedCount += processDataSave(cardMapping, fieldMapping, items, fieldValues, companyCode, userId, generatedCodes);
                        break;
                    case "data-update":
                        processedCount += processDataUpdate(task, items, fieldValues, companyCode, userId);
                        break;
                    case "data-delete":
                        deletedCount += processDataDelete(task, items, companyCode);
                        break;
                    case "cart-save":
                        if (cartChanges != null) {
                            int[] counts = processCartSave(cartChanges, companyCode);
                            insertedCount += counts[0];
                            processedCount += counts[1];
                            deletedCount += counts[2];
                        }
                        break;
                    default:
                        log.warn("[pop/execute-action] 프론트 전용 작업 타입, 무시: {}", type);
                }
            }
        } else if ("inbound-confirm".equals(action)) {
            // v1 레거시: 매핑 기반 INSERT + 상태 변경
            insertedCount += processDataSave(cardMapping, fieldMapping, items, fieldValues, companyCode, userId, generatedCodes);
            if (!statusChanges.isEmpty()) {
                processedCount += processStatusChanges(statusChanges, items, companyCode);
            }
        }

        log.info("[pop/execute-action] 완료: processed={}, inserted={}, deleted={}",
                processedCount, insertedCount, deletedCount);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("processed_count", processedCount);
        result.put("inserted_count", insertedCount);
        result.put("deleted_count", deletedCount);
        result.put("generated_codes", generatedCodes);
        return result;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // data-save
    // ──────────────────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    private int processDataSave(Map<String, Object> cardMapping, Map<String, Object> fieldMapping,
                                 List<Map<String, Object>> items, Map<String, Object> fieldValues,
                                 String companyCode, String userId,
                                 List<Map<String, Object>> generatedCodes) {
        if (cardMapping == null) return 0;
        String targetTable = str(cardMapping.get("target_table"), "");
        Map<String, String> columnMapping = (Map<String, String>) cardMapping.get("column_mapping");
        if (targetTable.isEmpty() || columnMapping == null || columnMapping.isEmpty()) return 0;
        if (!isSafeIdentifier(targetTable))
            throw new IllegalArgumentException("유효하지 않은 테이블명: " + targetTable);

        List<Map<String, Object>> cardAutoGen = safeList(cardMapping.get("auto_gen_mappings"));
        List<Map<String, Object>> fieldAutoGen = fieldMapping != null ? safeList(fieldMapping.get("auto_gen_mappings")) : new ArrayList<>();
        List<Map<String, Object>> allAutoGen = new ArrayList<>(fieldAutoGen);
        allAutoGen.addAll(cardAutoGen);

        List<Map<String, Object>> cardHidden = safeList(cardMapping.get("hidden_mappings"));
        List<Map<String, Object>> fieldHidden = fieldMapping != null ? safeList(fieldMapping.get("hidden_mappings")) : new ArrayList<>();
        List<Map<String, Object>> allHidden = new ArrayList<>(fieldHidden);
        allHidden.addAll(cardHidden);

        // shareAcrossItems=true 일괄 채번
        Map<String, String> sharedCodes = new HashMap<>();
        for (Map<String, Object> ag : allAutoGen) {
            if (!Boolean.TRUE.equals(ag.get("share_across_items"))) continue;
            String ruleId = str(ag.get("numbering_rule_id"), "");
            String targetCol = str(ag.get("target_column"), "");
            if (ruleId.isEmpty() || targetCol.isEmpty() || !isSafeIdentifier(targetCol)) continue;
            try {
                Map<String, Object> ctx = new HashMap<>(fieldValues);
                if (!items.isEmpty()) ctx.putAll(items.get(0));
                String code = numberingRuleService.allocateCode(ruleId, companyCode, ctx, null);
                sharedCodes.put(targetCol, code);
                generatedCodes.add(codeEntry(targetCol, code, Boolean.TRUE.equals(ag.get("show_result_modal"))));
            } catch (Exception e) {
                log.error("[pop/execute-action] 일괄 채번 실패: ruleId={}, err={}", ruleId, e.getMessage());
            }
        }

        int count = 0;
        for (Map<String, Object> item : items) {
            List<String> cols = new ArrayList<>();
            List<Object> vals = new ArrayList<>();
            cols.add("company_code"); vals.add(companyCode);

            // cardMapping columns
            for (Map.Entry<String, String> e : columnMapping.entrySet()) {
                if (!isSafeIdentifier(e.getValue())) continue;
                cols.add("\"" + e.getValue() + "\"");
                vals.add(item.get(e.getKey()));
            }

            // fieldMapping columns (동일 테이블 한정)
            if (fieldMapping != null) {
                String fieldTable = str(fieldMapping.get("target_table"), "");
                Map<String, String> fColMap = (Map<String, String>) fieldMapping.get("column_mapping");
                if (targetTable.equals(fieldTable) && fColMap != null) {
                    for (Map.Entry<String, String> e : fColMap.entrySet()) {
                        if (!isSafeIdentifier(e.getValue())) continue;
                        String quoted = "\"" + e.getValue() + "\"";
                        if (cols.contains(quoted)) continue;
                        cols.add(quoted); vals.add(fieldValues.get(e.getKey()));
                    }
                }
            }

            // hidden mappings
            for (Map<String, Object> hm : allHidden) {
                String tc = str(hm.get("target_column"), "");
                if (tc.isEmpty() || !isSafeIdentifier(tc) || cols.contains("\"" + tc + "\"")) continue;
                cols.add("\"" + tc + "\"");
                vals.add(resolveHiddenValue(hm, item, fieldValues));
            }

            // auto-gen mappings
            for (Map<String, Object> ag : allAutoGen) {
                String ruleId = str(ag.get("numbering_rule_id"), "");
                String tc = str(ag.get("target_column"), "");
                if (ruleId.isEmpty() || tc.isEmpty() || !isSafeIdentifier(tc) || cols.contains("\"" + tc + "\"")) continue;
                if (Boolean.TRUE.equals(ag.get("share_across_items")) && sharedCodes.containsKey(tc)) {
                    cols.add("\"" + tc + "\""); vals.add(sharedCodes.get(tc));
                } else if (!Boolean.TRUE.equals(ag.get("share_across_items"))) {
                    try {
                        Map<String, Object> ctx = new HashMap<>(fieldValues);
                        ctx.putAll(item);
                        String code = numberingRuleService.allocateCode(ruleId, companyCode, ctx, null);
                        cols.add("\"" + tc + "\""); vals.add(code);
                        generatedCodes.add(codeEntry(tc, code, Boolean.TRUE.equals(ag.get("show_result_modal"))));
                    } catch (Exception e) {
                        log.error("[pop/execute-action] 채번 실패: ruleId={}, err={}", ruleId, e.getMessage());
                    }
                }
            }

            // 감사 컬럼
            Timestamp now = new Timestamp(System.currentTimeMillis());
            if (!cols.contains("\"created_date\"")) { cols.add("\"created_date\""); vals.add(now); }
            if (!cols.contains("\"updated_date\"")) { cols.add("\"updated_date\""); vals.add(now); }
            if (!cols.contains("\"writer\"") && userId != null) { cols.add("\"writer\""); vals.add(userId); }

            if (cols.size() > 1) {
                String placeholders = repeat("?", vals.size());
                jdbcTemplate.update(
                        "INSERT INTO \"" + targetTable + "\" (" + String.join(", ", cols) + ") VALUES (" + placeholders + ")",
                        vals.toArray());
                count++;
            }
        }

        // fieldMapping 별도 테이블 INSERT
        if (fieldMapping != null) {
            String fieldTable = str(fieldMapping.get("target_table"), "");
            Map<String, String> fColMap = (Map<String, String>) fieldMapping.get("column_mapping");
            if (!fieldTable.isEmpty() && !fieldTable.equals(targetTable) && fColMap != null && !fColMap.isEmpty()) {
                if (!isSafeIdentifier(fieldTable))
                    throw new IllegalArgumentException("유효하지 않은 테이블명: " + fieldTable);
                List<String> fCols = new ArrayList<>();
                List<Object> fVals = new ArrayList<>();
                fCols.add("company_code"); fVals.add(companyCode);
                for (Map.Entry<String, String> e : fColMap.entrySet()) {
                    if (!isSafeIdentifier(e.getValue())) continue;
                    fCols.add("\"" + e.getValue() + "\""); fVals.add(fieldValues.get(e.getKey()));
                }
                Timestamp now = new Timestamp(System.currentTimeMillis());
                if (!fCols.contains("\"created_date\"")) { fCols.add("\"created_date\""); fVals.add(now); }
                if (!fCols.contains("\"updated_date\"")) { fCols.add("\"updated_date\""); fVals.add(now); }
                if (!fCols.contains("\"writer\"") && userId != null) { fCols.add("\"writer\""); fVals.add(userId); }
                if (fCols.size() > 1) {
                    jdbcTemplate.update(
                            "INSERT INTO \"" + fieldTable + "\" (" + String.join(", ", fCols) + ") VALUES (" + repeat("?", fVals.size()) + ")",
                            fVals.toArray());
                }
            }
        }

        return count;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // data-update
    // ──────────────────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    private int processDataUpdate(Map<String, Object> task, List<Map<String, Object>> items,
                                   Map<String, Object> fieldValues, String companyCode, String userId) {
        String targetTable = str(task.get("target_table"), "");
        String targetColumn = str(task.get("target_column"), "");
        if (targetTable.isEmpty() || targetColumn.isEmpty()) return 0;
        if (!isSafeIdentifier(targetTable) || !isSafeIdentifier(targetColumn)) return 0;

        String opType = str(task.get("operation_type"), "assign");
        String valSource = str(task.get("value_source"), "fixed");
        String lookupMode = str(task.get("lookup_mode"), "auto");
        Map<String, Object> preCondition = (Map<String, Object>) task.get("pre_condition");
        Map<String, Object> conditionalValue = (Map<String, Object>) task.get("conditional_value");
        String fixedValue = str(task.get("fixed_value"), "");

        String itemField;
        String pkColumn;
        if ("manual".equals(lookupMode) && task.get("manual_item_field") != null && task.get("manual_pk_column") != null) {
            String manualPk = str(task.get("manual_pk_column"), "");
            if (!isSafeIdentifier(manualPk)) return 0;
            itemField = str(task.get("manual_item_field"), "");
            pkColumn = manualPk;
        } else if ("cart_items".equals(targetTable)) {
            itemField = "__cart_id"; pkColumn = "id";
        } else {
            itemField = "__cart_row_key";
            pkColumn = getPkColumn(targetTable);
        }

        List<Object> lookupValues = collectLookupValues(items, itemField);
        if (lookupValues.isEmpty()) return 0;

        String autoUpd = !"updated_date".equals(targetColumn) ? ", \"updated_date\" = NOW()" : "";
        int processed = 0;

        if ("conditional".equals(opType) && conditionalValue != null) {
            for (int i = 0; i < lookupValues.size(); i++) {
                Map<String, Object> item = i < items.size() ? items.get(i) : new HashMap<>();
                String resolved = resolveStatusValue("conditional", fixedValue, conditionalValue, item);
                String whereSql = "WHERE company_code = ? AND \"" + pkColumn + "\" = ?";
                List<Object> params = new ArrayList<>();
                params.add(resolved); params.add(companyCode); params.add(lookupValues.get(i));
                if (preCondition != null) {
                    String pcCol = str(preCondition.get("column"), "");
                    if (!isSafeIdentifier(pcCol)) throw new IllegalArgumentException("유효하지 않은 preCondition 컬럼: " + pcCol);
                    whereSql += " AND \"" + pcCol + "\" = ?";
                    params.add(preCondition.get("expected_value"));
                }
                int rows = jdbcTemplate.update(
                        "UPDATE \"" + targetTable + "\" SET \"" + targetColumn + "\" = ?" + autoUpd + " " + whereSql,
                        params.toArray());
                if (preCondition != null && rows == 0)
                    throw new PreConditionFailException(str(preCondition.get("fail_message"), "조건이 일치하지 않아 처리할 수 없습니다."));
                processed++;
            }
        } else if ("db-conditional".equals(opType)) {
            String compareCol = str(task.get("compare_column"), "");
            String compareOp = str(task.get("compare_operator"), "=");
            String compareWith = str(task.get("compare_with"), "");
            if (compareCol.isEmpty() || compareWith.isEmpty() || !isSafeIdentifier(compareCol) || !isSafeIdentifier(compareWith)) return 0;
            String[] valid = {"=", "!=", ">", "<", ">=", "<="};
            boolean validOp = Arrays.stream(valid).anyMatch(op -> op.equals(compareOp));
            if (!validOp) return 0;

            String caseSql = "CASE WHEN COALESCE(\"" + compareCol + "\"::numeric, 0) " + compareOp
                    + " COALESCE(\"" + compareWith + "\"::numeric, 0) THEN ? ELSE ? END";
            List<Object> params = new ArrayList<>();
            params.add(str(task.get("db_then_value"), ""));
            params.add(str(task.get("db_else_value"), ""));
            params.add(companyCode);
            params.addAll(lookupValues);
            jdbcTemplate.update(
                    "UPDATE \"" + targetTable + "\" SET \"" + targetColumn + "\" = " + caseSql + autoUpd
                            + " WHERE company_code = ? AND \"" + pkColumn + "\" IN (" + repeat("?", lookupValues.size()) + ")",
                    params.toArray());
            processed += lookupValues.size();
        } else {
            for (int i = 0; i < lookupValues.size(); i++) {
                Map<String, Object> item = i < items.size() ? items.get(i) : new HashMap<>();
                Object value;
                if ("linked".equals(valSource)) {
                    value = item.get(str(task.get("source_field"), ""));
                } else {
                    String raw = fixedValue;
                    if ("__CURRENT_USER__".equals(raw)) value = userId;
                    else if ("__CURRENT_TIME__".equals(raw)) value = new Timestamp(System.currentTimeMillis());
                    else value = raw;
                }

                String setSql;
                List<Object> params = new ArrayList<>();
                if ("add".equals(opType)) {
                    setSql = "\"" + targetColumn + "\" = COALESCE(\"" + targetColumn + "\"::numeric, 0) + ?::numeric";
                    params.add(value);
                } else if ("subtract".equals(opType)) {
                    setSql = "\"" + targetColumn + "\" = COALESCE(\"" + targetColumn + "\"::numeric, 0) - ?::numeric";
                    params.add(value);
                } else if ("multiply".equals(opType)) {
                    setSql = "\"" + targetColumn + "\" = COALESCE(\"" + targetColumn + "\"::numeric, 0) * ?::numeric";
                    params.add(value);
                } else if ("divide".equals(opType)) {
                    setSql = "\"" + targetColumn + "\" = CASE WHEN ?::numeric = 0 THEN COALESCE(\"" + targetColumn
                            + "\"::numeric, 0) ELSE COALESCE(\"" + targetColumn + "\"::numeric, 0) / ?::numeric END";
                    params.add(value); params.add(value);
                } else {
                    setSql = "\"" + targetColumn + "\" = ?";
                    params.add(value);
                }

                String whereSql = "WHERE company_code = ? AND \"" + pkColumn + "\" = ?";
                params.add(companyCode); params.add(lookupValues.get(i));
                if (preCondition != null) {
                    String pcCol = str(preCondition.get("column"), "");
                    if (!isSafeIdentifier(pcCol)) throw new IllegalArgumentException("유효하지 않은 preCondition 컬럼: " + pcCol);
                    whereSql += " AND \"" + pcCol + "\" = ?";
                    params.add(preCondition.get("expected_value"));
                }
                int rows = jdbcTemplate.update(
                        "UPDATE \"" + targetTable + "\" SET " + setSql + autoUpd + " " + whereSql,
                        params.toArray());
                if (preCondition != null && rows == 0)
                    throw new PreConditionFailException(str(preCondition.get("fail_message"), "조건이 일치하지 않아 처리할 수 없습니다."));
                processed++;
            }
        }

        log.info("[pop/execute-action] data-update: table={}, col={}, op={}, count={}", targetTable, targetColumn, opType, processed);
        return processed;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // data-delete
    // ──────────────────────────────────────────────────────────────────────────
    private int processDataDelete(Map<String, Object> task, List<Map<String, Object>> items, String companyCode) {
        String targetTable = str(task.get("target_table"), "");
        if (targetTable.isEmpty() || !isSafeIdentifier(targetTable)) return 0;
        String pkCol = getPkColumn(targetTable);
        List<Object> deleteKeys = new ArrayList<>();
        for (Map<String, Object> item : items) {
            Object k = item.get(pkCol);
            if (k == null) k = item.get("id");
            if (k != null) deleteKeys.add(k);
        }
        if (deleteKeys.isEmpty()) return 0;
        List<Object> params = new ArrayList<>();
        params.add(companyCode); params.addAll(deleteKeys);
        jdbcTemplate.update(
                "DELETE FROM \"" + targetTable + "\" WHERE company_code = ? AND \"" + pkCol + "\" IN (" + repeat("?", deleteKeys.size()) + ")",
                params.toArray());
        return deleteKeys.size();
    }

    // ──────────────────────────────────────────────────────────────────────────
    // cart-save
    // ──────────────────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    private int[] processCartSave(Map<String, Object> cartChanges, String companyCode) {
        int inserted = 0, updated = 0, deleted = 0;
        List<Map<String, Object>> toCreate = safeList(cartChanges.get("to_create"));
        List<Map<String, Object>> toUpdate = safeList(cartChanges.get("to_update"));
        List<Object> toDelete = (List<Object>) cartChanges.get("to_delete");

        for (Map<String, Object> item : toCreate) {
            List<String> cols = new ArrayList<>();
            List<Object> vals = new ArrayList<>();
            cols.add("company_code"); vals.add(companyCode);
            for (Map.Entry<String, Object> e : item.entrySet()) {
                if (!isSafeIdentifier(e.getKey())) continue;
                cols.add("\"" + e.getKey() + "\""); vals.add(e.getValue());
            }
            if (cols.size() <= 1) continue;
            jdbcTemplate.update(
                    "INSERT INTO \"cart_items\" (" + String.join(", ", cols) + ") VALUES (" + repeat("?", vals.size()) + ")",
                    vals.toArray());
            inserted++;
        }

        for (Map<String, Object> item : toUpdate) {
            Object id = item.get("id");
            if (id == null) continue;
            List<String> setClauses = new ArrayList<>();
            List<Object> vals = new ArrayList<>();
            for (Map.Entry<String, Object> e : item.entrySet()) {
                if ("id".equals(e.getKey()) || !isSafeIdentifier(e.getKey())) continue;
                setClauses.add("\"" + e.getKey() + "\" = ?"); vals.add(e.getValue());
            }
            if (setClauses.isEmpty()) continue;
            vals.add(id); vals.add(companyCode);
            jdbcTemplate.update(
                    "UPDATE \"cart_items\" SET " + String.join(", ", setClauses) + " WHERE id = ? AND company_code = ?",
                    vals.toArray());
            updated++;
        }

        if (toDelete != null && !toDelete.isEmpty()) {
            List<Object> params = new ArrayList<>();
            params.add(companyCode); params.addAll(toDelete);
            jdbcTemplate.update(
                    "DELETE FROM \"cart_items\" WHERE company_code = ? AND id IN (" + repeat("?", toDelete.size()) + ")",
                    params.toArray());
            deleted += toDelete.size();
        }

        log.info("[pop/execute-action] cart-save: created={}, updated={}, deleted={}", inserted, updated, deleted);
        return new int[]{inserted, updated, deleted};
    }

    // ──────────────────────────────────────────────────────────────────────────
    // 상태 변경 (v1 레거시)
    // ──────────────────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    private int processStatusChanges(List<Map<String, Object>> statusChanges,
                                      List<Map<String, Object>> items, String companyCode) {
        int count = 0;
        for (Map<String, Object> rule : statusChanges) {
            String targetTable = str(rule.get("target_table"), "");
            String targetColumn = str(rule.get("target_column"), "");
            if (targetTable.isEmpty() || targetColumn.isEmpty()
                    || !isSafeIdentifier(targetTable) || !isSafeIdentifier(targetColumn)) continue;

            String valueType = str(rule.get("value_type"), "fixed");
            Object fvRaw = rule.get("fixed_value") != null ? rule.get("fixed_value") : rule.get("value");
            String fixedValue = str(fvRaw, "");
            String lookupMode = str(rule.get("lookup_mode"), "auto");
            Map<String, Object> conditionalValue = (Map<String, Object>) rule.get("conditional_value");

            String itemField;
            String pkColumn;
            if ("manual".equals(lookupMode) && rule.get("manual_item_field") != null && rule.get("manual_pk_column") != null) {
                String manualPk = str(rule.get("manual_pk_column"), "");
                if (!isSafeIdentifier(manualPk)) continue;
                itemField = str(rule.get("manual_item_field"), "");
                pkColumn = manualPk;
            } else if ("cart_items".equals(targetTable)) {
                itemField = "__cart_id"; pkColumn = "id";
            } else {
                itemField = "__cart_row_key";
                pkColumn = getPkColumn(targetTable);
            }

            List<Object> lookupValues = collectLookupValues(items, itemField);
            if (lookupValues.isEmpty()) continue;

            String autoUpd = !"updated_date".equals(targetColumn) ? ", \"updated_date\" = NOW()" : "";

            if ("fixed".equals(valueType)) {
                List<Object> params = new ArrayList<>();
                params.add(fixedValue); params.add(companyCode); params.addAll(lookupValues);
                jdbcTemplate.update(
                        "UPDATE \"" + targetTable + "\" SET \"" + targetColumn + "\" = ?" + autoUpd
                                + " WHERE company_code = ? AND \"" + pkColumn + "\" IN (" + repeat("?", lookupValues.size()) + ")",
                        params.toArray());
                count += lookupValues.size();
            } else {
                for (int i = 0; i < lookupValues.size(); i++) {
                    Map<String, Object> item = i < items.size() ? items.get(i) : new HashMap<>();
                    String resolved = resolveStatusValue(valueType, fixedValue, conditionalValue, item);
                    jdbcTemplate.update(
                            "UPDATE \"" + targetTable + "\" SET \"" + targetColumn + "\" = ?" + autoUpd
                                    + " WHERE company_code = ? AND \"" + pkColumn + "\" = ?",
                            resolved, companyCode, lookupValues.get(i));
                    count++;
                }
            }
            log.info("[pop/execute-action] 상태 변경: table={}, col={}, count={}", targetTable, targetColumn, lookupValues.size());
        }
        return count;
    }

    // ──────────────────────────────────────────────────────────────────────────
    // 유틸리티
    // ──────────────────────────────────────────────────────────────────────────
    private boolean isSafeIdentifier(String name) {
        return name != null && SAFE_IDENTIFIER.matcher(name).matches();
    }

    private String getPkColumn(String tableName) {
        try {
            String pk = jdbcTemplate.queryForObject(
                    "SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = ?::regclass AND i.indisprimary LIMIT 1",
                    String.class, tableName);
            return pk != null ? pk : "id";
        } catch (Exception e) {
            return "id";
        }
    }

    @SuppressWarnings("unchecked")
    private String resolveStatusValue(String valueType, String fixedValue,
                                       Map<String, Object> conditionalValue, Map<String, Object> item) {
        if (!"conditional".equals(valueType) || conditionalValue == null) return fixedValue;
        List<Map<String, Object>> conditions = (List<Map<String, Object>>) conditionalValue.get("conditions");
        if (conditions == null) return str(conditionalValue.get("default_value"), fixedValue);

        for (Map<String, Object> cond : conditions) {
            String actual = str(item.get(str(cond.get("when_column"), "")), "");
            String expected = str(cond.get("when_value"), "");
            String op = str(cond.get("operator"), "=");
            boolean match;
            switch (op) {
                case "!=": match = !actual.equals(expected); break;
                case ">":  match = toDouble(actual) > toDouble(expected); break;
                case "<":  match = toDouble(actual) < toDouble(expected); break;
                case ">=": match = toDouble(actual) >= toDouble(expected); break;
                case "<=": match = toDouble(actual) <= toDouble(expected); break;
                default:   match = actual.equals(expected);
            }
            if (match) return str(cond.get("then_value"), "");
        }
        return str(conditionalValue.get("default_value"), fixedValue);
    }

    @SuppressWarnings("unchecked")
    private Object resolveHiddenValue(Map<String, Object> hm, Map<String, Object> item, Map<String, Object> fieldValues) {
        String valueSource = str(hm.get("value_source"), "");
        if ("static".equals(valueSource)) return hm.get("static_value");
        if ("json_extract".equals(valueSource)) {
            String srcCol = str(hm.get("source_json_column"), "");
            String srcKey = str(hm.get("source_json_key"), "");
            Object jsonCol = item.get(srcCol);
            if (jsonCol instanceof Map) return ((Map<String, Object>) jsonCol).get(srcKey);
            return null;
        }
        if ("db_column".equals(valueSource)) {
            String srcDbCol = str(hm.get("source_db_column"), "");
            Object v = item.get(srcDbCol);
            return v != null ? v : fieldValues.get(srcDbCol);
        }
        return null;
    }

    private List<Object> collectLookupValues(List<Map<String, Object>> items, String itemField) {
        List<Object> result = new ArrayList<>();
        String alt = itemField.replace("__cart_", "");
        for (Map<String, Object> item : items) {
            Object v = item.get(itemField);
            if (v == null) v = item.get(alt);
            if (v != null) result.add(v);
        }
        return result;
    }

    private Map<String, Object> codeEntry(String col, String code, boolean showModal) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("target_column", col);
        m.put("code", code);
        m.put("show_result_modal", showModal);
        return m;
    }

    /** 쉼표로 구분된 ? 플레이스홀더 생성 */
    private String repeat(String token, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) { if (i > 0) sb.append(", "); sb.append(token); }
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> safeList(Object raw) {
        return raw instanceof List ? (List<Map<String, Object>>) raw : new ArrayList<>();
    }

    private String str(Object value, String defaultValue) {
        return value != null ? value.toString() : defaultValue;
    }

    private double toDouble(String s) {
        try { return Double.parseDouble(s); } catch (Exception e) { return 0.0; }
    }
}
