/**
 * DB Console JavaScript
 */
var DbConsole = (function() {
    'use strict';

    // Private 변수
    var editor = null;
    var grid = null;
    var gridApi = null;
    var config = {
        contextPath: '',
        sessionId: '',
        datasources: []
    };
    var currentConnection = {
        connected: false,
        datasourceType: '',
        jndiName: '',
        jdbcUrl: '',
        username: '',
        schema: ''
    };
    var sqlLogs = [];
    var heartbeatInterval = null;

    // 상수
    var DOWNLOAD_THRESHOLD = 500;
    var HEARTBEAT_INTERVAL = 60000; // 1분

    // ========================================
    // 초기화
    // ========================================
    function init(options) {
        config = $.extend(config, options);

        initEditor();
        initGrid();
        initEventHandlers();
        initDatasourceSelect();
        initContextMenu();
        restoreSettings();

        // 페이지 종료 시 세션 정리
        $(window).on('beforeunload', function() {
            if (currentConnection.connected) {
                disconnect();
            }
        });
    }

    // Monaco Editor 초기화
    function initEditor() {
        require.config({ paths: { 'vs': config.contextPath + '/addon/monaco' }});

        require(['vs/editor/editor.main'], function() {
            // 저장된 테마 불러오기
            var savedTheme = loadSetting('editorTheme', 'vs-dark');

            // 초기 SQL (샘플)
            var initialSql = '-- DB Console\n' +
                             '-- 테이블 더블클릭: SELECT 문 자동 생성\n' +
                             '-- Ctrl+Enter: 커서 위치 SQL 실행\n' +
                             '-- Ctrl+Shift+Enter: 전체 SQL 실행\n' +
                             '-- 셀 우클릭: 복호화/복사 메뉴\n' +
                             '\n';

            editor = monaco.editor.create(document.getElementById('sql-editor'), {
                value: initialSql,
                language: 'sql',
                theme: savedTheme,
                minimap: { enabled: false },
                automaticLayout: true,
                fontSize: 13,
                lineNumbers: 'on',
                scrollBeyondLastLine: false,
                wordWrap: 'on',
                tabSize: 4
            });

            // 테마 셀렉트 동기화
            $('#editor-theme').val(savedTheme);

            // Ctrl+Enter로 실행 (커서 위치)
            editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function() {
                execute();
            });

            // Ctrl+Shift+Enter로 전체 실행 (Run All)
            editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter, function() {
                executeAll();
            });

            // F5로 실행
            editor.addCommand(monaco.KeyCode.F5, function() {
                execute();
            });
        });
    }

    // AG Grid 초기화
    function initGrid() {
        var gridOptions = {
            columnDefs: [],
            rowData: [],
            defaultColDef: {
                sortable: true,
                resizable: true,
                filter: true,
                minWidth: 120,
                editable: true  // 셀 편집 가능 (복사 편의)
            },
            rowSelection: {
                mode: 'multiRow',
                enableClickSelection: true
            },
            animateRows: true,
            suppressCellFocus: false,
            enableCellTextSelection: false,  // 텍스트 선택 비활성화 (셀 선택과 충돌 방지)
            ensureDomOrder: true,
            onGridReady: function(params) {
                gridApi = params.api;
            },
            // 셀 편집 시작 시 전체 선택
            onCellEditingStarted: function(params) {
                setTimeout(function() {
                    var input = document.querySelector('.ag-cell-edit-input');
                    if (input) {
                        input.select();
                    }
                }, 10);
            },
            // 편집 완료 시 원래 값 유지 (읽기 전용 동작)
            onCellEditingStopped: function(params) {
                // 원래 값으로 복원 (수정 방지)
                if (params.oldValue !== params.newValue) {
                    params.node.setDataValue(params.column.colId, params.oldValue);
                }
            }
        };

        var gridDiv = document.getElementById('result-grid');
        grid = agGrid.createGrid(gridDiv, gridOptions);
    }

    // 이벤트 핸들러 초기화
    function initEventHandlers() {
        // 실행 버튼 (커서 위치)
        $('#btn-execute').on('click', execute);

        // Run All 버튼 (전체 실행)
        $('#btn-execute-all').on('click', executeAll);

        // COMMIT 버튼
        $('#btn-commit').on('click', function() {
            showCommitConfirm();
        });

        // ROLLBACK 버튼
        $('#btn-rollback').on('click', function() {
            if (confirm('변경사항을 롤백하시겠습니까?')) {
                rollback();
            }
        });

        // 연결/해제 버튼
        $('#btn-connect').on('click', connect);
        $('#btn-disconnect').on('click', function() {
            if (confirm('연결을 해제하시겠습니까? 미커밋 트랜잭션은 롤백됩니다.')) {
                disconnect();
            }
        });

        // 데이터소스 변경 - localStorage 저장
        $('#datasource-select').on('change', function() {
            var idx = $(this).val();
            if (idx !== '') {
                var ds = config.datasources[idx];
                if (ds.type === 'DIRECT' && !ds.jdbcUrl) {
                    // 직접 입력인 경우
                    $('#direct-input-area').show();
                } else {
                    $('#direct-input-area').hide();
                }
                // 데이터소스 선택값 저장
                saveSetting('datasource', idx);
            }
        });

        // 결과 탭
        $('.dbconsole-result-tab').on('click', function() {
            var target = $(this).data('target');
            $('.dbconsole-result-tab').removeClass('active');
            $(this).addClass('active');
            $('.dbconsole-result-content').removeClass('active');
            $('#' + target).addClass('active');
        });

        // 내보내기 버튼
        $('#btn-export-csv').on('click', function() { exportResult('csv'); });
        $('#btn-export-md').on('click', function() { exportResult('md'); });
        $('#btn-export-sql').on('click', function() { exportResult('sql'); });
        $('#btn-export-json').on('click', function() { exportResult('json'); });

        // Fetch Count 변경 - localStorage 저장
        $('#fetch-count').on('change', function() {
            saveSetting('fetchCount', $(this).val());
        });

        // 에디터 테마 변경 - localStorage 저장
        $('#editor-theme').on('change', function() {
            var theme = $(this).val();
            if (editor) {
                monaco.editor.setTheme(theme);
            }
            saveSetting('editorTheme', theme);
        });

        // 복호화 모드 체크박스 - localStorage 저장
        $('#chk-decrypt-mode').on('change', function() {
            var enabled = $(this).is(':checked');
            toggleDecryptMode(enabled);
            saveSetting('decryptMode', enabled);
        });

        // 리사이저
        initResizer();

        // 키보드 단축키 (그리드 영역)
        $('#result-grid').on('keydown', function(e) {
            // Ctrl+Shift+C: 선택 영역 CSV 복사
            if (e.ctrlKey && e.shiftKey && e.key === 'C') {
                e.preventDefault();
                copySelectedCells('csv');
            }
        });
    }

    // 데이터소스 셀렉트 초기화
    function initDatasourceSelect() {
        var $select = $('#datasource-select');
        $select.empty();
        $select.append('<option value="">-- 데이터소스 선택 --</option>');

        $.each(config.datasources, function(idx, ds) {
            $select.append('<option value="' + idx + '">' + ds.displayName + '</option>');
        });
    }

    // 리사이저 초기화
    function initResizer() {
        var $resizer = $('.dbconsole-resizer');
        var $sidebar = $('.dbconsole-sidebar');
        var isResizing = false;

        $resizer.on('mousedown', function(e) {
            isResizing = true;
            $('body').css('cursor', 'col-resize');
        });

        $(document).on('mousemove', function(e) {
            if (!isResizing) return;
            var newWidth = e.clientX;
            if (newWidth >= 200 && newWidth <= 400) {
                $sidebar.css('width', newWidth + 'px');
            }
        });

        $(document).on('mouseup', function() {
            if (isResizing) {
                isResizing = false;
                $('body').css('cursor', '');
            }
        });
    }

    // ========================================
    // 연결 관리
    // ========================================
    function connect() {
        var dsIdx = $('#datasource-select').val();
        if (dsIdx === '') {
            alert('데이터소스를 선택하세요.');
            return;
        }

        var ds = config.datasources[dsIdx];
        var request = {
            sessionId: config.sessionId,
            datasourceType: ds.type,
            jndiName: ds.jndiName || '',
            jdbcUrl: ds.jdbcUrl || $('#direct-jdbc-url').val(),
            username: ds.username || $('#direct-username').val(),
            password: $('#direct-password').val(),
            driverClassName: ds.driverClassName || 'oracle.jdbc.OracleDriver'
        };

        // 직접 입력 검증
        if (ds.type === 'DIRECT' && !ds.jdbcUrl) {
            if (!request.jdbcUrl) {
                alert('JDBC URL을 입력하세요.');
                return;
            }
            if (!request.username) {
                alert('사용자명을 입력하세요.');
                return;
            }
        }

        showLoading(true);

        // 간단한 테스트 쿼리로 연결 확인
        request.sql = 'SELECT 1 FROM DUAL';
        request.maxRows = 1;

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/execute.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                showLoading(false);
                if (result.success) {
                    currentConnection = {
                        connected: true,
                        datasourceType: ds.type,
                        jndiName: ds.jndiName,
                        jdbcUrl: request.jdbcUrl,
                        username: request.username,
                        schema: ds.schema || request.username
                    };
                    updateConnectionStatus();
                    loadSchemas();
                    startHeartbeat();
                    addSqlLog('연결 성공', 'CONNECT', true, '데이터소스: ' + ds.displayName);
                } else {
                    alert('연결 실패: ' + result.message);
                    addSqlLog('연결 실패', 'CONNECT', false, result.message);
                }
            },
            error: function(xhr) {
                showLoading(false);
                alert('연결 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
            }
        });
    }

    function disconnect() {
        if (!currentConnection.connected) return;

        var request = {
            sessionId: config.sessionId
        };

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/disconnect.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                currentConnection.connected = false;
                updateConnectionStatus();
                clearSchemaTree();
                stopHeartbeat();
                addSqlLog('연결 해제', 'DISCONNECT', true, '');
            },
            error: function() {
                currentConnection.connected = false;
                updateConnectionStatus();
                stopHeartbeat();
            }
        });
    }

    function updateConnectionStatus() {
        var $status = $('#connection-status');
        var $dot = $status.find('.status-dot');
        var $text = $status.find('.status-text');

        if (currentConnection.connected) {
            $dot.removeClass('disconnected pending').addClass('connected');
            $text.text('연결됨: ' + (currentConnection.schema || currentConnection.username));
            $('#btn-connect').hide();
            $('#btn-disconnect').show();
            $('#btn-execute, #btn-execute-all, #btn-commit, #btn-rollback').prop('disabled', false);
        } else {
            $dot.removeClass('connected pending').addClass('disconnected');
            $text.text('연결 안됨');
            $('#btn-connect').show();
            $('#btn-disconnect').hide();
            $('#btn-execute, #btn-execute-all, #btn-commit, #btn-rollback').prop('disabled', true);
        }
    }

    // ========================================
    // SQL 실행
    // ========================================

    // 커서 위치의 SQL 구문 찾기
    function getSqlAtCursor() {
        if (!editor) return '';

        var model = editor.getModel();
        var position = editor.getPosition();
        var fullText = model.getValue();

        // 세미콜론으로 구문 분리 (문자열 내 세미콜론 제외)
        var statements = parseSqlStatements(fullText);
        if (statements.length === 0) return fullText.trim();

        // 커서 위치(offset) 계산
        var cursorOffset = model.getOffsetAt(position);

        // 커서가 속한 구문 찾기
        var currentOffset = 0;
        for (var i = 0; i < statements.length; i++) {
            var stmt = statements[i];
            var stmtStart = fullText.indexOf(stmt.text, currentOffset);
            var stmtEnd = stmtStart + stmt.text.length;

            if (cursorOffset >= stmtStart && cursorOffset <= stmtEnd + 1) {
                return stmt.text.trim();
            }
            currentOffset = stmtEnd + 1; // +1 for semicolon
        }

        // 찾지 못하면 마지막 구문 반환
        return statements[statements.length - 1].text.trim();
    }

    // SQL 구문 파싱 (세미콜론 기준, 문자열 내 세미콜론 무시)
    function parseSqlStatements(text) {
        var statements = [];
        var current = '';
        var inSingleQuote = false;
        var inDoubleQuote = false;
        var inLineComment = false;
        var inBlockComment = false;

        for (var i = 0; i < text.length; i++) {
            var char = text[i];
            var nextChar = text[i + 1] || '';

            // 라인 주석 시작
            if (!inSingleQuote && !inDoubleQuote && !inBlockComment && char === '-' && nextChar === '-') {
                inLineComment = true;
            }
            // 라인 주석 끝
            if (inLineComment && (char === '\n' || char === '\r')) {
                inLineComment = false;
            }
            // 블록 주석 시작
            if (!inSingleQuote && !inDoubleQuote && !inLineComment && char === '/' && nextChar === '*') {
                inBlockComment = true;
            }
            // 블록 주석 끝
            if (inBlockComment && char === '*' && nextChar === '/') {
                current += char;
                current += nextChar;
                i++;
                inBlockComment = false;
                continue;
            }

            // 문자열 처리
            if (!inLineComment && !inBlockComment) {
                if (char === "'" && !inDoubleQuote) {
                    inSingleQuote = !inSingleQuote;
                } else if (char === '"' && !inSingleQuote) {
                    inDoubleQuote = !inDoubleQuote;
                }
            }

            // 세미콜론 (구문 구분자)
            if (char === ';' && !inSingleQuote && !inDoubleQuote && !inLineComment && !inBlockComment) {
                var trimmed = current.trim();
                if (trimmed) {
                    statements.push({ text: trimmed });
                }
                current = '';
                continue;
            }

            current += char;
        }

        // 마지막 구문 (세미콜론 없이 끝난 경우)
        var lastTrimmed = current.trim();
        if (lastTrimmed) {
            statements.push({ text: lastTrimmed });
        }

        return statements;
    }

    // SQL에서 주석 제거
    function removeComments(sql) {
        var result = '';
        var inSingleQuote = false;
        var inDoubleQuote = false;
        var inLineComment = false;
        var inBlockComment = false;

        for (var i = 0; i < sql.length; i++) {
            var char = sql[i];
            var nextChar = sql[i + 1] || '';

            // 라인 주석 시작
            if (!inSingleQuote && !inDoubleQuote && !inBlockComment && char === '-' && nextChar === '-') {
                inLineComment = true;
                i++; // skip next '-'
                continue;
            }
            // 라인 주석 끝
            if (inLineComment && (char === '\n' || char === '\r')) {
                inLineComment = false;
                result += char; // 줄바꿈은 유지
                continue;
            }
            // 블록 주석 시작
            if (!inSingleQuote && !inDoubleQuote && !inLineComment && char === '/' && nextChar === '*') {
                inBlockComment = true;
                i++; // skip '*'
                continue;
            }
            // 블록 주석 끝
            if (inBlockComment && char === '*' && nextChar === '/') {
                inBlockComment = false;
                i++; // skip '/'
                continue;
            }

            // 주석 내부면 skip
            if (inLineComment || inBlockComment) {
                continue;
            }

            // 문자열 처리
            if (char === "'" && !inDoubleQuote) {
                inSingleQuote = !inSingleQuote;
            } else if (char === '"' && !inSingleQuote) {
                inDoubleQuote = !inDoubleQuote;
            }

            result += char;
        }

        return result.trim();
    }

    // 현재 커서 위치 기준 실행
    function execute() {
        if (!currentConnection.connected) {
            alert('먼저 데이터소스에 연결하세요.');
            return;
        }

        var sql = '';
        // 선택 영역이 있으면 선택 영역 실행
        if (editor) {
            var selection = editor.getSelection();
            if (selection && !selection.isEmpty()) {
                sql = editor.getModel().getValueInRange(selection);
            } else {
                // 커서 위치의 구문 실행
                sql = getSqlAtCursor();
            }
        }

        // 주석 제거 후 실행할 SQL이 있는지 확인
        var sqlWithoutComments = removeComments(sql);
        if (!sqlWithoutComments) {
            alert('실행할 SQL을 입력하세요. (주석만 있습니다)');
            return;
        }

        executeSql(sql);
    }

    // 전체 SQL 실행 (Run All)
    function executeAll() {
        if (!currentConnection.connected) {
            alert('먼저 데이터소스에 연결하세요.');
            return;
        }

        if (!editor) return;

        var fullText = editor.getModel().getValue();
        var statements = parseSqlStatements(fullText);

        // 주석만 있는 구문 필터링
        var executableStatements = statements.filter(function(stmt) {
            return removeComments(stmt.text).length > 0;
        });

        if (executableStatements.length === 0) {
            alert('실행할 SQL을 입력하세요. (주석만 있습니다)');
            return;
        }

        // 여러 구문 순차 실행
        executeStatementsSequentially(executableStatements, 0);
    }

    // 구문 순차 실행
    function executeStatementsSequentially(statements, index) {
        if (index >= statements.length) {
            return;
        }

        var sql = statements[index].text;

        // 주석 제거 후 COMMIT/ROLLBACK 체크
        var sqlWithoutComments = removeComments(sql);
        var upperSql = sqlWithoutComments.toUpperCase();

        // COMMIT/ROLLBACK은 특별 처리
        if (upperSql === 'COMMIT') {
            showCommitConfirm();
            return; // COMMIT 후에는 중단 (사용자 확인 필요)
        }
        if (upperSql === 'ROLLBACK') {
            if (confirm('변경사항을 롤백하시겠습니까?')) {
                rollback();
            }
            return;
        }

        executeSql(sql, function() {
            // 다음 구문 실행
            executeStatementsSequentially(statements, index + 1);
        });
    }

    // 단일 SQL 실행
    function executeSql(sql, callback) {
        // COMMIT/ROLLBACK 인터셉트
        var upperSql = sql.toUpperCase().trim();
        if (upperSql === 'COMMIT') {
            showCommitConfirm();
            return;
        }
        if (upperSql === 'ROLLBACK') {
            if (confirm('변경사항을 롤백하시겠습니까?')) {
                rollback();
            }
            return;
        }

        var fetchCount = $('#fetch-count').val();
        var maxRows = fetchCount === 'all' ? 0 : parseInt(fetchCount);

        var request = {
            sessionId: config.sessionId,
            sql: sql,
            maxRows: maxRows,
            datasourceType: currentConnection.datasourceType,
            jndiName: currentConnection.jndiName,
            jdbcUrl: currentConnection.jdbcUrl,
            username: currentConnection.username
        };

        showLoading(true);
        var startTime = Date.now();

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/execute.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                showLoading(false);
                var elapsed = Date.now() - startTime;

                if (result.success) {
                    if (result.queryType === 'SELECT') {
                        displaySelectResult(result);
                    } else {
                        displayUpdateResult(result);
                    }
                    addSqlLog(sql, result.queryType, true, result.message + ' (' + elapsed + 'ms)');
                    clearEditorErrors();
                } else {
                    displayError(result.message);
                    addSqlLog(sql, 'ERROR', false, result.message);
                    markEditorError(sql, result.message);
                }

                updateTransactionIndicator(result.transactionActive);

                if (callback) callback();
            },
            error: function(xhr) {
                showLoading(false);
                var msg = xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText;
                displayError('실행 오류: ' + msg);
                addSqlLog(sql, 'ERROR', false, msg);
            }
        });
    }

    // ========================================
    // COMMIT / ROLLBACK
    // ========================================
    function showCommitConfirm() {
        $('#commit-modal').modal('show');
        $('#commit-password').val('').focus();
    }

    function commit() {
        var password = $('#commit-password').val();
        if (!password) {
            alert('비밀번호를 입력하세요.');
            return;
        }

        var request = {
            sessionId: config.sessionId,
            password: password
        };

        showLoading(true);

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/commit.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                showLoading(false);
                $('#commit-modal').modal('hide');

                if (result.success) {
                    alert('COMMIT 완료');
                    addSqlLog('COMMIT', 'COMMIT', true, result.message);
                } else {
                    alert('COMMIT 실패: ' + result.message);
                    addSqlLog('COMMIT', 'COMMIT', false, result.message);
                }
            },
            error: function(xhr) {
                showLoading(false);
                alert('COMMIT 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
            }
        });
    }

    function rollback() {
        var request = {
            sessionId: config.sessionId
        };

        showLoading(true);

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/rollback.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                showLoading(false);

                if (result.success) {
                    alert('ROLLBACK 완료');
                    addSqlLog('ROLLBACK', 'ROLLBACK', true, result.message);
                } else {
                    alert('ROLLBACK 실패: ' + result.message);
                    addSqlLog('ROLLBACK', 'ROLLBACK', false, result.message);
                }
            },
            error: function(xhr) {
                showLoading(false);
                alert('ROLLBACK 오류: ' + (xhr.responseJSON ? xhr.responseJSON.message : xhr.statusText));
            }
        });
    }

    // ========================================
    // 암호화 데이터 복호화
    // ========================================

    var decryptModeEnabled = false;
    var contextMenuTarget = null;

    // 복호화 모드 토글
    function toggleDecryptMode(enabled) {
        decryptModeEnabled = enabled;
        // 그리드 행 다시 그리기 (cellRenderer가 다시 호출되도록)
        if (gridApi) {
            gridApi.redrawRows();
        }
    }

    // 암호화된 값인지 확인 (Base64 패턴)
    // 예: "FnJ/5lM72/Q7gBn7KG+piiABZLALMVQQ4YK8akyFJ5Q="
    function isEncryptedValue(value) {
        if (!value || typeof value !== 'string') {
            return false;
        }

        var trimmed = value.trim();

        // 최소 길이 체크 (Base64로 인코딩된 최소 16바이트 = 24자 정도)
        if (trimmed.length < 20) {
            return false;
        }

        // Base64 패턴: A-Z, a-z, 0-9, +, /, = (패딩)
        // 끝에 =나 ==가 있거나, +나 /가 포함된 경우 Base64로 판단
        var base64Pattern = /^[A-Za-z0-9+/]+=*$/;
        if (!base64Pattern.test(trimmed)) {
            return false;
        }

        // Base64 특수문자(+, /)가 포함되어 있거나 =로 끝나면 암호화 데이터로 판단
        var hasBase64Chars = /[+/]/.test(trimmed) || /=$/.test(trimmed);

        // 길이가 4의 배수인지 확인 (유효한 Base64)
        var isValidLength = trimmed.length % 4 === 0;

        return hasBase64Chars && isValidLength;
    }

    // 셀 복호화 (버튼 클릭)
    function decryptCell(btn) {
        var $cell = $(btn).closest('.cell-encrypted');
        var encryptedValue = $cell.data('value');
        var rowIndex = $cell.data('row');
        var colId = $cell.data('col');
        doDecrypt(encryptedValue, rowIndex, colId);
    }

    // 실제 복호화 수행
    function doDecrypt(encryptedValue, rowIndex, colId) {
        if (!encryptedValue) {
            alert('복호화할 값이 없습니다.');
            return;
        }

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/decrypt.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({ value: encryptedValue }),
            success: function(response) {
                if (response.success) {
                    // 복호화 성공 - 값 저장소에 저장하고 그리드 새로고침
                    if (rowIndex !== null && rowIndex !== undefined && colId) {
                        var key = rowIndex + '_' + colId;
                        decryptedValues[key] = response.decryptedValue;
                        // 그리드 셀 새로고침
                        if (gridApi) {
                            gridApi.refreshCells({ force: true });
                        }
                    } else {
                        // row/col 정보 없으면 alert로 표시
                        alert('복호화 결과:\n' + response.decryptedValue);
                    }
                } else {
                    alert('복호화 실패: ' + (response.message || '알 수 없는 오류'));
                }
            },
            error: function(xhr) {
                alert('복호화 요청 오류: ' + xhr.statusText);
            }
        });
    }

    // 컨텍스트 메뉴 초기화
    function initContextMenu() {
        // 컨텍스트 메뉴 HTML (셀 우클릭용)
        var menuHtml = '<div id="dbconsole-context-menu" class="dbconsole-context-menu">' +
                       '<div class="context-menu-item" data-action="decrypt"><i class="bi bi-unlock"></i> 이 셀 복호화</div>' +
                       '<div class="context-menu-item" data-action="copy"><i class="bi bi-clipboard"></i> 이 셀 복사</div>' +
                       '<div class="context-menu-divider"></div>' +
                       '<div class="context-menu-item" data-action="copy-selection-csv"><i class="bi bi-file-earmark-spreadsheet"></i> 선택 행 복사 (CSV)</div>' +
                       '<div class="context-menu-item" data-action="copy-selection-md"><i class="bi bi-markdown"></i> 선택 행 복사 (MD)</div>' +
                       '<div class="context-menu-item" data-action="copy-selection-sql"><i class="bi bi-file-code"></i> 선택 행 복사 (SQL)</div>' +
                       '<div class="context-menu-item" data-action="copy-selection-json"><i class="bi bi-filetype-json"></i> 선택 행 복사 (JSON)</div>' +
                       '</div>';
        $('body').append(menuHtml);

        var $menu = $('#dbconsole-context-menu');

        // 그리드 셀 우클릭
        $('#result-grid').on('contextmenu', '.ag-cell', function(e) {
            e.preventDefault();

            var cellValue = $(this).text().trim();
            // (NULL) 표시 제거
            if (cellValue === '(NULL)') cellValue = '';

            // row/col 정보 추출
            var rowIndex = null;
            var colId = null;

            // data-value 속성이 있으면 (암호화된 값) 그걸 사용
            var $encrypted = $(this).find('.cell-encrypted');
            if ($encrypted.length) {
                cellValue = $encrypted.data('value') || cellValue;
                rowIndex = $encrypted.data('row');
                colId = $encrypted.data('col');
            }

            // row/col 정보가 없으면 AG Grid API에서 추출 시도
            if (rowIndex === null || rowIndex === undefined) {
                var $row = $(this).closest('.ag-row');
                if ($row.length) {
                    var rowIdAttr = $row.attr('row-index');
                    if (rowIdAttr !== undefined) {
                        rowIndex = parseInt(rowIdAttr, 10);
                    }
                }
                // colId 추출
                var colIdAttr = $(this).attr('col-id');
                if (colIdAttr) {
                    colId = colIdAttr;
                }
            }

            contextMenuTarget = {
                element: $(this),
                value: cellValue,
                rowIndex: rowIndex,
                colId: colId
            };

            $menu.css({
                top: e.pageY + 'px',
                left: e.pageX + 'px'
            }).show();
        });

        // 메뉴 아이템 클릭
        $menu.on('click', '.context-menu-item', function() {
            var action = $(this).data('action');

            if (action === 'decrypt' && contextMenuTarget) {
                // 셀에 바로 복호화 결과 표시
                doDecrypt(contextMenuTarget.value, contextMenuTarget.rowIndex, contextMenuTarget.colId);
            } else if (action === 'copy' && contextMenuTarget) {
                copyToClipboard(contextMenuTarget.value || '');
            } else if (action.startsWith('copy-selection-')) {
                // 선택 영역 복사
                var format = action.replace('copy-selection-', '');
                copySelectedCells(format);
            }

            $menu.hide();
            contextMenuTarget = null;
        });

        // 다른 곳 클릭시 메뉴 숨김
        $(document).on('click', function() {
            $menu.hide();
        });
    }

    // 클립보드 복사
    function copyToClipboard(text) {
        if (navigator.clipboard) {
            navigator.clipboard.writeText(text);
        } else {
            var textarea = document.createElement('textarea');
            textarea.value = text;
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
        }
    }

    // 선택된 행 복사 (다양한 형식 지원)
    function copySelectedCells(format) {
        if (!gridApi) {
            alert('그리드가 초기화되지 않았습니다.');
            return;
        }

        // 선택된 행 가져오기 (AG Grid Community는 행 선택만 지원)
        var selectedRows = gridApi.getSelectedRows();
        if (!selectedRows || selectedRows.length === 0) {
            alert('복사할 행을 선택하세요.\n(클릭: 단일 선택, Ctrl+클릭: 다중 선택, Shift+클릭: 범위 선택)');
            return;
        }

        // 선택된 데이터 추출
        var selectedData = extractSelectedData(selectedRows);
        if (selectedData.rows.length === 0) {
            alert('복사할 데이터가 없습니다.');
            return;
        }

        var content = '';
        switch (format) {
            case 'csv':
                content = formatSelectionToCsv(selectedData);
                break;
            case 'md':
                content = formatSelectionToMarkdown(selectedData);
                break;
            case 'sql':
                content = formatSelectionToSql(selectedData);
                break;
            case 'json':
                content = formatSelectionToJson(selectedData);
                break;
            default:
                content = formatSelectionToCsv(selectedData);
        }

        copyToClipboard(content);
        alert('선택된 ' + selectedData.rows.length + '개 행이 ' + format.toUpperCase() + ' 형식으로 클립보드에 복사되었습니다.');
    }

    // 선택된 행에서 데이터 추출
    function extractSelectedData(selectedRows) {
        if (!selectedRows || selectedRows.length === 0) {
            return { columns: [], rows: [] };
        }

        // 컬럼 정보 가져오기 (행 번호 컬럼 제외)
        var columnDefs = gridApi.getColumnDefs() || [];
        var columns = [];
        columnDefs.forEach(function(colDef) {
            // 행 번호 컬럼(_rowNum) 제외
            if (colDef.field && colDef.field !== '_rowNum') {
                columns.push({
                    colId: colDef.field,
                    headerName: colDef.headerName || colDef.field
                });
            }
        });

        // 데이터 추출
        var rows = selectedRows.map(function(rowData) {
            var row = {};
            columns.forEach(function(col) {
                row[col.colId] = rowData[col.colId];
            });
            return row;
        });

        return {
            columns: columns,
            rows: rows
        };
    }

    // 선택 데이터 -> CSV 형식
    function formatSelectionToCsv(data) {
        var lines = [];
        // 헤더
        lines.push(data.columns.map(function(c) { return c.headerName; }).join(','));
        // 데이터
        data.rows.forEach(function(row) {
            var values = data.columns.map(function(col) {
                var val = row[col.colId];
                if (val === null || val === undefined) return '';
                val = String(val).replace(/"/g, '""');
                if (val.indexOf(',') >= 0 || val.indexOf('"') >= 0 || val.indexOf('\n') >= 0) {
                    val = '"' + val + '"';
                }
                return val;
            });
            lines.push(values.join(','));
        });
        return lines.join('\n');
    }

    // 선택 데이터 -> Markdown 형식
    function formatSelectionToMarkdown(data) {
        var lines = [];
        // 헤더
        lines.push('| ' + data.columns.map(function(c) { return c.headerName; }).join(' | ') + ' |');
        lines.push('| ' + data.columns.map(function() { return '---'; }).join(' | ') + ' |');
        // 데이터
        data.rows.forEach(function(row) {
            var values = data.columns.map(function(col) {
                var val = row[col.colId];
                if (val === null || val === undefined) return '';
                return String(val).replace(/\|/g, '\\|').replace(/\n/g, ' ');
            });
            lines.push('| ' + values.join(' | ') + ' |');
        });
        return lines.join('\n');
    }

    // 선택 데이터 -> INSERT SQL 형식
    function formatSelectionToSql(data) {
        var tableName = 'TABLE_NAME';
        var columnNames = data.columns.map(function(c) { return c.colId; }).join(', ');
        var lines = [];

        data.rows.forEach(function(row) {
            var values = data.columns.map(function(col) {
                var val = row[col.colId];
                if (val === null || val === undefined) return 'NULL';
                val = String(val);
                // CLOB 처리 (4000바이트 초과 시 분할)
                if (val.length > 4000) {
                    return splitClobValue(val);
                }
                return "'" + val.replace(/'/g, "''") + "'";
            });
            lines.push('INSERT INTO ' + tableName + ' (' + columnNames + ') VALUES (' + values.join(', ') + ');');
        });

        return lines.join('\n');
    }

    // 선택 데이터 -> JSON 형식
    function formatSelectionToJson(data) {
        var result = data.rows.map(function(row) {
            var obj = {};
            data.columns.forEach(function(col) {
                obj[col.colId] = row[col.colId];
            });
            return obj;
        });
        return JSON.stringify(result, null, 2);
    }

    // ========================================
    // 결과 표시
    // ========================================

    // 복호화된 값 저장소 (rowIndex_colId -> decryptedValue)
    var decryptedValues = {};

    function displaySelectResult(result) {
        if (!gridApi) return;

        // 복호화 값 저장소 초기화
        decryptedValues = {};

        // 행 번호 컬럼 (첫 번째)
        var rowNumCol = {
            headerName: '#',
            field: '_rowNum',
            width: 60,
            minWidth: 50,
            maxWidth: 80,
            pinned: 'left',
            editable: false,
            sortable: false,
            filter: false,
            resizable: false,
            suppressMovable: true,
            cellClass: 'row-number-cell',
            valueGetter: function(params) {
                return params.node.rowIndex + 1;
            }
        };

        // 데이터 컬럼 정의
        var dataColumnDefs = result.columns.map(function(col, idx) {
            var colMeta = result.columnMetas ? result.columnMetas[idx] : null;
            return {
                headerName: col,
                field: col,
                // valueGetter로 실제 값 반환 (복호화된 값 포함)
                valueGetter: function(params) {
                    var key = params.node.rowIndex + '_' + col;
                    // 복호화된 값이 있으면 그 값 반환
                    if (decryptedValues[key] !== undefined) {
                        return decryptedValues[key];
                    }
                    return params.data[col];
                },
                cellRenderer: function(params) {
                    var key = params.node.rowIndex + '_' + col;
                    var originalValue = params.data[col];
                    var displayValue = params.value;

                    // NULL 체크
                    if (originalValue === null || originalValue === undefined) {
                        return '<span class="cell-null">(NULL)</span>';
                    }

                    var originalStr = String(originalValue);
                    var displayStr = String(displayValue);

                    // LOB 타입
                    if (colMeta && colMeta.lobType) {
                        return '<span class="cell-lob">' + escapeHtml(displayStr) + '</span>';
                    }

                    // 복호화된 값인 경우 (저장소에 있음)
                    if (decryptedValues[key] !== undefined) {
                        return '<span class="cell-decrypted" title="원본: ' + escapeHtml(originalStr) + '">' +
                               '<i class="bi bi-unlock-fill"></i> ' + escapeHtml(displayStr) + '</span>';
                    }

                    // 복호화 모드 활성화 + 암호화된 값 감지 시에만 버튼 표시 (원본값 기준)
                    if (decryptModeEnabled && isEncryptedValue(originalStr)) {
                        var shortValue = originalStr.length > 20 ? originalStr.substring(0, 20) + '...' : originalStr;
                        return '<span class="cell-encrypted" data-value="' + escapeHtml(originalStr) + '" data-row="' + params.node.rowIndex + '" data-col="' + col + '">' +
                               escapeHtml(shortValue) +
                               ' <button class="btn-decrypt" onclick="DbConsole.decryptCell(this)" title="복호화">' +
                               '<i class="bi bi-unlock"></i></button></span>';
                    }
                    return escapeHtml(displayStr);
                }
            };
        });

        var columnDefs = [rowNumCol].concat(dataColumnDefs);

        gridApi.updateGridOptions({
            columnDefs: columnDefs,
            rowData: result.rows || []
        });
        gridApi.sizeColumnsToFit();

        // 결과 탭으로 전환
        $('.dbconsole-result-tab[data-target="result-grid-container"]').click();

        // 메시지 표시
        $('#result-message').text(result.message || '');
    }

    function displayUpdateResult(result) {
        // 그리드 초기화
        if (gridApi) {
            gridApi.updateGridOptions({
                columnDefs: [],
                rowData: []
            });
        }

        // 메시지 표시
        $('#result-message').text(result.message || '');
        alert(result.message);
    }

    function displayError(message) {
        $('#result-message').text('오류: ' + message);
    }

    function updateTransactionIndicator(active) {
        var $indicator = $('#transaction-indicator');
        if (active) {
            $indicator.addClass('active').text('트랜잭션 활성');
        } else {
            $indicator.removeClass('active').text('');
        }
    }

    // ========================================
    // 스키마 탐색기
    // ========================================
    function loadSchemas() {
        var request = {
            sessionId: config.sessionId,
            datasourceType: currentConnection.datasourceType,
            jndiName: currentConnection.jndiName,
            jdbcUrl: currentConnection.jdbcUrl,
            username: currentConnection.username
        };

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/schemas.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(schemas) {
                renderSchemaTree(schemas);
            }
        });
    }

    function renderSchemaTree(schemas) {
        var $tree = $('#schema-tree');
        $tree.empty();

        // 현재 스키마를 우선 표시
        var currentSchema = (currentConnection.schema || currentConnection.username || '').toUpperCase();
        var sortedSchemas = schemas.sort(function(a, b) {
            if (a === currentSchema) return -1;
            if (b === currentSchema) return 1;
            return a.localeCompare(b);
        });

        $.each(sortedSchemas, function(idx, schema) {
            var $item = $('<div class="schema-item" data-schema="' + schema + '">' +
                '<span class="tree-toggle tree-icon"><i class="bi bi-chevron-right"></i></span>' +
                '<span class="tree-icon"><i class="bi bi-database"></i></span>' +
                '<span class="schema-name">' + schema + '</span>' +
                '</div>');

            var $children = $('<div class="tree-children" data-schema="' + schema + '"></div>');

            $item.on('click', function(e) {
                e.stopPropagation();
                var $toggle = $(this).find('.tree-toggle i');

                if ($children.hasClass('expanded')) {
                    $children.removeClass('expanded');
                    $toggle.removeClass('bi-chevron-down').addClass('bi-chevron-right');
                } else {
                    $children.addClass('expanded');
                    $toggle.removeClass('bi-chevron-right').addClass('bi-chevron-down');

                    if ($children.children().length === 0) {
                        loadTables(schema, $children);
                    }
                }
            });

            $tree.append($item).append($children);

            // 현재 스키마 자동 확장
            if (schema === currentSchema) {
                $item.click();
            }
        });
    }

    function loadTables(schema, $container) {
        var request = {
            sessionId: config.sessionId,
            schemaName: schema,
            datasourceType: currentConnection.datasourceType,
            jndiName: currentConnection.jndiName,
            jdbcUrl: currentConnection.jdbcUrl,
            username: currentConnection.username
        };

        $container.html('<div class="text-muted small p-2">로딩 중...</div>');

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/tables.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(tables) {
                renderTables(schema, tables, $container);
            },
            error: function() {
                $container.html('<div class="text-danger small p-2">로드 실패</div>');
            }
        });
    }

    function renderTables(schema, tables, $container) {
        $container.empty();

        if (tables.length === 0) {
            $container.html('<div class="text-muted small p-2">테이블 없음</div>');
            return;
        }

        $.each(tables, function(idx, table) {
            var typeClass = table.tableType === 'VIEW' ? 'view' : '';
            var $item = $('<div class="table-item" data-schema="' + schema + '" data-table="' + table.tableName + '">' +
                '<span class="tree-toggle tree-icon"><i class="bi bi-chevron-right"></i></span>' +
                '<span class="tree-icon"><i class="bi bi-table"></i></span>' +
                '<span class="table-name" title="' + (table.comments || '') + '">' + table.tableName + '</span>' +
                '<span class="table-type ' + typeClass + '">' + table.tableType + '</span>' +
                '</div>');

            var $columns = $('<div class="tree-children"></div>');

            // 테이블 클릭 - 확장/축소
            $item.find('.tree-toggle').on('click', function(e) {
                e.stopPropagation();
                var $toggle = $(this).find('i');

                if ($columns.hasClass('expanded')) {
                    $columns.removeClass('expanded');
                    $toggle.removeClass('bi-chevron-down').addClass('bi-chevron-right');
                } else {
                    $columns.addClass('expanded');
                    $toggle.removeClass('bi-chevron-right').addClass('bi-chevron-down');

                    if ($columns.children().length === 0) {
                        loadColumns(schema, table.tableName, $columns);
                    }
                }
            });

            // 테이블 더블클릭 - SELECT 생성 (새 라인에 세미콜론 포함)
            $item.on('dblclick', function(e) {
                e.stopPropagation();
                var sql = 'SELECT * FROM ' + schema + '.' + table.tableName + ';';
                if (editor) {
                    var model = editor.getModel();
                    var lastLine = model.getLineCount();
                    var lastLineContent = model.getLineContent(lastLine);
                    // 마지막 라인이 비어있지 않으면 새 라인 추가
                    var prefix = lastLineContent.trim() ? '\n' : '';
                    editor.executeEdits('', [{
                        range: new monaco.Range(lastLine, model.getLineMaxColumn(lastLine), lastLine, model.getLineMaxColumn(lastLine)),
                        text: prefix + sql
                    }]);
                    // 커서를 새로 추가된 SQL 끝으로 이동
                    var newLastLine = model.getLineCount();
                    editor.setPosition({ lineNumber: newLastLine, column: model.getLineMaxColumn(newLastLine) });
                    editor.focus();
                }
            });

            // 우클릭 - DDL 조회
            $item.on('contextmenu', function(e) {
                e.preventDefault();
                showTableDdl(schema, table.tableName);
            });

            $container.append($item).append($columns);
        });
    }

    function loadColumns(schema, tableName, $container) {
        var request = {
            sessionId: config.sessionId,
            schemaName: schema,
            tableName: tableName,
            datasourceType: currentConnection.datasourceType,
            jndiName: currentConnection.jndiName,
            jdbcUrl: currentConnection.jdbcUrl,
            username: currentConnection.username
        };

        $container.html('<div class="text-muted small p-2">로딩 중...</div>');

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/columns.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(columns) {
                renderColumns(columns, $container);
            },
            error: function() {
                $container.html('<div class="text-danger small p-2">로드 실패</div>');
            }
        });
    }

    function renderColumns(columns, $container) {
        $container.empty();

        $.each(columns, function(idx, col) {
            var badges = '';
            if (col.primaryKey) badges += '<span class="column-pk" title="Primary Key">PK</span> ';
            if (col.foreignKey) badges += '<span class="column-fk" title="Foreign Key">FK</span> ';
            if (col.indexed && !col.primaryKey) badges += '<span class="column-idx" title="' + (col.indexName || 'Indexed') + '">IDX</span> ';

            var $item = $('<div class="column-item" title="' + (col.comments || '') + '">' +
                '<span class="column-name">' + col.columnName + '</span>' +
                badges +
                '<span class="column-type">' + col.dataType +
                (col.dataLength ? '(' + col.dataLength + ')' : '') +
                (col.nullable === 'N' ? ' NOT NULL' : '') +
                '</span>' +
                '</div>');

            $container.append($item);
        });
    }

    function clearSchemaTree() {
        $('#schema-tree').empty();
    }

    function showTableDdl(schema, tableName) {
        var request = {
            sessionId: config.sessionId,
            schemaName: schema,
            tableName: tableName,
            datasourceType: currentConnection.datasourceType,
            jndiName: currentConnection.jndiName,
            jdbcUrl: currentConnection.jdbcUrl,
            username: currentConnection.username
        };

        $.ajax({
            url: config.contextPath + '/admin/dbconsole/ddl.do',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(request),
            success: function(result) {
                $('#ddl-modal .modal-title').text('DDL: ' + schema + '.' + tableName);
                $('#ddl-content').text(result.ddl || '-- DDL 조회 실패');
                $('#ddl-modal').modal('show');
            }
        });
    }

    // ========================================
    // 내보내기
    // ========================================
    function exportResult(format) {
        if (!gridApi) return;

        var rowData = [];
        gridApi.forEachNode(function(node) {
            rowData.push(node.data);
        });

        if (rowData.length === 0) {
            alert('내보낼 데이터가 없습니다.');
            return;
        }

        var columns = gridApi.getColumnDefs().map(function(col) {
            return col.field;
        });

        var content = '';
        var filename = '';
        var mimeType = '';

        switch (format) {
            case 'csv':
                content = exportToCsv(columns, rowData);
                filename = 'query_result.csv';
                mimeType = 'text/csv;charset=utf-8;';
                break;
            case 'md':
                content = exportToMarkdown(columns, rowData);
                filename = 'query_result.md';
                mimeType = 'text/markdown;charset=utf-8;';
                break;
            case 'sql':
                content = exportToInsertSql(columns, rowData);
                filename = 'insert_statements.sql';
                mimeType = 'text/plain;charset=utf-8;';
                break;
            case 'json':
                content = JSON.stringify(rowData, null, 2);
                filename = 'query_result.do';
                mimeType = 'application/json;charset=utf-8;';
                break;
        }

        if (rowData.length >= DOWNLOAD_THRESHOLD) {
            downloadFile(content, filename, mimeType);
        } else {
            copyToClipboard(content);
            alert('클립보드에 복사되었습니다. (' + rowData.length + '건)');
        }
    }

    function exportToCsv(columns, rows) {
        var lines = [];
        lines.push(columns.join(','));

        $.each(rows, function(idx, row) {
            var values = columns.map(function(col) {
                var val = row[col];
                if (val === null || val === undefined) return '';
                val = String(val).replace(/"/g, '""');
                if (val.indexOf(',') >= 0 || val.indexOf('"') >= 0 || val.indexOf('\n') >= 0) {
                    val = '"' + val + '"';
                }
                return val;
            });
            lines.push(values.join(','));
        });

        return lines.join('\n');
    }

    function exportToMarkdown(columns, rows) {
        var lines = [];
        lines.push('| ' + columns.join(' | ') + ' |');
        lines.push('| ' + columns.map(function() { return '---'; }).join(' | ') + ' |');

        $.each(rows, function(idx, row) {
            var values = columns.map(function(col) {
                var val = row[col];
                if (val === null || val === undefined) return '';
                return String(val).replace(/\|/g, '\\|');
            });
            lines.push('| ' + values.join(' | ') + ' |');
        });

        return lines.join('\n');
    }

    function exportToInsertSql(columns, rows) {
        var tableName = 'TABLE_NAME'; // 실제 테이블명은 알 수 없으므로 placeholder
        var lines = [];

        $.each(rows, function(idx, row) {
            var values = columns.map(function(col) {
                var val = row[col];
                if (val === null || val === undefined) return 'NULL';

                val = String(val);

                // CLOB 처리 (4000바이트 초과 시 분할)
                if (val.length > 4000) {
                    return splitClobValue(val);
                }

                return "'" + val.replace(/'/g, "''") + "'";
            });

            lines.push('INSERT INTO ' + tableName + ' (' + columns.join(', ') + ') VALUES (' + values.join(', ') + ');');
        });

        return lines.join('\n');
    }

    function splitClobValue(str) {
        var chunks = [];
        var maxBytes = 3900; // 안전 마진
        var currentChunk = '';
        var currentBytes = 0;

        for (var i = 0; i < str.length; i++) {
            var char = str.charAt(i);
            var charBytes = getByteLength(char);

            if (currentBytes + charBytes > maxBytes) {
                chunks.push(currentChunk);
                currentChunk = char;
                currentBytes = charBytes;
            } else {
                currentChunk += char;
                currentBytes += charBytes;
            }
        }

        if (currentChunk) {
            chunks.push(currentChunk);
        }

        if (chunks.length === 1) {
            return "TO_CLOB('" + chunks[0].replace(/'/g, "''") + "')";
        }

        return chunks.map(function(chunk) {
            return "TO_CLOB('" + chunk.replace(/'/g, "''") + "')";
        }).join(' || ');
    }

    function getByteLength(char) {
        var code = char.charCodeAt(0);
        if (code <= 0x7f) return 1;
        if (code <= 0x7ff) return 2;
        if (code <= 0xffff) return 3;
        return 4;
    }

    function downloadFile(content, filename, mimeType) {
        var blob = new Blob(['\ufeff' + content], { type: mimeType }); // BOM 포함
        var link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = filename;
        link.click();
        URL.revokeObjectURL(link.href);
    }

    function copyToClipboard(text) {
        if (navigator.clipboard) {
            navigator.clipboard.writeText(text);
        } else {
            var textarea = document.createElement('textarea');
            textarea.value = text;
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
        }
    }

    // ========================================
    // SQL Log
    // ========================================
    function addSqlLog(sql, type, success, result) {
        var entry = {
            time: new Date().toLocaleTimeString(),
            sql: sql,
            type: type,
            success: success,
            result: result
        };

        sqlLogs.unshift(entry);
        if (sqlLogs.length > 100) {
            sqlLogs.pop();
        }

        renderSqlLogs();
    }

    function renderSqlLogs() {
        var $log = $('#sql-log');
        $log.empty();

        $.each(sqlLogs, function(idx, entry) {
            var statusClass = entry.success ? 'success' : 'error';
            var $entry = $('<div class="sql-log-entry ' + statusClass + '">' +
                '<div class="sql-log-time">[' + entry.time + '] ' + entry.type + '</div>' +
                '<div class="sql-log-sql">' + escapeHtml(entry.sql) + '</div>' +
                '<div class="sql-log-result">' + escapeHtml(entry.result) + '</div>' +
                '</div>');
            $log.append($entry);
        });
    }

    // ========================================
    // 에디터 에러 마커
    // ========================================
    function markEditorError(sql, message) {
        if (!editor) return;

        var model = editor.getModel();
        monaco.editor.setModelMarkers(model, 'sql-error', [{
            startLineNumber: 1,
            startColumn: 1,
            endLineNumber: model.getLineCount(),
            endColumn: model.getLineMaxColumn(model.getLineCount()),
            message: message,
            severity: monaco.MarkerSeverity.Error
        }]);
    }

    function clearEditorErrors() {
        if (!editor) return;
        monaco.editor.setModelMarkers(editor.getModel(), 'sql-error', []);
    }

    // ========================================
    // 하트비트
    // ========================================
    function startHeartbeat() {
        stopHeartbeat();
        heartbeatInterval = setInterval(function() {
            if (!currentConnection.connected) {
                stopHeartbeat();
                return;
            }

            $.ajax({
                url: config.contextPath + '/admin/dbconsole/heartbeat.do',
                type: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({ sessionId: config.sessionId }),
                success: function(result) {
                    if (!result.success) {
                        currentConnection.connected = false;
                        updateConnectionStatus();
                        stopHeartbeat();
                        alert('세션이 만료되었습니다. 다시 연결하세요.');
                    }
                }
            });
        }, HEARTBEAT_INTERVAL);
    }

    function stopHeartbeat() {
        if (heartbeatInterval) {
            clearInterval(heartbeatInterval);
            heartbeatInterval = null;
        }
    }

    // ========================================
    // 유틸리티
    // ========================================
    function showLoading(show) {
        if (show) {
            $('.dbconsole-loading').addClass('show');
        } else {
            $('.dbconsole-loading').removeClass('show');
        }
    }

    function escapeHtml(text) {
        if (!text) return '';
        return text
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    // ========================================
    // localStorage 설정 관리
    // ========================================
    var STORAGE_PREFIX = 'dbconsole_';

    function saveSetting(key, value) {
        try {
            localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
        } catch (e) {
            // localStorage 사용 불가 시 무시
        }
    }

    function loadSetting(key, defaultValue) {
        try {
            var value = localStorage.getItem(STORAGE_PREFIX + key);
            return value !== null ? JSON.parse(value) : defaultValue;
        } catch (e) {
            return defaultValue;
        }
    }

    // 저장된 설정값들을 UI에 적용
    function restoreSettings() {
        // Fetch Count
        var savedFetchCount = loadSetting('fetchCount', '1000');
        $('#fetch-count').val(savedFetchCount);

        // 복호화 모드
        var savedDecryptMode = loadSetting('decryptMode', false);
        $('#chk-decrypt-mode').prop('checked', savedDecryptMode);
        decryptModeEnabled = savedDecryptMode;

        // 데이터소스
        var savedDatasource = loadSetting('datasource', '');
        if (savedDatasource !== '' && config.datasources[savedDatasource]) {
            $('#datasource-select').val(savedDatasource).trigger('change');
        }
    }

    // ========================================
    // Public API
    // ========================================
    return {
        init: init,
        execute: execute,
        executeAll: executeAll,
        commit: commit,
        rollback: rollback,
        connect: connect,
        disconnect: disconnect,
        exportResult: exportResult,
        decryptCell: decryptCell,
        toggleDecryptMode: toggleDecryptMode,
        copySelectedCells: copySelectedCells
    };
})();
