package com.erp.service;

import com.erp.common.BaseService;
import com.erp.constants.InputTypeConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Service
@Slf4j
public class DdlService extends BaseService {

    private static final String NS = "ddl.";

    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;

    private static final Set<String> SYSTEM_TABLES = Set.of(
        "user_info", "company_mng", "menu_info", "auth_group",
        "table_labels", "column_labels", "screen_definitions", "screen_layouts",
        "common_code", "multi_lang_key_master", "multi_lang_text",
        "button_action_standards", "ddl_execution_log"
    );

    private static final Set<String> RESERVED_WORDS = Set.of(
        "user", "order", "group", "table", "column", "index", "select", "insert",
        "update", "delete", "from", "where", "join", "on", "as", "and", "or", "not",
        "null", "true", "false", "create", "alter", "drop", "primary", "key",
        "foreign", "references", "constraint", "default", "unique", "check",
        "view", "procedure", "function"
    );

    private static final Set<String> RESERVED_COLUMNS = Set.of(
        "id", "created_date", "updated_date", "company_code"
    );

    public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
        this.jdbcTemplate = jdbcTemplate;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CREATE TABLE
    // ─────────────────────────────────────────────────────────────────────────

    public Map<String, Object> createTable(String tableName, List<Map<String, Object>> columns,
                                           String companyCode, String userId, String description) {
        // 1. 검증
        Map<String, Object> validation = validateTableCreation(tableName, columns);
        if (!(Boolean) validation.get("is_valid")) {
            @SuppressWarnings("unchecked")
            List<String> errors = (List<String>) validation.get("errors");
            String errorMsg = "테이블 생성 검증 실패: " + String.join(", ", errors);
            logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, "VALIDATION_FAILED", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "VALIDATION_FAILED");
        }

        // 2. 테이블 존재 여부 확인
        if (tableExists(tableName)) {
            String errorMsg = "테이블 '" + tableName + "'이 이미 존재합니다.";
            logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, "TABLE_EXISTS", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_EXISTS");
        }

        // 3. DDL 쿼리 생성
        String ddlQuery = generateCreateTableQuery(tableName, columns);

        // 4. 트랜잭션으로 DDL 실행 + 메타데이터 저장
        try {
            final String finalTableName = tableName;
            final List<Map<String, Object>> finalColumns = columns;
            final String finalCompanyCode = companyCode;
            final String finalDescription = description;
            transactionTemplate.execute(status -> {
                jdbcTemplate.execute(ddlQuery);
                saveTableMetadata(finalTableName, finalDescription);
                saveColumnMetadata(finalTableName, finalColumns, finalCompanyCode);
                return null;
            });

            // 5. 성공 로그 (트랜잭션 밖)
            logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName, ddlQuery, true, null);
            log.info("테이블 생성 성공: {}, 사용자: {}, 컬럼수: {}", tableName, userId, columns.size());

