import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';

import oracledb from 'oracledb';
import * as z from 'zod/v4';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const ENV_FILE_PATH = resolveEnvFilePath();

loadEnvFile(ENV_FILE_PATH);

const FETCH_ARRAY_SIZE = parsePositiveInt(process.env.ORACLE_FETCH_ARRAY_SIZE, 100);
const DEFAULT_MAX_ROWS = parsePositiveInt(process.env.ORACLE_DEFAULT_MAX_ROWS, 200);
const HARD_MAX_ROWS = parsePositiveInt(process.env.ORACLE_HARD_MAX_ROWS, 1000);
const CALL_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_CALL_TIMEOUT_MS, 15000);
const POOL_MIN = parseNonNegativeInt(process.env.ORACLE_POOL_MIN, 0);
const POOL_MAX = parsePositiveInt(process.env.ORACLE_POOL_MAX, 4);
const POOL_INCREMENT = parsePositiveInt(process.env.ORACLE_POOL_INCREMENT, 1);
const POOL_TIMEOUT = parsePositiveInt(process.env.ORACLE_POOL_TIMEOUT, 60);
const QUEUE_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_QUEUE_TIMEOUT_MS, 5000);
const STMT_CACHE_SIZE = parsePositiveInt(process.env.ORACLE_STMT_CACHE_SIZE, 30);
const ORACLE_BACKEND = normalizeBackendMode(process.env.ORACLE_BACKEND || 'auto');
const ORACLE_SQLCL_TIMEOUT_MS = parsePositiveInt(process.env.ORACLE_SQLCL_TIMEOUT_MS, CALL_TIMEOUT_MS + 5000);
const ORACLE_SQLCL_PATH = resolveSqlclPath(process.env.ORACLE_SQLCL_PATH);

oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
oracledb.fetchAsString = [oracledb.CLOB];
oracledb.fetchArraySize = FETCH_ARRAY_SIZE;
oracledb.prefetchRows = FETCH_ARRAY_SIZE;

const runtimeConfig = buildRuntimeConfig();
const poolCache = new Map();

function loadEnvFile(filePath) {
  if (!fs.existsSync(filePath)) {
    return;
  }

  const content = fs.readFileSync(filePath, 'utf8');
  const lines = content.split(/\r?\n/);

  for (let index = 0; index < lines.length; index += 1) {
    const rawLine = lines[index];
    const line = rawLine.trim();

    if (!line || line.charAt(0) == '#') {
      continue;
    }

    const separatorIndex = rawLine.indexOf('=');
    if (separatorIndex <= 0) {
      continue;
    }

    const key = rawLine.slice(0, separatorIndex).trim();
    let value = rawLine.slice(separatorIndex + 1).trim();

    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1);
    }

    value = value.replace(/\\n/g, '\n');

    if (process.env[key] === undefined) {
      process.env[key] = value;
    }
  }
}

function resolveEnvFilePath() {
  const explicitPath = String(process.env.MCP_ORACLE_ENV_FILE || process.env.ORACLE_ENV_FILE || '').trim();
  const candidates = [];
  const visitedDirs = new Set();

  if (explicitPath) {
    candidates.push(path.resolve(process.cwd(), explicitPath));
  }

  collectSearchDirectories(process.cwd(), visitedDirs).forEach((searchDir) => {
    candidates.push(path.join(searchDir, '.env'));
    candidates.push(path.join(searchDir, 'mcp-oracle-custom', '.env'));
  });

  candidates.push(path.join(__dirname, '.env'));

  for (let index = 0; index < candidates.length; index += 1) {
    const candidate = String(candidates[index] || '').trim();
    if (candidate && fs.existsSync(candidate)) {
      return candidate;
    }
  }

  return explicitPath ? path.resolve(process.cwd(), explicitPath) : path.join(__dirname, '.env');
}

function collectSearchDirectories(startDir, visitedDirs) {
  const directories = [];
  let currentDir = path.resolve(startDir || process.cwd());

  while (currentDir && !visitedDirs.has(currentDir)) {
    visitedDirs.add(currentDir);
    directories.push(currentDir);

    const parentDir = path.dirname(currentDir);
    if (parentDir == currentDir) {
      break;
    }

    currentDir = parentDir;
  }

  return directories;
}

