package egovframework.config;

import com.zaxxer.hikari.HikariDataSource;
import net.sf.jsqlparser.expression.CaseExpression;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.WhenClause;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;
import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener;
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.util.List;

/**
 * DataSource Proxy 설정 클래스
 *
 * datasource-proxy 라이브러리를 사용하여 파라미터 바인딩이 된 실제 SQL 쿼리를 로그로 출력하도록 설정
 * MyBatis의 include, foreach 등 복잡한 쿼리에도 적용됨
 *
 * Environment를 사용하여 수동으로 DataSource를 구성하고 프록시 적용
 * Multi DB 환경 지원
 *
 * @author XIT Framework
 */
@Configuration
public class DataSourceProxyConfig {

    @Autowired
    private Environment environment;

    /**
     * Primary 데이터소스 빈 생성
     * Environment에서 설정값을 읽어서 HikariDataSource를 수동으로 구성
     */
    @Bean
    public DataSource actualDataSource() {
        return createHikariDataSource("spring.datasource");
    }

    /**
     * Secondary 데이터소스 빈 생성 (필요한 경우)
     * Multi DB 환경에서 두 번째 DB를 사용할 경우 활성화
     *
     * 예시:
     * @Bean
     * public DataSource actualSecondaryDataSource() {
     *     return createHikariDataSource("spring.datasource.secondary");
     * }
     */

    /**
     * HikariDataSource 생성 헬퍼 메서드
     *
     * @param prefix application.yml의 설정 prefix
     * @return HikariDataSource
     */
    private HikariDataSource createHikariDataSource(String prefix) {
        HikariDataSource dataSource = new HikariDataSource();

        // application.yml에서 설정값 읽기
        dataSource.setJdbcUrl(environment.getProperty(prefix + ".url"));
        dataSource.setUsername(environment.getProperty(prefix + ".username"));
        dataSource.setPassword(environment.getProperty(prefix + ".password"));
        dataSource.setDriverClassName(environment.getProperty(prefix + ".driver-class-name"));

        // HikariCP 설정
        dataSource.setMaximumPoolSize(environment.getProperty(prefix + ".hikari.maximum-pool-size", Integer.class, 10));
        dataSource.setMinimumIdle(environment.getProperty(prefix + ".hikari.minimum-idle", Integer.class, 5));
        dataSource.setConnectionTimeout(environment.getProperty(prefix + ".hikari.connection-timeout", Long.class, 30000L));
        dataSource.setIdleTimeout(environment.getProperty(prefix + ".hikari.idle-timeout", Long.class, 600000L));
        dataSource.setMaxLifetime(environment.getProperty(prefix + ".hikari.max-lifetime", Long.class, 1800000L));
        dataSource.setValidationTimeout(environment.getProperty(prefix + ".hikari.validation-timeout", Long.class, 60000L));

        return dataSource;
    }

    /**
     * Primary 프록시 데이터소스 빈 생성
     * actualDataSource를 래핑하여 SQL 쿼리 로깅 기능을 추가
     *
     * @param actualDataSource 실제 데이터소스
     * @return 프록시가 적용된 데이터소스
     */
    @Bean
    @Primary
    public DataSource dataSource(@Qualifier("actualDataSource") DataSource actualDataSource) {
        return createProxyDataSource(actualDataSource, "PRIMARY-DB");
    }

    /**
     * Secondary 프록시 데이터소스 빈 생성 (필요한 경우)
     * Multi DB 환경에서 두 번째 DB를 사용할 경우 활성화
     *
     * 예시:
     * @Bean
     * public DataSource secondaryDataSource(@Qualifier("actualSecondaryDataSource") DataSource actualSecondaryDataSource) {
     *     return createProxyDataSource(actualSecondaryDataSource, "SECONDARY-DB");
     * }
     */

