package com.erp.service;

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

import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDate;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class DataService extends BaseService {

    @Autowired private JdbcTemplate jdbcTemplate;
    @Autowired private CommonService commonService;
    @Autowired private ObjectMapper objectMapper;

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

    private static final Set<String> BLOCKED_TABLES = new HashSet<>(Arrays.asList(
            "pg_catalog", "pg_statistic", "pg_statistic_ext", "pg_database",
            "pg_user", "pg_shadow", "pg_auth_members", "pg_authid",
            "information_schema", "session_tokens", "password_history",
            "user_credentials", "api_keys", "oauth_tokens"));

    // ── 테이블명 검증 ──────────────────────────────────────────────────────────
    private void validateTable(String tableName) {
        if (tableName == null || !IDENTIFIER.matcher(tableName).matches()) {
            throw new IllegalArgumentException("유효하지 않은 테이블명입니다: " + tableName);
        }
        if (BLOCKED_TABLES.contains(tableName.toLowerCase())) {
            throw new IllegalArgumentException("접근이 허용되지 않는 테이블입니다: " + tableName);
        }
    }

    private void validateColumn(String col) {
        if (col == null || !IDENTIFIER.matcher(col).matches()) {
            throw new IllegalArgumentException("유효하지 않은 컬럼명입니다: " + col);
        }
    }

    // ── 테이블 존재 여부 확인 ──────────────────────────────────────────────────
    private boolean tableExists(String tableName) {
        Integer cnt = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = ? AND table_schema NOT IN ('pg_catalog','information_schema')",
                Integer.class, tableName.toLowerCase());
        return cnt != null && cnt > 0;
    }

    // ── company_code 컬럼 존재 여부 ─────────────────────────────────────────
    private boolean hasCompanyCodeColumn(String tableName) {
        Integer cnt = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = ? AND column_name = 'company_code'",
                Integer.class, tableName.toLowerCase());
        return cnt != null && cnt > 0;
    }

    // ── PK 컬럼 자동 감지 ─────────────────────────────────────────────────────
    private List<String> getPrimaryKeys(String tableName) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT a.attname FROM pg_index i" +
                " JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)" +
                " WHERE i.indrelid = ?::regclass AND i.indisprimary",
                tableName.toLowerCase());
        return rows.stream()
                .map(r -> String.valueOf(r.get("attname")))
                .collect(Collectors.toList());
    }

    // ── 유효한 컬럼 목록 조회 ─────────────────────────────────────────────────
    private Set<String> getValidColumns(String tableName) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT column_name FROM information_schema.columns WHERE table_name = ?",
                tableName.toLowerCase());
        return rows.stream()
                .map(r -> String.valueOf(r.get("column_name")))
                .collect(Collectors.toSet());
    }

    // ── 목록 조회 ─────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    public Map<String, Object> getTableData(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        validateTable(tableName);
        if (!tableExists(tableName)) {
            throw new IllegalArgumentException("테이블을 찾을 수 없습니다: " + tableName);
        }

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        int page = toInt(params.get("page"), 1);
        // Node.js는 size/limit 둘 다 사용 — size를 limit의 alias로 지원
        Object limitVal = params.get("limit") != null ? params.get("limit") : params.get("size");
        int limit = Math.min(toInt(limitVal, 20), 1000);
        // offset 직접 지정 시 page 대신 사용 (Node.js limit/offset 방식 호환)
        int offset = params.get("offset") != null && params.get("page") == null
                ? toInt(params.get("offset"), 0)
                : (page - 1) * limit;

        List<Object> args = new ArrayList<>();
        StringBuilder where = new StringBuilder("1=1");

        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            where.append(" AND company_code = ?");
            args.add(companyCode);
        }

        // 추가 필터 (filters 맵)
        Object filtersObj = params.get("filters");
        if (filtersObj instanceof Map) {
            Map<String, Object> filters = (Map<String, Object>) filtersObj;
            Set<String> validCols = getValidColumns(tableName);
            for (Map.Entry<String, Object> entry : filters.entrySet()) {
                String col = entry.getKey();
                if (!validCols.contains(col)) continue;
                validateColumn(col);
                if (entry.getValue() instanceof String) {
                    where.append(" AND \"").append(col).append("\" ILIKE ?");
                    args.add("%" + entry.getValue() + "%");
                } else {
                    where.append(" AND \"").append(col).append("\" = ?");
                    args.add(entry.getValue());
                }
            }
        }

        // search 파라미터
        String search = (String) params.get("search");
        String searchColumn = (String) params.get("search_column");
        if (search != null && !search.isBlank() && searchColumn != null && !searchColumn.isBlank()) {
            validateColumn(searchColumn);
            Set<String> validCols = getValidColumns(tableName);
            if (validCols.contains(searchColumn)) {
                where.append(" AND \"").append(searchColumn).append("\" ILIKE ?");
                args.add("%" + search + "%");
            }
        }

        // ORDER BY
        String orderBy = (String) params.get("order_by");
        String orderDir = "DESC".equalsIgnoreCase((String) params.get("order_dir")) ? "DESC" : "ASC";
        String orderClause = "";
        if (orderBy != null && !orderBy.isBlank() && IDENTIFIER.matcher(orderBy).matches()) {
            orderClause = " ORDER BY \"" + orderBy + "\" " + orderDir;
        } else {
            // 기본 날짜 컬럼 정렬 (Node.js 호환)
            Set<String> tblCols = getValidColumns(tableName);
            for (String dateCol : new String[]{"created_date", "updated_date"}) {
                if (tblCols.contains(dateCol)) {
                    orderClause = " ORDER BY \"" + dateCol + "\" DESC";
                    break;
                }
            }
        }

        String countSql = "SELECT COUNT(*) FROM \"" + tableName + "\" WHERE " + where;
        Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, args.toArray());
        if (total == null) total = 0;

        String listSql = "SELECT * FROM \"" + tableName + "\" WHERE " + where + orderClause + " LIMIT ? OFFSET ?";
        List<Object> listArgs = new ArrayList<>(args);
        listArgs.add(limit);
        listArgs.add(offset);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(listSql, listArgs.toArray());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("list", rows);
        result.put("total", total);
        result.put("page", page);
        result.put("limit", limit);
        result.put("total_pages", (int) Math.ceil((double) total / limit));
        return result;
    }

    // ── 컬럼 목록 조회 ────────────────────────────────────────────────────────
    public List<Map<String, Object>> getTableColumns(String tableName) {
        validateTable(tableName);

        List<Map<String, Object>> cols = jdbcTemplate.queryForList(
                "SELECT column_name, data_type, udt_name, is_nullable, column_default" +
                " FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position",
                tableName.toLowerCase());

        List<String> pks = new ArrayList<>();
        try {
            pks = getPrimaryKeys(tableName);
        } catch (Exception e) {
            log.warn("PK 조회 실패: {}", e.getMessage());
        }
        final List<String> pkList = pks;

        return cols.stream().map(col -> {
            Map<String, Object> c = new LinkedHashMap<>();
            String colName = String.valueOf(col.get("column_name"));
            c.put("column_name", colName);
            c.put("data_type", col.get("data_type"));
            c.put("udt_name", col.get("udt_name"));
            c.put("nullable", "YES".equals(col.get("is_nullable")));
            c.put("default_value", col.get("column_default"));
            c.put("is_pk", pkList.contains(colName));
            c.put("column_label", colName);
            return c;
        }).collect(Collectors.toList());
    }

    // ── 단건 조회 ─────────────────────────────────────────────────────────────
    public Map<String, Object> getRecordDetail(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        String id = String.valueOf(params.get("id"));
        validateTable(tableName);

        List<String> pks = getPrimaryKeys(tableName);
        String pkOverride = (String) params.get("primary_key_column");
        String pkCol = pkOverride != null && !pkOverride.isBlank() ? pkOverride
                : (pks.isEmpty() ? "id" : pks.get(0));

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        List<Object> args = new ArrayList<>();
        args.add(id);
        StringBuilder where = new StringBuilder("\"" + pkCol + "\" = ?");
        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            where.append(" AND company_code = ?");
            args.add(companyCode);
        }

        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT * FROM \"" + tableName + "\" WHERE " + where + " LIMIT 1",
                args.toArray());
        return rows.isEmpty() ? null : rows.get(0);
    }

    // ── 생성 ──────────────────────────────────────────────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> createRecord(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        validateTable(tableName);

        Set<String> validCols = getValidColumns(tableName);
        boolean hasCC = hasCompanyCodeColumn(tableName);
        String companyCode = (String) params.get("company_code");

        // data 키가 있으면 그 안을 사용, 없으면 params 전체에서 메타 키 제거
        Map<String, Object> data;
        if (params.get("data") instanceof Map) {
            data = (Map<String, Object>) params.get("data");
        } else {
            data = new LinkedHashMap<>(params);
            data.remove("table_name");
            data.remove("company_code");
        }

        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            data.put("company_code", companyCode);
        }

        List<String> cols = new ArrayList<>();
        List<Object> vals = new ArrayList<>();
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String col = entry.getKey();
            if (!validCols.contains(col)) continue;
            validateColumn(col);
            cols.add("\"" + col + "\"");
            vals.add(entry.getValue());
        }

        if (cols.isEmpty()) throw new IllegalArgumentException("삽입할 유효한 컬럼이 없습니다.");

        String sql = "INSERT INTO \"" + tableName + "\" (" + String.join(", ", cols) + ")" +
                " VALUES (" + cols.stream().map(c -> "?").collect(Collectors.joining(", ")) + ")" +
                " RETURNING *";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, vals.toArray());
        return result.isEmpty() ? new HashMap<>() : result.get(0);
    }

    // ── 수정 ──────────────────────────────────────────────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> updateRecord(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        String id = String.valueOf(params.get("id"));
        validateTable(tableName);

        Set<String> validCols = getValidColumns(tableName);
        List<String> pks = getPrimaryKeys(tableName);
        String pkCol = pks.isEmpty() ? "id" : pks.get(0);

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        Map<String, Object> data;
        if (params.get("data") instanceof Map) {
            data = (Map<String, Object>) params.get("data");
        } else {
            data = new LinkedHashMap<>(params);
            data.remove("table_name");
            data.remove("company_code");
            data.remove("id");
        }

        List<String> sets = new ArrayList<>();
        List<Object> args = new ArrayList<>();
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String col = entry.getKey();
            if (!validCols.contains(col)) continue;
            if (col.equals(pkCol)) continue; // PK는 수정 대상 제외
            validateColumn(col);
            sets.add("\"" + col + "\" = ?");
            args.add(entry.getValue());
        }

        if (sets.isEmpty()) throw new IllegalArgumentException("수정할 유효한 컬럼이 없습니다.");

        args.add(id);
        StringBuilder where = new StringBuilder("\"" + pkCol + "\" = ?");
        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            where.append(" AND company_code = ?");
            args.add(companyCode);
        }

        String sql = "UPDATE \"" + tableName + "\" SET " + String.join(", ", sets) +
                " WHERE " + where + " RETURNING *";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, args.toArray());
        Map<String, Object> updated = result.isEmpty() ? null : result.get(0);

        // _relationInfo: FK cascade update (다른 테이블의 FK 컬럼을 동기화)
        @SuppressWarnings("unchecked")
        Object relInfoRaw = params.get("_relationInfo");
        if (relInfoRaw instanceof Map && updated != null) {
            Map<String, Object> relInfo = (Map<String, Object>) relInfoRaw;
            String relatedTable = (String) relInfo.get("related_table");
            String foreignKey = (String) relInfo.get("foreign_key");
            Object oldValue = relInfo.get("old_value");
            Object newValue = relInfo.get("new_value");
            if (relatedTable != null && foreignKey != null && oldValue != null && newValue != null) {
                try {
                    validateTable(relatedTable);
                    validateColumn(foreignKey);
                    jdbcTemplate.update(
                        "UPDATE \"" + relatedTable + "\" SET \"" + foreignKey + "\" = ? WHERE \"" + foreignKey + "\" = ?",
                        newValue, oldValue);
                } catch (Exception e) {
                    log.warn("_relationInfo FK cascade update 실패: {}", e.getMessage());
                }
            }
        }

        return updated;
    }

    // ── 삭제 ──────────────────────────────────────────────────────────────────
    @Transactional
    public Map<String, Object> deleteRecord(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        String id = String.valueOf(params.get("id"));
        validateTable(tableName);

        List<String> pks = getPrimaryKeys(tableName);
        String pkCol = pks.isEmpty() ? "id" : pks.get(0);

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        List<Object> args = new ArrayList<>();
        args.add(id);
        StringBuilder where = new StringBuilder("\"" + pkCol + "\" = ?");
        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            where.append(" AND company_code = ?");
            args.add(companyCode);
        }

        String sql = "DELETE FROM \"" + tableName + "\" WHERE " + where + " RETURNING *";
        List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, args.toArray());
        return result.isEmpty() ? null : result.get(0);
    }

    // ── JOIN 조회 ─────────────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> getJoinData(Map<String, Object> params) {
        String leftTable = (String) params.get("left_table");
        String rightTable = (String) params.get("right_table");
        // Node.js는 leftColumn/rightColumn, 내부 호출은 leftKey/rightKey — 둘 다 지원
        String leftKey = params.get("left_key") != null
                ? (String) params.get("left_key") : (String) params.get("left_column");
        String rightKey = params.get("right_key") != null
                ? (String) params.get("right_key") : (String) params.get("right_column");

        validateTable(leftTable);
        validateTable(rightTable);
        validateColumn(leftKey);
        validateColumn(rightKey);

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(leftTable);

        List<Object> args = new ArrayList<>();
        String joinCond = "l.\"" + leftKey + "\" = r.\"" + rightKey + "\"";
        StringBuilder where = new StringBuilder("1=1");
        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            where.append(" AND l.company_code = ?");
            args.add(companyCode);
        }
        // leftValue: Node.js가 특정 값으로 필터링할 때 사용
        String leftValue = (String) params.get("left_value");
        if (leftValue != null && !leftValue.isBlank()) {
            where.append(" AND l.\"").append(leftKey).append("\" = ?");
            args.add(leftValue);
        }

        String listSql = "SELECT DISTINCT r.* FROM \"" + leftTable + "\" l" +
                " INNER JOIN \"" + rightTable + "\" r ON " + joinCond +
                " WHERE " + where;
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(listSql, args.toArray());

        // 중복 제거 (deduplication 설정이 있을 경우)
        Object deduplicateObj = params.get("deduplication");
        if (deduplicateObj instanceof String && !((String) deduplicateObj).isBlank()) {
            try {
                deduplicateObj = objectMapper.readValue((String) deduplicateObj, Map.class);
            } catch (Exception e) {
                deduplicateObj = null;
            }
        }
        if (deduplicateObj instanceof Map) {
            rows = deduplicateData(rows, (Map<String, Object>) deduplicateObj);
        }

        return rows;
    }

    // ── 그룹 UPSERT (INSERT/UPDATE/DELETE 동기화) ──────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> upsertGroupedRecords(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        validateTable(tableName);

        Set<String> validCols = getValidColumns(tableName);
        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        Map<String, Object> parentKeys = params.get("parent_keys") instanceof Map
                ? (Map<String, Object>) params.get("parent_keys") : new HashMap<>();
        List<?> records = params.get("records") instanceof List
                ? (List<?>) params.get("records") : new ArrayList<>();
        boolean deleteOrphans = Boolean.TRUE.equals(params.get("delete_orphans"));

        List<String> pks = getPrimaryKeys(tableName);
        String pkCol = pks.isEmpty() ? "id" : pks.get(0);

        // 기존 레코드 PK 목록 조회
        Set<String> existingIds = new HashSet<>();
        if (!parentKeys.isEmpty()) {
            List<String> conds = new ArrayList<>();
            List<Object> cArgs = new ArrayList<>();
            for (Map.Entry<String, Object> e : parentKeys.entrySet()) {
                validateColumn(e.getKey());
                conds.add("\"" + e.getKey() + "\" = ?");
                cArgs.add(e.getValue());
            }
            if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
                conds.add("company_code = ?");
                cArgs.add(companyCode);
            }
            jdbcTemplate.queryForList(
                    "SELECT \"" + pkCol + "\" FROM \"" + tableName + "\" WHERE " + String.join(" AND ", conds),
                    cArgs.toArray())
                .stream()
                .filter(r -> r.get(pkCol) != null)
                .forEach(r -> existingIds.add(String.valueOf(r.get(pkCol))));
        }

        int inserted = 0, updated = 0, deleted = 0;
        List<String> savedIds = new ArrayList<>();

        for (Object recObj : records) {
            if (!(recObj instanceof Map)) continue;
            Map<String, Object> rec = new LinkedHashMap<>((Map<String, Object>) recObj);
            rec.putAll(parentKeys);
            if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
                rec.put("company_code", companyCode);
            }

            String recordId = rec.get(pkCol) != null ? String.valueOf(rec.get(pkCol)) : null;
            boolean exists = recordId != null && existingIds.contains(recordId);

            if (exists) {
                // UPDATE
                List<String> sets = new ArrayList<>();
                List<Object> args = new ArrayList<>();
                for (Map.Entry<String, Object> e : rec.entrySet()) {
                    String col = e.getKey();
                    if (!validCols.contains(col) || col.equals(pkCol)) continue;
                    validateColumn(col);
                    sets.add("\"" + col + "\" = ?");
                    args.add(e.getValue());
                }
                if (!sets.isEmpty()) {
                    if (validCols.contains("updated_date")) sets.add("updated_date = NOW()");
                    args.add(recordId);
                    jdbcTemplate.update(
                            "UPDATE \"" + tableName + "\" SET " + String.join(", ", sets) +
                            " WHERE \"" + pkCol + "\" = ?", args.toArray());
                    updated++;
                    savedIds.add(recordId);
                }
            } else {
                // INSERT — UUID 자동 생성
                if (validCols.contains(pkCol)
                        && (rec.get(pkCol) == null || rec.get(pkCol).toString().isBlank())) {
                    rec.put(pkCol, UUID.randomUUID().toString());
                }
                recordId = rec.get(pkCol) != null ? String.valueOf(rec.get(pkCol)) : null;

                List<String> cols = new ArrayList<>();
                List<Object> vals = new ArrayList<>();
                for (Map.Entry<String, Object> e : rec.entrySet()) {
                    String col = e.getKey();
                    if (!validCols.contains(col)) continue;
                    validateColumn(col);
                    cols.add("\"" + col + "\"");
                    vals.add(e.getValue());
                }
                if (cols.isEmpty()) continue;

                List<String> extraCols = new ArrayList<>();
                List<String> extraExprs = new ArrayList<>();
                if (validCols.contains("created_date") && !rec.containsKey("created_date")) {
                    extraCols.add("created_date");
                    extraExprs.add("NOW()");
                }
                if (validCols.contains("updated_date") && !rec.containsKey("updated_date")) {
                    extraCols.add("updated_date");
                    extraExprs.add("NOW()");
                }

                String allCols = String.join(", ", cols)
                        + (extraCols.isEmpty() ? "" : ", " + String.join(", ", extraCols));
                String allVals = cols.stream().map(c -> "?").collect(Collectors.joining(", "))
                        + (extraExprs.isEmpty() ? "" : ", " + String.join(", ", extraExprs));

                jdbcTemplate.update(
                        "INSERT INTO \"" + tableName + "\" (" + allCols + ") VALUES (" + allVals + ")",
                        vals.toArray());
                inserted++;
                if (recordId != null) savedIds.add(recordId);
            }
        }

        // 고아 레코드 삭제 (deleteOrphans=true)
        if (deleteOrphans && !parentKeys.isEmpty()) {
            List<String> delConds = new ArrayList<>();
            List<Object> delArgs = new ArrayList<>();
            for (Map.Entry<String, Object> e : parentKeys.entrySet()) {
                delConds.add("\"" + e.getKey() + "\" = ?");
                delArgs.add(e.getValue());
            }
            if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
                delConds.add("company_code = ?");
                delArgs.add(companyCode);
            }
            if (!savedIds.isEmpty()) {
                String placeholders = savedIds.stream().map(id -> "?").collect(Collectors.joining(", "));
                delConds.add("\"" + pkCol + "\" NOT IN (" + placeholders + ")");
                delArgs.addAll(savedIds);
            }
            deleted = jdbcTemplate.update(
                    "DELETE FROM \"" + tableName + "\" WHERE " + String.join(" AND ", delConds),
                    delArgs.toArray());
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("inserted", inserted);
        result.put("updated", updated);
        result.put("deleted", deleted);
        result.put("saved_ids", savedIds);
        return result;
    }

    // ── 복합키 삭제 ───────────────────────────────────────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> deleteByCompositeKey(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        validateTable(tableName);

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        // keys: { colName: value, ... }
        Map<String, Object> keys = params.get("keys") instanceof Map
                ? (Map<String, Object>) params.get("keys") : new HashMap<>();
        if (keys.isEmpty()) throw new IllegalArgumentException("삭제 키가 필요합니다.");

        Set<String> validCols = getValidColumns(tableName);
        List<String> conds = new ArrayList<>();
        List<Object> args = new ArrayList<>();
        for (Map.Entry<String, Object> e : keys.entrySet()) {
            if (!validCols.contains(e.getKey())) continue;
            validateColumn(e.getKey());
            conds.add("\"" + e.getKey() + "\" = ?");
            args.add(e.getValue());
        }
        if (conds.isEmpty()) throw new IllegalArgumentException("유효한 삭제 키가 없습니다.");

        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            conds.add("company_code = ?");
            args.add(companyCode);
        }

        String sql = "DELETE FROM \"" + tableName + "\" WHERE " +
                String.join(" AND ", conds) + " RETURNING *";
        List<Map<String, Object>> deleted = jdbcTemplate.queryForList(sql, args.toArray());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("deleted", deleted.size());
        result.put("rows", deleted);
        return result;
    }

    // ── 그룹 삭제 (필터 조건) ────────────────────────────────────────────────
    @Transactional
    @SuppressWarnings("unchecked")
    public Map<String, Object> deleteGroupRecords(Map<String, Object> params) {
        String tableName = (String) params.get("table_name");
        validateTable(tableName);

        String companyCode = (String) params.get("company_code");
        boolean hasCC = hasCompanyCodeColumn(tableName);

        Map<String, Object> filters = params.get("filters") instanceof Map
                ? (Map<String, Object>) params.get("filters") : new HashMap<>();
        if (filters.isEmpty()) throw new IllegalArgumentException("삭제 조건이 필요합니다.");

        Set<String> validCols = getValidColumns(tableName);
        List<String> conds = new ArrayList<>();
        List<Object> args = new ArrayList<>();
        for (Map.Entry<String, Object> e : filters.entrySet()) {
            if (!validCols.contains(e.getKey())) continue;
            validateColumn(e.getKey());
            conds.add("\"" + e.getKey() + "\" = ?");
            args.add(e.getValue());
        }
        if (conds.isEmpty()) throw new IllegalArgumentException("유효한 삭제 조건이 없습니다.");

        if (hasCC && companyCode != null && !companyCode.isEmpty() && !"*".equals(companyCode)) {
            conds.add("company_code = ?");
            args.add(companyCode);
        }

        String sql = "DELETE FROM \"" + tableName + "\" WHERE " +
                String.join(" AND ", conds) + " RETURNING *";
        List<Map<String, Object>> deleted = jdbcTemplate.queryForList(sql, args.toArray());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("deleted", deleted.size());
        result.put("rows", deleted);
        return result;
    }

    // ── 중복 제거 헬퍼 ───────────────────────────────────────────────────────
    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> deduplicateData(List<Map<String, Object>> rows,
                                                       Map<String, Object> config) {
        String groupByColumn = (String) config.get("group_by_column");
        String keepStrategy = config.get("keep_strategy") != null
                ? (String) config.get("keep_strategy") : "latest";
        if (groupByColumn == null || groupByColumn.isBlank()) return rows;

        Map<Object, Map<String, Object>> grouped = new LinkedHashMap<>();
        for (Map<String, Object> row : rows) {
            Object key = row.get(groupByColumn);
            if (!grouped.containsKey(key)) {
                grouped.put(key, row);
            } else {
                Map<String, Object> existing = grouped.get(key);
                if ("latest".equals(keepStrategy)) {
                    if (compareValues(row.get("updated_date"), existing.get("updated_date")) > 0)
                        grouped.put(key, row);
                } else if ("earliest".equals(keepStrategy)) {
                    if (compareValues(row.get("created_date"), existing.get("created_date")) < 0)
                        grouped.put(key, row);
                } else if ("current_date".equals(keepStrategy)) {
                    if (findCurrentDateRow(row, existing) == row)
                        grouped.put(key, row);
                }
                // "base_price": 첫 번째 행 유지 (기본값)
            }
        }
        return new ArrayList<>(grouped.values());
    }

    private int compareValues(Object a, Object b) {
        if (a == null && b == null) return 0;
        if (a == null) return -1;
        if (b == null) return 1;
        return a.toString().compareTo(b.toString());
    }

    private Map<String, Object> findCurrentDateRow(Map<String, Object> r1, Map<String, Object> r2) {
        LocalDate today = LocalDate.now();
        LocalDate d1 = parseLocalDate(r1);
        LocalDate d2 = parseLocalDate(r2);
        if (d1 == null && d2 == null) return r1;
        if (d1 == null) return r2;
        if (d2 == null) return r1;
        long diff1 = Math.abs(today.toEpochDay() - d1.toEpochDay());
        long diff2 = Math.abs(today.toEpochDay() - d2.toEpochDay());
        return diff1 <= diff2 ? r1 : r2;
    }

    private LocalDate parseLocalDate(Map<String, Object> row) {
        for (String key : new String[]{"start_date", "effective_date", "created_date", "date"}) {
            Object v = row.get(key);
            if (v instanceof java.sql.Date) return ((java.sql.Date) v).toLocalDate();
            if (v instanceof LocalDate) return (LocalDate) v;
            if (v instanceof String) {
                try { return LocalDate.parse((String) v); } catch (Exception ignored) {}
            }
        }
        return null;
    }

    // ── helpers ───────────────────────────────────────────────────────────────
    private int toInt(Object v, int def) {
        if (v == null) return def;
        if (v instanceof Number) return ((Number) v).intValue();
        try { return Integer.parseInt(v.toString()); } catch (Exception e) { return def; }
    }
}