function parsePositiveInt(value, fallbackValue) {
  const parsed = Number.parseInt(String(value || ''), 10);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return fallbackValue;
  }

  return parsed;
}

function parseNonNegativeInt(value, fallbackValue) {
  const parsed = Number.parseInt(String(value || ''), 10);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return fallbackValue;
  }

  return parsed;
}

function normalizeBackendMode(value) {
  const normalized = String(value || 'auto').trim().toLowerCase();
  if (normalized == 'oracledb' || normalized == 'sqlcl') {
    return normalized;
  }

  return 'auto';
}

function resolveSqlclPath(explicitPath) {
  const candidates = [];
  const normalizedExplicitPath = String(explicitPath || '').trim();

  if (normalizedExplicitPath) {
    candidates.push(normalizedExplicitPath);
  }

  collectVsCodeExtensionDirs().forEach((extensionsDir) => {
    if (!fs.existsSync(extensionsDir)) {
      return;
    }

    const extensionNames = fs.readdirSync(extensionsDir)
      .filter((entry) => /^oracle\.sql-developer-.*-(linux-x64|win32-x64|darwin-(x64|arm64))$/.test(entry))
      .sort()
      .reverse();

    extensionNames.forEach((entry) => {
      candidates.push(path.join(extensionsDir, entry, 'dbtools', 'sqlcl', 'bin', process.platform == 'win32' ? 'sql.exe' : 'sql'));
    });
  });

  const defaultCandidates = [
    '/usr/local/bin/sql',
    '/usr/bin/sql',
    path.join(os.homedir(), '.local', 'bin', 'sql')
  ];

  defaultCandidates.forEach((candidate) => {
    candidates.push(candidate);
  });

  for (let index = 0; index < candidates.length; index += 1) {
    const candidate = String(candidates[index] || '').trim();
    if (candidate && fs.existsSync(candidate)) {
      return candidate;
    }
  }

  return normalizedExplicitPath;
}

function collectVsCodeExtensionDirs() {
  const homeDir = os.homedir();
  const baseDirs = [];
  const uniqueDirs = new Set();

  if (process.env.VSCODE_AGENT_FOLDER) {
    baseDirs.push(process.env.VSCODE_AGENT_FOLDER);
  }

  if (homeDir) {
    baseDirs.push(
      path.join(homeDir, '.vscode-server'),
      path.join(homeDir, '.vscode-server-insiders'),
      path.join(homeDir, '.vscode'),
      path.join(homeDir, '.vscode-insiders')
    );
  }

  return baseDirs.reduce((directories, baseDir) => {
    const normalizedBaseDir = String(baseDir || '').trim();
    if (!normalizedBaseDir) {
      return directories;
    }

    const extensionsDir = path.join(normalizedBaseDir, 'extensions');
    if (!uniqueDirs.has(extensionsDir)) {
      uniqueDirs.add(extensionsDir);
      directories.push(extensionsDir);
    }

    return directories;
  }, []);
}

function isSqlclAvailable() {
  return Boolean(ORACLE_SQLCL_PATH) && fs.existsSync(ORACLE_SQLCL_PATH);
}

function buildRuntimeConfig() {
  const connections = {};
  let defaultConnection = String(process.env.ORACLE_DEFAULT_CONNECTION || '').trim();

  if (process.env.ORACLE_CONNECTIONS_JSON) {
    let jsonConfig;
    try {
      jsonConfig = JSON.parse(process.env.ORACLE_CONNECTIONS_JSON);
    } catch (error) {
      throw new Error('ORACLE_CONNECTIONS_JSON invalido: ' + String(error));
    }

    Object.keys(jsonConfig || {}).forEach((connectionName) => {
      const row = jsonConfig[connectionName] || {};
      const normalized = normalizeConnectionConfig(connectionName, row);
      connections[normalized.name] = normalized;
    });
  }

  if (process.env.ORACLE_USER && process.env.ORACLE_PASSWORD && process.env.ORACLE_CONNECT_STRING) {
    const singleName = String(process.env.ORACLE_CONNECTION_NAME || defaultConnection || 'default').trim();
    const normalized = normalizeConnectionConfig(singleName, {
      user: process.env.ORACLE_USER,
      password: process.env.ORACLE_PASSWORD,
      connectString: process.env.ORACLE_CONNECT_STRING
    });

    connections[normalized.name] = normalized;
    if (!defaultConnection) {
      defaultConnection = normalized.name;
    }
  }

  const connectionNames = Object.keys(connections).sort();
  if (!defaultConnection && connectionNames.length > 0) {
    defaultConnection = connectionNames[0];
  }

  return {
    connections,
    defaultConnection,
    connectionNames
  };
}

