'use strict';

/**
 * db-executor — Executor koneksi + transaksi tipis (read-write) per dialect.
 *
 * Membuka koneksi read-write dari config terparse (`dbschema-kit/connection.loadConfig`)
 * dan mengekspos antarmuka kompatibel sebagai `dbUtils` untuk QueryBuilder
 * (`executeQuery`, `executeTransaction`) plus `disconnect`.
 *
 * Berbeda dari `DatabaseIntrospector` (read-only), executor ini:
 * - SQLite dibuka TANPA `readonly` (WAL + foreign_keys = ON).
 * - Oracle memakai ALTER SESSION NLS agar string ISO-8601 ⇒ DATE/TIMESTAMP.
 * - `toBindableParam` direplikasi (boolean→'true'/'false' untuk MySQL/Oracle/SQLite;
 *   SQLite juga undefined→null; PostgreSQL tidak dikoersi).
 *
 * Koneksi dibuka LAZY pada pemakaian pertama sehingga `createExecutor()` bersifat
 * sinkron dan tidak menyentuh DB hingga query benar-benar dijalankan.
 *
 * JANGAN impor `src/utils/db*.js` (singleton env-coupled) — pola di sini direplikasi
 * dari runtime + DatabaseIntrospector (Phase 00b E4). Driver dimuat lazy.
 *
 * @module generators/lib/data/db-executor
 */

// Cache driver module-level (lazy require, pola database-introspector.loadDriver).
let pgDriver = null;
let mysqlDriver = null;
let oracleDriver = null;
let sqliteDriver = null;

/**
 * Lazy-load driver database berdasarkan dialect kanonik.
 * @param {string} dialect - 'postgresql'|'mysql'|'oracle'|'sqlite'
 * @returns {Object} Module driver
 */
function loadDriver(dialect) {
  switch (dialect) {
    case 'postgresql':
      if (!pgDriver) {
        try { pgDriver = require('pg'); } catch (_e) {
          throw new Error('PostgreSQL driver (pg) is not installed. Run: npm install pg');
        }
      }
      return pgDriver;
    case 'mysql':
      if (!mysqlDriver) {
        try { mysqlDriver = require('mysql2/promise'); } catch (_e) {
          throw new Error('MySQL driver (mysql2) is not installed. Run: npm install mysql2');
        }
      }
      return mysqlDriver;
    case 'oracle':
      if (!oracleDriver) {
        try { oracleDriver = require('oracledb'); } catch (_e) {
          throw new Error('Oracle driver (oracledb) is not installed. Run: npm install oracledb');
        }
      }
      return oracleDriver;
    case 'sqlite':
      if (!sqliteDriver) {
        try { sqliteDriver = require('better-sqlite3'); } catch (_e) {
          throw new Error('SQLite driver (better-sqlite3) is not installed. Run: npm rebuild better-sqlite3');
        }
      }
      return sqliteDriver;
    default:
      throw new Error(`Unsupported dialect: ${dialect}. Supported: postgresql, mysql, oracle, sqlite`);
  }
}

/**
 * Normalisasi nilai dialect dari config menjadi nama kanonik.
 * connection.loadConfig memetakan postgresql→'postgres'; di sini dikembalikan
 * 'postgresql' agar konsisten dengan nama dialect QueryBuilder.
 * @param {string} dialect
 * @returns {string} 'postgresql'|'mysql'|'oracle'|'sqlite'
 */
function normalizeDialect(dialect) {
  const d = (dialect || '').toLowerCase();
  if (d === 'postgres' || d === 'pg' || d === 'postgresql') return 'postgresql';
  return d;
}

/**
 * Koersi satu nilai bind ke tipe yang diterima driver target.
 * Direplikasi dari runtime (db-mysql / db-oracle / db-sqlite). PostgreSQL tidak
 * dikoersi karena driver pg menerima JS boolean native.
 *
 * @param {*} value
 * @param {string} dialect - dialect kanonik
 * @returns {*}
 */
function toBindableParam(value, dialect) {
  if (dialect === 'postgresql') return value;
  if (typeof value === 'boolean') return value ? 'true' : 'false';
  if (dialect === 'sqlite' && value === undefined) return null;
  return value;
}

/**
 * Koersi array params.
 * @param {Array} params
 * @param {string} dialect
 * @returns {Array}
 */
function toBindableParams(params, dialect) {
  return Array.isArray(params) ? params.map((p) => toBindableParam(p, dialect)) : params;
}

// Deteksi jenis query (untuk SQLite synchronous .all() vs .run()).
const RE_RETURNING = /\bRETURNING\b/i;
const RE_SELECTISH = /^\s*(SELECT|PRAGMA|EXPLAIN|WITH\s)/i;

// ─── PostgreSQL ───────────────────────────────────────────────────────────────

