package com.erp.service;

import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.*;
import java.util.stream.Collectors;

/**
 * 노드 플로우 서비스
 *
 * - CRUD : NodeFlowMapper (MyBatis XML)
 * - Execute : JdbcTemplate 동적 SQL (런타임에 결정되는 테이블/컬럼 — XML 불가)
 * - 감사 로그 : AuditLogService 위임
 */
@Service
@Slf4j
public class NodeFlowService extends BaseService {

    @Autowired private AuditLogService    auditLogService;
    @Autowired private ObjectMapper       objectMapper;
    @Autowired private JdbcTemplate       jdbcTemplate;

    // ══════════════════════════════════════════════════════════════
    // 목록 조회 (topology summary 포함, flow_data 제외)
    // ══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getFlowList(String companyCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("company_code", companyCode);

        List<Map<String, Object>> flows = sqlSession.selectList("nodeFlow.getNodeFlowList", params);

        // flow_data 없이 summary 추가 (Node 동일 로직)
        return flows.stream().map(flow -> {
            Map<String, Object> result = new LinkedHashMap<>(flow);
            result.put("summary", Map.of("node_count", 0, "edge_count", 0,
                    "node_types", Map.of(), "topology", null));
            return result;
        }).collect(Collectors.toList());
    }

    // ══════════════════════════════════════════════════════════════
    // 상세 조회
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getFlowById(int flowId, String companyCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("flow_id",      flowId);
        params.put("company_code", companyCode);

        Map<String, Object> flow = sqlSession.selectOne("nodeFlow.getNodeFlowInfo", params);
        if (flow == null) return null;

        // flowData JSONB → Java 객체
        parseFlowDataField(flow, "flow_data");
        return flow;
    }

    // ══════════════════════════════════════════════════════════════
    // 생성
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> createFlow(Map<String, Object> params) {
        String companyCode = (String) params.get("company_code");
        String flowName    = (String) params.get("flow_name");

        if (flowName == null || params.get("flow_data") == null) {
            throw new IllegalArgumentException("플로우 이름과 데이터는 필수입니다.");
        }

        // flowDescription null 방지
        params.putIfAbsent("flow_description", "");

        // flowData 직렬화
        serializeFlowData(params);

        sqlSession.insert("nodeFlow.insertNodeFlow", params); // useGeneratedKeys → params.get("flow_id") 주입

        Object flowId = params.get("flow_id");
        log.info("플로우 저장 성공: {} (회사: {})", flowId, companyCode);

        // 감사 로그
        insertAuditLog(companyCode, (String) params.get("user_id"),
                (String) params.get("user_name"),
                "CREATE", String.valueOf(flowId), flowName,
                "노드 플로우 \"" + flowName + "\" 생성",
                Map.of("after", Map.of("flow_name", flowName,
                        "flow_description", params.getOrDefault("flow_description", ""))),
                null);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("flow_id", flowId);
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // 수정
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> updateFlow(Map<String, Object> params) {
        Object flowId   = params.get("flow_id");
        String flowName = (String) params.get("flow_name");

        if (flowId == null || flowName == null || params.get("flow_data") == null) {
            throw new IllegalArgumentException("플로우 ID, 이름, 데이터는 필수입니다.");
        }

        // 수정 전 기존 값 조회 (감사 로그용)
        Map<String, Object> oldFlow = sqlSession.selectOne("nodeFlow.getNodeFlowForAudit",
                Map.of("flow_id", toInt(flowId, 0)));

        // flowDescription null 방지
        params.putIfAbsent("flow_description", "");

        // flowData 직렬화
        serializeFlowData(params);

        sqlSession.update("nodeFlow.updateNodeFlow", params);
        log.info("플로우 수정 성공: {}", flowId);

        // 감사 로그
        String companyCode = (String) params.get("company_code");
        Map<String, Object> changes = new LinkedHashMap<>();
        if (oldFlow != null) changes.put("before", Map.of(
                "flow_name", oldFlow.getOrDefault("flow_name", ""),
                "flow_description", oldFlow.getOrDefault("flow_description", "")));
        changes.put("after", Map.of("flow_name", flowName,
                "flow_description", params.getOrDefault("flow_description", "")));

        insertAuditLog(companyCode, (String) params.get("user_id"),
                (String) params.get("user_name"),
                "UPDATE", String.valueOf(flowId), flowName,
                "노드 플로우 \"" + flowName + "\" 수정", changes, null);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("flow_id", flowId);
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // 삭제
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public void deleteFlow(int flowId, String companyCode, String userId, String userName) {
        // 삭제 전 기존 값 조회 (감사 로그용)
        Map<String, Object> oldFlow = sqlSession.selectOne("nodeFlow.getNodeFlowForAudit", Map.of("flow_id", flowId));

        sqlSession.delete("nodeFlow.deleteNodeFlow", Map.of("flow_id", flowId));
        log.info("플로우 삭제 성공: {}", flowId);

        String flowName = oldFlow != null
                ? (String) oldFlow.getOrDefault("flow_name", "ID:" + flowId)
                : "ID:" + flowId;

        Map<String, Object> changes = new LinkedHashMap<>();
        if (oldFlow != null) changes.put("before", Map.of(
                "flow_name", oldFlow.getOrDefault("flow_name", ""),
                "flow_description", oldFlow.getOrDefault("flow_description", "")));

        insertAuditLog(companyCode, userId, userName,
                "DELETE", String.valueOf(flowId), flowName,
                "노드 플로우 \"" + flowName + "\" 삭제", changes, null);
    }

    // ══════════════════════════════════════════════════════════════
    // 소스 테이블 조회
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getSourceTable(int flowId) {
        Map<String, Object> row = sqlSession.selectOne("nodeFlow.getNodeFlowData", Map.of("flow_id", flowId));
        if (row == null) return null;

        Map<String, Object> flowData = parseJsonToMap(row.get("flow_data"));
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> nodes = (List<Map<String, Object>>) flowData.getOrDefault("nodes", List.of());

        // tableSource 또는 externalDBSource 타입의 첫 노드 탐색
        Map<String, Object> sourceNode = nodes.stream()
                .filter(n -> "table_source".equals(n.get("type")) || "external_db_source".equals(n.get("type")))
                .findFirst().orElse(null);

        Map<String, Object> result = new LinkedHashMap<>();
        if (sourceNode == null) {
            result.put("source_table",    null);
            result.put("source_node_type", null);
            result.put("message",        "소스 노드가 없거나 테이블명이 설정되지 않았습니다.");
            return result;
        }

        @SuppressWarnings("unchecked")
        Map<String, Object> nodeData = (Map<String, Object>) sourceNode.getOrDefault("data", Map.of());
        String tableName = (String) nodeData.get("table_name");

        if (tableName == null) {
            result.put("source_table",    null);
            result.put("source_node_type", null);
            result.put("message",        "소스 노드가 없거나 테이블명이 설정되지 않았습니다.");
            return result;
        }

        log.info("플로우 소스 테이블 조회: flowId={}, table={}", flowId, tableName);

        result.put("source_table",    tableName);
        result.put("source_node_type", sourceNode.get("type"));
        result.put("source_node_id",   sourceNode.get("id"));
        result.put("display_name",    nodeData.get("display_name"));
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // 플로우 실행 (위상 정렬 + 레벨별 실행)
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> executeFlow(int flowId, Map<String, Object> contextData) {
        long startTime = System.currentTimeMillis();

        log.info("플로우 실행 시작: flowId={}", flowId);

        Map<String, Object> row = sqlSession.selectOne("nodeFlow.getNodeFlowData", Map.of("flow_id", flowId));
        if (row == null) throw new IllegalArgumentException("플로우를 찾을 수 없습니다: flowId=" + flowId);

        Map<String, Object> flowData = parseJsonToMap(row.get("flow_data"));

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> nodes = (List<Map<String, Object>>) flowData.getOrDefault("nodes", List.of());
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> edges = (List<Map<String, Object>>) flowData.getOrDefault("edges", List.of());

        log.info("플로우 정보: flowName={}, nodes={}, edges={}",
                row.get("flow_name"), nodes.size(), edges.size());

        // ── 위상 정렬 (레벨별)
        List<List<String>> levels = topologicalSort(nodes, edges);
        log.info("실행 순서 (레벨별): {}", levels);

        // ── 실행 컨텍스트
        Map<String, Object> nodeResults  = new LinkedHashMap<>(); // nodeId → result map
        List<String> executionOrder = new ArrayList<>();

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> sourceData = contextData.get("source_data") instanceof List
                ? (List<Map<String, Object>>) contextData.get("source_data")
                : List.of();

        String companyCode = (String) contextData.getOrDefault("company_code",
                contextData.getOrDefault("context.company_code", "*"));
        String userId = (String) contextData.getOrDefault("user_id",
                contextData.getOrDefault("context.user_id", "system"));

        // ── 레벨별 순차 실행 (레벨 내부는 독립 실행 — 실패 시 전체 롤백)
        List<String> failedNodes = new ArrayList<>();

        for (List<String> level : levels) {
            for (String nodeId : level) {
                Map<String, Object> nodeInfo = findNode(nodes, nodeId);
                if (nodeInfo == null) continue;

                String nodeType  = (String) nodeInfo.get("type");
                String nodeName  = getNodeDisplayName(nodeInfo, nodeId);

                // 부모 실패 시 스킵
                List<String> parents = getParentIds(nodeId, edges);
                boolean parentFailed = parents.stream()
                        .anyMatch(pid -> "failed".equals(getNodeResultStatus(nodeResults, pid)));

                if (parentFailed) {
                    log.warn("노드 스킵 (부모 실패): {}", nodeId);
                    setNodeResult(nodeResults, nodeId, "skipped", null, null);
                    executionOrder.add(nodeId);
                    continue;
                }

                // 입력 데이터 수집
                Object inputData = prepareInputData(nodeId, parents, edges, nodeResults, sourceData);

                // 노드 실행
                try {
                    Object result = executeNode(nodeInfo, inputData, companyCode, userId, nodeResults, edges);
                    setNodeResult(nodeResults, nodeId, "success", result, null);
                    executionOrder.add(nodeId);
                    log.info("노드 실행 성공: {} ({})", nodeId, nodeType);
                } catch (Exception e) {
                    log.error("노드 실행 실패: {} ({}) — {}", nodeId, nodeType, e.getMessage());
                    setNodeResult(nodeResults, nodeId, "failed", null, e.getMessage());
                    executionOrder.add(nodeId);
                    failedNodes.add(nodeId);
                }
            }
        }

        // 액션 노드 실패 시 트랜잭션 롤백
        boolean hasFailedActionNode = failedNodes.stream().anyMatch(nid -> {
            Map<String, Object> n = findNode(nodes, nid);
            return n != null && isActionNode((String) n.get("type"));
        });
        if (hasFailedActionNode) {
            throw new RuntimeException("액션 노드 실패: " + failedNodes);
        }

        // ── 결과 생성
        long executionTime = System.currentTimeMillis() - startTime;
        return buildExecutionResult(nodes, nodeResults, executionOrder, executionTime);
    }

    // ══════════════════════════════════════════════════════════════
    // 실행 엔진 - 위상 정렬
    // ══════════════════════════════════════════════════════════════

    private List<List<String>> topologicalSort(List<Map<String, Object>> nodes,
                                                List<Map<String, Object>> edges) {
        Map<String, Integer> inDegree   = new HashMap<>();
        Map<String, List<String>> adj   = new HashMap<>();

        for (Map<String, Object> node : nodes) {
            String id = (String) node.get("id");
            inDegree.put(id, 0);
            adj.put(id, new ArrayList<>());
        }
        for (Map<String, Object> edge : edges) {
            String src = (String) edge.get("source");
            String tgt = (String) edge.get("target");
            if (src != null && tgt != null) {
                inDegree.merge(tgt, 1, Integer::sum);
                adj.computeIfAbsent(src, k -> new ArrayList<>()).add(tgt);
            }
        }

        List<List<String>> levels = new ArrayList<>();
        List<String> current = nodes.stream()
                .map(n -> (String) n.get("id"))
                .filter(id -> inDegree.getOrDefault(id, 0) == 0)
                .collect(Collectors.toList());

        while (!current.isEmpty()) {
            levels.add(new ArrayList<>(current));
            List<String> next = new ArrayList<>();
            for (String nid : current) {
                for (String nbr : adj.getOrDefault(nid, List.of())) {
                    int deg = inDegree.merge(nbr, -1, Integer::sum);
                    if (deg == 0) next.add(nbr);
                }
            }
            current = next;
        }
        return levels;
    }

    // ══════════════════════════════════════════════════════════════
    // 실행 엔진 - 노드 실행
    // ══════════════════════════════════════════════════════════════

    @SuppressWarnings("unchecked")
    private Object executeNode(Map<String, Object> node, Object inputData,
                                String companyCode, String userId,
                                Map<String, Object> nodeResults,
                                List<Map<String, Object>> edges) {
        String type     = (String) node.get("type");
        Map<String, Object> data = (Map<String, Object>) node.getOrDefault("data", Map.of());

        return switch (type) {
            case "table_source", "external_db_source" ->
                    executeTableSource(data, inputData);

            case "insert_action" ->
                    executeInsertAction(data, inputData, companyCode, userId);

            case "update_action" ->
                    executeUpdateAction(data, inputData, companyCode, userId);

            case "delete_action" ->
                    executeDeleteAction(data, inputData);

            case "upsert_action" ->
                    executeUpsertAction(data, inputData, companyCode, userId);

            case "condition" ->
                    executeCondition(data, inputData);

            case "data_transform", "aggregate", "formula_transform" ->
                    // 변환 노드: 입력 데이터 그대로 통과 (단순 pass-through)
                    inputData;

            case "comment", "log" -> {
                log.info("{} 노드: {}", type, data.getOrDefault("display_name", node.get("id")));
                yield Map.of("message", "Logged");
            }

            default -> {
                log.warn("지원하지 않는 노드 타입: {}", type);
                yield Map.of("message", "Unsupported node type: " + type);
            }
        };
    }

    // ─── Source ──────────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private Object executeTableSource(Map<String, Object> data, Object inputData) {
        String dataSourceType = (String) data.getOrDefault("data_source_type", "context-data");
        if ("table-all".equals(dataSourceType)) {
            String tableName = (String) data.get("table_name");
            if (tableName == null || tableName.isBlank()) return List.of();
            String safeName = sanitize(tableName);
            List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                    "SELECT * FROM \"" + safeName + "\"");
            log.info("테이블 소스 전체 조회: {}, {}건", safeName, rows.size());
            return rows;
        }
        // context-data: 입력 데이터 반환
        if (inputData instanceof List) return inputData;
        return List.of();
    }

    // ─── INSERT ──────────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private Object executeInsertAction(Map<String, Object> data, Object inputData,
                                        String companyCode, String userId) {
        String tableName    = (String) data.get("target_table");
        List<Map<String, Object>> fieldMappings =
                (List<Map<String, Object>>) data.getOrDefault("field_mappings", List.of());

        if (tableName == null || fieldMappings.isEmpty()) return List.of();

        String safeTable = sanitize(tableName);
        List<Map<String, Object>> dataArray = toList(inputData);
        List<Map<String, Object>> inserted  = new ArrayList<>();

        for (Map<String, Object> row : dataArray) {
            List<String> cols   = new ArrayList<>();
            List<Object> values = new ArrayList<>();

            for (Map<String, Object> mapping : fieldMappings) {
                String targetField = (String) mapping.get("target_field");
                Object value;
                if (mapping.get("static_value") != null) {
                    value = mapping.get("static_value");
                } else {
                    value = row.get(mapping.get("source_field"));
                }
                cols.add("\"" + sanitize(targetField) + "\"");
                values.add(value);
            }
            // company_code 자동 추가
            if (cols.stream().noneMatch(c -> c.contains("company_code"))
                    && companyCode != null && !"*".equals(companyCode)) {
                cols.add("company_code");
                values.add(companyCode);
            }
            // writer 자동 추가
            if (cols.stream().noneMatch(c -> c.contains("writer")) && userId != null) {
                cols.add("writer");
                values.add(userId);
            }

            String colStr = String.join(", ", cols);
            String phStr  = cols.stream().map(c -> "?").collect(Collectors.joining(", "));
            String sql    = "INSERT INTO \"" + safeTable + "\" (" + colStr + ") VALUES (" + phStr + ")";

            jdbcTemplate.update(sql, values.toArray());
            inserted.add(row);
        }

        log.info("INSERT 완료: {}, {}건", safeTable, inserted.size());
        return inserted;
    }

    // ─── UPDATE ──────────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private Object executeUpdateAction(Map<String, Object> data, Object inputData,
                                        String companyCode, String userId) {
        String tableName    = (String) data.get("target_table");
        List<Map<String, Object>> fieldMappings =
                (List<Map<String, Object>>) data.getOrDefault("field_mappings", List.of());

        if (tableName == null) return List.of();

        String safeTable = sanitize(tableName);
        List<Map<String, Object>> dataArray = toList(inputData);
        int count = 0;

        for (Map<String, Object> row : dataArray) {
            // PK 컬럼 탐색 (fieldMappings에서 isPrimaryKey=true 인 것 우선, 없으면 id)
            String pkCol   = findPkColumn(fieldMappings);
            Object pkValue = row.get(pkCol);
            if (pkValue == null) { log.warn("UPDATE 스킵: PK 없음 ({})", pkCol); continue; }

            List<String> setCols   = new ArrayList<>();
            List<Object> setValues = new ArrayList<>();

            for (Map<String, Object> mapping : fieldMappings) {
                String targetField = (String) mapping.get("target_field");
                if (targetField.equals(pkCol)) continue; // PK 제외
                Object value = mapping.get("static_value") != null
                        ? mapping.get("static_value")
                        : row.get(mapping.get("source_field"));
                setCols.add("\"" + sanitize(targetField) + "\" = ?");
                setValues.add(value);
            }
            if (setCols.isEmpty()) continue;

            setValues.add(pkValue);
            String sql = "UPDATE \"" + safeTable + "\" SET " + String.join(", ", setCols)
                    + " WHERE \"" + sanitize(pkCol) + "\" = ?";
            jdbcTemplate.update(sql, setValues.toArray());
            count++;
        }

        log.info("UPDATE 완료: {}, {}건", safeTable, count);
        return dataArray;
    }

    // ─── DELETE ──────────────────────────────────────────────────

    private Object executeDeleteAction(Map<String, Object> data, Object inputData) {
        String tableName = (String) data.get("target_table");
        if (tableName == null) return List.of();

        String safeTable = sanitize(tableName);
        List<Map<String, Object>> dataArray = toList(inputData);
        int count = 0;

        for (Map<String, Object> row : dataArray) {
            Object id = row.get("id");
            if (id == null) continue;
            jdbcTemplate.update("DELETE FROM \"" + safeTable + "\" WHERE id = ?", id);
            count++;
        }

        log.info("DELETE 완료: {}, {}건", safeTable, count);
        return dataArray;
    }

    // ─── UPSERT ──────────────────────────────────────────────────

    private Object executeUpsertAction(Map<String, Object> data, Object inputData,
                                        String companyCode, String userId) {
        try {
            return executeInsertAction(data, inputData, companyCode, userId);
        } catch (Exception e) {
            log.info("UPSERT: INSERT 실패, UPDATE 시도. 원인: {}", e.getMessage());
            return executeUpdateAction(data, inputData, companyCode, userId);
        }
    }

    // ─── CONDITION ───────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private Object executeCondition(Map<String, Object> data, Object inputData) {
        // 조건 노드: 입력 데이터를 그대로 통과 (조건 분기는 edge sourceHandle로 처리)
        // 단순 pass-through 구현 (Node의 복잡한 조건 평가는 미구현)
        List<Map<String, Object>> all  = toList(inputData);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("condition_result", true);
        result.put("true_data",        all);
        result.put("false_data",       List.of());
        result.put("all_data",         all);
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // 실행 엔진 - 결과 생성
    // ══════════════════════════════════════════════════════════════

    private Map<String, Object> buildExecutionResult(
            List<Map<String, Object>> nodes,
            Map<String, Object> nodeResults,
            List<String> executionOrder,
            long executionTime) {

        List<Map<String, Object>> nodeSummaries = new ArrayList<>();
        int total = 0, success = 0, failed = 0, skipped = 0;

        for (String nodeId : executionOrder) {
            Map<String, Object> nodeInfo = findNode(nodes, nodeId);
            if (nodeInfo == null) continue;

            @SuppressWarnings("unchecked")
            Map<String, Object> nr = (Map<String, Object>) nodeResults.get(nodeId);
            String status = nr != null ? (String) nr.get("status") : "pending";

            Map<String, Object> summary = new LinkedHashMap<>();
            summary.put("node_id",   nodeId);
            summary.put("node_name", getNodeDisplayName(nodeInfo, nodeId));
            summary.put("node_type", nodeInfo.get("type"));
            summary.put("status",   status);
            if (nr != null && nr.get("error") != null) summary.put("error", nr.get("error"));
            nodeSummaries.add(summary);

            total++;
            if ("success".equals(status)) success++;
            else if ("failed".equals(status)) failed++;
            else if ("skipped".equals(status)) skipped++;
        }

        Map<String, Object> summaryMap = new LinkedHashMap<>();
        summaryMap.put("total",   total);
        summaryMap.put("success", success);
        summaryMap.put("failed",  failed);
        summaryMap.put("skipped", skipped);

        boolean allOk = failed == 0;
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success",       allOk);
        result.put("message",       allOk ? "플로우 실행이 완료되었습니다." : "플로우 실행 중 오류가 발생했습니다.");
        result.put("execution_time", executionTime);
        result.put("nodes",         nodeSummaries);
        result.put("summary",       summaryMap);
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // Private helpers
    // ══════════════════════════════════════════════════════════════

    private Object prepareInputData(String nodeId, List<String> parents,
                                     List<Map<String, Object>> edges,
                                     Map<String, Object> nodeResults,
                                     List<Map<String, Object>> sourceData) {
        if (parents.isEmpty()) return sourceData;
        if (parents.size() == 1) {
            String pid = parents.get(0);
            @SuppressWarnings("unchecked")
            Map<String, Object> pr = (Map<String, Object>) nodeResults.get(pid);
            Object data = pr != null ? pr.get("data") : sourceData;

            // 조건 노드 출력 핸들 처리
            Map<String, Object> edge = edges.stream()
                    .filter(e -> pid.equals(e.get("source")) && nodeId.equals(e.get("target")))
                    .findFirst().orElse(null);
            if (edge != null && data instanceof Map && ((Map<?, ?>) data).containsKey("condition_result")) {
                @SuppressWarnings("unchecked")
                Map<String, Object> condData = (Map<String, Object>) data;
                String handle = (String) edge.get("source_handle");
                if ("true".equals(handle))  return condData.getOrDefault("true_data",  List.of());
                if ("false".equals(handle)) return condData.getOrDefault("false_data", List.of());
                return condData.getOrDefault("all_data", data);
            }
            return data != null ? data : sourceData;
        }
        // 다중 부모 병합
        List<Object> merged = new ArrayList<>();
        for (String pid : parents) {
            @SuppressWarnings("unchecked")
            Map<String, Object> pr = (Map<String, Object>) nodeResults.get(pid);
            Object data = pr != null ? pr.get("data") : null;
            if (data instanceof List) merged.addAll((List<?>) data);
            else if (data != null)    merged.add(data);
        }
        return merged;
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> toList(Object data) {
        if (data instanceof List) return (List<Map<String, Object>>) data;
        if (data instanceof Map)  return List.of((Map<String, Object>) data);
        return List.of();
    }

    private List<String> getParentIds(String nodeId, List<Map<String, Object>> edges) {
        return edges.stream()
                .filter(e -> nodeId.equals(e.get("target")))
                .map(e -> (String) e.get("source"))
                .collect(Collectors.toList());
    }

    private Map<String, Object> findNode(List<Map<String, Object>> nodes, String nodeId) {
        return nodes.stream().filter(n -> nodeId.equals(n.get("id"))).findFirst().orElse(null);
    }

    @SuppressWarnings("unchecked")
    private String getNodeDisplayName(Map<String, Object> node, String fallback) {
        Map<String, Object> data = (Map<String, Object>) node.getOrDefault("data", Map.of());
        Object name = data.get("display_name");
        return name != null ? name.toString() : fallback;
    }

    private boolean isActionNode(String type) {
        return type != null && (type.endsWith("Action") || type.contains("Action"));
    }

    private void setNodeResult(Map<String, Object> results, String nodeId,
                                String status, Object data, String error) {
        Map<String, Object> r = new LinkedHashMap<>();
        r.put("status", status);
        if (data  != null) r.put("data",  data);
        if (error != null) r.put("error", error);
        results.put(nodeId, r);
    }

    private String getNodeResultStatus(Map<String, Object> results, String nodeId) {
        @SuppressWarnings("unchecked")
        Map<String, Object> r = (Map<String, Object>) results.get(nodeId);
        return r != null ? (String) r.get("status") : "pending";
    }

    @SuppressWarnings("unchecked")
    private String findPkColumn(List<Map<String, Object>> fieldMappings) {
        return fieldMappings.stream()
                .filter(m -> Boolean.TRUE.equals(m.get("is_primary_key")))
                .map(m -> (String) m.get("target_field"))
                .findFirst().orElse("id");
    }

    private String sanitize(String name) {
        if (name == null) return "";
        return name.replaceAll("[^a-zA-Z0-9_]", "");
    }

    private void parseFlowDataField(Map<String, Object> row, String fieldName) {
        Object raw = row.get(fieldName);
        if (raw == null || raw instanceof Map || raw instanceof List) return;
        try {
            row.put(fieldName, objectMapper.readValue(raw.toString(), Object.class));
        } catch (Exception e) {
            log.warn("flowData 파싱 실패: {}", e.getMessage());
        }
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> parseJsonToMap(Object raw) {
        if (raw == null) return Map.of();
        if (raw instanceof Map) return (Map<String, Object>) raw;
        try {
            return objectMapper.readValue(raw.toString(), Map.class);
        } catch (Exception e) {
            log.warn("JSON→Map 파싱 실패: {}", e.getMessage());
            return Map.of();
        }
    }

    private void serializeFlowData(Map<String, Object> params) {
        Object flowData = params.get("flow_data");
        if (flowData == null) return;
        if (flowData instanceof String) return; // 이미 문자열
        try {
            params.put("flow_data", objectMapper.writeValueAsString(flowData));
        } catch (Exception e) {
            log.warn("flowData 직렬화 실패: {}", e.getMessage());
            params.put("flow_data", "{}");
        }
    }

    private void insertAuditLog(String companyCode, String userId, String userName,
                                 String action, String resourceId, String resourceName,
                                 String summary, Object changes, String ipAddress) {
        try {
            Map<String, Object> params = new LinkedHashMap<>();
            params.put("company_code",   companyCode);
            params.put("user_id",        userId);
            params.put("user_name",      userName);
            params.put("action",        action);
            params.put("resource_type",  "NODE_FLOW");
            params.put("resource_id",    resourceId);
            params.put("resource_name",  resourceName);
            params.put("table_name",     "node_flows");
            params.put("summary",       summary);
            params.put("changes",       changes);
            params.put("ip_address",     ipAddress);
            auditLogService.insertAuditLog(params);
        } catch (Exception e) {
            log.warn("감사 로그 삽입 실패: {}", e.getMessage());
        }
    }

    private int toInt(Object val, int def) {
        if (val instanceof Number) return ((Number) val).intValue();
        try { return Integer.parseInt(String.valueOf(val)); } catch (Exception e) { return def; }
    }
}