function normalizeConnectionConfig(connectionName, config) {
  const normalizedName = String(connectionName || '').trim();
  const user = String(config.user || '').trim();
  const password = String(config.password || '').trim();
  const connectString = String(config.connectString || config.connect_string || '').trim();

  if (!normalizedName || !user || !password || !connectString) {
    throw new Error('Configuracao de conexao invalida para ' + String(connectionName || '<sem nome>') + '.');
  }

  return {
    name: normalizedName,
    user,
    password,
    connectString
  };
}

function getConnectionConfig(connectionName) {
  const requested = String(connectionName || runtimeConfig.defaultConnection || '').trim();

  if (!requested) {
    throw new Error('Nenhuma conexao configurada. Preencha o arquivo .env do MCP Oracle.');
  }

  const config = runtimeConfig.connections[requested];
  if (!config) {
    throw new Error('Conexao nao encontrada: ' + requested + '. Conexoes disponiveis: ' + runtimeConfig.connectionNames.join(', '));
  }

  return config;
}

async function getPool(connectionName) {
  const config = getConnectionConfig(connectionName);

  if (poolCache.has(config.name)) {
    return poolCache.get(config.name);
  }

  const pool = await oracledb.createPool({
    user: config.user,
    password: config.password,
    connectString: config.connectString,
    poolMin: POOL_MIN,
    poolMax: POOL_MAX,
    poolIncrement: POOL_INCREMENT,
    poolTimeout: POOL_TIMEOUT,
    queueTimeout: QUEUE_TIMEOUT_MS,
    stmtCacheSize: STMT_CACHE_SIZE,
    homogeneous: true
  });

  poolCache.set(config.name, pool);
  return pool;
}

async function withConnection(connectionName, callback) {
  const config = getConnectionConfig(connectionName);
  const pool = await getPool(config.name);
  let connection;

  try {
    connection = await pool.getConnection();
    connection.callTimeout = CALL_TIMEOUT_MS;
    return await callback(connection, config);
  } finally {
    if (connection) {
      try {
        await connection.close();
      } catch (error) {
        console.error('Falha ao fechar conexao Oracle:', error);
      }
    }
  }
}

function stripLeadingComments(sql) {
  let cleaned = String(sql || '');
  let changed = true;

  while (changed) {
    changed = false;
    cleaned = cleaned.replace(/^\s+/, '');

    if (cleaned.startsWith('--')) {
      const newLineIndex = cleaned.indexOf('\n');
      cleaned = newLineIndex >= 0 ? cleaned.slice(newLineIndex + 1) : '';
      changed = true;
      continue;
    }

    if (cleaned.startsWith('/*')) {
      const endIndex = cleaned.indexOf('*/');
      cleaned = endIndex >= 0 ? cleaned.slice(endIndex + 2) : '';
      changed = true;
    }
  }

  return cleaned.trim();
}

function validateReadOnlySql(sql) {
  const original = String(sql || '').trim();
  if (!original) {
    throw new Error('Informe um SQL para executar.');
  }

  if (original.indexOf(';') >= 0) {
    throw new Error('Envie apenas uma consulta por vez e sem ponto e virgula.');
  }

  const cleaned = stripLeadingComments(original);
  if (!/^(select|with)\b/i.test(cleaned)) {
    throw new Error('Este MCP aceita apenas consultas de leitura iniciando com SELECT ou WITH.');
  }

  const withoutStrings = cleaned.replace(/'([^']|'')*'/g, "''");
  if (/\b(insert|update|delete|merge|alter|drop|truncate|grant|revoke|commit|rollback|begin|declare|execute|call)\b/i.test(withoutStrings)) {
    throw new Error('Foi detectado um comando nao permitido para este MCP de leitura.');
  }

  return cleaned;
}