            return Map.of(
                "success", true,
                "message", "테이블 '" + tableName + "'이 성공적으로 생성되었습니다.",
                "table_name", tableName,
                "column_count", columns.size(),
                "executed_query", ddlQuery
            );
        } catch (Exception e) {
            String errorMsg = "테이블 생성 실패: " + e.getMessage();
            logDdlOperation(userId, companyCode, "CREATE_TABLE", tableName,
                "FAILED: " + e.getMessage(), false, errorMsg);
            log.error("테이블 생성 실패: {}, 사용자: {}, 오류: {}", tableName, userId, e.getMessage(), e);
            return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED");
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // ADD COLUMN
    // ─────────────────────────────────────────────────────────────────────────

    public Map<String, Object> addColumn(String tableName, Map<String, Object> column,
                                         String companyCode, String userId) {
        // 1. 검증
        List<String> errors = validateColumnForAddition(tableName, column);
        if (!errors.isEmpty()) {
            String errorMsg = "컬럼 추가 검증 실패: " + String.join(", ", errors);
            logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "VALIDATION_FAILED", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "VALIDATION_FAILED");
        }

        // 2. 테이블 존재 여부 확인
        if (!tableExists(tableName)) {
            String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다.";
            logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "TABLE_NOT_EXISTS", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_EXISTS");
        }

        // 3. 컬럼 존재 여부 확인
        String colName = (String) column.get("name");
        if (columnExists(tableName, colName)) {
            String errorMsg = "컬럼 '" + colName + "'이 이미 존재합니다.";
            logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, "COLUMN_EXISTS", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "COLUMN_EXISTS");
        }

        // 4. DDL 쿼리 생성
        String ddlQuery = generateAddColumnQuery(tableName, column);

        // 5. 트랜잭션으로 DDL 실행 + 메타데이터 저장
        try {
            transactionTemplate.execute(status -> {
                jdbcTemplate.execute(ddlQuery);
                String inputType = convertToInputType(column);
                if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
                    throw new IllegalArgumentException(
                        "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES
                        + " (받은 값: " + inputType + ")"
                    );
                }
                String detailSettings = column.containsKey("detail_settings")
                    ? column.get("detail_settings").toString() : "{}";
                Integer maxOrder = jdbcTemplate.queryForObject(
                    "SELECT COALESCE(MAX(display_order), 0) FROM table_type_columns " +
                    "WHERE table_name = ? AND company_code = ?",
                    Integer.class, tableName, companyCode);
                saveColumnMeta(tableName, colName, companyCode, inputType,
                    detailSettings, (maxOrder != null ? maxOrder : 0) + 1);
                return null;
            });

            // 6. 성공 로그 (트랜잭션 밖)
            logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName, ddlQuery, true, null);
            log.info("컬럼 추가 성공: {}.{}, 사용자: {}", tableName, colName, userId);

            return Map.of(
                "success", true,
                "message", "컬럼 '" + colName + "'이 성공적으로 추가되었습니다.",
                "table_name", tableName,
                "column_name", colName,
                "executed_query", ddlQuery
            );
        } catch (Exception e) {
            String errorMsg = "컬럼 추가 실패: " + e.getMessage();
            logDdlOperation(userId, companyCode, "ADD_COLUMN", tableName,
                "FAILED: " + e.getMessage(), false, errorMsg);
            log.error("컬럼 추가 실패: {}.{}, 사용자: {}, 오류: {}", tableName, colName, userId, e.getMessage(), e);
            return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED");
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DROP TABLE
    // ─────────────────────────────────────────────────────────────────────────

    public Map<String, Object> dropTable(String tableName, String companyCode, String userId) {
        // 1. 시스템 테이블 보호
        if (SYSTEM_TABLES.contains(tableName.toLowerCase())) {
            String errorMsg = "'" + tableName + "'은 시스템 테이블이므로 삭제할 수 없습니다.";
            logDdlOperation(userId, companyCode, "DROP_TABLE", tableName,
                "SYSTEM_TABLE_PROTECTED", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "SYSTEM_TABLE_PROTECTED");
        }

        // 2. 테이블 존재 여부 확인
        if (!tableExists(tableName)) {
            String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다.";
            logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, "TABLE_NOT_FOUND", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_FOUND");
        }

        String ddlQuery = "DROP TABLE IF EXISTS \"" + sanitize(tableName) + "\" CASCADE";

        try {
            transactionTemplate.execute(status -> {
                jdbcTemplate.execute(ddlQuery);
                jdbcTemplate.update("DELETE FROM table_type_columns WHERE table_name = ?", tableName);
                jdbcTemplate.update("DELETE FROM table_labels WHERE table_name = ?", tableName);
                return null;
            });

            logDdlOperation(userId, companyCode, "DROP_TABLE", tableName, ddlQuery, true, null);
            log.info("테이블 삭제 성공: {}, 사용자: {}", tableName, userId);

            return Map.of(
                "success", true,
                "message", "테이블 '" + tableName + "'이 성공적으로 삭제되었습니다.",
                "table_name", tableName,
                "executed_query", ddlQuery
            );
        } catch (Exception e) {
            String errorMsg = "테이블 삭제 실패: " + e.getMessage();
            logDdlOperation(userId, companyCode, "DROP_TABLE", tableName,
                "FAILED: " + e.getMessage(), false, errorMsg);
            log.error("테이블 삭제 실패: {}, 사용자: {}, 오류: {}", tableName, userId, e.getMessage(), e);
            return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED");
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DROP COLUMN (DBeaver 방식: FK 등 위반은 Postgres 가 던지는 에러를 그대로 노출)
    // ─────────────────────────────────────────────────────────────────────────

    public Map<String, Object> dropColumn(String tableName, String columnName,
                                          String companyCode, String userId) {
        // 1. 시스템 테이블 보호
        if (SYSTEM_TABLES.contains(tableName.toLowerCase())) {
            String errorMsg = "'" + tableName + "'은 시스템 테이블이므로 컬럼을 삭제할 수 없습니다.";
            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName,
                "SYSTEM_TABLE_PROTECTED", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "SYSTEM_TABLE_PROTECTED");
        }

        // 2. 예약 컬럼 보호 (id / created_date / updated_date / company_code / writer)
        if (RESERVED_COLUMNS.contains(columnName.toLowerCase()) || "writer".equalsIgnoreCase(columnName)) {
            String errorMsg = "'" + columnName + "'은 시스템 예약 컬럼이므로 삭제할 수 없습니다.";
            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName,
                "RESERVED_COLUMN_PROTECTED", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "RESERVED_COLUMN_PROTECTED");
        }

        // 3. 테이블/컬럼 존재 여부
        if (!tableExists(tableName)) {
            String errorMsg = "테이블 '" + tableName + "'이 존재하지 않습니다.";
            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "TABLE_NOT_FOUND", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "TABLE_NOT_FOUND");
        }
        if (!columnExists(tableName, columnName)) {
            String errorMsg = "컬럼 '" + columnName + "'이 존재하지 않습니다.";
            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, "COLUMN_NOT_FOUND", false, errorMsg);
            return Map.of("success", false, "message", errorMsg, "error_code", "COLUMN_NOT_FOUND");
        }

        // 4. DDL 실행 — CASCADE 안 붙임 → FK 참조 있으면 Postgres 가 거부 (DBeaver 와 동일)
        String ddlQuery = "ALTER TABLE \"" + sanitize(tableName) + "\" DROP COLUMN \"" + sanitize(columnName) + "\"";

        try {
            transactionTemplate.execute(status -> {
                jdbcTemplate.execute(ddlQuery);
                // 컬럼 메타 청소
                jdbcTemplate.update(
                    "DELETE FROM table_type_columns WHERE table_name = ? AND column_name = ?",
                    tableName, columnName);
                jdbcTemplate.update(
                    "DELETE FROM column_labels WHERE table_name = ? AND column_name = ?",
                    tableName, columnName);
                return null;
            });

            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName, ddlQuery, true, null);
            log.info("컬럼 삭제 성공: {}.{}, 사용자: {}", tableName, columnName, userId);

            return Map.of(
                "success", true,
                "message", "컬럼 '" + columnName + "'이 성공적으로 삭제되었습니다.",
                "table_name", tableName,
                "column_name", columnName,
                "executed_query", ddlQuery
            );
        } catch (Exception e) {
            String rawMsg = e.getMessage() != null ? e.getMessage() : "";
            String guidance = rawMsg.toLowerCase().contains("depend") || rawMsg.toLowerCase().contains("foreign key")
                ? " (다른 테이블에서 외래키로 참조 중인 컬럼은 삭제할 수 없습니다)"
                : "";
            String errorMsg = "컬럼 삭제 실패: " + rawMsg + guidance;
            logDdlOperation(userId, companyCode, "DROP_COLUMN", tableName,
                "FAILED: " + rawMsg, false, errorMsg);
            log.error("컬럼 삭제 실패: {}.{}, 사용자: {}, 오류: {}", tableName, columnName, userId, rawMsg, e);
            return Map.of("success", false, "message", errorMsg, "error_code", "EXECUTION_FAILED");
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // VALIDATE
    // ─────────────────────────────────────────────────────────────────────────

    public Map<String, Object> validateTableCreation(String tableName, List<Map<String, Object>> columns) {
        List<String> errors = new ArrayList<>();
        List<String> warnings = new ArrayList<>();

        errors.addAll(validateTableName(tableName));

        if (columns == null || columns.isEmpty()) {
            errors.add("최소 1개의 컬럼이 필요합니다.");
        } else {
            Set<String> seen = new LinkedHashSet<>();
            Set<String> duplicates = new LinkedHashSet<>();
            for (int i = 0; i < columns.size(); i++) {
                Map<String, Object> col = columns.get(i);
                errors.addAll(validateSingleColumn(col, i + 1));
                String colName = col.get("name") != null ? col.get("name").toString().toLowerCase() : "";
                if (!colName.isEmpty() && !seen.add(colName)) {
                    duplicates.add(colName);
                }
            }
            if (!duplicates.isEmpty()) {
                errors.add("중복된 컬럼명이 있습니다: " + String.join(", ", duplicates));
            }
        }

        int colCount = columns != null ? columns.size() : 0;
        return Map.of(
            "is_valid", errors.isEmpty(),
            "errors", errors,
            "warnings", warnings,
            "summary", buildValidationSummary(tableName, colCount, errors, warnings)
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // LOG QUERIES
    // ─────────────────────────────────────────────────────────────────────────

    public List<Map<String, Object>> getDdlLogs(int limit, String userId, String ddlType) {
        Map<String, Object> params = new HashMap<>();
        params.put("limit", Math.min(limit, 200));
        params.put("user_id", userId);
        params.put("ddl_type", ddlType);
        return sqlSession.selectList(NS + "selectDdlLogs", params);
    }

    public Map<String, Object> getDdlStatistics(String fromDate, String toDate) {
        Map<String, Object> params = new HashMap<>();
        params.put("from_date", fromDate);
        params.put("to_date", toDate);

        Map<String, Object> totalStats = sqlSession.selectOne(NS + "selectDdlTotalStats", params);
        List<Map<String, Object>> byType = sqlSession.selectList(NS + "selectDdlStatsByType", params);
        List<Map<String, Object>> byUser = sqlSession.selectList(NS + "selectDdlStatsByUser", params);
        List<Map<String, Object>> recentFailures = sqlSession.selectList(NS + "selectRecentFailures", params);

        Map<String, Long> byDdlType = new LinkedHashMap<>();
        for (Map<String, Object> row : byType) {
            byDdlType.put(String.valueOf(row.get("ddl_type")), toLong(row.get("count")));
        }
        Map<String, Long> byUserMap = new LinkedHashMap<>();
        for (Map<String, Object> row : byUser) {
            byUserMap.put(String.valueOf(row.get("user_id")), toLong(row.get("count")));
        }

        return Map.of(
            "total_executions", toLong(totalStats != null ? totalStats.get("total_executions") : null),
            "successful_executions", toLong(totalStats != null ? totalStats.get("successful_executions") : null),
            "failed_executions", toLong(totalStats != null ? totalStats.get("failed_executions") : null),
            "by_ddl_type", byDdlType,
            "by_user", byUserMap,
            "recent_failures", recentFailures
        );
    }

    public List<Map<String, Object>> getTableDdlHistory(String tableName) {
        Map<String, Object> params = new HashMap<>();
        params.put("table_name", tableName);
        return sqlSession.selectList(NS + "selectTableDdlHistory", params);
    }

    public Map<String, Object> getTableInfo(String tableName) {
        Map<String, Object> params = new HashMap<>();
        params.put("table_name", tableName);
        Map<String, Object> tableInfo = sqlSession.selectOne(NS + "selectTableInfo", params);
        if (tableInfo == null) return null;
        List<Map<String, Object>> columns = sqlSession.selectList(NS + "selectTableColumns", params);
        return Map.of("table_info", tableInfo, "columns", columns);
    }

    public int cleanupOldLogs(int retentionDays) {
        LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays);
        Map<String, Object> params = new HashMap<>();
        params.put("cutoff_date", cutoff.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        int deleted = sqlSession.delete(NS + "deleteOldDdlLogs", params);
        log.info("DDL 로그 정리 완료: {}개 삭제, 보존 기간: {}일", deleted, retentionDays);
        return deleted;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DDL QUERY GENERATION
    // ─────────────────────────────────────────────────────────────────────────

    private String generateCreateTableQuery(String tableName, List<Map<String, Object>> columns) {
        StringBuilder sb = new StringBuilder();
        sb.append("CREATE TABLE \"").append(sanitize(tableName)).append("\" (\n");
        sb.append("  \"id\" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,\n");
        sb.append("  \"created_date\" timestamp DEFAULT now(),\n");
        sb.append("  \"updated_date\" timestamp DEFAULT now(),\n");
        sb.append("  \"writer\" varchar(500) DEFAULT NULL,\n");
        sb.append("  \"company_code\" varchar(500)");

        for (Map<String, Object> col : columns) {
            sb.append(",\n  \"").append(sanitize((String) col.get("name"))).append("\" varchar(500)");
            Boolean nullable = col.containsKey("nullable") ? (Boolean) col.get("nullable") : true;
            if (Boolean.FALSE.equals(nullable)) sb.append(" NOT NULL");
            if (col.get("default_value") != null && !col.get("default_value").toString().isBlank()) {
                sb.append(" DEFAULT '").append(col.get("default_value").toString().replace("'", "''")).append("'");
            }
        }
        sb.append("\n);");
        return sb.toString();
    }

    private String generateAddColumnQuery(String tableName, Map<String, Object> column) {
        StringBuilder sb = new StringBuilder();
        sb.append("ALTER TABLE \"").append(sanitize(tableName)).append("\" ADD COLUMN ");
        sb.append("\"").append(sanitize((String) column.get("name"))).append("\" varchar(500)");
        Boolean nullable = column.containsKey("nullable") ? (Boolean) column.get("nullable") : true;
        if (Boolean.FALSE.equals(nullable)) sb.append(" NOT NULL");
        if (column.get("default_value") != null && !column.get("default_value").toString().isBlank()) {
            sb.append(" DEFAULT '").append(column.get("default_value").toString().replace("'", "''")).append("'");
        }
        sb.append(";");
        return sb.toString();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // TABLE / COLUMN EXISTS
    // ─────────────────────────────────────────────────────────────────────────

    private boolean tableExists(String tableName) {
        Integer cnt = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM information_schema.tables " +
            "WHERE table_schema = 'public' AND table_name = ?",
            Integer.class, tableName);
        return cnt != null && cnt > 0;
    }

    private boolean columnExists(String tableName, String columnName) {
        Integer cnt = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM information_schema.columns " +
            "WHERE table_schema = 'public' AND table_name = ? AND column_name = ?",
            Integer.class, tableName, columnName);
        return cnt != null && cnt > 0;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // METADATA SAVE
    // ─────────────────────────────────────────────────────────────────────────

    private void saveTableMetadata(String tableName, String description) {
        String desc = description != null && !description.isBlank()
            ? description : "사용자 생성 테이블: " + tableName;
        jdbcTemplate.update(
            "INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date) " +
            "VALUES (?, ?, ?, now(), now()) " +
            "ON CONFLICT (table_name) DO UPDATE SET table_label = ?, description = ?, updated_date = now()",
            tableName, tableName, desc, tableName, desc);
    }

    private void saveColumnMetadata(String tableName, List<Map<String, Object>> columns, String companyCode) {
        // 기본 시스템 컬럼
        List<Object[]> defaultCols = List.of(
            new Object[]{"id",           "text", -5},
            new Object[]{"created_date", "date", -4},
            new Object[]{"updated_date", "date", -3},
            new Object[]{"writer",       "text", -2},
            new Object[]{"company_code", "text", -1}
        );
        for (Object[] dc : defaultCols) {
            saveColumnMeta(tableName, (String) dc[0], companyCode, (String) dc[1], "{}", (int) dc[2]);
        }
        // 사용자 정의 컬럼
        for (int i = 0; i < columns.size(); i++) {
            Map<String, Object> col = columns.get(i);
            String inputType = convertToInputType(col);
            if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
                throw new IllegalArgumentException(
                    "INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES
                    + " (받은 값: " + inputType + ")"
                );
            }
            String detailSettings = col.containsKey("detail_settings")
                ? col.get("detail_settings").toString() : "{}";
            saveColumnMeta(tableName, (String) col.get("name"), companyCode,
                inputType, detailSettings, i);
        }
    }

    private void saveColumnMeta(String tableName, String colName, String companyCode,
                                String inputType, String detailSettings, int order) {
        jdbcTemplate.update(
            "INSERT INTO table_type_columns " +
            "  (table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date) " +
            "VALUES (?, ?, ?, ?, ?::jsonb, 'Y', ?, now(), now()) " +
            "ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET " +
            "  input_type = ?, detail_settings = ?::jsonb, display_order = ?, updated_date = now()",
            tableName, colName, companyCode, inputType, detailSettings, order,
            inputType, detailSettings, order);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // VALIDATION HELPERS
    // ─────────────────────────────────────────────────────────────────────────

    private List<String> validateTableName(String tableName) {
        List<String> errors = new ArrayList<>();
        if (tableName == null || tableName.isBlank()) {
            errors.add("테이블명은 필수입니다.");
            return errors;
        }
        if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
            errors.add("유효하지 않은 테이블명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다.");
        }
        if (tableName.length() > 63) errors.add("테이블명은 63자를 초과할 수 없습니다.");
        if (tableName.length() < 2)  errors.add("테이블명은 최소 2자 이상이어야 합니다.");
        if (SYSTEM_TABLES.contains(tableName.toLowerCase())) {
            errors.add("'" + tableName + "'은 시스템 테이블명으로 사용할 수 없습니다.");
        }
        if (RESERVED_WORDS.contains(tableName.toLowerCase())) {
            errors.add("'" + tableName + "'은 SQL 예약어이므로 테이블명으로 사용할 수 없습니다.");
        }
        if (tableName.startsWith("_") || tableName.endsWith("_")) {
            errors.add("테이블명은 언더스코어로 시작하거나 끝날 수 없습니다.");
        }
        if (tableName.contains("__")) {
            errors.add("테이블명에 연속된 언더스코어는 사용할 수 없습니다.");
        }
        return errors;
    }

    private List<String> validateSingleColumn(Map<String, Object> column, int position) {
        List<String> errors = new ArrayList<>();
        String name = column.get("name") != null ? column.get("name").toString().trim() : "";
        String prefix = "컬럼 " + position + "(" + name + "): ";

        if (name.isBlank()) {
            errors.add(prefix + "컬럼명은 필수입니다.");
            return errors;
        }
        if (!name.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
            errors.add(prefix + "유효하지 않은 컬럼명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다.");
        }
        if (name.length() > 63) errors.add(prefix + "컬럼명은 63자를 초과할 수 없습니다.");
        if (name.length() < 2)  errors.add(prefix + "컬럼명은 최소 2자 이상이어야 합니다.");
        if (RESERVED_COLUMNS.contains(name.toLowerCase())) {
            errors.add(prefix + "'" + name + "'은 예약된 컬럼명입니다 (id, created_date, updated_date, company_code).");
        }
        if (RESERVED_WORDS.contains(name.toLowerCase())) {
            errors.add(prefix + "'" + name + "'은 SQL 예약어이므로 컬럼명으로 사용할 수 없습니다.");
        }
        if (name.contains("__")) {
            errors.add(prefix + "컬럼명에 연속된 언더스코어는 사용할 수 없습니다.");
        }
        return errors;
    }

    private List<String> validateColumnForAddition(String tableName, Map<String, Object> column) {
        List<String> errors = new ArrayList<>();
        if (SYSTEM_TABLES.contains(tableName.toLowerCase())) {
            errors.add("'" + tableName + "'은 시스템 테이블이므로 컬럼을 추가할 수 없습니다.");
        }
        errors.addAll(validateSingleColumn(column, 1));
        return errors;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // UTILITIES
    // ─────────────────────────────────────────────────────────────────────────

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

    private String convertToInputType(Map<String, Object> col) {
        Object inputType = col.get("input_type");
        Object webType   = col.get("web_type");
        String type = inputType != null ? inputType.toString()
                    : (webType != null ? webType.toString() : "text");
        return switch (type) {
            case "number", "decimal"       -> "number";
            case "date", "datetime"        -> "date";
            case "select", "dropdown"      -> "select";
            case "checkbox", "boolean"     -> "checkbox";
            case "radio"                   -> "radio";
            case "code"                    -> "code";
            case "entity"                  -> "entity";
            case "file"                    -> "file";
            case "image"                   -> "image";
            case "numbering"               -> "numbering";
            default                        -> "text";
        };
    }

    private void logDdlOperation(String userId, String companyCode, String ddlType,
                                  String tableName, String ddlQuery, boolean success, String errorMessage) {
        try {
            Map<String, Object> params = new HashMap<>();
            params.put("user_id", userId);
            params.put("company_code", companyCode);
            params.put("ddl_type", ddlType);
            params.put("table_name", tableName);
            params.put("ddl_query", ddlQuery);
            params.put("success", success);
            params.put("error_message", errorMessage);
            sqlSession.insert(NS + "insertDdlLog", params);
        } catch (Exception e) {
            log.error("DDL 로그 기록 실패: userId={}, ddlType={}, tableName={}", userId, ddlType, tableName, e);
        }
    }

    private long toLong(Object value) {
        if (value == null) return 0L;
        if (value instanceof Number) return ((Number) value).longValue();
        try { return Long.parseLong(value.toString()); } catch (NumberFormatException ex) { return 0L; }
    }

    private String buildValidationSummary(String tableName, int columnCount,
                                          List<String> errors, List<String> warnings) {
        StringBuilder sb = new StringBuilder("테이블 '").append(tableName).append("' 검증 완료. ");
        sb.append("컬럼 ").append(columnCount).append("개 중 ");
        sb.append(errors.isEmpty() ? "모든 검증 통과." : errors.size() + "개 오류 발견.");
        if (!warnings.isEmpty()) sb.append(" ").append(warnings.size()).append("개 경고 있음.");
        return sb.toString();
    }
}