    /**
     * 프록시 데이터소스 생성 헬퍼 메서드
     *
     * @param actualDataSource 실제 데이터소스
     * @param dataSourceName 데이터소스 이름 (로그 식별용)
     * @return 프록시가 적용된 데이터소스
     */
    private DataSource createProxyDataSource(DataSource actualDataSource, String dataSourceName) {
        // SLF4J 쿼리 로깅 리스너 생성
        SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener();

        // 로그 레벨 설정 (DEBUG 레벨로 출력)
        loggingListener.setLogLevel(SLF4JLogLevel.DEBUG);

        // 로거 이름 설정 (쿼리 로그 식별을 위함)
        loggingListener.setLogger("go.kr.project.sql");

        // 쿼리 로그 엔트리 생성자 설정
        DefaultQueryLogEntryCreator logEntryCreator = new DefaultQueryLogEntryCreator();
        logEntryCreator.setMultiline(true); // 멀티라인으로 보기 좋게 출력
        loggingListener.setQueryLogEntryCreator(logEntryCreator);

        // 커스텀 파라미터 바인딩 리스너 생성
        CustomParameterBindingListener customListener = new CustomParameterBindingListener();

        // 프록시 데이터소스 빌더를 사용하여 프록시 데이터소스 생성
        return ProxyDataSourceBuilder
                .create(actualDataSource)
                .name(dataSourceName) // 데이터소스 이름 설정
                .listener(loggingListener) // 기본 로깅 리스너 추가
                .listener(customListener) // 커스텀 파라미터 바인딩 리스너 추가
                .asJson() // JSON 형태로 파라미터 바인딩된 쿼리 출력
                .build();
    }

    /**
     * 파라미터가 바인딩된 실제 SQL 쿼리를 출력하는 커스텀 리스너
     */
    private static class CustomParameterBindingListener implements QueryExecutionListener {
        
        private static final Logger logger = LoggerFactory.getLogger("go.kr.project.sql.binding");

        @Override
        public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
            // 쿼리 실행 전 처리 (필요시 구현)
        }