function parseBindsJson(bindsJson) {
  if (bindsJson === undefined || bindsJson === null || String(bindsJson).trim() == '') {
    return {};
  }

  let parsed;
  try {
    parsed = JSON.parse(bindsJson);
  } catch (error) {
    throw new Error('bindsJson invalido: ' + String(error));
  }

  if (Array.isArray(parsed)) {
    return parsed;
  }

  if (parsed && typeof parsed == 'object') {
    return parsed;
  }

  throw new Error('bindsJson deve ser um objeto JSON ou um array JSON.');
}

function serializeValue(value) {
  if (value === null || value === undefined) {
    return null;
  }

  if (value instanceof Date) {
    return value.toISOString();
  }

  if (Buffer.isBuffer(value)) {
    return value.toString('base64');
  }

  if (Array.isArray(value)) {
    return value.map((item) => serializeValue(item));
  }

  if (typeof value == 'bigint') {
    return String(value);
  }

  if (typeof value == 'object') {
    const serialized = {};
    Object.keys(value).forEach((key) => {
      serialized[key] = serializeValue(value[key]);
    });
    return serialized;
  }

  return value;
}

function stripAnsi(text) {
  return String(text || '').replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '');
}

function extractJsonPayload(text) {
  const source = stripAnsi(text);
  let startIndex = -1;
  let openingChar = '';

  for (let index = 0; index < source.length; index += 1) {
    const current = source.charAt(index);
    if (current == '{' || current == '[') {
      startIndex = index;
      openingChar = current;
      break;
    }
  }

  if (startIndex < 0) {
    throw new Error('Nao foi possivel localizar o payload JSON retornado pelo SQLcl.');
  }

  const closingChar = openingChar == '{' ? '}' : ']';
  let depth = 0;
  let inString = false;
  let escaped = false;

  for (let index = startIndex; index < source.length; index += 1) {
    const current = source.charAt(index);

    if (inString) {
      if (escaped) {
        escaped = false;
        continue;
      }

      if (current == '\\') {
        escaped = true;
        continue;
      }

      if (current == '"') {
        inString = false;
      }

      continue;
    }

    if (current == '"') {
      inString = true;
      continue;
    }

    if (current == openingChar) {
      depth += 1;
      continue;
    }

    if (current == closingChar) {
      depth -= 1;
      if (depth == 0) {
        return source.slice(startIndex, index + 1);
      }
    }
  }

  throw new Error('O payload JSON retornado pelo SQLcl parece incompleto.');
}

function parseJsonStrictOrLenient(payloadText) {
  const payload = String(payloadText || '').trim();

  try {
    return JSON.parse(payload);
  } catch (strictError) {
    // SQLcl pode retornar JSON quase valido com chaves sem aspas e/ou virgula final.
    const withoutTrailingCommas = payload.replace(/,\s*([}\]])/g, '$1');
    const withQuotedKeys = withoutTrailingCommas.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_$]*)(\s*:)/g, '$1"$2"$3');
    const withNormalizedNumbers = normalizeJsonLocaleNumbers(withQuotedKeys);

    try {
      return JSON.parse(withNormalizedNumbers);
    } catch (lenientError) {
      throw strictError;
    }
  }
}

function isDigitCharacter(value) {
  const code = String(value || '').charCodeAt(0);
  return code >= 48 && code <= 57;
}

function isLooseJsonNumberStart(text, index) {
  const current = text.charAt(index);

  if (isDigitCharacter(current)) {
    return true;
  }

  return (current == '-' || current == '+') && isDigitCharacter(text.charAt(index + 1));
}

function readLooseJsonNumberToken(text, startIndex) {
  let token = '';
  let index = startIndex;

  while (index < text.length) {
    const current = text.charAt(index);
    const next = text.charAt(index + 1);
    const afterNext = text.charAt(index + 2);

    if (isDigitCharacter(current)) {
      token += current;
      index += 1;
      continue;
    }

    if ((current == '+' || current == '-') && token && /[eE]$/.test(token)) {
      token += current;
      index += 1;
      continue;
    }

    if ((current == 'e' || current == 'E') && token && (isDigitCharacter(next) || ((next == '+' || next == '-') && isDigitCharacter(afterNext)))) {
      token += current;
      index += 1;
      continue;
    }

    if ((current == '.' || current == ',') && isDigitCharacter(next)) {
      token += current;
      index += 1;
      continue;
    }

    break;
  }

  return {
    token,
    endIndex: index
  };
}

