package com.erp.service;

import com.erp.common.BaseService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

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

    private static final String NS = "dynamicForm.";

    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;

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

    // ── 내부 유틸 ──────────────────────────────────────────────────────────────

    private void validateName(String name) {
        if (name == null || !VALID_NAME.matcher(name).matches()) {
            throw new IllegalArgumentException("유효하지 않은 테이블명 또는 컬럼명: " + name);
        }
    }

    /**
     * VIEW인 경우 원본 기본 테이블을 반환
     */
    public String resolveBaseTable(String tableName) {
        try {
            Map<String, Object> params = Map.of("table_name", tableName);
            Map<String, Object> tableInfo = sqlSession.selectOne(NS + "selectTableType", params);

            if (tableInfo == null || !"VIEW".equals(tableInfo.get("table_type"))) {
                return tableName;
            }

            Map<String, Object> viewDef = sqlSession.selectOne(NS + "selectViewDefinition", params);
            if (viewDef != null) {
                String definition = (String) viewDef.get("view_definition");
                if (definition != null) {
                    java.util.regex.Matcher m = Pattern.compile("FROM\\s+\\(?(?:public\\.)?([a-zA-Z_][a-zA-Z0-9_]*)\\s", java.util.regex.Pattern.CASE_INSENSITIVE).matcher(definition);
                    if (m.find()) {
                        String baseTable = m.group(1);
                        log.info("VIEW {} → 원본 테이블 {}", tableName, baseTable);
                        return baseTable;
                    }
                }
            }
        } catch (Exception e) {
            log.warn("VIEW 원본 테이블 조회 실패: {}", e.getMessage());
        }
        return tableName;
    }

    /**
     * 테이블 컬럼명 목록 조회
     */
    private List<String> getColumnNames(String tableName) {
        Map<String, Object> params = Map.of("table_name", tableName);
        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectColumnNames", params);
        return rows.stream()
                .map(r -> (String) r.get("column_name"))
                .collect(Collectors.toList());
    }

    /**
     * 테이블 컬럼 타입 맵 (column_name → data_type)
     */
    private Map<String, String> getColumnTypeMap(String tableName) {
        Map<String, Object> params = Map.of("table_name", tableName);
        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectColumnTypes", params);
        Map<String, String> result = new LinkedHashMap<>();
        for (Map<String, Object> row : rows) {
            result.put((String) row.get("column_name"), (String) row.get("data_type"));
        }
        return result;
    }

    /**
     * 테이블 기본키 목록 조회
     */
    private List<String> getPrimaryKeyList(String tableName) {
        Map<String, Object> params = Map.of("table_name", tableName);
        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectPrimaryKeys", params);
        return rows.stream()
                .map(r -> (String) r.get("column_name"))
                .collect(Collectors.toList());
    }

    /**
     * 값을 PostgreSQL 타입에 맞게 변환
     */
    private Object convertValue(Object value, String dataType) {
        if (value == null || "".equals(value)) return null;

        String dt = dataType.toLowerCase();

        if (dt.contains("integer") || dt.contains("bigint") || dt.contains("serial")) {
            try {
                return Long.parseLong(value.toString());
            } catch (NumberFormatException e) {
                return null;
            }
        }
        if (dt.contains("numeric") || dt.contains("decimal") || dt.contains("real") || dt.contains("double")) {
            try {
                return Double.parseDouble(value.toString());
            } catch (NumberFormatException e) {
                return null;
            }
        }
        if (dt.contains("boolean")) {
            if (value instanceof Boolean) return value;
            String s = value.toString().toLowerCase();
            return "true".equals(s) || "1".equals(s);
        }
        if (dt.contains("date") || dt.contains("timestamp") || dt.contains("time")) {
            if (value instanceof java.util.Date) return value;
            String s = value.toString().trim();
            if (s.isEmpty()) return null;
            try {
                if (s.matches("\\d{4}-\\d{2}-\\d{2}")) {
                    if ("date".equals(dt)) return s;
                    return LocalDateTime.of(LocalDate.parse(s), java.time.LocalTime.MIDNIGHT);
                }
                if (s.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
                    return LocalDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                }
                return LocalDateTime.parse(s);
            } catch (Exception e) {
                return null;
            }
        }
        return value;
    }

    /**
     * company_code UUID 정규화 (36자 → 32자)
     */
    private String normalizeCompanyCode(String code) {
        if (code == null) return null;
        if (code.length() == 36 && code.contains("-")) {
            code = code.replace("-", "");
        }
        if (code.length() > 32) {
            code = code.substring(0, 32);
        }
        return code;
    }

    // ── 폼 저장 (기본) ─────────────────────────────────────────────────────────

    /**
     * 폼 데이터 저장 (실제 테이블에 UPSERT)
     */
    @Transactional
    public Map<String, Object> saveFormData(int screenId, String tableNameInput,
                                            Map<String, Object> data, String ipAddress) {
        String tableName = resolveBaseTable(tableNameInput);
        validateName(tableName);

        List<String> tableColumns = getColumnNames(tableName);
        List<String> primaryKeys = getPrimaryKeyList(tableName);
        Map<String, String> columnTypes = getColumnTypeMap(tableName);

        // 메타 필드 분리
        Object createdBy = data.get("created_by");
        Object updatedBy = data.get("updated_by");
        Object writer = data.get("writer");
        Object companyCodeRaw = data.get("company_code");

        Map<String, Object> dataToInsert = new LinkedHashMap<>(data);
        dataToInsert.remove("created_by");
        dataToInsert.remove("updated_by");
        dataToInsert.remove("writer");
        dataToInsert.remove("company_code");
        dataToInsert.remove("screen_id");

        // 공통 타임스탬프
        if (tableColumns.contains("created_date")) dataToInsert.put("created_date", new java.util.Date());
        if (tableColumns.contains("updated_date")) dataToInsert.put("updated_date", new java.util.Date());
        if (tableColumns.contains("created_date")) dataToInsert.put("created_date", new java.util.Date());
        if (tableColumns.contains("updated_date") && !dataToInsert.containsKey("updated_date")) dataToInsert.put("updated_date", new java.util.Date());
        if (tableColumns.contains("created_date") && !dataToInsert.containsKey("created_date")) dataToInsert.put("created_date", new java.util.Date());

        // 작성자 정보
        if (writer != null && tableColumns.contains("writer")) dataToInsert.put("writer", writer);
        if (createdBy != null && tableColumns.contains("created_by")) dataToInsert.put("created_by", createdBy);
        if (updatedBy != null && tableColumns.contains("updated_by")) dataToInsert.put("updated_by", updatedBy);
        if (companyCodeRaw != null && tableColumns.contains("company_code")) {
            dataToInsert.put("company_code", normalizeCompanyCode(companyCodeRaw.toString()));
        }

        // Repeater 데이터 분리 (배열 타입)
        List<Map<String, Object>> separateRepeaters = new ArrayList<>();
        List<Map<String, Object>> mergedRepeaters = new ArrayList<>();
        List<String> keysToRemove = new ArrayList<>();

        for (Map.Entry<String, Object> entry : dataToInsert.entrySet()) {
            Object val = entry.getValue();
            List<Object> parsedArray = null;

            if (val instanceof List) {
                parsedArray = (List<Object>) val;
            } else if (val instanceof String) {
                String s = ((String) val).trim();
                if (s.startsWith("[") && s.endsWith("]")) {
                    try {
                        parsedArray = objectMapper.readValue(s, new TypeReference<>() {});
                    } catch (Exception ignored) {}
                }
            }

            if (parsedArray != null && !parsedArray.isEmpty()) {
                String targetTable = null;
                List<Object> actualItems = parsedArray;
                if (!parsedArray.isEmpty() && parsedArray.get(0) instanceof Map) {
                    Map<?, ?> firstItem = (Map<?, ?>) parsedArray.get(0);
                    if (firstItem.containsKey("_targetTable")) {
                        targetTable = (String) firstItem.get("_targetTable");
                        actualItems = parsedArray.stream()
                                .map(item -> {
                                    Map<String, Object> copy = new LinkedHashMap<>((Map<String, Object>) item);
                                    copy.remove("_targetTable");
                                    return (Object) copy;
                                }).collect(Collectors.toList());
                    }
                }
                keysToRemove.add(entry.getKey());
                Map<String, Object> repeaterMeta = new LinkedHashMap<>();
                repeaterMeta.put("component_id", entry.getKey());
                repeaterMeta.put("target_table", targetTable);
                repeaterMeta.put("data", actualItems);

                if (targetTable != null && !targetTable.equals(tableName)) {
                    separateRepeaters.add(repeaterMeta);
                } else {
                    mergedRepeaters.add(repeaterMeta);
                }
            }
        }
        keysToRemove.forEach(dataToInsert::remove);

        // 존재하지 않는 컬럼 제거
        dataToInsert.keySet().removeIf(key -> !tableColumns.contains(key));

        // 타입 변환
        for (String col : new ArrayList<>(dataToInsert.keySet())) {
            String dt = columnTypes.get(col);
            if (dt != null) {
                dataToInsert.put(col, convertValue(dataToInsert.get(col), dt));
            }
        }

        List<Map<String, Object>> results = new ArrayList<>();

        if (!mergedRepeaters.isEmpty()) {
            // 병합 모드: 헤더 + 품목 각각 INSERT
            for (Map<String, Object> repeater : mergedRepeaters) {
                List<Object> items = (List<Object>) repeater.get("data");
                for (Object item : items) {
                    Map<String, Object> itemMap = (Map<String, Object>) item;
                    Map<String, Object> mergedData = new LinkedHashMap<>(dataToInsert);
                    Map<String, Object> itemCopy = new LinkedHashMap<>(itemMap);
                    itemCopy.remove("created_date");
                    itemCopy.remove("_isNewItem");
                    boolean isExisting = Boolean.TRUE.equals(itemCopy.remove("_existingRecord"));
                    if (!isExisting) itemCopy.remove("id");

                    // 실제 컬럼만 필터링 + 타입 변환
                    itemCopy.keySet().removeIf(k -> !tableColumns.contains(k));
                    for (String col : new ArrayList<>(itemCopy.keySet())) {
                        String dt = columnTypes.get(col);
                        if (dt != null) itemCopy.put(col, convertValue(itemCopy.get(col), dt));
                    }
                    mergedData.putAll(itemCopy);

                    Map<String, Object> inserted = executeUpsert(tableName, mergedData, primaryKeys);
                    results.add(inserted);
                }
            }
        } else {
            Map<String, Object> inserted = executeUpsert(tableName, dataToInsert, primaryKeys);
            results.add(inserted);
        }

        // 별도 테이블 Repeater 저장
        for (Map<String, Object> repeater : separateRepeaters) {
            String targetTable = (String) repeater.get("target_table");
            validateName(targetTable);
            List<String> targetCols = getColumnNames(targetTable);
            List<String> targetPks = getPrimaryKeyList(targetTable);
            Map<String, String> targetTypes = getColumnTypeMap(targetTable);
            List<Object> items = (List<Object>) repeater.get("data");

            for (Object item : items) {
                Map<String, Object> itemData = new LinkedHashMap<>((Map<String, Object>) item);
                if (createdBy != null && targetCols.contains("created_by")) itemData.put("created_by", createdBy);
                if (updatedBy != null && targetCols.contains("updated_by")) itemData.put("updated_by", updatedBy);
                if (companyCodeRaw != null && targetCols.contains("company_code")) {
                    itemData.put("company_code", normalizeCompanyCode(companyCodeRaw.toString()));
                }
                if (targetCols.contains("created_date") && !itemData.containsKey("created_date")) itemData.put("created_date", new java.util.Date());
                itemData.keySet().removeIf(k -> !targetCols.contains(k));
                for (String col : new ArrayList<>(itemData.keySet())) {
                    String dt = targetTypes.get(col);
                    if (dt != null) itemData.put(col, convertValue(itemData.get(col), dt));
                }
                executeUpsert(targetTable, itemData, targetPks);
            }
        }

        Map<String, Object> insertedRecord = results.isEmpty() ? new HashMap<>() : results.get(0);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", insertedRecord.getOrDefault("id", insertedRecord.get("objid")));
        result.put("screen_id", screenId);
        result.put("table_name", tableName);
        result.put("data", insertedRecord);
        result.put("created_at", insertedRecord.get("created_date"));
        result.put("updated_at", insertedRecord.get("updated_date"));
        result.put("created_by", insertedRecord.getOrDefault("created_by", createdBy));
        result.put("updated_by", insertedRecord.getOrDefault("updated_by", updatedBy));
        return result;
    }

    /**
     * UPSERT 실행 (JdbcTemplate)
     */
    private Map<String, Object> executeUpsert(String tableName, Map<String, Object> data,
                                               List<String> primaryKeys) {
        if (data.isEmpty()) return new HashMap<>();

        List<String> cols = new ArrayList<>(data.keySet());
        List<Object> vals = cols.stream().map(data::get).collect(Collectors.toList());
        String placeholders = cols.stream().map(c -> "?").collect(Collectors.joining(", "));
        String colList = cols.stream().map(c -> "\"" + c + "\"").collect(Collectors.joining(", "));

        String sql;
        if (!primaryKeys.isEmpty()) {
            String conflict = primaryKeys.stream().collect(Collectors.joining(", "));
            List<String> updateCols = cols.stream().filter(c -> !primaryKeys.contains(c)).collect(Collectors.toList());
            String updateSet = updateCols.stream().map(c -> "\"" + c + "\" = EXCLUDED.\"" + c + "\"").collect(Collectors.joining(", "));

            if (!updateSet.isEmpty()) {
                sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s RETURNING *",
                        tableName, colList, placeholders, conflict, updateSet);
            } else {
                sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO NOTHING RETURNING *",
                        tableName, colList, placeholders, conflict);
            }
        } else {
            sql = String.format("INSERT INTO %s (%s) VALUES (%s) RETURNING *",
                    tableName, colList, placeholders);
        }

        log.debug("UPSERT SQL: {}", sql);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
        return rows.isEmpty() ? new HashMap<>() : rows.get(0);
    }

    // ── 폼 저장 (enhanced, 동일 로직) ─────────────────────────────────────────

    @Transactional
    public Map<String, Object> saveFormDataEnhanced(int screenId, String tableName,
                                                     Map<String, Object> data) {
        return saveFormData(screenId, tableName, data, null);
    }

    // ── 폼 업데이트 ────────────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> updateFormData(String id, String tableNameInput,
                                              Map<String, Object> data) {
        String tableName = resolveBaseTable(tableNameInput);
        validateName(tableName);

        List<String> tableColumns = getColumnNames(tableName);
        Map<String, String> columnTypes = getColumnTypeMap(tableName);
        List<String> primaryKeys = getPrimaryKeyList(tableName);

        if (primaryKeys.isEmpty()) {
            throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다.");
        }

        Object updatedBy = data.get("updated_by");
        Map<String, Object> dataToUpdate = new LinkedHashMap<>(data);
        dataToUpdate.remove("created_by");
        dataToUpdate.remove("company_code");
        dataToUpdate.remove("screen_id");

        if (tableColumns.contains("updated_date")) dataToUpdate.put("updated_date", new java.util.Date());
        if (tableColumns.contains("updated_date")) dataToUpdate.put("updated_date", new java.util.Date());
        if (tableColumns.contains("created_date") && !dataToUpdate.containsKey("created_date")) dataToUpdate.put("created_date", new java.util.Date());
        if (updatedBy != null && tableColumns.contains("updated_by")) dataToUpdate.put("updated_by", updatedBy);

        dataToUpdate.keySet().removeIf(key -> !tableColumns.contains(key));

        // 타입 변환
        for (String col : new ArrayList<>(dataToUpdate.keySet())) {
            String dt = columnTypes.get(col);
            if (dt != null) dataToUpdate.put(col, convertValue(dataToUpdate.get(col), dt));
        }

        String primaryKeyColumn = primaryKeys.get(0);
        String pkType = columnTypes.getOrDefault(primaryKeyColumn, "text");
        String pkCast = buildPkCast(pkType);

        List<String> cols = new ArrayList<>(dataToUpdate.keySet());
        List<Object> vals = cols.stream().map(dataToUpdate::get).collect(Collectors.toList());
        String setClause = cols.stream()
                .map(c -> "\"" + c + "\" = ?")
                .collect(Collectors.joining(", "));
        vals.add(id);

        String sql = String.format("UPDATE %s SET %s WHERE %s = ?%s RETURNING *",
                tableName, setClause, primaryKeyColumn, pkCast);

        log.debug("UPDATE SQL: {}", sql);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
        Map<String, Object> updated = rows.isEmpty() ? new HashMap<>() : rows.get(0);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", updated.getOrDefault("id", updated.getOrDefault("objid", id)));
        result.put("screen_id", 0);
        result.put("table_name", tableName);
        result.put("data", updated);
        result.put("created_at", updated.get("created_date"));
        result.put("updated_at", updated.get("updated_date"));
        result.put("created_by", updated.get("created_by"));
        result.put("updated_by", updated.getOrDefault("updated_by", updatedBy));
        return result;
    }

    // ── 부분 업데이트 ──────────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> updateFormDataPartial(String id, String tableNameInput,
                                                     Map<String, Object> originalData,
                                                     Map<String, Object> newData) {
        String tableName = resolveBaseTable(tableNameInput);
        validateName(tableName);

        List<String> tableColumns = getColumnNames(tableName);
        Map<String, String> columnTypes = getColumnTypeMap(tableName);
        List<String> primaryKeys = getPrimaryKeyList(tableName);

        if (primaryKeys.isEmpty()) {
            throw new IllegalStateException("테이블 " + tableName + "의 기본키를 찾을 수 없습니다.");
        }

        Map<String, Object> changedFields = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : newData.entrySet()) {
            String key = entry.getKey();
            if (List.of("created_by", "updated_by", "company_code", "screen_id").contains(key)) continue;
            if (!tableColumns.contains(key)) continue;
            if (!Objects.equals(originalData.get(key), entry.getValue())) {
                changedFields.put(key, entry.getValue());
            }
        }

        if (changedFields.isEmpty()) {
            Map<String, Object> noChange = new LinkedHashMap<>();
            noChange.put("success", true);
            noChange.put("data", originalData);
            noChange.put("message", "변경사항이 없어 업데이트하지 않았습니다.");
            return noChange;
        }

        if (tableColumns.contains("updated_date")) changedFields.put("updated_date", new java.util.Date());
        if (tableColumns.contains("updated_date")) changedFields.put("updated_date", new java.util.Date());

        String primaryKeyColumn = primaryKeys.get(0);
        String pkType = columnTypes.getOrDefault(primaryKeyColumn, "text");
        String pkCast = buildPkCast(pkType);

        List<String> cols = new ArrayList<>(changedFields.keySet());
        List<Object> vals = cols.stream().map(c -> {
            Object v = changedFields.get(c);
            String dt = columnTypes.getOrDefault(c, "text");
            // 빈 값 → null
            if (v == null || "".equals(v)) return null;
            // jsonb: 객체/배열은 JSON 문자열로
            if ((dt.equals("jsonb") || dt.equals("json")) && (v instanceof Map || v instanceof List)) {
                try { return objectMapper.writeValueAsString(v); } catch (Exception e) { return v; }
            }
            return v;
        }).collect(Collectors.toList());

        // SET 절 with type cast
        List<String> setClauses = new ArrayList<>();
        for (int i = 0; i < cols.size(); i++) {
            String col = cols.get(i);
            String dt = columnTypes.getOrDefault(col, "text");
            String cast = buildCast(dt);
            setClauses.add("\"" + col + "\" = ?" + cast);
        }
        vals.add(id);

        String sql = String.format("UPDATE %s SET %s WHERE %s = ?%s RETURNING *",
                tableName, String.join(", ", setClauses), primaryKeyColumn, pkCast);

        log.debug("PARTIAL UPDATE SQL: {}", sql);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, vals.toArray());
        Map<String, Object> updated = rows.isEmpty() ? new HashMap<>() : rows.get(0);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", true);
        result.put("data", updated);
        result.put("message", "데이터가 성공적으로 업데이트되었습니다.");
        return result;
    }

    // ── 폼 삭제 ───────────────────────────────────────────────────────────────

    @Transactional
    public void deleteFormData(String id, String tableName, String companyCode,
                               String userId, Integer screenId) {
        String actualTable = resolveBaseTable(tableName);
        validateName(actualTable);

        Map<String, Object> pkInfo = sqlSession.selectOne(NS + "selectPrimaryKeyWithType", Map.of("table_name", actualTable));
        if (pkInfo == null) {
            throw new IllegalStateException("테이블 " + actualTable + "의 기본키를 찾을 수 없습니다.");
        }

        String pkColumn = (String) pkInfo.get("column_name");
        String pkDataType = (String) pkInfo.get("data_type");
        String pkCast = buildPkCast(pkDataType);

        String sql = String.format("DELETE FROM %s WHERE %s = ?%s RETURNING *",
                actualTable, pkColumn, pkCast);

        log.debug("DELETE SQL: {}", sql);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, id);
        if (rows.isEmpty()) {
            throw new IllegalStateException("테이블 " + actualTable + "에서 ID '" + id + "'에 해당하는 레코드를 찾을 수 없습니다.");
        }
    }

    // ── 단건 조회 ─────────────────────────────────────────────────────────────

    public Map<String, Object> getFormData(int id) {
        Map<String, Object> params = Map.of("id", id);
        Map<String, Object> row = sqlSession.selectOne(NS + "selectFormData", params);
        if (row == null) return null;

        // form_data jsonb를 Map으로 파싱
        Object formDataRaw = row.get("form_data");
        if (formDataRaw instanceof String) {
            try {
                row.put("form_data", objectMapper.readValue((String) formDataRaw, new TypeReference<Map<String, Object>>() {}));
            } catch (Exception ignored) {}
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", row.get("id"));
        result.put("screen_id", row.get("screen_id"));
        result.put("table_name", row.get("table_name"));
        result.put("data", row.get("form_data"));
        result.put("created_at", row.get("created_date"));
        result.put("updated_at", row.get("updated_date"));
        result.put("created_by", row.get("created_by"));
        result.put("updated_by", row.get("updated_by"));
        return result;
    }

    // ── 목록 조회 (페이징) ────────────────────────────────────────────────────

    public Map<String, Object> getFormDataList(int screenId, Map<String, Object> queryParams) {
        int page = ((Number) queryParams.getOrDefault("page", 1)).intValue();
        int size = ((Number) queryParams.getOrDefault("size", 10)).intValue();
        String search = (String) queryParams.get("search");
        String sortBy = (String) queryParams.getOrDefault("sort_by", "created_date");
        String sortOrder = (String) queryParams.getOrDefault("sort_order", "desc");

        int offset = (page - 1) * size;
        String searchParam = (search != null && !search.isEmpty()) ? "%" + search + "%" : null;

        Map<String, Object> params = new LinkedHashMap<>();
        params.put("screen_id", screenId);
        params.put("search", searchParam);
        params.put("sort_by", sortBy);
        params.put("sort_order", sortOrder);
        params.put("size", size);
        params.put("offset", offset);

        List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectFormDataList", params);
        Integer totalObj = sqlSession.selectOne(NS + "countFormDataList", params);
        int total = totalObj != null ? totalObj : 0;

        // form_data 파싱
        List<Map<String, Object>> content = rows.stream().map(row -> {
            Object raw = row.get("form_data");
            if (raw instanceof String) {
                try {
                    row.put("form_data", objectMapper.readValue((String) raw, new TypeReference<Map<String, Object>>() {}));
                } catch (Exception ignored) {}
            }
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("id", row.get("id"));
            item.put("screen_id", row.get("screen_id"));
            item.put("table_name", row.get("table_name"));
            item.put("data", row.get("form_data"));
            item.put("created_at", row.get("created_date"));
            item.put("updated_at", row.get("updated_date"));
            item.put("created_by", row.get("created_by"));
            item.put("updated_by", row.get("updated_by"));
            return item;
        }).collect(Collectors.toList());

        int totalPages = (int) Math.ceil((double) total / size);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("content", content);
        result.put("total_elements", total);
        result.put("total_pages", totalPages);
        result.put("current_page", page);
        result.put("size", size);
        return result;
    }

    // ── 검증 ──────────────────────────────────────────────────────────────────

    public Map<String, Object> validateFormData(String tableName, Map<String, Object> data) {
        List<Map<String, Object>> errors = new ArrayList<>();
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("valid", errors.isEmpty());
        result.put("errors", errors);
        return result;
    }

    // ── 테이블 컬럼 조회 ──────────────────────────────────────────────────────

    public List<Map<String, Object>> getTableColumns(String tableName) {
        Map<String, Object> params = Map.of("table_name", tableName);
        List<Map<String, Object>> columns = sqlSession.selectList(NS + "selectTableColumns", params);
        List<Map<String, Object>> pks = sqlSession.selectList(NS + "selectPrimaryKeys", params);
        Set<String> pkSet = pks.stream().map(r -> (String) r.get("column_name")).collect(Collectors.toSet());

        return columns.stream().map(col -> {
            Map<String, Object> item = new LinkedHashMap<>(col);
            item.put("primary_key", pkSet.contains(col.get("column_name")));
            item.put("nullable", "YES".equals(col.get("is_nullable")));
            return item;
        }).collect(Collectors.toList());
    }

    // ── 기본키 조회 ───────────────────────────────────────────────────────────

    public List<String> getTablePrimaryKeys(String tableName) {
        return getPrimaryKeyList(tableName);
    }

    // ── 단일 필드 업데이트 ────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> updateFieldValue(String tableName, String keyField, Object keyValue,
                                                String updateField, Object updateValue,
                                                String companyCode, String userId) {
        validateName(tableName);
        validateName(keyField);
        validateName(updateField);

        // 컬럼 존재 여부 확인 (updated_by, updated_at, company_code)
        List<String> tableColumns = getColumnNames(tableName);
        boolean hasUpdatedBy = tableColumns.contains("updated_by");
        boolean hasUpdatedAt = tableColumns.contains("updated_date");
        boolean hasCompanyCode = tableColumns.contains("company_code");

        List<Object> params = new ArrayList<>();
        StringBuilder setClause = new StringBuilder("\"" + updateField + "\" = ?");
        params.add(updateValue);

        if (hasUpdatedBy) {
            setClause.append(", updated_by = ?");
            params.add(userId);
        }
        if (hasUpdatedAt) {
            setClause.append(", updated_at = NOW()");
        }

        StringBuilder whereClause = new StringBuilder("\"" + keyField + "\" = ?");
        params.add(keyValue);

        if (hasCompanyCode && companyCode != null && !"*".equals(companyCode)) {
            whereClause.append(" AND company_code = ?");
            params.add(companyCode);
        }

        String sql = String.format("UPDATE \"%s\" SET %s WHERE %s",
                tableName, setClause, whereClause);

        log.debug("UPDATE FIELD SQL: {}", sql);
        int affected = jdbcTemplate.update(sql, params.toArray());

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

    // ── 위치 이력 ─────────────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> saveLocationHistory(Map<String, Object> data) {
        Map<String, Object> params = new HashMap<>(data);
        // recordedAt String → Date
        Object recAt = params.get("recorded_at");
        if (recAt instanceof String) {
            try {
                params.put("recorded_at", new java.util.Date(
                        java.time.Instant.parse((String) recAt).toEpochMilli()));
            } catch (Exception e) {
                params.put("recorded_at", new java.util.Date());
            }
        } else if (recAt == null) {
            params.put("recorded_at", new java.util.Date());
        }
        if (!params.containsKey("trip_status")) params.put("trip_status", "active");

        sqlSession.insert(NS + "insertLocationHistory", params);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", params.get("id"));
        return result;
    }

    public List<Map<String, Object>> getLocationHistory(Map<String, Object> params) {
        if (!params.containsKey("limit")) params.put("limit", 1000);
        // startDate / endDate String → Date if present
        convertDateParam(params, "start_date");
        convertDateParam(params, "end_date");
        return sqlSession.selectList(NS + "selectLocationHistory", params);
    }

    private void convertDateParam(Map<String, Object> params, String key) {
        Object val = params.get(key);
        if (val instanceof String && !((String) val).isEmpty()) {
            try {
                params.put(key, new java.util.Date(java.time.Instant.parse((String) val).toEpochMilli()));
            } catch (Exception e) {
                try {
                    params.put(key, java.sql.Date.valueOf((String) val));
                } catch (Exception ignored) {}
            }
        }
    }

    // ── PK cast 유틸 ─────────────────────────────────────────────────────────

    private String buildPkCast(String dataType) {
        if (dataType == null) return "";
        String dt = dataType.toLowerCase();
        if (dt.contains("character") || dt.contains("text") || dt.contains("varchar")) return "::text";
        if (dt.contains("bigint")) return "::bigint";
        if (dt.contains("integer") || dt.contains("numeric")) return "::numeric";
        if (dt.contains("uuid")) return "::uuid";
        return "";
    }

    private String buildCast(String dataType) {
        if (dataType == null) return "";
        String dt = dataType.toLowerCase();
        if (dt.equals("integer") || dt.equals("smallint")) return "::integer";
        if (dt.equals("bigint")) return "::bigint";
        if (dt.contains("numeric") || dt.contains("decimal") || dt.contains("real") || dt.contains("double")) return "::numeric";
        if (dt.equals("boolean")) return "::boolean";
        if (dt.equals("jsonb") || dt.equals("json")) return "::jsonb";
        return "";
    }
}
