package com.erp.service;

import com.erp.common.BaseService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Entity 조인 서비스.
 * ID 값을 의미 있는 데이터로 자동 변환하는 동적 테이블 조회 기능을 제공한다.
 *
 * ※ 동적 테이블명/컬럼명 SQL은 MyBatis XML 정적 바인딩으로 처리할 수 없으므로
 *   JdbcTemplate 를 사용한다. (Security: tableName 은 information_schema 검증 후 사용)
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class EntityJoinService extends BaseService {

    private final JdbcTemplate jdbcTemplate;

    private static final String NS = "entityJoin.";

    private final Map<String, Map<String, String>> referenceCache = new ConcurrentHashMap<>();
    private final Map<String, CacheStats> cacheStats = new ConcurrentHashMap<>();

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

    // ═══════════════════════════════════════════════════════════════════════
    // Public API
    // ═══════════════════════════════════════════════════════════════════════

    public Map<String, Object> getTableDataWithJoins(String tableName, Map<String, Object> options) {
        validateIdentifier(tableName);
        long startTime = System.currentTimeMillis();

        int page = toInt(options.get("page"), 1);
        int size = toInt(options.get("size"), 20);
        int offset = (page - 1) * size;

        boolean enableEntityJoin = !"false".equalsIgnoreCase(
                String.valueOf(options.getOrDefault("enable_entity_join", "true")));

        String companyCode = (String) options.get("company_code");

        List<Map<String, Object>> joinConfigs = enableEntityJoin
                ? detectEntityJoins(tableName, null, companyCode)
                : Collections.emptyList();

        @SuppressWarnings("unchecked")
        Map<String, Object> searchConditions = options.containsKey("search")
                ? (Map<String, Object>) options.get("search")
                : Collections.emptyMap();

        String sortBy = (String) options.get("sort_by");
        String sortOrder = (String) options.getOrDefault("sort_order", "asc");

        List<Object> whereParams = new ArrayList<>();
        String whereClause = buildWhereClause(tableName, searchConditions, whereParams);
        Map<String, String> aliasMap = buildAliasMap(joinConfigs);

        String countSql = buildCountQuery(tableName, joinConfigs, aliasMap, whereClause);
        Integer total = jdbcTemplate.queryForObject(countSql, whereParams.toArray(), Integer.class);
        if (total == null) total = 0;

        String dataSql = buildDataQuery(tableName, joinConfigs, aliasMap, whereClause, sortBy, sortOrder);
        List<Object> dataParams = new ArrayList<>(whereParams);
        dataParams.add(size);
        dataParams.add(offset);
        List<Map<String, Object>> data = jdbcTemplate.queryForList(dataSql, dataParams.toArray());

        int totalPages = size > 0 ? (int) Math.ceil((double) total / size) : 0;

        Map<String, Object> result = new HashMap<>();
        result.put("data", data);
        result.put("total", total);
        result.put("page", page);
        result.put("size", size);
        result.put("total_pages", totalPages);

        if (enableEntityJoin && !joinConfigs.isEmpty()) {
            Map<String, Object> entityJoinInfo = new HashMap<>();
            entityJoinInfo.put("join_configs", joinConfigs);
            entityJoinInfo.put("strategy", "full_join");
            Map<String, Object> perf = new HashMap<>();
            perf.put("query_time", System.currentTimeMillis() - startTime);
            entityJoinInfo.put("performance", perf);
            result.put("entity_join_info", entityJoinInfo);
        }

        return result;
    }

    public List<Map<String, Object>> detectEntityJoins(
            String tableName,
            Map<String, Object> screenEntityConfigs,
            String companyCode) {

        Map<String, Object> params = new HashMap<>();
        params.put("table_name", tableName);
        params.put("company_code", companyCode);

        List<Map<String, Object>> entityColumns = sqlSession.selectList(NS + "selectEntityColumns", params);
        List<Map<String, Object>> joinConfigs = new ArrayList<>();

        Map<String, Object> writerCheck = new HashMap<>();
        writerCheck.put("table_name", tableName);
        writerCheck.put("column_name", "writer");
        if (sqlSession.selectOne(NS + "selectColumnExists", writerCheck) != null) {
            Map<String, Object> writerConfig = new LinkedHashMap<>();
            writerConfig.put("source_table", tableName);
            writerConfig.put("source_column", "writer");
            writerConfig.put("reference_table", "user_info");
            writerConfig.put("reference_column", "user_id");
            writerConfig.put("display_column", "user_name");
            writerConfig.put("display_columns", List.of("user_name"));
            writerConfig.put("alias_column", "writer_name");
            writerConfig.put("separator", " - ");
            if (validateJoinConfig(writerConfig)) {
                joinConfigs.add(writerConfig);
            }
        }

        for (Map<String, Object> col : entityColumns) {
            String columnName  = (String) col.get("column_name");
            String inputType   = (String) col.get("input_type");
            String refTable    = (String) col.get("reference_table");
            String refCol      = (String) col.get("reference_column");
            String displayCol  = (String) col.get("display_column");

            if ("category".equals(inputType)) {
                refTable   = notBlank(refTable)   ? refTable   : "category_values";
                refCol     = notBlank(refCol)      ? refCol     : "value_code";
                displayCol = notBlank(displayCol)  ? displayCol : "value_label";
            }

            if (columnName == null || refTable == null || refCol == null) continue;

            String finalDisplayCol;
            if (notBlank(displayCol) && !"none".equals(displayCol)) {
                finalDisplayCol = displayCol;
            } else {
                finalDisplayCol = autoDetectDisplayColumn(refTable, refCol);
            }

            String aliasColumn = columnName + "_" + finalDisplayCol;

            Map<String, Object> joinConfig = new LinkedHashMap<>();
            joinConfig.put("source_table", tableName);
            joinConfig.put("source_column", columnName);
            joinConfig.put("reference_table", refTable);
            joinConfig.put("reference_column", refCol);
            joinConfig.put("display_column", finalDisplayCol);
            joinConfig.put("display_columns", List.of(finalDisplayCol));
            joinConfig.put("alias_column", aliasColumn);
            joinConfig.put("separator", " - ");

            if (validateJoinConfig(joinConfig)) {
                joinConfigs.add(joinConfig);
            }
        }

        log.info("Entity 조인 감지 완료: tableName={}, joinCount={}", tableName, joinConfigs.size());
        return joinConfigs;
    }

    public List<Map<String, Object>> getReferenceTableColumns(String tableName, String companyCode) {
        Map<String, Object> schemaParams = new HashMap<>();
        schemaParams.put("table_name", tableName);
        List<Map<String, Object>> schemaCols = sqlSession.selectList(NS + "selectTableSchemaColumns", schemaParams);

        Map<String, Object> metaParams = new HashMap<>();
        metaParams.put("table_name", tableName);
        metaParams.put("company_code", companyCode);
        List<Map<String, Object>> metaCols = sqlSession.selectList(NS + "selectColumnMetadata", metaParams);
        Map<String, Map<String, Object>> metaByCol = metaCols.stream()
                .collect(Collectors.toMap(
                        m -> (String) m.get("column_name"),
                        m -> m,
                        (a, b) -> a));

        List<Map<String, Object>> result = new ArrayList<>();
        for (Map<String, Object> col : schemaCols) {
            String colName   = (String) col.get("column_name");
            String dataType  = (String) col.get("data_type");
            Map<String, Object> meta = metaByCol.getOrDefault(colName, Collections.emptyMap());
            String label     = (String) meta.getOrDefault("column_label", colName);
            String inputType = (String) meta.get("input_type");

            Map<String, Object> item = new LinkedHashMap<>();
            item.put("column_name", colName);
            item.put("display_name", label);
            item.put("data_type", dataType);
            item.put("input_type", inputType);
            result.add(item);
        }
        return result;
    }

    public void updateColumnEntitySettings(String tableName, String columnName,
                                            Map<String, Object> settings, String companyCode) {
        Map<String, Object> params = new HashMap<>(settings);
        params.put("table_name", tableName);
        params.put("column_name", columnName);
        params.put("company_code", companyCode);
        sqlSession.update(NS + "upsertColumnEntitySettings", params);
        log.info("Entity 설정 업데이트: {}.{}", tableName, columnName);
    }

    public Map<String, Object> getEntityJoinColumns(String tableName, String companyCode) {
        List<Map<String, Object>> allJoinConfigs = detectEntityJoins(tableName, null, companyCode);
        List<Map<String, Object>> joinConfigs = allJoinConfigs.stream()
                .filter(c -> !"category_values".equals(c.get("reference_table")))
                .collect(Collectors.toList());

        if (joinConfigs.isEmpty()) {
            Map<String, Object> empty = new LinkedHashMap<>();
            empty.put("table_name", tableName);
            empty.put("join_tables", Collections.emptyList());
            empty.put("available_columns", Collections.emptyList());
            empty.put("summary", Map.of("total_join_tables", 0, "total_available_columns", 0));
            return empty;
        }

        List<Map<String, Object>> joinTablesInfo = new ArrayList<>();
        List<Map<String, Object>> allAvailableColumns = new ArrayList<>();
        Set<String> generatedAliases = new HashSet<>();

        for (Map<String, Object> config : joinConfigs) {
            String refTable    = (String) config.get("reference_table");
            String srcCol      = (String) config.get("source_column");
            String currentDisp = (String) config.get("display_column");

            List<Map<String, Object>> availCols = getReferenceTableColumns(refTable, companyCode);

            List<Map<String, Object>> colInfoList = availCols.stream()
                    .map(c -> {
                        Map<String, Object> ci = new LinkedHashMap<>();
                        ci.put("column_name", c.get("column_name"));
                        ci.put("column_label", c.get("display_name"));
                        ci.put("data_type", c.get("data_type"));
                        ci.put("input_type", c.get("input_type"));
                        ci.put("is_nullable", true);
                        return ci;
                    }).collect(Collectors.toList());

            Map<String, Object> joinTableInfo = new LinkedHashMap<>();
            joinTableInfo.put("join_config", config);
            joinTableInfo.put("table_name", refTable);
            joinTableInfo.put("current_display_column", currentDisp);
            joinTableInfo.put("available_columns", colInfoList);
            joinTablesInfo.add(joinTableInfo);

            for (Map<String, Object> col : colInfoList) {
                String colName   = (String) col.get("column_name");
                String joinAlias = srcCol + "_" + colName;
                if (!generatedAliases.add(joinAlias)) continue;

                Map<String, Object> avail = new LinkedHashMap<>();
                avail.put("table_name", refTable);
                avail.put("column_name", colName);
                avail.put("column_label", col.get("column_label"));
                avail.put("data_type", col.get("data_type"));
                avail.put("input_type", col.get("input_type"));
                avail.put("join_alias", joinAlias);
                avail.put("suggested_label", col.get("column_label"));
                allAvailableColumns.add(avail);
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("table_name", tableName);
        result.put("join_tables", joinTablesInfo);
        result.put("available_columns", allAvailableColumns);
        result.put("summary", Map.of(
                "total_join_tables", joinConfigs.size(),
                "total_available_columns", allAvailableColumns.size()));
        return result;
    }

    // ─── Cache Management ───────────────────────────────────────────────────

    public Map<String, Object> getCacheStatus() {
        List<Map<String, Object>> caches = new ArrayList<>();
        for (Map.Entry<String, CacheStats> e : cacheStats.entrySet()) {
            CacheStats s = e.getValue();
            Map<String, Object> info = new LinkedHashMap<>();
            info.put("cache_key", e.getKey());
            info.put("size", s.dataSize);
            info.put("hit_rate", s.getHitRate());
            info.put("last_updated", s.lastUpdated.toString());
            caches.add(info);
        }
        double overall = cacheStats.values().stream()
                .mapToDouble(CacheStats::getHitRate).average().orElse(0);
        int totalSize = cacheStats.values().stream().mapToInt(s -> s.dataSize).sum();

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("overall_hit_rate", overall);
        result.put("caches", caches);
        result.put("summary", Map.of(
                "total_caches", caches.size(),
                "total_size", totalSize,
                "average_hit_rate", overall));
        return result;
    }

    public void invalidateCache(String table, String keyColumn, String displayColumn) {
        if (table != null && keyColumn != null && displayColumn != null) {
            String key = table + ":" + keyColumn + ":" + displayColumn;
            referenceCache.remove(key);
            cacheStats.remove(key);
            log.info("캐시 무효화: {}", key);
        } else {
            referenceCache.clear();
            cacheStats.clear();
            log.info("전체 캐시 무효화");
        }
    }

    public void preloadCommonCaches() {
        List<String[]> common = List.of(
                new String[]{"user_info",  "user_id",   "user_name"},
                new String[]{"dept_info",  "dept_code", "dept_name"},
                new String[]{"company_mng","company_code","company_name"}
        );
        for (String[] c : common) {
            try {
                loadReferenceCache(c[0], c[1], c[2]);
            } catch (Exception e) {
                log.warn("캐시 프리로드 실패: {}:{} - {}", c[0], c[1], e.getMessage());
            }
        }
    }

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

    private void loadReferenceCache(String tableName, String keyColumn, String displayColumn) {
        validateIdentifier(tableName);
        validateIdentifier(keyColumn);
        validateIdentifier(displayColumn);

        String sql = String.format(
                "SELECT \"%s\"::TEXT AS k, \"%s\"::TEXT AS v FROM %s LIMIT 10000",
                keyColumn, displayColumn, tableName);

        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
        Map<String, String> cache = new LinkedHashMap<>();
        for (Map<String, Object> row : rows) {
            Object k = row.get("k");
            Object v = row.get("v");
            if (k != null) cache.put(k.toString(), v != null ? v.toString() : "");
        }

        String cacheKey = tableName + ":" + keyColumn + ":" + displayColumn;
        referenceCache.put(cacheKey, cache);
        CacheStats stats = cacheStats.computeIfAbsent(cacheKey, k -> new CacheStats());
        stats.dataSize = cache.size();
        stats.lastUpdated = Instant.now();
        log.info("캐시 로드 완료: {} ({}건)", cacheKey, cache.size());
    }

    private boolean validateJoinConfig(Map<String, Object> config) {
        String refTable   = (String) config.get("reference_table");
        String refCol     = (String) config.get("reference_column");
        String displayCol = (String) config.get("display_column");

        Map<String, Object> p = new HashMap<>();
        p.put("table_name", refTable);
        if (sqlSession.selectOne(NS + "selectTableExists", p) == null) {
            log.warn("참조 테이블 없음: {}", refTable);
            return false;
        }

        Map<String, Object> cp = new HashMap<>();
        cp.put("table_name", refTable);
        cp.put("column_name", refCol);
        if (sqlSession.selectOne(NS + "selectColumnExists", cp) == null) {
            cp.put("column_name", "id");
            if (sqlSession.selectOne(NS + "selectColumnExists", cp) != null) {
                config.put("reference_column", "id");
            } else {
                log.warn("참조 컬럼 없음: {}.{}", refTable, refCol);
                return false;
            }
        }

        if (notBlank(displayCol) && !"none".equals(displayCol)) {
            Map<String, Object> dp = new HashMap<>();
            dp.put("table_name", refTable);
            dp.put("column_name", displayCol);
            if (sqlSession.selectOne(NS + "selectColumnExists", dp) == null) {
                log.warn("표시 컬럼 없음: {}.{}", refTable, displayCol);
                return false;
            }
        }
        return true;
    }

    private String autoDetectDisplayColumn(String refTable, String refCol) {
        Map<String, Object> p = new HashMap<>();
        p.put("table_name", refTable);
        List<Map<String, Object>> cols = sqlSession.selectList(NS + "selectTableSchemaColumns", p);
        List<String> colNames = cols.stream()
                .map(c -> (String) c.get("column_name"))
                .collect(Collectors.toList());

        return colNames.stream()
                .filter(c -> c.endsWith("_name") && !"company_name".equals(c))
                .findFirst()
                .or(() -> colNames.stream().filter("name"::equals).findFirst())
                .or(() -> colNames.stream().filter(c -> c.equals("label") || c.endsWith("_label")).findFirst())
                .or(() -> colNames.stream().filter("title"::equals).findFirst())
                .orElse(refCol);
    }

    private Map<String, String> buildAliasMap(List<Map<String, Object>> joinConfigs) {
        Map<String, String> aliasMap = new LinkedHashMap<>();
        Set<String> used = new HashSet<>();
        for (Map<String, Object> config : joinConfigs) {
            String refTable = (String) config.get("reference_table");
            String srcCol   = (String) config.get("source_column");
            String key = refTable + ":" + srcCol;
            if (!aliasMap.containsKey(key)) {
                String alias = generateAlias(refTable, used);
                aliasMap.put(key, alias);
            }
        }
        return aliasMap;
    }

    private String generateAlias(String tableName, Set<String> used) {
        String base = tableName.length() >= 3 ? tableName.substring(0, 3) : tableName;
        String alias = base;
        int i = 1;
        while (!used.add(alias)) {
            alias = base + i++;
        }
        return alias;
    }

    private String buildDataQuery(String tableName, List<Map<String, Object>> joinConfigs,
                                   Map<String, String> aliasMap,
                                   String whereClause, String sortBy, String sortOrder) {
        StringBuilder sql = new StringBuilder("SELECT main.*");
        Set<String> generatedAliases = new HashSet<>();

        for (Map<String, Object> config : joinConfigs) {
            String refTable   = (String) config.get("reference_table");
            String srcCol     = (String) config.get("source_column");
            String displayCol = (String) config.get("display_column");
            String aliasCol   = (String) config.get("alias_column");
            String tblAlias   = aliasMap.get(refTable + ":" + srcCol);
            if (tblAlias == null || displayCol == null) continue;

            if (generatedAliases.add(aliasCol)) {
                sql.append(", COALESCE(").append(tblAlias).append(".\"").append(displayCol)
                   .append("\"::TEXT, '') AS \"").append(aliasCol).append("\"");
            }
            String labelAlias = srcCol + "_label";
            if (generatedAliases.add(labelAlias)) {
                sql.append(", COALESCE(").append(tblAlias).append(".\"").append(displayCol)
                   .append("\"::TEXT, '') AS \"").append(labelAlias).append("\"");
            }
        }

        sql.append(" FROM ").append(tableName).append(" main");

        Set<String> addedJoins = new HashSet<>();
        for (Map<String, Object> config : joinConfigs) {
            String refTable = (String) config.get("reference_table");
            String srcCol   = (String) config.get("source_column");
            String refCol   = (String) config.get("reference_column");
            String joinKey  = refTable + ":" + srcCol;
            String tblAlias = aliasMap.get(joinKey);
            if (tblAlias == null || !addedJoins.add(joinKey)) continue;

            sql.append(" LEFT JOIN ").append(refTable).append(" ").append(tblAlias)
               .append(" ON main.\"").append(srcCol).append("\"::TEXT = ")
               .append(tblAlias).append(".\"").append(refCol).append("\"::TEXT");

            if ("category_values".equals(refTable)) {
                sql.append(" AND ").append(tblAlias).append(".table_name = '").append(tableName).append("'")
                   .append(" AND ").append(tblAlias).append(".column_name = '").append(srcCol).append("'")
                   .append(" AND ").append(tblAlias).append(".company_code = main.company_code");
            } else if (!"user_info".equals(refTable)) {
                sql.append(" AND ").append(tblAlias).append(".company_code = main.company_code");
            }
        }

        if (!whereClause.isEmpty()) {
            sql.append(" WHERE ").append(whereClause);
        }

        if (notBlank(sortBy) && IDENTIFIER_PATTERN.matcher(sortBy).matches()) {
            sql.append(" ORDER BY main.\"").append(sortBy).append("\" ")
               .append("desc".equalsIgnoreCase(sortOrder) ? "DESC" : "ASC");
        } else {
            sql.append(" ORDER BY 1");
        }

        sql.append(" LIMIT ? OFFSET ?");
        return sql.toString();
    }

    private String buildCountQuery(String tableName, List<Map<String, Object>> joinConfigs,
                                    Map<String, String> aliasMap, String whereClause) {
        StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM ").append(tableName).append(" main");

        Set<String> addedJoins = new HashSet<>();
        for (Map<String, Object> config : joinConfigs) {
            String refTable = (String) config.get("reference_table");
            String srcCol   = (String) config.get("source_column");
            String refCol   = (String) config.get("reference_column");
            String joinKey  = refTable + ":" + srcCol;
            String tblAlias = aliasMap.get(joinKey);
            if (tblAlias == null || !addedJoins.add(joinKey)) continue;

            sql.append(" LEFT JOIN ").append(refTable).append(" ").append(tblAlias)
               .append(" ON main.\"").append(srcCol).append("\"::TEXT = ")
               .append(tblAlias).append(".\"").append(refCol).append("\"::TEXT");

            if ("category_values".equals(refTable)) {
                sql.append(" AND ").append(tblAlias).append(".table_name = '").append(tableName).append("'")
                   .append(" AND ").append(tblAlias).append(".column_name = '").append(srcCol).append("'")
                   .append(" AND ").append(tblAlias).append(".company_code = main.company_code");
            } else if (!"user_info".equals(refTable)) {
                sql.append(" AND ").append(tblAlias).append(".company_code = main.company_code");
            }
        }

        if (!whereClause.isEmpty()) {
            sql.append(" WHERE ").append(whereClause);
        }
        return sql.toString();
    }

    private String buildWhereClause(String tableName,
                                     Map<String, Object> searchConditions,
                                     List<Object> params) {
        if (searchConditions == null || searchConditions.isEmpty()) return "";
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> e : searchConditions.entrySet()) {
            if (isKeywordSearchKey(e.getKey())) {
                SearchCondition condition = normalizeSearchCondition(e.getValue());
                if (condition == null || isBlankValue(condition.value())) continue;
                appendKeywordSearchCondition(sb, params, tableName, condition);
                continue;
            }
            if (!IDENTIFIER_PATTERN.matcher(e.getKey()).matches()) continue;
            SearchCondition condition = normalizeSearchCondition(e.getValue());
            if (condition == null) continue;
            if (sb.length() > 0) sb.append(" AND ");
            appendSearchCondition(sb, params, e.getKey(), condition);
        }
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    private SearchCondition normalizeSearchCondition(Object rawValue) {
        if (rawValue == null) return null;

        if (rawValue instanceof Map<?, ?> rawMap) {
            Map<String, Object> map = (Map<String, Object>) rawMap;
            String operator = String.valueOf(map.getOrDefault("operator", "contains"));
            Object value = map.get("value");

            if (!"isEmpty".equalsIgnoreCase(operator)
                    && !"isNotEmpty".equalsIgnoreCase(operator)
                    && isBlankValue(value)) {
                return null;
            }

            return new SearchCondition(operator, value);
        }

        if (isBlankValue(rawValue)) return null;
        return new SearchCondition("equals", rawValue);
    }

    private void appendSearchCondition(StringBuilder sb,
                                       List<Object> params,
                                       String columnName,
                                       SearchCondition condition) {
        String op = condition.operator();
        Object value = condition.value();
        String colExpr = "main.\"" + columnName + "\"";

        switch (op.toLowerCase()) {
            case "contains" -> {
                sb.append(colExpr).append("::text ILIKE ?");
                params.add("%" + value + "%");
            }
            case "startswith" -> {
                sb.append(colExpr).append("::text ILIKE ?");
                params.add(value + "%");
            }
            case "endswith" -> {
                sb.append(colExpr).append("::text ILIKE ?");
                params.add("%" + value);
            }
            case "notequals" -> {
                sb.append("COALESCE(").append(colExpr).append("::text, '') <> ?");
                params.add(String.valueOf(value));
            }
            case "isempty" -> sb.append("(").append(colExpr).append(" IS NULL OR ")
                    .append(colExpr).append("::text = '')");
            case "isnotempty" -> sb.append("(").append(colExpr).append(" IS NOT NULL AND ")
                    .append(colExpr).append("::text <> '')");
            case "greaterthan" -> {
                sb.append(colExpr).append("::text > ?");
                params.add(String.valueOf(value));
            }
            case "lessthan" -> {
                sb.append(colExpr).append("::text < ?");
                params.add(String.valueOf(value));
            }
            case "equals", "=", "exact" -> {
                sb.append(colExpr).append("::text = ?");
                params.add(String.valueOf(value));
            }
            default -> {
                sb.append(colExpr).append("::text ILIKE ?");
                params.add("%" + value + "%");
            }
        }
    }

    private void appendKeywordSearchCondition(StringBuilder sb,
                                              List<Object> params,
                                              String tableName,
                                              SearchCondition condition) {
        List<String> columns = getKeywordSearchColumns(tableName);
        if (columns.isEmpty()) return;

        if (sb.length() > 0) sb.append(" AND ");
        sb.append("(");
        for (int i = 0; i < columns.size(); i++) {
            if (i > 0) sb.append(" OR ");
            sb.append("main.\"").append(columns.get(i)).append("\"::text ILIKE ?");
            params.add("%" + String.valueOf(condition.value()) + "%");
        }
        sb.append(")");
    }

    private List<String> getKeywordSearchColumns(String tableName) {
        validateIdentifier(tableName);

        String sql = """
                SELECT column_name
                FROM information_schema.columns
                WHERE table_schema = current_schema()
                  AND table_name = ?
                ORDER BY ordinal_position
                """;

        List<String> allColumns = jdbcTemplate.queryForList(sql, String.class, tableName);

        return allColumns.stream()
                .filter(Objects::nonNull)
                .filter(col -> IDENTIFIER_PATTERN.matcher(col).matches())
                .filter(col -> !Set.of(
                        "company_code",
                        "created_date",
                        "updated_date",
                        "created_at",
                        "updated_at",
                        "create_user",
                        "update_user"
                ).contains(col))
                .toList();
    }

    private boolean isKeywordSearchKey(String key) {
        return "keyword".equalsIgnoreCase(key)
                || "search".equalsIgnoreCase(key)
                || "searchterm".equalsIgnoreCase(key)
                || "q".equalsIgnoreCase(key);
    }

    private boolean isBlankValue(Object value) {
        return value == null || value.toString().isBlank() || "__ALL__".equals(value.toString());
    }

    private void validateIdentifier(String name) {
        if (name == null || !IDENTIFIER_PATTERN.matcher(name).matches()) {
            throw new IllegalArgumentException("유효하지 않은 식별자: " + name);
        }
    }

    private boolean notBlank(String s) {
        return s != null && !s.isBlank();
    }

    private int toInt(Object val, int def) {
        if (val == null) return def;
        try { return Integer.parseInt(val.toString()); }
        catch (NumberFormatException e) { return def; }
    }

    private static class CacheStats {
        long hits = 0;
        long misses = 0;
        int dataSize = 0;
        Instant lastUpdated = Instant.now();

        double getHitRate() {
            long total = hits + misses;
            return total == 0 ? 0.0 : (double) hits / total;
        }
    }

    private record SearchCondition(String operator, Object value) {}
}