function normalizeLooseJsonNumberToken(token) {
  let normalized = String(token || '');

  if (normalized.indexOf(',') < 0) {
    return normalized;
  }

  let exponent = '';
  const exponentMatch = normalized.match(/[eE][+-]?\d+$/);
  if (exponentMatch) {
    exponent = exponentMatch[0];
    normalized = normalized.slice(0, -exponent.length);
  }

  let sign = '';
  if (normalized.charAt(0) == '-' || normalized.charAt(0) == '+') {
    sign = normalized.charAt(0);
    normalized = normalized.slice(1);
  }

  if (normalized.indexOf('.') >= 0) {
    normalized = normalized.replace(/\./g, '');
  }

  const lastCommaIndex = normalized.lastIndexOf(',');
  normalized = normalized.slice(0, lastCommaIndex).replace(/,/g, '') + '.' + normalized.slice(lastCommaIndex + 1);

  return sign + normalized + exponent;
}

function normalizeJsonLocaleNumbers(text) {
  let normalized = '';
  let inString = false;
  let escaped = false;

  for (let index = 0; index < text.length; index += 1) {
    const current = text.charAt(index);

    if (inString) {
      normalized += current;

      if (escaped) {
        escaped = false;
        continue;
      }

      if (current == '\\') {
        escaped = true;
        continue;
      }

      if (current == '"') {
        inString = false;
      }

      continue;
    }

    if (current == '"') {
      inString = true;
      normalized += current;
      continue;
    }

    if (isLooseJsonNumberStart(text, index)) {
      const parsed = readLooseJsonNumberToken(text, index);
      normalized += normalizeLooseJsonNumberToken(parsed.token);
      index = parsed.endIndex - 1;
      continue;
    }

    normalized += current;
  }

  return normalized;
}

function ensureSqlclAvailable() {
  if (!isSqlclAvailable()) {
    throw new Error('Fallback SQLcl indisponivel. Ajuste ORACLE_SQLCL_PATH para um executavel sql valido.');
  }
}

function escapeSqlclCredential(value) {
  const normalized = String(value || '');

  if (normalized.indexOf('"') >= 0) {
    throw new Error('O fallback SQLcl nao suporta aspas duplas nas credenciais Oracle.');
  }

  return normalized;
}

function parseSqlclJsonResult(stdout) {
  const payload = parseJsonStrictOrLenient(extractJsonPayload(stdout));
  const firstResult = Array.isArray(payload.results) && payload.results.length > 0 ? payload.results[0] : {};
  const columns = Array.isArray(firstResult.columns) ? firstResult.columns : [];
  const items = Array.isArray(firstResult.items) ? firstResult.items : [];

  return {
    columns,
    items
  };
}

function normalizeSqlclRow(row, columns) {
  const normalized = {};
  const source = row && typeof row == 'object' ? row : {};

  columns.forEach((column) => {
    const columnName = String(column.name || '').trim();
    const lowerCaseName = columnName.toLowerCase();

    if (Object.prototype.hasOwnProperty.call(source, columnName)) {
      normalized[columnName] = serializeValue(source[columnName]);
      return;
    }

    if (Object.prototype.hasOwnProperty.call(source, lowerCaseName)) {
      normalized[columnName] = serializeValue(source[lowerCaseName]);
      return;
    }

    normalized[columnName] = null;
  });

  return normalized;
}

function shouldUseSqlclFallback(error) {
  const message = String(error && error.message ? error.message : error || '');
  return /NJS-116/i.test(message);
}