// OID tipe tanggal/waktu PG yang dikembalikan sebagai STRING mentah (identitas),
// meniru `dateStrings: true` MySQL. Parser default `pg` mem-parse OID ini ke JS
// Date (zona lokal) sehingga `encodeValue` memakai `toISOString()` (UTC) dan pada
// mesin non-UTC menggeser nilai secara asimetris lintas push → round-trip gagal
// (`env2 ≠ env1`). Dengan string mentah, encodeValue masuk cabang string (slice /
// normalisasi separator) tanpa konversi zona → round-trip stabil seperti MySQL.
//   1082 = date, 1114 = timestamp, 1184 = timestamptz
const PG_DATETIME_OIDS = new Set([1082, 1114, 1184]);

/**
 * Bangun objek `types` per-pool untuk opsi `new Pool({ types })`. Mengembalikan
 * parser identitas `(v) => v` untuk OID date/timestamp/timestamptz, dan delegasi
 * ke `pg.types.getTypeParser` default untuk OID lain.
 *
 * Per-pool (BUKAN `pg.types.setTypeParser` global) agar pemakaian `pg` lain di
 * proses yang sama tidak terpengaruh.
 *
 * @param {Object} driver - modul `require('pg')`
 * @returns {{getTypeParser: Function}}
 */
function buildPgTypeParsers(driver) {
  const identity = (v) => v;
  return {
    getTypeParser(oid, format) {
      if (PG_DATETIME_OIDS.has(oid)) return identity;
      return driver.types.getTypeParser(oid, format);
    }
  };
}

function createPgExecutor(config) {
  const driver = loadDriver('postgresql');
  let pool = null;

  function ensurePool() {
    if (!pool) {
      pool = new driver.Pool({
        host: config.host,
        port: config.port,
        user: config.user,
        password: config.password,
        database: config.database,
        // Per-pool type parser: date/timestamp/timestamptz → string mentah.
        types: buildPgTypeParsers(driver)
      });
    }
    return pool;
  }

  async function executeQuery(sql, params = []) {
    const client = await ensurePool().connect();
    try {
      const result = await client.query(sql, toBindableParams(params, 'postgresql'));
      return result.rows;
    } finally {
      client.release();
    }
  }

  async function executeTransaction(queries) {
    const client = await ensurePool().connect();
    try {
      await client.query('BEGIN');
      const results = [];
      for (const q of queries) {
        const result = await client.query(q.sql, toBindableParams(q.params || [], 'postgresql'));
        results.push(result.rows);
      }
      await client.query('COMMIT');
      return results;
    } catch (err) {
      try { await client.query('ROLLBACK'); } catch (_e) { /* swallow rollback error */ }
      throw err;
    } finally {
      client.release();
    }
  }

  async function disconnect() {
    if (pool) { await pool.end(); pool = null; }
  }

  return { executeQuery, executeTransaction, disconnect };
}

// ─── MySQL ────────────────────────────────────────────────────────────────────

function createMysqlExecutor(config) {
  const driver = loadDriver('mysql');
  let pool = null;

  function ensurePool() {
    if (!pool) {
      pool = driver.createPool({
        host: config.host,
        port: config.port,
        user: config.user,
        password: config.password,
        database: config.database,
        waitForConnections: true,
        connectionLimit: 10,
        queueLimit: 0,
        // Opsi penting untuk pull: bigint/decimal sebagai string (presisi),
        // tanggal sebagai string, zona UTC.
        dateStrings: true,
        supportBigNumbers: true,
        bigNumberStrings: true,
        timezone: '+00:00',
        charset: 'utf8mb4'
      });
    }
    return pool;
  }

  function normalizeDml(rows) {
    return {
      rowsAffected: rows.affectedRows,
      insertId: rows.insertId,
      changedRows: rows.changedRows
    };
  }

  async function executeQuery(sql, params = []) {
    const conn = await ensurePool().getConnection();
    try {
      const [rows] = await conn.execute(sql, toBindableParams(params, 'mysql'));
      return Array.isArray(rows) ? rows : normalizeDml(rows);
    } finally {
      conn.release();
    }
  }

  async function executeTransaction(queries) {
    const conn = await ensurePool().getConnection();
    try {
      await conn.beginTransaction();
      const results = [];
      for (const q of queries) {
        const params = q.params || [];
        // execute() (prepared) untuk parameterized; query() untuk SQL tanpa param.
        const [rows] = params.length > 0
          ? await conn.execute(q.sql, toBindableParams(params, 'mysql'))
          : await conn.query(q.sql);
        results.push(Array.isArray(rows) ? rows : normalizeDml(rows));
      }
      await conn.commit();
      return results;
    } catch (err) {
      try { await conn.rollback(); } catch (_e) { /* swallow rollback error */ }
      throw err;
    } finally {
      conn.release();
    }
  }

  async function disconnect() {
    if (pool) { await pool.end(); pool = null; }
  }

  return { executeQuery, executeTransaction, disconnect };
}

