package com.erp.service;

import com.erp.common.BaseService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

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

    private static final String NS = "screenGroup.";

    /**
     * canonical table / legacy table-list / hidden v2-table-list 위젯 카운트 합산.
     * screen type inference 시 셋 모두 grid 화면으로 인식해야 한다 (frontend
     * isTableLikeComponentType 와 동일 정책 — 2026-05-19 canonical cleanup follow-up).
     */
    private static int countTableLikeWidgets(Map<String, Integer> widgetCounts) {
        if (widgetCounts == null) return 0;
        return widgetCounts.getOrDefault("table", 0)
             + widgetCounts.getOrDefault("table-list", 0)
             + widgetCounts.getOrDefault("v2-table-list", 0);
    }

    // ══════════════════════════════════════════════════════════════
    // Screen Groups
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getScreenGroups(Map<String, Object> params) {
        int page = toInt(params.getOrDefault("page", 1));
        int size = toInt(params.getOrDefault("size", 20));
        params.put("limit", size);
        params.put("offset", (page - 1) * size);

        int total  = sqlSession.selectOne(NS + "countScreenGroups", params);
        List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectScreenGroups", params);

        // screens 조립 (별도 쿼리)
        if (!groups.isEmpty()) {
            List<Object> groupIds = groups.stream()
                    .map(g -> g.get("id"))
                    .collect(Collectors.toList());
            Map<String, Object> screenParams = new HashMap<>();
            screenParams.put("group_ids", groupIds);
            List<Map<String, Object>> allScreens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", screenParams);
            Map<Object, List<Map<String, Object>>> byGroup = allScreens.stream()
                    .collect(Collectors.groupingBy(s -> s.get("group_id")));
            for (Map<String, Object> g : groups) {
                g.put("screens", byGroup.getOrDefault(g.get("id"), Collections.emptyList()));
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", true);
        result.put("data", groups);
        result.put("total", total);
        result.put("page", page);
        result.put("size", size);
        result.put("total_pages", (int) Math.ceil((double) total / size));
        return result;
    }

    public Map<String, Object> getScreenGroup(Map<String, Object> params) {
        Map<String, Object> group = sqlSession.selectOne(NS + "selectScreenGroupById", params);
        if (group == null) return null;

        List<Object> groupIds = Collections.singletonList(group.get("id"));
        Map<String, Object> sp = new HashMap<>();
        sp.put("group_ids", groupIds);
        List<Map<String, Object>> screens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", sp);
        group.put("screens", screens);
        return group;
    }

    @Transactional
    public Map<String, Object> createScreenGroup(Map<String, Object> params) {
        // 부모 그룹 계층 계산
        Object parentGroupId = params.get("parent_group_id");
        int groupLevel = 0;
        String parentHierarchyPath = "";
        if (parentGroupId != null) {
            Map<String, Object> pp = new HashMap<>();
            pp.put("parent_group_id", parentGroupId);
            Map<String, Object> parent = sqlSession.selectOne(NS + "selectParentGroupById", pp);
            if (parent != null) {
                groupLevel = toInt(parent.getOrDefault("group_level", 0)) + 1;
                parentHierarchyPath = (String) parent.getOrDefault("hierarchy_path",
                        "/" + parentGroupId + "/");
            }
        }
        params.put("group_level", groupLevel);

        sqlSession.insert(NS + "insertScreenGroup", params);
        Object newId = params.get("id");

        // hierarchy_path 업데이트
        String hierarchyPath;
        if (parentGroupId != null && !parentHierarchyPath.isEmpty()) {
            hierarchyPath = (parentHierarchyPath + newId + "/").replace("//", "/");
        } else {
            hierarchyPath = "/" + newId + "/";
        }
        Map<String, Object> hp = new HashMap<>();
        hp.put("id", newId);
        hp.put("hierarchy_path", hierarchyPath);
        sqlSession.update(NS + "updateScreenGroupHierarchyPath", hp);

        Map<String, Object> sp = new HashMap<>();
        sp.put("id", newId);
        return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
    }

    @Transactional
    public Map<String, Object> updateScreenGroup(Map<String, Object> params) {
        Object id = params.get("id");
        String userCompanyCode = (String) params.get("company_code");
        Object parentGroupId = params.get("parent_group_id");

        // 자기 자신을 부모로 지정하는 것 방지
        if (parentGroupId != null && toInt(parentGroupId) == toInt(id)) {
            throw new IllegalArgumentException("자기 자신을 상위 그룹으로 지정할 수 없습니다.");
        }

        // 부모 그룹 계층 재계산
        int groupLevel = 0;
        String hierarchyPath = "/" + id + "/";
        if (parentGroupId != null) {
            Map<String, Object> pp = new HashMap<>();
            pp.put("parent_group_id", parentGroupId);
            Map<String, Object> parent = sqlSession.selectOne(NS + "selectParentGroupById", pp);
            if (parent != null) {
                String parentPath = (String) parent.getOrDefault("hierarchy_path",
                        "/" + parentGroupId + "/");
                // 순환 참조 방지
                if (parentPath != null && parentPath.contains("/" + id + "/")) {
                    throw new IllegalArgumentException("하위 그룹을 상위 그룹으로 지정할 수 없습니다.");
                }
                groupLevel = toInt(parent.getOrDefault("group_level", 0)) + 1;
                hierarchyPath = (parentPath + id + "/").replace("//", "/");
            }
        }
        params.put("group_level", groupLevel);
        params.put("hierarchy_path", hierarchyPath);

        // 최고관리자가 회사 변경하는 경우
        Object targetCompanyCode = params.get("target_company_code");
        int rows;
        if ("*".equals(userCompanyCode) && targetCompanyCode != null) {
            rows = sqlSession.update(NS + "updateScreenGroupWithCompany", params);
        } else {
            rows = sqlSession.update(NS + "updateScreenGroup", params);
        }
        if (rows == 0) {
            throw new NoSuchElementException("화면 그룹을 찾을 수 없거나 권한이 없습니다.");
        }
        Map<String, Object> sp = new HashMap<>();
        sp.put("id", id);
        return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
    }

    @Transactional
    public void deleteScreenGroup(Map<String, Object> params) {
        Object id = params.get("id");
        String companyCode = (String) params.get("company_code");
        boolean deleteNumberingRules = Boolean.TRUE.equals(params.get("delete_numbering_rules"));

        // 대상 그룹의 company_code 확인
        Map<String, Object> target = sqlSession.selectOne(NS + "selectScreenGroupForDelete", params);
        if (target == null) {
            throw new NoSuchElementException("화면 그룹을 찾을 수 없습니다.");
        }
        String targetCompanyCode = (String) target.get("company_code");

        // 권한 체크
        if (!"*".equals(companyCode) && !companyCode.equals(targetCompanyCode)) {
            throw new SecurityException("권한이 없습니다.");
        }

        // 하위 그룹 ID 수집 (재귀)
        Map<String, Object> cp = new HashMap<>();
        cp.put("id", id);
        cp.put("target_company_code", targetCompanyCode);
        List<Map<String, Object>> children = sqlSession.selectList(NS + "selectAllChildGroupIds", cp);
        List<Object> groupIds = children.stream().map(c -> c.get("id")).collect(Collectors.toList());

        if (!groupIds.isEmpty()) {
            // 연결된 메뉴 조회 및 삭제
            Map<String, Object> mp = new HashMap<>();
            mp.put("group_ids", groupIds);
            mp.put("target_company_code", targetCompanyCode);
            List<Map<String, Object>> menus = sqlSession.selectList(NS + "selectMenusByGroupIds", mp);
            List<Object> menuObjids = menus.stream().map(m -> m.get("objid")).collect(Collectors.toList());

            if (!menuObjids.isEmpty()) {
                Map<String, Object> delp = new HashMap<>();
                delp.put("menu_objids", menuObjids);
                delp.put("target_company_code", targetCompanyCode);
                sqlSession.delete(NS + "deleteScreenMenuAssignmentsByMenuObjids", delp);
                sqlSession.delete(NS + "deleteMenusByGroupIds", mp);
            }

            // 채번 규칙 삭제 (최상위 그룹 + 명시 요청)
            if (deleteNumberingRules) {
                Map<String, Object> rp = new HashMap<>();
                rp.put("id", id);
                int isRoot = sqlSession.selectOne(NS + "isRootGroupById", rp);
                if (isRoot > 0) {
                    Map<String, Object> nrp = new HashMap<>();
                    nrp.put("target_company_code", targetCompanyCode);
                    sqlSession.delete(NS + "deleteNumberingRuleParts", nrp);
                    sqlSession.delete(NS + "deleteNumberingRules", nrp);
                }
            }
        }

        // 그룹 삭제
        int deleted = sqlSession.delete(NS + "deleteScreenGroupById", cp);
        if (deleted == 0) {
            throw new NoSuchElementException("화면 그룹을 찾을 수 없거나 권한이 없습니다.");
        }
    }

    // ══════════════════════════════════════════════════════════════
    // Screen Group Screens
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> addScreenToGroup(Map<String, Object> params) {
        sqlSession.insert(NS + "insertGroupScreen", params);
        // 삽입 후 조회
        List<Object> ids = Collections.singletonList(params.get("group_id"));
        Map<String, Object> sp = new HashMap<>();
        sp.put("group_ids", ids);
        List<Map<String, Object>> screens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", sp);
        return screens.stream()
                .filter(s -> Objects.equals(s.get("screen_id"), params.get("screen_id")))
                .findFirst()
                .orElse(params);
    }

    @Transactional
    public Map<String, Object> updateScreenInGroup(Map<String, Object> params) {
        int rows = sqlSession.update(NS + "updateGroupScreen", params);
        if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다.");
        return params;
    }

    @Transactional
    public void removeScreenFromGroup(Map<String, Object> params) {
        int rows = sqlSession.delete(NS + "deleteGroupScreen", params);
        if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다.");
    }

    // ══════════════════════════════════════════════════════════════
    // Field Joins
    // ══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getFieldJoins(Map<String, Object> params) {
        return sqlSession.selectList(NS + "selectFieldJoins", params);
    }

    @Transactional
    public Map<String, Object> createFieldJoin(Map<String, Object> params) {
        sqlSession.insert(NS + "insertFieldJoin", params);
        return params;
    }

    @Transactional
    public Map<String, Object> updateFieldJoin(Map<String, Object> params) {
        int rows = sqlSession.update(NS + "updateFieldJoin", params);
        if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다.");
        return params;
    }

    @Transactional
    public void deleteFieldJoin(Map<String, Object> params) {
        int rows = sqlSession.delete(NS + "deleteFieldJoin", params);
        if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다.");
    }

    // ══════════════════════════════════════════════════════════════
    // Data Flows
    // ══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getDataFlows(Map<String, Object> params) {
        return sqlSession.selectList(NS + "selectDataFlows", params);
    }

    @Transactional
    public Map<String, Object> createDataFlow(Map<String, Object> params) {
        // data_mapping을 JSON 문자열로 변환
        convertToJsonString(params, "data_mapping");
        sqlSession.insert(NS + "insertDataFlow", params);
        return params;
    }

    @Transactional
    public Map<String, Object> updateDataFlow(Map<String, Object> params) {
        convertToJsonString(params, "data_mapping");
        int rows = sqlSession.update(NS + "updateDataFlow", params);
        if (rows == 0) throw new NoSuchElementException("데이터 흐름을 찾을 수 없거나 권한이 없습니다.");
        return params;
    }

    @Transactional
    public void deleteDataFlow(Map<String, Object> params) {
        int rows = sqlSession.delete(NS + "deleteDataFlow", params);
        if (rows == 0) throw new NoSuchElementException("데이터 흐름을 찾을 수 없거나 권한이 없습니다.");
    }

    // ══════════════════════════════════════════════════════════════
    // Table Relations
    // ══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getTableRelations(Map<String, Object> params) {
        return sqlSession.selectList(NS + "selectTableRelations", params);
    }

    @Transactional
    public Map<String, Object> createTableRelation(Map<String, Object> params) {
        sqlSession.insert(NS + "insertTableRelation", params);
        return params;
    }

    @Transactional
    public Map<String, Object> updateTableRelation(Map<String, Object> params) {
        int rows = sqlSession.update(NS + "updateTableRelation", params);
        if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다.");
        return params;
    }

    @Transactional
    public void deleteTableRelation(Map<String, Object> params) {
        int rows = sqlSession.delete(NS + "deleteTableRelation", params);
        if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다.");
    }

    // ══════════════════════════════════════════════════════════════
    // Layout Summary
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getScreenLayoutSummary(Map<String, Object> params) {
        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectLayoutComponents", params);

        Map<String, Integer> widgetCounts = new LinkedHashMap<>();
        List<String> labels = new ArrayList<>();
        List<Map<String, Object>> fields = new ArrayList<>();

        for (Map<String, Object> row : rows) {
            String widgetType = row.get("widget_type") != null ? (String) row.get("widget_type") : "text";
            widgetCounts.merge(widgetType, 1, Integer::sum);

            String label = (String) row.get("label");
            if (label != null && !label.equals("기본 버튼")) {
                labels.add(label);
                Map<String, Object> field = new LinkedHashMap<>();
                field.put("label", label);
                field.put("widget_type", widgetType);
                field.put("field_name", row.get("field_name"));
                fields.add(field);
            }
        }

        // 화면 타입 추론
        // table-like (canonical 'table' / legacy 'table-list' / hidden 'v2-table-list')
        // 어느 것이든 있으면 grid 로 본다.
        String screenType = "form";
        if (countTableLikeWidgets(widgetCounts) > 0) {
            screenType = "grid";
        } else if (widgetCounts.getOrDefault("custom", 0) > 2) {
            screenType = "dashboard";
        } else if (widgetCounts.size() <= 2 && widgetCounts.getOrDefault("button", 0) > 0) {
            screenType = "action";
        }

        Map<String, Object> data = new LinkedHashMap<>();
        data.put("screen_id", toInt(params.get("screen_id")));
        data.put("screen_type", screenType);
        data.put("widget_counts", widgetCounts);
        data.put("total_components", rows.size());
        data.put("fields", fields.subList(0, Math.min(fields.size(), 10)));
        data.put("labels", labels.subList(0, Math.min(labels.size(), 8)));
        return data;
    }

    public Map<String, Object> getMultipleScreenLayoutSummary(List<Integer> screenIds) {
        if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>();

        Map<String, Object> params = new HashMap<>();
        params.put("screen_ids", screenIds);
        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectMultipleLayoutComponents", params);

        // 화면별 summary 초기화
        Map<Integer, Map<String, Object>> summaryMap = new LinkedHashMap<>();
        for (Integer sid : screenIds) {
            Map<String, Object> s = new LinkedHashMap<>();
            s.put("screen_id", sid);
            s.put("screen_type", "form");
            s.put("widget_counts", new LinkedHashMap<String, Integer>());
            s.put("total_components", 0);
            s.put("layout_items", new ArrayList<Map<String, Object>>());
            s.put("canvas_width", 0);
            s.put("canvas_height", 0);
            summaryMap.put(sid, s);
        }

        for (Map<String, Object> row : rows) {
            int sid = toInt(row.get("screen_id"));
            Map<String, Object> summary = summaryMap.get(sid);
            if (summary == null) continue;

            String componentKind = row.get("component_kind") != null
                    ? (String) row.get("component_kind")
                    : (row.get("widget_type") != null ? (String) row.get("widget_type") : "text");
            String widgetType = row.get("widget_type") != null ? (String) row.get("widget_type") : "text";

            @SuppressWarnings("unchecked")
            Map<String, Integer> wc = (Map<String, Integer>) summary.get("widget_counts");
            wc.merge(componentKind, 1, Integer::sum);
            summary.put("total_components", toInt(summary.get("total_components")) + 1);

            Map<String, Object> item = new LinkedHashMap<>();
            item.put("x", row.getOrDefault("position_x", 0));
            item.put("y", row.getOrDefault("position_y", 0));
            item.put("width", row.getOrDefault("width", 100));
            item.put("height", row.getOrDefault("height", 30));
            item.put("component_kind", componentKind);
            item.put("widget_type", widgetType);
            item.put("label", row.get("label"));
            item.put("bind_field", row.get("bind_field"));
            item.put("used_columns", new ArrayList<>());
            item.put("join_columns", new ArrayList<>());

            @SuppressWarnings("unchecked")
            List<Map<String, Object>> layoutItems = (List<Map<String, Object>>) summary.get("layout_items");
            layoutItems.add(item);

            int rightEdge = toInt(row.getOrDefault("position_x", 0)) + toInt(row.getOrDefault("width", 100));
            int bottomEdge = toInt(row.getOrDefault("position_y", 0)) + toInt(row.getOrDefault("height", 30));
            if (rightEdge > toInt(summary.get("canvas_width"))) summary.put("canvas_width", rightEdge);
            if (bottomEdge > toInt(summary.get("canvas_height"))) summary.put("canvas_height", bottomEdge);
        }

        // 화면 타입 추론 — canonical / legacy / hidden v2 모두 grid 로 인식
        summaryMap.values().forEach(summary -> {
            @SuppressWarnings("unchecked")
            Map<String, Integer> wc = (Map<String, Integer>) summary.get("widget_counts");
            if (countTableLikeWidgets(wc) > 0) {
                summary.put("screen_type", "grid");
            } else if (wc.getOrDefault("table-search-widget", 0) > 1) {
                summary.put("screen_type", "dashboard");
            } else if (toInt(summary.get("total_components")) <= 5 && wc.getOrDefault("button-primary", 0) > 0) {
                summary.put("screen_type", "action");
            }
        });

        Map<String, Object> result = new LinkedHashMap<>();
        summaryMap.forEach((k, v) -> result.put(String.valueOf(k), v));
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // Sub Tables
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getScreenSubTables(List<Integer> screenIds) {
        if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>();

        Map<String, Object> p = new HashMap<>();
        p.put("screen_ids", screenIds);

        // ── 1. 컴포넌트 config 기반 서브 테이블 수집 ─────────────────
        List<Map<String, Object>> compRows = sqlSession.selectList(NS + "selectSubTableComponentConfigs", p);

        // column label lookup 수집
        List<Map<String, Object>> columnPairs = new ArrayList<>();
        compRows.forEach(row -> {
            Object fms = row.get("field_mappings");
            if (fms instanceof List) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> fieldMappings = (List<Map<String, Object>>) fms;
                String mainTable = (String) row.get("main_table");
                String subTable  = (String) row.get("sub_table");
                fieldMappings.forEach(fm -> {
                    if (fm.get("source_field") != null && subTable != null) {
                        columnPairs.add(pairOf(subTable, (String) fm.get("source_field")));
                    }
                    if (fm.get("target_field") != null && mainTable != null) {
                        columnPairs.add(pairOf(mainTable, (String) fm.get("target_field")));
                    }
                });
            }
        });

        // column labels 조회
        Map<String, String> colLabelMap = new HashMap<>();
        if (!columnPairs.isEmpty()) {
            Map<String, Object> lp = new HashMap<>();
            lp.put("pairs", columnPairs);
            sqlSession.<Map<String, Object>>selectList(NS + "selectColumnLabelsByPairs", lp)
                    .forEach(r -> colLabelMap.put(r.get("table_name") + "." + r.get("column_name"),
                            (String) r.get("column_label")));
        }

        // screenSubTables 조립
        Map<Integer, Map<String, Object>> screenSubTables = new LinkedHashMap<>();
        compRows.forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String subTable  = (String) row.get("sub_table");
            if (subTable == null || subTable.equals(mainTable)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);

            @SuppressWarnings("unchecked")
            List<Map<String, Object>> subTables = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");
            boolean exists = subTables.stream().anyMatch(st -> subTable.equals(st.get("table_name")));
            if (exists) return;

            String componentType = (String) row.get("component_type");
            String relationType  = inferRelationType(componentType);
            List<Map<String, Object>> fieldMappings = buildFieldMappings(row, subTable, mainTable, colLabelMap);

            Map<String, Object> stEntry = new LinkedHashMap<>();
            stEntry.put("table_name", subTable);
            stEntry.put("component_type", componentType);
            stEntry.put("relation_type", relationType);
            if (fieldMappings != null) stEntry.put("field_mappings", fieldMappings);
            subTables.add(stEntry);
        });

        // ── 2. reference_table 기반 참조 서브 테이블 ──────────────────
        sqlSession.<Map<String, Object>>selectList(NS + "selectReferenceColumns", p).forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String refTable  = (String) row.get("reference_table");
            if (refTable == null || refTable.equals(mainTable)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> subTables = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");

            Map<String, Object> existing = subTables.stream()
                    .filter(st -> refTable.equals(st.get("table_name"))).findFirst().orElse(null);

            Map<String, Object> mapping = new LinkedHashMap<>();
            mapping.put("source_field", row.get("column_name"));
            mapping.put("target_field", row.getOrDefault("reference_column", "id"));
            mapping.put("source_display_name", row.getOrDefault("source_display_name", row.get("column_name")));
            mapping.put("target_display_name", row.getOrDefault("target_display_name", row.getOrDefault("reference_column", "id")));

            if (existing != null) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> fms = (List<Map<String, Object>>) existing.get("field_mappings");
                if (fms != null && fms.stream().noneMatch(fm -> row.get("column_name").equals(fm.get("source_field")))) {
                    fms.add(mapping);
                }
            } else {
                Map<String, Object> stEntry = new LinkedHashMap<>();
                stEntry.put("table_name", refTable);
                stEntry.put("component_type", "column_reference");
                stEntry.put("relation_type", "reference");
                List<Map<String, Object>> fms = new ArrayList<>();
                fms.add(mapping);
                stEntry.put("field_mappings", fms);
                subTables.add(stEntry);
            }
        });

        // ── 3. parentDataMapping ───────────────────────────────────
        sqlSession.<Map<String, Object>>selectList(NS + "selectParentDataMappingConfigs", p).forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String compType  = (String) row.get("component_type");
            Object pdm = row.get("parent_data_mapping");
            if (!(pdm instanceof List)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> subTables = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> mappings = (List<Map<String, Object>>) pdm;

            mappings.forEach(mapping -> {
                String sourceTable = (String) mapping.get("source_table");
                if (sourceTable == null || sourceTable.equals(mainTable)) return;

                Map<String, Object> newMapping = new LinkedHashMap<>();
                newMapping.put("source_table", sourceTable);
                newMapping.put("source_field", mapping.getOrDefault("source_field", ""));
                newMapping.put("target_field", mapping.getOrDefault("target_field", ""));
                newMapping.put("source_display_name", mapping.getOrDefault("source_field", ""));
                newMapping.put("target_display_name", mapping.getOrDefault("target_field", ""));

                Map<String, Object> existing = subTables.stream()
                        .filter(st -> sourceTable.equals(st.get("table_name"))).findFirst().orElse(null);
                if (existing != null) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> fms = (List<Map<String, Object>>) existing.computeIfAbsent("field_mappings", k -> new ArrayList<>());
                    String sf = (String) newMapping.get("source_field");
                    String tf = (String) newMapping.get("target_field");
                    boolean exists = fms.stream().anyMatch(fm ->
                            sf.equals(fm.get("source_field")) && tf.equals(fm.get("target_field")));
                    if (!exists) fms.add(newMapping);
                } else {
                    Map<String, Object> stEntry = new LinkedHashMap<>();
                    stEntry.put("table_name", sourceTable);
                    stEntry.put("component_type", compType);
                    stEntry.put("relation_type", "parent_mapping");
                    List<Map<String, Object>> fms = new ArrayList<>();
                    fms.add(newMapping);
                    stEntry.put("field_mappings", fms);
                    subTables.add(stEntry);
                }
            });
        });

        // ── 4. rightPanel.relation ─────────────────────────────────
        List<Map<String, Object>> rpRows = sqlSession.selectList(NS + "selectRightPanelRelations", p);
        // rightPanel columns에서 dot-notation 참조 테이블 수집
        Map<String, Set<String>> rpJoinedTables = new HashMap<>();
        rpRows.forEach(row -> {
            Object cols = row.get("right_panel_columns");
            String rpTable = (String) row.get("right_panel_table");
            if (cols instanceof List && rpTable != null) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> columns = (List<Map<String, Object>>) cols;
                int sid = toInt(row.get("screen_id"));
                String key = sid + "_" + rpTable;
                columns.forEach(col -> {
                    String colName = (String) col.getOrDefault("name",
                            col.getOrDefault("column_name", col.get("field")));
                    if (colName != null && colName.contains(".")) {
                        rpJoinedTables.computeIfAbsent(key, k -> new HashSet<>()).add(colName.split("\\.")[0]);
                    }
                });
            }
        });

        rpRows.forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String compType  = (String) row.get("component_type");
            Object relation  = row.get("right_panel_relation");
            String rpTable   = (String) row.get("right_panel_table");

            String subTable = rpTable;
            if (subTable == null && relation instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String, Object> rel = (Map<String, Object>) relation;
                subTable = (String) rel.getOrDefault("target_table", rel.get("table_name"));
            }
            if (subTable == null || subTable.equals(mainTable)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> subTables = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");

            String key = sid + "_" + subTable;
            List<String> joinedTables = rpJoinedTables.containsKey(key)
                    ? new ArrayList<>(rpJoinedTables.get(key)) : new ArrayList<>();

            List<Map<String, Object>> fieldMappings = new ArrayList<>();
            if (relation instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String, Object> rel = (Map<String, Object>) relation;
                if (rel.get("source_field") != null && rel.get("target_field") != null) {
                    Map<String, Object> fm = new LinkedHashMap<>();
                    fm.put("source_field", rel.get("source_field"));
                    fm.put("target_field", rel.get("target_field"));
                    fm.put("source_display_name", rel.get("source_field"));
                    fm.put("target_display_name", rel.get("target_field"));
                    fieldMappings.add(fm);
                }
                if (rel.get("field_mappings") instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> rfms = (List<Map<String, Object>>) rel.get("field_mappings");
                    rfms.forEach(rfm -> {
                        Map<String, Object> fm = new LinkedHashMap<>();
                        fm.put("source_field", rfm.getOrDefault("source_field", rfm.get("source_field")));
                        fm.put("target_field", rfm.getOrDefault("target_field", rfm.get("target_field")));
                        fm.put("source_display_name", fm.get("source_field"));
                        fm.put("target_display_name", fm.get("target_field"));
                        fieldMappings.add(fm);
                    });
                }
            }

            final String subTableFinal = subTable;
            Map<String, Object> existing = subTables.stream()
                    .filter(st -> subTableFinal.equals(st.get("table_name"))).findFirst().orElse(null);
            if (existing != null) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> fms = (List<Map<String, Object>>)
                        existing.computeIfAbsent("field_mappings", k -> new ArrayList<>());
                fieldMappings.forEach(fm -> {
                    boolean dup = fms.stream().anyMatch(e ->
                            Objects.equals(fm.get("source_field"), e.get("source_field")) &&
                            Objects.equals(fm.get("target_field"), e.get("target_field")));
                    if (!dup) fms.add(fm);
                });
            } else {
                Map<String, Object> stEntry = new LinkedHashMap<>();
                stEntry.put("table_name", subTable);
                stEntry.put("component_type", compType);
                stEntry.put("relation_type", "right_panel_relation");
                if (!joinedTables.isEmpty()) stEntry.put("joined_tables", joinedTables);
                if (!fieldMappings.isEmpty()) stEntry.put("field_mappings", fieldMappings);
                if (relation instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> rel = (Map<String, Object>) relation;
                    if (rel.get("type") != null)       stEntry.put("original_relation_type", rel.get("type"));
                    if (rel.get("foreign_key") != null)  stEntry.put("foreign_key", rel.get("foreign_key"));
                    if (rel.get("left_column") != null)  stEntry.put("left_column", rel.get("left_column"));
                }
                subTables.add(stEntry);
            }
        });

        // ── 5. FK 컬럼 조회 ───────────────────────────────────────
        Set<String> subTableNamesSet = new HashSet<>();
        Set<String> refTableNamesSet = new HashSet<>();
        screenSubTables.values().forEach(sd -> {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> sts = (List<Map<String, Object>>) sd.get("sub_tables");
            sts.forEach(st -> {
                Object jt = st.get("joined_tables");
                if (jt instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<String> joinedList = (List<String>) jt;
                    subTableNamesSet.add((String) st.get("table_name"));
                    refTableNamesSet.addAll(joinedList);
                }
            });
        });
        if (!subTableNamesSet.isEmpty() && !refTableNamesSet.isEmpty()) {
            Map<String, Object> fkp = new HashMap<>();
            fkp.put("sub_table_names", new ArrayList<>(subTableNamesSet));
            fkp.put("ref_table_names", new ArrayList<>(refTableNamesSet));
            Map<String, List<Map<String, Object>>> joinColRefs = new HashMap<>();
            sqlSession.<Map<String, Object>>selectList(NS + "selectFkColumnsForJoinedTables", fkp).forEach(row -> {
                String tbl = (String) row.get("table_name");
                joinColRefs.computeIfAbsent(tbl, k -> new ArrayList<>());
                String col = (String) row.get("column_name");
                String ref = (String) row.get("reference_table");
                boolean dup = joinColRefs.get(tbl).stream().anyMatch(r ->
                        col.equals(r.get("column")) && ref.equals(r.get("ref_table")));
                if (!dup) {
                    Map<String, Object> ref2 = new LinkedHashMap<>();
                    ref2.put("column", col);
                    ref2.put("column_label", row.getOrDefault("column_label", col));
                    ref2.put("ref_table", ref);
                    ref2.put("ref_table_label", row.getOrDefault("reference_table_label", ref));
                    ref2.put("ref_column", row.getOrDefault("reference_column", "id"));
                    joinColRefs.get(tbl).add(ref2);
                }
            });
            screenSubTables.values().forEach(sd -> {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> sts = (List<Map<String, Object>>) sd.get("sub_tables");
                sts.forEach(st -> {
                    List<Map<String, Object>> refs = joinColRefs.get(st.get("table_name"));
                    if (refs != null) {
                        st.put("join_columns", refs.stream().map(r -> r.get("column")).collect(Collectors.toList()));
                        st.put("join_column_refs", refs);
                    }
                });
            });
        }

        // ── 6. v2-repeater ────────────────────────────────────────
        sqlSession.<Map<String, Object>>selectList(NS + "selectV2Repeaters", p).forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String subTable  = (String) row.get("sub_table");
            String fk        = (String) row.get("foreign_key");
            if (subTable == null || subTable.equals(mainTable)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> sts = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");
            if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) {
                Map<String, Object> stEntry = new LinkedHashMap<>();
                stEntry.put("table_name", subTable);
                stEntry.put("component_type", "v2-repeater");
                stEntry.put("relation_type", "right_panel_relation");
                if (fk != null) {
                    Map<String, Object> fm = new LinkedHashMap<>();
                    fm.put("source_field", "id");
                    fm.put("target_field", fk);
                    fm.put("source_display_name", "ID");
                    fm.put("target_display_name", fk);
                    stEntry.put("field_mappings", Collections.singletonList(fm));
                }
                sts.add(stEntry);
            }
        });

        // ── 7. v2-bom-tree detailTable ────────────────────────────
        sqlSession.<Map<String, Object>>selectList(NS + "selectV2DetailTables", p).forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable = (String) row.get("main_table");
            String subTable  = (String) row.get("sub_table");
            String fk        = (String) row.get("foreign_key");
            if (subTable == null || subTable.equals(mainTable)) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> sts = (List<Map<String, Object>>)
                    screenSubTables.get(sid).get("sub_tables");
            if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) {
                Map<String, Object> stEntry = new LinkedHashMap<>();
                stEntry.put("table_name", subTable);
                stEntry.put("component_type", row.getOrDefault("component_type", "v2-bom-tree"));
                stEntry.put("relation_type", "right_panel_relation");
                if (fk != null) {
                    Map<String, Object> fm = new LinkedHashMap<>();
                    fm.put("source_field", "id");
                    fm.put("target_field", fk);
                    fm.put("source_display_name", "ID");
                    fm.put("target_display_name", fk);
                    stEntry.put("field_mappings", Collections.singletonList(fm));
                }
                sts.add(stEntry);
            }
        });

        // ── 8. Save Tables ────────────────────────────────────────
        sqlSession.<Map<String, Object>>selectList(NS + "selectSaveTableActions", p).forEach(row -> {
            int sid = toInt(row.get("screen_id"));
            String mainTable  = (String) row.get("main_table");
            String actionType = (String) row.get("action_type");
            String compType   = (String) row.getOrDefault("component_type", "component");
            String targetTable = row.get("target_table") != null
                    ? (String) row.get("target_table")
                    : (row.get("transfer_target_table") != null
                            ? (String) row.get("transfer_target_table") : mainTable);
            if (targetTable == null) return;

            ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable);
            Map<String, Object> sd = screenSubTables.get(sid);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> saveTables = (List<Map<String, Object>>)
                    sd.computeIfAbsent("save_tables", k -> new ArrayList<>());

            boolean dup = saveTables.stream().anyMatch(st ->
                    targetTable.equals(st.get("table_name")) && actionType.equals(st.get("save_type")));
            if (!dup) {
                Map<String, Object> stEntry = new LinkedHashMap<>();
                stEntry.put("table_name", targetTable);
                stEntry.put("save_type", actionType);
                stEntry.put("component_type", compType);
                stEntry.put("is_main_table", targetTable.equals(mainTable));
                saveTables.add(stEntry);
            }
        });

        // ── 9. 전역 메인 테이블 목록 ──────────────────────────────
        List<String> globalMainTables = sqlSession.<Map<String, Object>>selectList(NS + "selectGlobalMainTables", p).stream()
                .map(r -> (String) r.get("main_table"))
                .filter(t -> t != null && !t.isEmpty())
                .collect(Collectors.toList());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("data", screenSubTables);
        result.put("global_main_tables", globalMainTables);
        return result;
    }

    // ══════════════════════════════════════════════════════════════
    // POP Groups
    // ══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getPopScreenGroups(Map<String, Object> params) {
        List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectPopScreenGroups", params);
        if (!groups.isEmpty()) {
            List<Object> groupIds = groups.stream().map(g -> g.get("id")).collect(Collectors.toList());
            Map<String, Object> sp = new HashMap<>();
            sp.put("group_ids", groupIds);
            List<Map<String, Object>> allScreens = sqlSession.selectList(NS + "selectPopGroupScreens", sp);
            Map<Object, List<Map<String, Object>>> byGroup = allScreens.stream()
                    .collect(Collectors.groupingBy(s -> s.get("group_id")));
            groups.forEach(g -> g.put("screens", byGroup.getOrDefault(g.get("id"), Collections.emptyList())));
        }
        return groups;
    }

    @Transactional
    public Map<String, Object> createPopScreenGroup(Map<String, Object> params) {
        String userCompanyCode = (String) params.get("user_company_code");
        String effectiveCompanyCode = (String) params.getOrDefault("target_company_code", userCompanyCode);
        if (!"*".equals(userCompanyCode) && !effectiveCompanyCode.equals(userCompanyCode)) {
            throw new SecurityException("다른 회사의 그룹을 생성할 권한이 없습니다.");
        }
        params.put("company_code", effectiveCompanyCode);

        // hierarchy_path 계산
        Object parentGroupId = params.get("parent_group_id");
        String hierarchyPath;
        if (parentGroupId != null) {
            Map<String, Object> pp = new HashMap<>();
            pp.put("parent_group_id", parentGroupId);
            Map<String, Object> parent = sqlSession.selectOne(NS + "selectParentGroupById", pp);
            if (parent != null) {
                hierarchyPath = parent.get("hierarchy_path") + "/" + params.get("group_code");
            } else {
                hierarchyPath = "POP/" + params.get("group_code");
            }
        } else {
            hierarchyPath = "POP/" + params.get("group_code");
        }
        params.put("hierarchy_path", hierarchyPath);

        // 중복 체크
        int dupCount = sqlSession.selectOne(NS + "countGroupByCode", params);
        if (dupCount > 0) {
            throw new IllegalArgumentException("동일한 그룹코드가 이미 존재합니다.");
        }

        sqlSession.insert(NS + "insertPopScreenGroup", params);
        Map<String, Object> sp = new HashMap<>();
        sp.put("id", params.get("id"));
        return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
    }

    @Transactional
    public Map<String, Object> updatePopScreenGroup(Map<String, Object> params) {
        Map<String, Object> existing = sqlSession.selectOne(NS + "selectScreenGroupForUpdate", params);
        if (existing == null) throw new NoSuchElementException("그룹을 찾을 수 없습니다.");

        String hierarchyPath = (String) existing.get("hierarchy_path");
        if (hierarchyPath == null || !hierarchyPath.startsWith("POP")) {
            throw new IllegalArgumentException("POP 그룹만 수정할 수 있습니다.");
        }
        sqlSession.update(NS + "updatePopScreenGroup", params);
        Map<String, Object> sp = new HashMap<>();
        sp.put("id", params.get("id"));
        return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
    }

    @Transactional
    public void deletePopScreenGroup(Map<String, Object> params) {
        Map<String, Object> existing = sqlSession.selectOne(NS + "selectScreenGroupForUpdate", params);
        if (existing == null) {
            Map<String, Object> any = sqlSession.selectOne(NS + "selectAnyScreenGroupById", params);
            if (any != null) {
                String ownerCode = (String) any.get("company_code");
                throw new SecurityException("이 그룹은 " + ("*".equals(ownerCode) ? "최고관리자" : ownerCode)
                        + " 소속이라 삭제할 수 없습니다.");
            }
            throw new NoSuchElementException("그룹을 찾을 수 없습니다.");
        }
        String hierarchyPath = (String) existing.get("hierarchy_path");
        if (hierarchyPath == null || !hierarchyPath.startsWith("POP")) {
            throw new IllegalArgumentException("POP 그룹만 삭제할 수 있습니다.");
        }
        int childCount = sqlSession.selectOne(NS + "countChildGroupsByParentId", params);
        if (childCount > 0) throw new IllegalArgumentException("하위 그룹이 " + childCount + "개 있어 삭제할 수 없습니다. 하위 그룹을 먼저 삭제해주세요.");
        int screenCount = sqlSession.selectOne(NS + "countGroupScreensByGroupId", params);
        if (screenCount > 0) throw new IllegalArgumentException("그룹에 연결된 화면이 " + screenCount + "개 있어 삭제할 수 없습니다. 화면을 먼저 제거해주세요.");

        Map<String, Object> dp = new HashMap<>();
        dp.put("id", params.get("id"));
        dp.put("target_company_code", existing.get("company_code"));
        sqlSession.delete(NS + "deleteScreenGroupById", dp);
    }

    public Map<String, Object> ensurePopRootGroup(Map<String, Object> params) {
        String companyCode = (String) params.get("company_code");
        Map<String, Object> existing = sqlSession.selectOne(NS + "selectPopRootGroup", params);
        if (existing != null) return existing;

        if (!"*".equals(companyCode)) return null;

        sqlSession.insert(NS + "insertPopRootGroup", params);
        return sqlSession.selectOne(NS + "selectPopRootGroup", params);
    }

    // ══════════════════════════════════════════════════════════════
    // Sync: Screen Groups → Menu
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> syncScreenGroupsToMenu(String companyCode, String userId) {
        int created = 0, linked = 0, skipped = 0;
        List<String> errors = new ArrayList<>();
        List<Map<String, Object>> details = new ArrayList<>();

        try {
            Map<String, Object> p = new HashMap<>();
            p.put("company_code", companyCode);
            List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectScreenGroupsForSync", p);
            List<Map<String, Object>> existingMenus = sqlSession.selectList(NS + "selectExistingMenusForSync", p);

            // path/name → menu 매핑 (screen_group_id 없는 것만)
            Map<String, Map<String, Object>> menuByPath = new LinkedHashMap<>();
            Map<String, Map<String, Object>> menuByName = new LinkedHashMap<>();
            Set<Long> existingMenuObjids = new HashSet<>();
            existingMenus.forEach(m -> {
                existingMenuObjids.add(toLong(m.get("objid")));
                if (m.get("screen_group_id") == null) {
                    String mName = toStr(m.get("menu_name_kor")).trim().toLowerCase();
                    String pName = toStr(m.get("parent_name")).trim().toLowerCase();
                    String pathKey = !pName.isEmpty() ? pName + ">" + mName : mName;
                    menuByPath.put(pathKey, m);
                    menuByName.putIfAbsent(mName, m);
                }
            });

            // 사용자 메뉴 루트 확보
            Map<String, Object> rootMenu = sqlSession.selectOne(NS + "selectUserMenuRoot", p);
            long userMenuRootObjid;
            if (rootMenu != null) {
                userMenuRootObjid = toLong(rootMenu.get("objid"));
            } else {
                long rootObjid = System.currentTimeMillis();
                Map<String, Object> rp = new HashMap<>();
                rp.put("objid", rootObjid);
                rp.put("company_code", companyCode);
                rp.put("user_id", userId);
                sqlSession.insert(NS + "insertUserMenuRoot", rp);
                userMenuRootObjid = rootObjid;
            }

            // 최상위 회사 폴더 (level=0) 매핑
            Map<Integer, Long> groupToMenuMap = new LinkedHashMap<>();
            Map<Integer, String> groupIdToName = new LinkedHashMap<>();
            Set<Integer> topLevelFolderIds = new HashSet<>();
            groups.forEach(g -> groupIdToName.put(toInt(g.get("id")), toStr(g.get("group_name")).trim().toLowerCase()));
            groups.forEach(g -> {
                if (toInt(g.get("group_level")) == 0 && g.get("parent_group_id") == null) {
                    topLevelFolderIds.add(toInt(g.get("id")));
                    groupToMenuMap.put(toInt(g.get("id")), userMenuRootObjid);
                }
            });

            long nextObjid = System.currentTimeMillis();

            for (Map<String, Object> group : groups) {
                int gid   = toInt(group.get("id"));
                String gName = toStr(group.get("group_name")).trim();

                if (topLevelFolderIds.contains(gid)) {
                    skipped++;
                    details.add(detail("skipped", gName, gid, null, "최상위 회사 폴더 (메뉴 생성 스킵)"));
                    continue;
                }

                // 이미 연결된 경우
                Object menuObjidObj = group.get("menu_objid");
                if (menuObjidObj != null) {
                    if (existingMenuObjids.contains(toLong(menuObjidObj))) {
                        skipped++;
                        details.add(detail("skipped", gName, gid, toLong(menuObjidObj), "이미 메뉴와 연결됨"));
                        groupToMenuMap.put(gid, toLong(menuObjidObj));
                        continue;
                    } else {
                        Map<String, Object> cp = new HashMap<>(); cp.put("id", gid);
                        sqlSession.update(NS + "clearScreenGroupMenuObjid", cp);
                    }
                }

                // 경로 기반 매칭
                String parentName = group.get("parent_group_id") != null
                        ? groupIdToName.getOrDefault(toInt(group.get("parent_group_id")), "") : "";
                String gNameLower = gName.toLowerCase();
                String pathKey = !parentName.isEmpty() ? parentName + ">" + gNameLower : gNameLower;

                Map<String, Object> matchedMenu = menuByPath.getOrDefault(pathKey, menuByName.get(gNameLower));

                if (matchedMenu != null) {
                    long mObjid = toLong(matchedMenu.get("objid"));
                    Map<String, Object> up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid);
                    sqlSession.update(NS + "updateScreenGroupMenuObjid", up);
                    Map<String, Object> up2 = new HashMap<>(); up2.put("group_id", gid); up2.put("objid", mObjid);
                    sqlSession.update(NS + "updateMenuScreenGroupId", up2);
                    // URL 업데이트
                    Map<String, Object> dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode);
                    Map<String, Object> defaultScreen = sqlSession.selectOne(NS + "selectDefaultScreenForGroup", dp);
                    if (defaultScreen != null) {
                        Map<String, Object> urlp = new HashMap<>();
                        urlp.put("menu_url", "/screens/" + defaultScreen.get("screen_id"));
                        urlp.put("screen_code", defaultScreen.get("screen_code"));
                        urlp.put("objid", mObjid);
                        sqlSession.update(NS + "updateMenuUrlAndScreenCode", urlp);
                    }
                    groupToMenuMap.put(gid, mObjid);
                    linked++;
                    details.add(detail("linked", gName, gid, mObjid, null));
                    menuByPath.remove(pathKey);
                    menuByName.remove(gNameLower);
                } else {
                    // 새 메뉴 생성
                    long newObjid = nextObjid++;
                    long parentMenuObjid = userMenuRootObjid;
                    if (group.get("parent_group_id") != null) {
                        int pgid = toInt(group.get("parent_group_id"));
                        if (groupToMenuMap.containsKey(pgid)) parentMenuObjid = groupToMenuMap.get(pgid);
                        else if (group.get("parent_menu_objid") != null
                                && existingMenuObjids.contains(toLong(group.get("parent_menu_objid")))) {
                            parentMenuObjid = toLong(group.get("parent_menu_objid"));
                        }
                    }
                    Map<String, Object> seqp = new HashMap<>();
                    seqp.put("parent_objid", parentMenuObjid);
                    seqp.put("company_code", companyCode);
                    int seq = sqlSession.selectOne(NS + "getNextMenuSeqUnderParent", seqp);

                    Map<String, Object> dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode);
                    Map<String, Object> defScreen = sqlSession.selectOne(NS + "selectDefaultScreenForGroup", dp);
                    String menuUrl  = defScreen != null ? "/screens/" + defScreen.get("screen_id") : null;
                    String screenCode = defScreen != null ? (String) defScreen.get("screen_code") : null;

                    Map<String, Object> ins = new HashMap<>();
                    ins.put("objid", newObjid); ins.put("parent_objid", parentMenuObjid);
                    ins.put("group_name", gName); ins.put("group_code", group.getOrDefault("group_code", gName));
                    ins.put("seq", seq); ins.put("company_code", companyCode); ins.put("user_id", userId);
                    ins.put("group_id", gid); ins.put("description", group.get("description"));
                    ins.put("menu_url", menuUrl); ins.put("screen_code", screenCode); ins.put("icon", group.get("icon"));
                    sqlSession.insert(NS + "insertMenuForGroup", ins);

                    Map<String, Object> up = new HashMap<>(); up.put("menu_objid", newObjid); up.put("id", gid);
                    sqlSession.update(NS + "updateScreenGroupMenuObjid", up);

                    groupToMenuMap.put(gid, newObjid);
                    created++;
                    details.add(detail("created", gName, gid, newObjid, null));
                }
            }
        } catch (Exception e) {
            log.error("화면관리 → 메뉴 동기화 실패", e);
            errors.add(e.getMessage());
            Map<String, Object> r = new LinkedHashMap<>();
            r.put("success", false); r.put("created", created); r.put("linked", linked);
            r.put("skipped", skipped); r.put("errors", errors); r.put("details", details);
            return r;
        }

        Map<String, Object> r = new LinkedHashMap<>();
        r.put("success", true); r.put("created", created); r.put("linked", linked);
        r.put("skipped", skipped); r.put("errors", errors); r.put("details", details);
        return r;
    }

    // ══════════════════════════════════════════════════════════════
    // Sync: Menu → Screen Groups
    // ══════════════════════════════════════════════════════════════

    @Transactional
    public Map<String, Object> syncMenuToScreenGroups(String companyCode, String userId) {
        int created = 0, linked = 0, skipped = 0;
        List<String> errors = new ArrayList<>();
        List<Map<String, Object>> details = new ArrayList<>();

        try {
            Map<String, Object> p = new HashMap<>();
            p.put("company_code", companyCode);

            // 회사명 조회
            Map<String, Object> companyRow = sqlSession.selectOne(NS + "selectCompanyName", p);
            String companyName = companyRow != null ? toStr(companyRow.get("company_name")) : companyCode;

            List<Map<String, Object>> menus  = sqlSession.selectList(NS + "selectMenusForSync", p);
            List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectGroupsForSync", p);

            // path/name → group 매핑 (menu_objid 없는 것만)
            Map<String, Map<String, Object>> groupByPath = new LinkedHashMap<>();
            Map<String, Map<String, Object>> groupByName = new LinkedHashMap<>();
            Set<Integer> existingGroupIds = new HashSet<>();
            groups.forEach(g -> {
                existingGroupIds.add(toInt(g.get("id")));
                if (g.get("menu_objid") == null) {
                    String gn = toStr(g.get("group_name")).trim().toLowerCase();
                    String pn = toStr(g.get("parent_name")).trim().toLowerCase();
                    String pk = !pn.isEmpty() ? pn + ">" + gn : gn;
                    groupByPath.put(pk, g);
                    groupByName.putIfAbsent(gn, g);
                }
            });

            // 회사 폴더 확보
            Map<String, Object> folderRow = sqlSession.selectOne(NS + "selectRootCompanyFolder", p);
            int companyFolderId;
            if (folderRow != null) {
                companyFolderId = toInt(folderRow.get("id"));
            } else {
                int maxOrder = sqlSession.selectOne(NS + "getMaxRootDisplayOrder", null);
                Map<String, Object> fp = new HashMap<>();
                fp.put("company_name", companyName);
                fp.put("group_code", companyCode.toLowerCase());
                fp.put("display_order", maxOrder);
                fp.put("company_code", companyCode);
                fp.put("user_id", userId);
                sqlSession.insert(NS + "insertCompanyFolder", fp);
                companyFolderId = toInt(fp.get("id"));
                Map<String, Object> hp = new HashMap<>();
                hp.put("id", companyFolderId);
                hp.put("hierarchy_path", "/" + companyFolderId + "/");
                sqlSession.update(NS + "updateGroupHierarchyPathById", hp);
            }

            Map<Long, Integer> menuToGroupMap = new LinkedHashMap<>();
            menuToGroupMap.put(0L, companyFolderId);

            for (Map<String, Object> menu : menus) {
                long mObjid = toLong(menu.get("objid"));
                String mName = toStr(menu.get("menu_name_kor")).trim();

                // 이미 연결된 경우
                Object sgId = menu.get("screen_group_id");
                if (sgId != null && existingGroupIds.contains(toInt(sgId))) {
                    skipped++;
                    details.add(detail("skipped", mName, mObjid, toInt(sgId), "이미 그룹과 연결됨"));
                    menuToGroupMap.put(mObjid, toInt(sgId));
                    continue;
                }

                // 경로 기반 매칭
                String mNameLower = mName.toLowerCase();
                String pathKey = mNameLower;

                Map<String, Object> matchedGroup = groupByPath.getOrDefault(pathKey, groupByName.get(mNameLower));

                if (matchedGroup != null) {
                    int gid = toInt(matchedGroup.get("id"));
                    Map<String, Object> up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid);
                    sqlSession.update(NS + "updateScreenGroupForMenuSync", up);
                    Map<String, Object> up2 = new HashMap<>(); up2.put("group_id", gid); up2.put("objid", mObjid);
                    sqlSession.update(NS + "updateMenuScreenGroupId", up2);
                    menuToGroupMap.put(mObjid, gid);
                    linked++;
                    details.add(detail("linked", mName, mObjid, gid, null));
                    groupByPath.remove(pathKey);
                    groupByName.remove(mNameLower);
                } else {
                    // 새 그룹 생성
                    long parentObjid = toLong(menu.get("parent_obj_id"));
                    int parentGid = menuToGroupMap.getOrDefault(parentObjid, companyFolderId);
                    // 부모 그룹 level + hierarchy_path 조회
                    Map<String, Object> pp = new HashMap<>();
                    pp.put("parent_group_id", parentGid);
                    Map<String, Object> parentGroup = sqlSession.selectOne(NS + "selectParentGroupById", pp);
                    int parentLevel = parentGroup != null ? toInt(parentGroup.getOrDefault("group_level", 0)) : 0;
                    String parentPath = parentGroup != null ? toStr(parentGroup.get("hierarchy_path")) : "/" + parentGid + "/";

                    Map<String, Object> ins = new HashMap<>();
                    ins.put("group_name", mName);
                    ins.put("group_code", mNameLower.replaceAll("\\s+", "_"));
                    ins.put("parent_group_id", parentGid);
                    ins.put("group_level", parentLevel + 1);
                    ins.put("display_order", toInt(menu.getOrDefault("seq", 0)));
                    ins.put("company_code", companyCode);
                    ins.put("user_id", userId);
                    ins.put("hierarchy_path", parentPath + "0/");
                    ins.put("menu_objid", mObjid);
                    ins.put("description", menu.get("menu_desc"));
                    sqlSession.insert(NS + "insertScreenGroupForSync", ins);

                    int newGid = toInt(ins.get("id"));
                    String hp = (parentPath + newGid + "/").replace("//", "/");
                    Map<String, Object> hpu = new HashMap<>();
                    hpu.put("id", newGid);
                    hpu.put("hierarchy_path", hp);
                    sqlSession.update(NS + "updateGroupHierarchyPathById", hpu);

                    Map<String, Object> up2 = new HashMap<>(); up2.put("group_id", newGid); up2.put("objid", mObjid);
                    sqlSession.update(NS + "updateMenuScreenGroupId", up2);

                    menuToGroupMap.put(mObjid, newGid);
                    existingGroupIds.add(newGid);
                    created++;
                    details.add(detail("created", mName, mObjid, newGid, null));
                }
            }
        } catch (Exception e) {
            log.error("메뉴 → 화면관리 동기화 실패", e);
            errors.add(e.getMessage());
            Map<String, Object> r = new LinkedHashMap<>();
            r.put("success", false); r.put("created", created); r.put("linked", linked);
            r.put("skipped", skipped); r.put("errors", errors); r.put("details", details);
            return r;
        }

        Map<String, Object> r = new LinkedHashMap<>();
        r.put("success", true); r.put("created", created); r.put("linked", linked);
        r.put("skipped", skipped); r.put("errors", errors); r.put("details", details);
        return r;
    }

    // ══════════════════════════════════════════════════════════════
    // Sync: Status
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getSyncStatus(String companyCode) {
        Map<String, Object> p = new HashMap<>();
        p.put("company_code", companyCode);
        List<Map<String, Object>> groupStats = sqlSession.selectList(NS + "selectSyncStatusGroups", p);
        List<Map<String, Object>> menuStats  = sqlSession.selectList(NS + "selectSyncStatusMenus", p);
        Map<String, Object> status = new LinkedHashMap<>();
        status.put("company_code", companyCode);
        status.put("groups", groupStats);
        status.put("menus",  menuStats);
        return status;
    }

    // ══════════════════════════════════════════════════════════════
    // Sync: All Companies
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> syncAllCompanies(String userId) {
        List<Map<String, Object>> companies = sqlSession.selectList(NS + "selectAllCompanyCodes", null);
        List<Map<String, Object>> results = new ArrayList<>();
        int successCount = 0, failedCount = 0;

        for (Map<String, Object> company : companies) {
            String code = (String) company.get("company_code");
            try {
                Map<String, Object> r = syncScreenGroupsToMenu(code, userId);
                r.put("company_code", code);
                results.add(r);
                if (Boolean.TRUE.equals(r.get("success"))) successCount++;
                else failedCount++;
            } catch (Exception e) {
                failedCount++;
                Map<String, Object> r = new LinkedHashMap<>();
                r.put("company_code", code); r.put("success", false);
                r.put("error", e.getMessage());
                results.add(r);
            }
        }

        Map<String, Object> res = new LinkedHashMap<>();
        res.put("success", failedCount == 0);
        res.put("total_companies", companies.size());
        res.put("success_count", successCount);
        res.put("failed_count", failedCount);
        res.put("results", results);
        return res;
    }

    // ══════════════════════════════════════════════════════════════
    // Helpers
    // ══════════════════════════════════════════════════════════════

    private void ensureScreenEntry(Map<Integer, Map<String, Object>> map, int sid,
                                   String screenName, String mainTable) {
        if (!map.containsKey(sid)) {
            Map<String, Object> entry = new LinkedHashMap<>();
            entry.put("screen_id", sid);
            entry.put("screen_name", screenName);
            entry.put("main_table", mainTable != null ? mainTable : "");
            entry.put("sub_tables", new ArrayList<Map<String, Object>>());
            map.put(sid, entry);
        }
    }

    private String inferRelationType(String componentType) {
        if (componentType == null) return "lookup";
        if (componentType.contains("autocomplete") || componentType.contains("entity-search")) return "lookup";
        if (componentType.contains("modal-repeater") || componentType.contains("selected-items")) return "source";
        if (componentType.contains("table")) return "join";
        return "lookup";
    }

    private List<Map<String, Object>> buildFieldMappings(Map<String, Object> row, String subTable,
                                                          String mainTable, Map<String, String> colLabelMap) {
        Object fmsObj = row.get("field_mappings");
        Object colsObj = row.get("columns_config");
        List<Map<String, Object>> result = new ArrayList<>();

        if (fmsObj instanceof List) {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> fms = (List<Map<String, Object>>) fmsObj;
            fms.forEach(fm -> {
                String sf = toStr(fm.getOrDefault("source_field", fm.get("source_field")));
                String tf = toStr(fm.getOrDefault("target_field", fm.get("target_field")));
                Map<String, Object> m = new LinkedHashMap<>();
                m.put("source_field", sf);
                m.put("target_field", tf);
                m.put("source_display_name", colLabelMap.getOrDefault(subTable + "." + sf, sf));
                m.put("target_display_name", colLabelMap.getOrDefault(mainTable + "." + tf, tf));
                result.add(m);
            });
        } else if (colsObj instanceof List) {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> cols = (List<Map<String, Object>>) colsObj;
            cols.forEach(col -> {
                Object mapping = col.get("mapping");
                if (mapping instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> mp = (Map<String, Object>) mapping;
                    if ("source".equals(mp.get("type")) && mp.get("source_field") != null) {
                        Map<String, Object> m = new LinkedHashMap<>();
                        m.put("source_field", col.getOrDefault("field", ""));
                        m.put("target_field", mp.get("source_field"));
                        m.put("source_display_name", col.getOrDefault("label", col.getOrDefault("field", "")));
                        m.put("target_display_name", mp.get("source_field"));
                        result.add(m);
                    }
                }
            });
        }
        return result.isEmpty() ? null : result;
    }

    private Map<String, Object> pairOf(String tableName, String columnName) {
        Map<String, Object> m = new HashMap<>();
        m.put("table_name", tableName);
        m.put("column_name", columnName);
        return m;
    }

    private Map<String, Object> detail(String action, String name, Object sourceId,
                                        Object targetId, String reason) {
        Map<String, Object> d = new LinkedHashMap<>();
        d.put("action", action);
        d.put("source_name", name);
        d.put("source_id", sourceId);
        if (targetId != null) d.put("target_id", targetId);
        if (reason != null)   d.put("reason", reason);
        return d;
    }

    private void convertToJsonString(Map<String, Object> params, String key) {
        Object val = params.get(key);
        if (val != null && !(val instanceof String)) {
            params.put(key, val.toString());
        }
    }

    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 (NumberFormatException 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 (NumberFormatException e) { return 0L; }
    }

    private String toStr(Object val) {
        return val != null ? val.toString() : "";
    }
}