function runSqlclScript(connectionName, scriptLines) {
  const config = getConnectionConfig(connectionName);

  ensureSqlclAvailable();

  const sqlLines = [
    'WHENEVER OSERROR EXIT 9',
    'WHENEVER SQLERROR EXIT SQL.SQLCODE',
    'connect ' + config.user + '/"' + escapeSqlclCredential(config.password) + '"@' + config.connectString,
    'set pagesize 50000',
    'set feedback off',
    'set verify off',
    'set echo off',
    'set heading off'
  ].concat(scriptLines || [], ['exit', '']);

  return new Promise((resolve, reject) => {
    const child = spawn(ORACLE_SQLCL_PATH, ['-s', '/nolog'], {
      cwd: __dirname,
      stdio: ['pipe', 'pipe', 'pipe']
    });

    let stdout = '';
    let stderr = '';
    let finished = false;

    const timeoutHandle = setTimeout(() => {
      if (!finished) {
        finished = true;
        child.kill('SIGKILL');
        reject(new Error('Tempo limite excedido ao executar SQLcl (' + ORACLE_SQLCL_TIMEOUT_MS + ' ms).'));
      }
    }, ORACLE_SQLCL_TIMEOUT_MS);

    child.stdout.setEncoding('utf8');
    child.stderr.setEncoding('utf8');

    child.stdout.on('data', (chunk) => {
      stdout += chunk;
    });

    child.stderr.on('data', (chunk) => {
      stderr += chunk;
    });

    child.on('error', (error) => {
      if (finished) {
        return;
      }

      finished = true;
      clearTimeout(timeoutHandle);
      reject(error);
    });

    child.on('close', (code) => {
      if (finished) {
        return;
      }

      finished = true;
      clearTimeout(timeoutHandle);

      const stdoutText = stripAnsi(stdout).trim();
      const stderrText = stripAnsi(stderr).trim();

      if (code !== 0) {
        reject(new Error(stderrText || stdoutText || ('SQLcl finalizou com codigo ' + String(code))));
        return;
      }

      resolve({
        config,
        stdout: stdoutText,
        stderr: stderrText
      });
    });

    child.stdin.end(sqlLines.join('\n'));
  });
}

async function runReadOnlyQueryWithOracleDb({ connectionName, sql, bindsJson, maxRows }) {
  const binds = parseBindsJson(bindsJson);

  return withConnection(connectionName, async (connection, config) => {
    const result = await connection.execute(sql, binds, {
      outFormat: oracledb.OUT_FORMAT_OBJECT,
      resultSet: true,
      fetchArraySize: Math.min(FETCH_ARRAY_SIZE, maxRows),
      prefetchRows: Math.min(FETCH_ARRAY_SIZE, maxRows)
    });

    const columns = (result.metaData || []).map((column) => ({
      name: column.name,
      dbTypeName: column.dbTypeName || null,
      nullable: column.nullable,
      precision: column.precision,
      scale: column.scale
    }));

    const rows = [];
    let truncated = false;

    if (result.resultSet) {
      try {
        while (rows.length < maxRows) {
          const batchSize = Math.min(FETCH_ARRAY_SIZE, maxRows - rows.length);
          const batch = await result.resultSet.getRows(batchSize);
          if (!batch || batch.length == 0) {
            break;
          }

          rows.push.apply(rows, batch.map((row) => serializeValue(row)));
        }

        const overflowCheck = await result.resultSet.getRows(1);
        truncated = Array.isArray(overflowCheck) && overflowCheck.length > 0;
      } finally {
        await result.resultSet.close();
      }
    }

    return {
      backend: 'oracledb',
      connection: config.name,
      connectString: config.connectString,
      rowsReturned: rows.length,
      maxRowsApplied: maxRows,
      truncated,
      columns,
      rows
    };
  });
}

async function runReadOnlyQueryWithSqlcl({ connectionName, sql, bindsJson, maxRows }) {
  if (bindsJson !== undefined && bindsJson !== null && String(bindsJson).trim() != '') {
    throw new Error('O fallback SQLcl nao suporta bindsJson. Ajuste ORACLE_BACKEND=oracledb ou remova os binds.');
  }

  const limitedSql = 'SELECT * FROM (\n' + sql + '\n) WHERE ROWNUM <= ' + String(maxRows + 1);
  const sqlclResult = await runSqlclScript(connectionName, [
    'set sqlformat json',
    limitedSql + ';'
  ]);
  const parsed = parseSqlclJsonResult(sqlclResult.stdout);
  const normalizedColumns = parsed.columns.map((column) => ({
    name: column.name,
    dbTypeName: column.type || null,
    nullable: null,
    precision: null,
    scale: null
  }));
  const normalizedRows = parsed.items.map((row) => normalizeSqlclRow(row, normalizedColumns));
  const truncated = normalizedRows.length > maxRows;

  return {
    backend: 'sqlcl',
    connection: sqlclResult.config.name,
    connectString: sqlclResult.config.connectString,
    rowsReturned: Math.min(normalizedRows.length, maxRows),
    maxRowsApplied: maxRows,
    truncated,
    columns: normalizedColumns,
    rows: normalizedRows.slice(0, maxRows)
  };
}