// ─── Oracle ─────────────────────────────────────────────────────────────────

function createOracleExecutor(config) {
  const driver = loadDriver('oracle');
  let pool = null;
  const serviceName = config.serviceName || config.database;
  const connectString = `//${config.host}:${config.port}/${serviceName}`;

  async function ensurePool() {
    if (!pool) {
      pool = await driver.createPool({
        user: config.user,
        password: config.password,
        connectString,
        poolMin: 1,
        poolMax: 5,
        poolTimeout: 60
      });
    }
    return pool;
  }

  async function ensureNls(connection) {
    if (connection._nlsReady) return;
    await connection.execute(`ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'`);
    await connection.execute(`ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD"T"HH24:MI:SS.FF3'`);
    await connection.execute(`ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM'`);
    connection._nlsReady = true;
  }

  function normalizeDml(result) {
    return { rowsAffected: result.rowsAffected, lastRowid: result.lastRowid };
  }

  async function executeQuery(sql, params = []) {
    const p = await ensurePool();
    const connection = await p.getConnection();
    try {
      await ensureNls(connection);
      const result = await connection.execute(
        sql,
        toBindableParams(params, 'oracle'),
        { outFormat: driver.OUT_FORMAT_OBJECT, autoCommit: true }
      );
      return result.rows ? result.rows : normalizeDml(result);
    } finally {
      await connection.close();
    }
  }

  async function executeTransaction(queries) {
    const p = await ensurePool();
    const connection = await p.getConnection();
    try {
      await ensureNls(connection);
      const results = [];
      for (const q of queries) {
        const result = await connection.execute(
          q.sql,
          toBindableParams(q.params || [], 'oracle'),
          { autoCommit: false, outFormat: driver.OUT_FORMAT_OBJECT }
        );
        results.push(result.rows ? result.rows : normalizeDml(result));
      }
      await connection.commit();
      return results;
    } catch (err) {
      try { await connection.rollback(); } catch (_e) { /* swallow rollback error */ }
      throw err;
    } finally {
      await connection.close();
    }
  }

  async function disconnect() {
    if (pool) { await pool.close(10); pool = null; }
  }

  return { executeQuery, executeTransaction, disconnect };
}

// ─── SQLite ─────────────────────────────────────────────────────────────────

function createSqliteExecutor(config) {
  const Database = loadDriver('sqlite');
  const filePath = config.file || config.database;
  if (!filePath) {
    throw new Error('SQLite: the database file path is required in config (field "file" or "database")');
  }
  let db = null;

  function ensureDb() {
    if (!db) {
      // READ-WRITE (tanpa readonly) — berbeda dari DatabaseIntrospector.
      db = new Database(filePath);
      db.pragma('journal_mode = WAL');
      db.pragma('foreign_keys = ON');
    }
    return db;
  }

  function runOne(database, sql, params) {
    const stmt = database.prepare(sql);
    const bound = toBindableParams(params, 'sqlite');
    if (RE_SELECTISH.test(sql) || RE_RETURNING.test(sql)) {
      return stmt.all(...bound);
    }
    const info = stmt.run(...bound);
    return { rowsAffected: info.changes, insertId: info.lastInsertRowid };
  }

  async function executeQuery(sql, params = []) {
    return runOne(ensureDb(), sql, params);
  }

  async function executeTransaction(queries) {
    const database = ensureDb();
    // Wrapper better-sqlite3: auto BEGIN/COMMIT, auto ROLLBACK saat throw.
    const txn = database.transaction((queryList) => {
      const results = [];
      for (const q of queryList) {
        results.push(runOne(database, q.sql, q.params || []));
      }
      return results;
    });
    return txn(queries);
  }

  async function disconnect() {
    if (db) { db.close(); db = null; }
  }

  return { executeQuery, executeTransaction, disconnect };
}

/**
 * Buat executor read-write untuk dialect pada config.
 *
 * @param {Object} config - Hasil dbschema-kit/connection.loadConfig:
 *   { dialect, host, port, database, user, password, serviceName, file }
 * @returns {{executeQuery: Function, executeTransaction: Function, disconnect: Function}}
 */
function createExecutor(config) {
  if (!config || typeof config !== 'object') {
    throw new Error('createExecutor: config must be an object produced by loadConfig');
  }
  const dialect = normalizeDialect(config.dialect);
  switch (dialect) {
    case 'postgresql': return createPgExecutor(config);
    case 'mysql': return createMysqlExecutor(config);
    case 'oracle': return createOracleExecutor(config);
    case 'sqlite': return createSqliteExecutor(config);
    default:
      throw new Error(`createExecutor: unsupported dialect '${config.dialect}'`);
  }
}

module.exports = {
  createExecutor,
  toBindableParam,
  toBindableParams,
  _internal: { loadDriver, normalizeDialect, buildPgTypeParsers, PG_DATETIME_OIDS }
};