        @Override
        public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
            try {
                // 쿼리 실행 후 파라미터가 바인딩된 SQL 출력
                for (QueryInfo queryInfo : queryInfoList) {
                    String query = queryInfo.getQuery();
                    List<List<ParameterSetOperation>> parametersList = queryInfo.getParametersList();

                    // Mapper 경로 및 메서드명 추출
                    String mapperInfo = extractMapperInfo();
                    logger.info(" ========================== Mapper: {} ========================== ", mapperInfo);

                    // 쿼리 실행 시간 및 기본 정보
                    long executionTime = execInfo.getElapsedTime();
                    logger.debug("실행 시간: {}ms", executionTime);

                    // 파라미터 값 추출
                    List<Object> parameterValues = extractParameterValues(parametersList);

                    // 파라미터 개수 정보
                    int questionMarkCount = countQuestionMarks(query);
                    logger.debug("파라미터 정보: ? 플레이스홀더 {}개, 바인딩 값 {}개", questionMarkCount, parameterValues.size());

                    // 파라미터 값들 전체 출력 (인덱스 포함)
                    if (!parameterValues.isEmpty()) {
                        logger.debug("파라미터 값 목록 (총 {}개):", parameterValues.size());
                        for (int i = 0; i < parameterValues.size(); i++) {
                            Object param = parameterValues.get(i);
                            String paramValue;

                            if (param == null) {
                                paramValue = "NULL";
                            } else if (param instanceof String) {
                                paramValue = "'" + param.toString() + "'";
                            } else {
                                paramValue = param.toString();
                            }

                            // 파라미터 인덱스와 함께 출력
                            logger.debug("   (parameter {}) = {}", i + 1, paramValue);
                        }
                    }

                    // 파라미터 바인딩된 SQL 생성
                    String boundQuery = bindParameters(query, parameterValues);

                    // SQL 포맷팅 적용 (jsqlparser 사용)
                    String formattedQuery = formatSql(boundQuery);

                    logger.info("\n{}\n", formattedQuery);
                }
            } finally {
                // ThreadLocal 정리 (메모리 누수 방지)
                // 쿼리 로깅이 완료된 후 반드시 정리
                SqlLoggingInterceptor.clearCurrentMapperInfo();
            }
        }

        /**
         * SQL 쿼리에서 ? 플레이스홀더 개수를 카운트하는 메서드
         * 주석 안의 ? 는 제외하고 실제 파라미터 플레이스홀더만 카운트
         * 
         * @param query SQL 쿼리 문자열
         * @return ? 플레이스홀더 개수
         */
        private int countQuestionMarks(String query) {
            int count = 0;
            boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
            boolean inLineComment = false;  // -- 라인 주석 내부인지 여부
            boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
            
            for (int i = 0; i < query.length(); i++) {
                char currentChar = query.charAt(i);
                char nextChar = (i + 1 < query.length()) ? query.charAt(i + 1) : '\0';
                
                // 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
                if (currentChar == '\'' && !inBlockComment && !inLineComment) {
                    inStringLiteral = !inStringLiteral;
                    continue;
                }
                
                // 블록 주석 시작 /* 검사
                if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
                    inBlockComment = true;
                    i++; // 다음 문자도 건너뛰기
                    continue;
                }
                
                // 블록 주석 끝 */ 검사
                if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
                    inBlockComment = false;
                    i++; // 다음 문자도 건너뛰기
                    continue;
                }
                
                // 라인 주석 시작 -- 검사
                if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
                    inLineComment = true;
                    i++; // 다음 문자도 건너뛰기
                    continue;
                }
                
                // 라인 주석 끝 (줄바꿈) 검사
                if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
                    inLineComment = false;
                    continue;
                }
                
                // ? 플레이스홀더 카운트 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
                if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
                    count++;
                }
            }
            
            return count;
        }

        /**
         * MyBatis Interceptor에서 정확한 Mapper 정보 추출
         * SqlLoggingInterceptor에서 ThreadLocal을 통해 제공하는 정보를 사용
         * 
         * @return Mapper 클래스명과 메서드명 (예: go.kr.project.login.mapper.LoginMapper.selectMenusByRoleIds)
         */
        private String extractMapperInfo() {
            try {
                // SqlLoggingInterceptor에서 제공하는 정확한 Mapper 정보 사용
                String mapperInfo = SqlLoggingInterceptor.getCurrentMapperInfo();
                return mapperInfo != null ? mapperInfo : "Unknown Mapper";
                
            } catch (Exception e) {
                logger.warn("Mapper 정보 추출 실패: {}", e.getMessage());
                return "Unknown Mapper";
            }
        }

        /**
         * ParameterSetOperation 리스트에서 실제 파라미터 값들을 추출
         * 모든 객체 타입(Integer, String, Date 등)의 null 값을 올바르게 처리하도록 완전 재구현
         */
        private List<Object> extractParameterValues(List<List<ParameterSetOperation>> parametersList) {
            java.util.Map<Integer, Object> parameterMap = new java.util.TreeMap<>();
            
            if (parametersList != null && !parametersList.isEmpty()) {
                // 첫 번째 파라미터 세트를 사용 (일반적으로 PreparedStatement는 하나의 파라미터 세트를 가짐)
                List<ParameterSetOperation> operations = parametersList.get(0);
                
                if (operations != null) {
                    for (ParameterSetOperation operation : operations) {
                        Object[] args = operation.getArgs();
                        
                        if (args != null && args.length >= 1) {
                            Integer paramIndex = (Integer) args[0];
                            Object paramValue = null;
                            
                            // 리플렉션을 사용하여 실제 메서드명 추출
                            String methodName = extractMethodName(operation);
                            //logger.debug("[METHOD_INFO] 파라미터 {}번 - 메서드명: {}, args 길이: {}",
                            //    paramIndex, methodName, args.length);
                            
                            // 메서드명을 통한 정확한 null 처리
                            if ("setNull".equals(methodName)) {
                                // setNull 메서드 호출 - 확실하게 null 값으로 설정
                                paramValue = null;
                                //logger.debug("[NULL_SET] setNull 메서드 호출됨 - 파라미터 {}번을 null로 설정", paramIndex);
                            } else {
                                // 다른 모든 set 메서드들 (setString, setInt, setObject 등)
                                if (args.length >= 2) {
                                    paramValue = args[1];
                                    
                                    // args[1]이 null인 경우에도 null로 처리
                                    if (paramValue == null) {
                                        //logger.debug("[NULL_VALUE] 파라미터 {}번 값이 null임", paramIndex);
                                    } else {
                                        //logger.debug("[VALUE_SET] 파라미터 {}번 - 메서드: {}, 값: {} (타입: {})",
                                        //    paramIndex, methodName, paramValue, paramValue.getClass().getSimpleName());
                                    }
                                } else {
                                    logger.warn("[WARN] 파라미터 {}번 - 메서드: {}, args 길이 부족: {}", 
                                        paramIndex, methodName, args.length);
                                }
                            }
                            
                            parameterMap.put(paramIndex, paramValue);
                        }
                    }
                }
            }
            
            // TreeMap을 사용했으므로 자동으로 인덱스 순서대로 정렬됨
            return new java.util.ArrayList<>(parameterMap.values());
        }
        
        /**
         * ParameterSetOperation에서 실제 호출된 메서드명을 추출
         * 리플렉션을 사용하여 정확한 메서드명을 획득
         */
        private String extractMethodName(ParameterSetOperation operation) {
            try {
                // ParameterSetOperation의 getMethod() 메서드 호출을 시도
                java.lang.reflect.Method getMethodMethod = operation.getClass().getMethod("getMethod");
                Object method = getMethodMethod.invoke(operation);
                
                if (method instanceof java.lang.reflect.Method) {
                    return ((java.lang.reflect.Method) method).getName();
                }
                
                // getMethod가 없거나 실패한 경우 toString()에서 메서드명 추출 시도
                String operationStr = operation.toString();
                if (operationStr != null) {
                    // "MethodName(args...)" 형태에서 메서드명 추출
                    int openParen = operationStr.indexOf('(');
                    if (openParen > 0) {
                        String methodPart = operationStr.substring(0, openParen);
                        int lastDot = methodPart.lastIndexOf('.');
                        return lastDot >= 0 ? methodPart.substring(lastDot + 1) : methodPart;
                    }
                }
                
            } catch (Exception e) {
                logger.debug("[METHOD_EXTRACT_ERROR] 메서드명 추출 실패: {}", e.getMessage());
            }
            
            return "unknown";
        }

        /**
         * SQL을 보기 좋게 포맷팅합니다.
         * 주석(/ * * /)을 원래 위치에 보존하면서 포맷팅합니다.
         *
         * @param sql 포맷팅할 SQL 문자열
         * @return 포맷팅된 SQL 문자열
         */
        private String formatSql(String sql) {
            // 주석이 포함된 경우 원본 SQL을 그대로 반환 (주석 위치 보존)
            if (sql.contains("/*")) {
                return sql;
            }

            // 주석이 없는 경우에만 jsqlparser로 포맷팅
            try {
                Statement statement = CCJSqlParserUtil.parse(sql);

                if (statement instanceof Select) {
                    return formatSelectStatement((Select) statement);
                } else if (statement instanceof Insert) {
                    return formatInsertStatement((Insert) statement);
                } else if (statement instanceof Update) {
                    return formatUpdateStatement((Update) statement);
                } else {
                    // SELECT, INSERT, UPDATE 외 다른 구문(DELETE 등)은 기본 포맷팅 적용
                    return applyBasicFormatting(statement.toString());
                }
            } catch (Exception e) {
                // SQL 파싱 실패 시 원본 SQL 반환
                logger.info("SQL 파싱 실패, 원본 SQL 반환: {}", e.getMessage());
                return sql;
            }
        }

        /**
         * UPDATE 구문을 AST 기반으로 포맷팅합니다.
         */
        private String formatUpdateStatement(Update update) {
            StringBuilder sb = new StringBuilder();

            sb.append("UPDATE ").append(update.getTable());

            // SET clause
            if (update.getUpdateSets() != null && !update.getUpdateSets().isEmpty()) {
                sb.append("\nSET");
                List<UpdateSet> updateSets = update.getUpdateSets();
                for (int i = 0; i < updateSets.size(); i++) {
                    if (i > 0) {
                        sb.append(",");
                    }
                    UpdateSet set = updateSets.get(i);
                    sb.append("\n    ");

                    if (set.getColumns().size() > 1) sb.append("(");
                    for (int j = 0; j < set.getColumns().size(); j++) {
                        if (j > 0) sb.append(", ");
                        sb.append(set.getColumns().get(j));
                    }
                    if (set.getColumns().size() > 1) sb.append(")");

                    sb.append(" = ");

                    if (set.getExpressions().size() > 1) sb.append("(");
                    for (int j = 0; j < set.getExpressions().size(); j++) {
                        if (j > 0) sb.append(", ");
                        sb.append(formatComplexExpression(set.getExpressions().get(j), 2));
                    }
                    if (set.getExpressions().size() > 1) sb.append(")");
                }
            }

            // FROM clause (for some dialects)
            if (update.getFromItem() != null) {
                sb.append("\nFROM ").append(update.getFromItem());
            }

            // JOIN clause (for some dialects)
            if (update.getJoins() != null) {
                for (Join join : update.getJoins()) {
                    sb.append("\n").append(formatJoin(join));
                }
            }

            // WHERE clause
            if (update.getWhere() != null) {
                sb.append("\nWHERE");
                appendExpression(update.getWhere(), sb, 1, false);
            }

            return sb.toString();
        }

        /**
         * INSERT 구문을 AST 기반으로 포맷팅합니다.
         * 컬럼 목록과 값 목록을 줄바꿈하여 가독성을 높입니다.
         * INSERT ... SELECT 구문도 지원합니다.
         */
        private String formatInsertStatement(Insert insert) {
            StringBuilder sb = new StringBuilder();
            sb.append("INSERT INTO ").append(insert.getTable());

            // 컬럼 목록 포맷팅
            if (insert.getColumns() != null && !insert.getColumns().isEmpty()) {
                sb.append("(");
                for (int i = 0; i < insert.getColumns().size(); i++) {
                    sb.append("\n    ").append(insert.getColumns().get(i).getColumnName());
                    if (i < insert.getColumns().size() - 1) {
                        sb.append(",");
                    }
                }
                sb.append("\n)");
            }

            // 값 목록 포맷팅
            if (insert.getItemsList() != null) {
                if (insert.getItemsList() instanceof SubSelect) {
                    // INSERT ... SELECT 구문인 경우
                    sb.append("\n");
                    SubSelect subSelect = (SubSelect) insert.getItemsList();
                    formatSelectBody(subSelect.getSelectBody(), sb, 0);
                } else {
                    sb.append("\nVALUES\n(");
                    if (insert.getItemsList() instanceof ExpressionList) {
                        List<Expression> values = ((ExpressionList) insert.getItemsList()).getExpressions();
                        for (int i = 0; i < values.size(); i++) {
                            sb.append("\n    ").append(formatComplexExpression(values.get(i), 2));
                            if (i < values.size() - 1) {
                                sb.append(",");
                            }
                        }
                    } else {
                        sb.append("\n    ").append(insert.getItemsList().toString());
                    }
                    sb.append("\n)");
                }
            }
            return sb.toString();
        }

        /**
         * SELECT 구문을 AST 기반으로 포맷팅합니다.
         *
         * @param select JSqlParser의 Select 객체
         * @return 포맷팅된 SELECT 구문
         */
        private String formatSelectStatement(Select select) {
            StringBuilder sb = new StringBuilder();
            SelectBody selectBody = select.getSelectBody();

            // 재귀적으로 SelectBody를 처리 (UNION 등 복합 쿼리 지원)
            formatSelectBody(selectBody, sb, 0);

            return sb.toString();
        }

        /**
         * SelectBody를 재귀적으로 포맷팅하여 UNION 등의 복합 쿼리를 처리합니다.
         *
         * @param selectBody SelectBody 객체
         * @param sb         결과를 담을 StringBuilder
         * @param indentLevel 들여쓰기 수준
         */
        private void formatSelectBody(SelectBody selectBody, StringBuilder sb, int indentLevel) {
            String indent = getIndent(indentLevel);

            if (selectBody instanceof PlainSelect) {
                PlainSelect plainSelect = (PlainSelect) selectBody;

                // SELECT 절
                sb.append(indent).append("SELECT");
                if (plainSelect.getDistinct() != null) {
                    sb.append(" ").append(plainSelect.getDistinct());
                }
                appendSelectItems(plainSelect.getSelectItems(), sb, indentLevel + 1);

                // FROM 절
                if (plainSelect.getFromItem() != null) {
                    sb.append("\n").append(indent).append("FROM ").append(plainSelect.getFromItem());
                }

                // JOIN 절
                if (plainSelect.getJoins() != null) {
                    for (Join join : plainSelect.getJoins()) {
                        sb.append("\n").append(indent).append(formatJoin(join));
                    }
                }

                // WHERE 절
                if (plainSelect.getWhere() != null) {
                    sb.append("\n").append(indent).append("WHERE");
                    appendExpression(plainSelect.getWhere(), sb, indentLevel + 1, false);
                }

                // GROUP BY 절
                if (plainSelect.getGroupBy() != null) {
                    sb.append("\n").append(indent).append("GROUP BY ").append(plainSelect.getGroupBy().toString());
                }

                // HAVING 절
                if (plainSelect.getHaving() != null) {
                    sb.append("\n").append(indent).append("HAVING");
                    appendExpression(plainSelect.getHaving(), sb, indentLevel + 1, false);
                }

                // ORDER BY 절
                if (plainSelect.getOrderByElements() != null && !plainSelect.getOrderByElements().isEmpty()) {
                    sb.append("\n").append(indent).append("ORDER BY ");
                    List<OrderByElement> orderByElements = plainSelect.getOrderByElements();
                    for (int i = 0; i < orderByElements.size(); i++) {
                        if (i > 0) {
                            sb.append(", ");
                        }
                        OrderByElement element = orderByElements.get(i);
                        sb.append(element.getExpression().toString());
                        if (element.isAsc()) {
                            sb.append(" ASC");
                        } else if (!element.isAsc() && element.toString().contains("DESC")) {
                            sb.append(" DESC");
                        }
                    }
                }

                // LIMIT 절
                if (plainSelect.getLimit() != null) {
                    sb.append("\n").append(indent).append(plainSelect.getLimit());
                }

            } else if (selectBody instanceof SetOperationList) {
                SetOperationList setOpList = (SetOperationList) selectBody;
                for (int i = 0; i < setOpList.getSelects().size(); i++) {
                    if (i > 0) {
                        sb.append("\n").append(indent).append(setOpList.getOperations().get(i - 1)).append("\n");
                    }
                    formatSelectBody(setOpList.getSelects().get(i), sb, indentLevel);
                }
            } else {
                // 기타 SelectBody 타입은 기본 toString() 사용
                sb.append(indent).append(selectBody.toString());
            }
        }

        /**
         * SELECT 항목들을 포맷팅하여 추가합니다.
         */
        private void appendSelectItems(List<SelectItem> selectItems, StringBuilder sb, int indentLevel) {
            String indent = getIndent(indentLevel);
            for (int i = 0; i < selectItems.size(); i++) {
                sb.append("\n").append(indent);
                if (i > 0) {
                    sb.append(", ");
                }
                
                SelectItem item = selectItems.get(i);
                if (item instanceof SelectExpressionItem) {
                    SelectExpressionItem exprItem = (SelectExpressionItem) item;
                    // CASE 표현식 등을 포함한 복잡한 표현식 포맷팅
                    sb.append(formatComplexExpression(exprItem.getExpression(), indentLevel));
                    if (exprItem.getAlias() != null) {
                        sb.append(" AS ").append(exprItem.getAlias().getName());
                    }
                } else {
                    sb.append(item.toString());
                }
            }
        }

        /**
         * 조건 표현식(WHERE, HAVING)을 포맷팅하여 추가합니다.
         * AND, OR를 기준으로 줄바꿈 및 들여쓰기를 적용합니다.
         */
        private void appendExpression(Expression expression, StringBuilder sb, int indentLevel, boolean isNested) {
            String indent = getIndent(indentLevel);
            if (expression instanceof AndExpression) {
                AndExpression and = (AndExpression) expression;
                appendExpression(and.getLeftExpression(), sb, indentLevel, isNested);
                sb.append("\n").append(indent).append("AND ");
                appendExpression(and.getRightExpression(), sb, indentLevel, true);
            } else if (expression instanceof OrExpression) {
                OrExpression or = (OrExpression) expression;
                appendExpression(or.getLeftExpression(), sb, indentLevel, isNested);
                sb.append("\n").append(indent).append("OR ");
                appendExpression(or.getRightExpression(), sb, indentLevel, true);
            } else {
                if (!isNested) {
                    sb.append("\n").append(indent);
                }
                sb.append(formatComplexExpression(expression, indentLevel));
            }
        }

        /**
         * CASE와 같은 복잡한 표현식을 포맷팅합니다.
         */
        private String formatComplexExpression(Expression expression, int indentLevel) {
            if (expression instanceof CaseExpression) {
                return formatCaseExpression((CaseExpression) expression, indentLevel);
            }
            if (expression instanceof CastExpression) {
                return formatCastExpression((CastExpression) expression, indentLevel);
            }
            // 다른 복잡한 표현식들도 여기에 추가 가능
            return expression.toString();
        }

        /**
         * CASE 표현식을 포맷팅합니다.
         */
        private String formatCaseExpression(CaseExpression caseExpr, int indentLevel) {
            String indent = getIndent(indentLevel);
            String innerIndent = getIndent(indentLevel + 1);
            StringBuilder sb = new StringBuilder("CASE");

            if (caseExpr.getSwitchExpression() != null) {
                sb.append(" ").append(caseExpr.getSwitchExpression().toString());
            }

            for (WhenClause when : caseExpr.getWhenClauses()) {
                sb.append("\n").append(innerIndent).append("WHEN ").append(when.getWhenExpression().toString());
                sb.append(" THEN ").append(when.getThenExpression().toString());
            }

            if (caseExpr.getElseExpression() != null) {
                sb.append("\n").append(innerIndent).append("ELSE ").append(caseExpr.getElseExpression().toString());
            }

            sb.append("\n").append(indent).append("END");
            return sb.toString();
        }
        
        /**
         * CAST 표현식을 포맷팅합니다.
         */
        private String formatCastExpression(CastExpression castExpr, int indentLevel) {
            return "CAST(" + formatComplexExpression(castExpr.getLeftExpression(), indentLevel) + " AS " + castExpr.getType().toString() + ")";
        }

        /**
         * JOIN 구문을 포맷팅합니다.
         */
        private String formatJoin(Join join) {
            String joinType = "";
            if (join.isSimple()) joinType += " ";
            if (join.isCross()) joinType += "CROSS ";
            if (join.isFull()) joinType += "FULL ";
            if (join.isInner()) joinType += "INNER ";
            if (join.isLeft()) joinType += "LEFT ";
            if (join.isNatural()) joinType += "NATURAL ";
            if (join.isOuter()) joinType += "OUTER ";
            if (join.isRight()) joinType += "RIGHT ";
            if (join.isSemi()) joinType += "SEMI ";
            
            return joinType + "JOIN " + join.getRightItem() + (join.getOnExpression() != null ? " ON " + join.getOnExpression() : "");
        }

        /**
         * INSERT, UPDATE, DELETE 등의 기본 포맷팅을 적용합니다.
         */
        private String applyBasicFormatting(String sql) {
            return sql.replaceAll("(?i)\\b(SET|VALUES|WHERE)\\b", "\\n$1");
        }

        /**
         * 들여쓰기 문자열을 생성합니다.
         */
        private String getIndent(int level) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < level; i++) {
                sb.append("    "); // 4 spaces for each level
            }
            return sb.toString();
        }

        /**
         * SQL 쿼리의 ? 플레이스홀더를 실제 파라미터 값으로 치환
         * 주석 안에 있는 ? 는 치환하지 않음
         */
        private String bindParameters(String query, List<Object> parameters) {
            if (parameters == null || parameters.isEmpty()) {
                return query;
            }

            StringBuilder result = new StringBuilder();
            int paramIndex = 0;
            int queryIndex = 0;
            boolean inBlockComment = false; // /* */ 블록 주석 내부인지 여부
            boolean inLineComment = false;  // -- 라인 주석 내부인지 여부
            boolean inStringLiteral = false; // ' ' 문자열 리터럴 내부인지 여부
            
            while (queryIndex < query.length()) {
                char currentChar = query.charAt(queryIndex);
                char nextChar = (queryIndex + 1 < query.length()) ? query.charAt(queryIndex + 1) : '\0';
                
                // 문자열 리터럴 처리 (홑따옴표로 둘러싸인 문자열)
                if (currentChar == '\'' && !inBlockComment && !inLineComment) {
                    inStringLiteral = !inStringLiteral;
                    result.append(currentChar);
                    queryIndex++;
                    continue;
                }
                
                // 블록 주석 시작 /* 검사
                if (!inStringLiteral && !inLineComment && currentChar == '/' && nextChar == '*') {
                    inBlockComment = true;
                    result.append(currentChar);
                    result.append(nextChar);
                    queryIndex += 2;
                    continue;
                }
                
                // 블록 주석 끝 */ 검사
                if (!inStringLiteral && inBlockComment && currentChar == '*' && nextChar == '/') {
                    inBlockComment = false;
                    result.append(currentChar);
                    result.append(nextChar);
                    queryIndex += 2;
                    continue;
                }
                
                // 라인 주석 시작 -- 검사
                if (!inStringLiteral && !inBlockComment && currentChar == '-' && nextChar == '-') {
                    inLineComment = true;
                    result.append(currentChar);
                    result.append(nextChar);
                    queryIndex += 2;
                    continue;
                }
                
                // 라인 주석 끝 (줄바꿈) 검사
                if (inLineComment && (currentChar == '\n' || currentChar == '\r')) {
                    inLineComment = false;
                    result.append(currentChar);
                    queryIndex++;
                    continue;
                }
                
                // ? 파라미터 플레이스홀더 처리 (주석이나 문자열 리터럴 내부가 아닌 경우에만)
                if (currentChar == '?' && !inBlockComment && !inLineComment && !inStringLiteral) {
                    if (paramIndex < parameters.size()) {
                        // ? 플레이스홀더를 실제 파라미터 값으로 치환
                        Object param = parameters.get(paramIndex);
                        String paramValue;
                        
                        if (param == null) {
                            paramValue = "NULL";
                        } else if (param instanceof String) {
                            paramValue = "'" + param.toString().replace("'", "''") + "'";
                        } else if (param instanceof java.util.Date || param instanceof java.time.LocalDateTime || param instanceof java.time.LocalDate) {
                            paramValue = "'" + param.toString() + "'";
                        } else {
                            paramValue = param.toString();
                        }
                        
                        result.append(paramValue);
                        paramIndex++;
                    } else {
                        // 파라미터가 부족한 경우 ? 그대로 유지
                        result.append('?');
                    }
                } else {
                    // 그 외의 모든 문자는 그대로 복사
                    result.append(currentChar);
                }
                queryIndex++;
            }
            
            return result.toString();
        }
    }
}