async function testConnectionWithOracleDb(connectionName) {
  return withConnection(connectionName, async (connection, config) => {
    const result = await connection.execute(
      "SELECT USER AS USUARIO, SYS_CONTEXT('USERENV', 'DB_NAME') AS BANCO, SYS_CONTEXT('USERENV', 'INSTANCE_NAME') AS INSTANCIA, TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS') AS DATA_SERVIDOR FROM DUAL",
      {},
      {
        outFormat: oracledb.OUT_FORMAT_OBJECT
      }
    );

    return {
      backend: 'oracledb',
      connection: config.name,
      connectString: config.connectString,
      data: serializeValue((result.rows || [])[0] || {})
    };
  });
}

async function testConnectionWithSqlcl(connectionName) {
  const sqlclResult = await runSqlclScript(connectionName, [
    'set sqlformat json',
    "SELECT USER AS USUARIO, SYS_CONTEXT('USERENV', 'DB_NAME') AS BANCO, SYS_CONTEXT('USERENV', 'INSTANCE_NAME') AS INSTANCIA, TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS') AS DATA_SERVIDOR FROM DUAL;"
  ]);
  const parsed = parseSqlclJsonResult(sqlclResult.stdout);
  const normalizedColumns = parsed.columns.map((column) => ({
    name: column.name,
    dbTypeName: column.type || null,
    nullable: null,
    precision: null,
    scale: null
  }));
  const firstRow = normalizeSqlclRow(parsed.items[0] || {}, normalizedColumns);

  return {
    backend: 'sqlcl',
    connection: sqlclResult.config.name,
    connectString: sqlclResult.config.connectString,
    data: firstRow
  };
}

async function runReadOnlyQuery({ connectionName, sql, bindsJson, maxRows }) {
  const validatedSql = validateReadOnlySql(sql);
  const effectiveMaxRows = Math.min(parsePositiveInt(maxRows, DEFAULT_MAX_ROWS), HARD_MAX_ROWS);

  if (ORACLE_BACKEND == 'sqlcl') {
    return runReadOnlyQueryWithSqlcl({
      connectionName,
      sql: validatedSql,
      bindsJson,
      maxRows: effectiveMaxRows
    });
  }

  if (ORACLE_BACKEND == 'oracledb') {
    return runReadOnlyQueryWithOracleDb({
      connectionName,
      sql: validatedSql,
      bindsJson,
      maxRows: effectiveMaxRows
    });
  }

  try {
    return await runReadOnlyQueryWithOracleDb({
      connectionName,
      sql: validatedSql,
      bindsJson,
      maxRows: effectiveMaxRows
    });
  } catch (error) {
    if (!shouldUseSqlclFallback(error)) {
      throw error;
    }

    return runReadOnlyQueryWithSqlcl({
      connectionName,
      sql: validatedSql,
      bindsJson,
      maxRows: effectiveMaxRows
    });
  }
}

async function testConnection(connectionName) {
  if (ORACLE_BACKEND == 'sqlcl') {
    return testConnectionWithSqlcl(connectionName);
  }

  if (ORACLE_BACKEND == 'oracledb') {
    return testConnectionWithOracleDb(connectionName);
  }

  try {
    return await testConnectionWithOracleDb(connectionName);
  } catch (error) {
    if (!shouldUseSqlclFallback(error)) {
      throw error;
    }

    return testConnectionWithSqlcl(connectionName);
  }
}

async function resetPools() {
  const names = Array.from(poolCache.keys());

  for (let index = 0; index < names.length; index += 1) {
    const name = names[index];
    const pool = poolCache.get(name);
    if (pool) {
      await pool.close(0);
    }
    poolCache.delete(name);
  }

  return {
    closedPools: names.length,
    connections: names
  };
}

function formatTextResult(payload) {
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(payload, null, 2)
      }
    ]
  };
}

function extractOracleErrorSummary(message) {
  const normalized = String(message || '').replace(/\r\n?/g, '\n').trim();
  const oracleMatch = normalized.match(/(?:Erro de SQL:\s*)?(ORA-\d{5}:[^\n]+)/i);

  if (!oracleMatch) {
    return '';
  }

  const summary = [oracleMatch[1].trim()];
  const commandLineMatch = normalized.match(/Erro na Linha de Comandos\s*:\s*([^\n]+)/i);

  if (commandLineMatch) {
    summary.push('Linha/coluna: ' + commandLineMatch[1].trim());
  }

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

function normalizeErrorMessage(error) {
  const message = String(error && error.message ? error.message : error || '').replace(/\r\n?/g, '\n').trim();

  if (!message) {
    return 'Erro nao identificado.';
  }

  const oracleSummary = extractOracleErrorSummary(message);
  if (oracleSummary) {
    return oracleSummary;
  }

  return message;
}

function formatErrorResult(error) {
  return {
    content: [
      {
        type: 'text',
        text: normalizeErrorMessage(error)
      }
    ],
    isError: true
  };
}

const server = new McpServer({
  name: 'oracle-davinti',
  version: '1.0.0'
});

server.registerTool(
  'list_connections',
  {
    title: 'List Oracle Connections',
    description: 'Lista as conexoes Oracle configuradas no arquivo .env do MCP.'
  },
  async () => {
    try {
      return formatTextResult({
        defaultConnection: runtimeConfig.defaultConnection || null,
        connections: runtimeConfig.connectionNames,
        configured: runtimeConfig.connectionNames.length > 0,
        backendMode: ORACLE_BACKEND,
        sqlclAvailable: isSqlclAvailable(),
        sqlclPath: isSqlclAvailable() ? ORACLE_SQLCL_PATH : null,
        hint: runtimeConfig.connectionNames.length > 0 ? null : 'Preencha mcp-oracle-custom/.env antes de usar.'
      });
    } catch (error) {
      return formatErrorResult(error);
    }
  }
);

server.registerTool(
  'test_connection',
  {
    title: 'Test Oracle Connection',
    description: 'Testa a conexao com o banco e retorna usuario, banco e horario do servidor.',
    inputSchema: {
      connectionName: z.string().optional().describe('Nome da conexao configurada no .env. Opcional se houver uma conexao padrao.')
    }
  },
  async ({ connectionName }) => {
    try {
      return formatTextResult(await testConnection(connectionName));
    } catch (error) {
      return formatErrorResult(error);
    }
  }
);

server.registerTool(
  'run_query',
  {
    title: 'Run Read-only Oracle Query',
    description: 'Executa consultas Oracle de leitura com timeout, pool e limite de linhas.',
    inputSchema: {
      sql: z.string().min(1).describe('Consulta Oracle iniciando com SELECT ou WITH.'),
      connectionName: z.string().optional().describe('Nome da conexao configurada no .env. Opcional se houver uma conexao padrao.'),
      bindsJson: z.string().optional().describe('JSON com binds nomeados ou posicionais. Exemplo: {"ParLoja":1219472}'),
      maxRows: z.number().int().positive().optional().describe('Quantidade maxima de linhas retornadas. Respeita o limite rigido configurado no servidor.')
    }
  },
  async ({ sql, connectionName, bindsJson, maxRows }) => {
    try {
      return formatTextResult(await runReadOnlyQuery({
        sql,
        connectionName,
        bindsJson,
        maxRows
      }));
    } catch (error) {
      return formatErrorResult(error);
    }
  }
);

server.registerTool(
  'reset_pools',
  {
    title: 'Reset Oracle Pools',
    description: 'Fecha todos os pools Oracle abertos por este servidor MCP.'
  },
  async () => {
    try {
      return formatTextResult(await resetPools());
    } catch (error) {
      return formatErrorResult(error);
    }
  }
);

async function shutdown() {
  try {
    await resetPools();
  } catch (error) {
    console.error('Falha ao fechar pools Oracle:', error);
  }
}

process.on('SIGINT', async () => {
  await shutdown();
  process.exit(0);
});

process.on('SIGTERM', async () => {
  await shutdown();
  process.exit(0);
});

const transport = new StdioServerTransport();

server.connect(transport).catch(async (error) => {
  console.error('Falha ao iniciar o MCP Oracle:', error);
  await shutdown();
  process.exit(1);
});
