'use strict';

const { loadDriver } = require('./driver-loader');

const VALID_DIALECTS = ['postgres', 'mysql', 'oracle', 'sqlite'];
const TRANSACTIONAL = new Set(['postgres', 'sqlite']);

function validateOptions(options) {
  if (!options || typeof options !== 'object') {
    throw new Error('applyStatements: options object is required');
  }
  if (!Array.isArray(options.statements) || options.statements.length === 0) {
    throw new Error('applyStatements: statements must be a non-empty array');
  }
  if (!VALID_DIALECTS.includes(options.dialect)) {
    throw new Error(
      `applyStatements: invalid dialect '${options.dialect}'. ` +
        `Supported: ${VALID_DIALECTS.join(', ')}`
    );
  }
  if (!options.config || typeof options.config !== 'object') {
    throw new Error('applyStatements: config object is required');
  }
}

function emitProgress(onProgress, payload) {
  if (typeof onProgress !== 'function') return;
  try {
    onProgress(payload);
  } catch (_err) {
    // Progress callback errors are intentionally swallowed so a buggy reporter
    // cannot abort the migration mid-way.
  }
}

async function connectPostgres(driver, config) {
  const Client = driver.Client;
  const client = new Client({
    host: config.host,
    port: config.port,
    database: config.database,
    user: config.user,
    password: config.password
  });
  await client.connect();
  return {
    exec: (sql) => client.query(sql),
    close: () => client.end()
  };
}

async function connectMysql(driver, config) {
  const conn = await driver.createConnection({
    host: config.host,
    port: config.port,
    database: config.database,
    user: config.user,
    password: config.password,
    multipleStatements: false
  });
  return {
    exec: async (sql) => {
      try {
        return await conn.query(sql);
      } catch (err) {
        // Idempotensi migrate: MySQL tidak punya `CREATE INDEX IF NOT EXISTS`
        // (lihat statement-modifier.js) sehingga re-run pada index yang sudah
        // ada melempar ER_DUP_KEYNAME (errno 1061). Abaikan khusus untuk
        // CREATE INDEX — setara dengan suppression ORA-00955 di jalur Oracle.
        const isDupIndex = err && (err.errno === 1061 || err.code === 'ER_DUP_KEYNAME');
        if (isDupIndex && /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\b/i.test(sql)) {
          return [{ affectedRows: 0, suppressed: 'ER_DUP_KEYNAME' }];
        }
        throw err;
      }
    },
    close: () => conn.end()
  };
}

async function connectOracle(driver, config) {
  const conn = await driver.getConnection({
    user: config.user,
    password: config.password,
    connectString: `${config.host}:${config.port}/${config.serviceName}`
  });
  return {
    exec: (sql) => conn.execute(sql),
    close: () => conn.close()
  };
}

async function connectSqlite(driver, config) {
  const SqliteCtor = driver;
  const db = new SqliteCtor(config.file);
  return {
    exec: async (sql) => db.exec(sql),
    close: async () => db.close()
  };
}

async function openConnection(dialect, driver, config) {
  switch (dialect) {
    case 'postgres':
      return connectPostgres(driver, config);
    case 'mysql':
      return connectMysql(driver, config);
    case 'oracle':
      return connectOracle(driver, config);
    case 'sqlite':
      return connectSqlite(driver, config);
    default:
      throw new Error(`Unsupported dialect: '${dialect}'`);
  }
}

async function applyStatements(options) {
  validateOptions(options);

  const { statements, dialect, config, onProgress } = options;
  const dryRun = options.dryRun === true;
  const total = statements.length;

  const startedAt = Date.now();

  if (dryRun) {
    return {
      applied: total,
      failed: 0,
      durationMs: Date.now() - startedAt,
      status: 'DRY_RUN'
    };
  }

  const driver = options._driver || loadDriver(dialect);
  const conn = await openConnection(dialect, driver, config);

  let applied = 0;
  const transactional = TRANSACTIONAL.has(dialect);

  try {
    if (transactional) {
      await conn.exec('BEGIN');
    }

    for (let i = 0; i < total; i++) {
      const statement = statements[i];
      const index = i + 1;

      emitProgress(onProgress, { index, total, statement, status: 'running' });

      try {
        await conn.exec(statement);
        applied++;
        emitProgress(onProgress, { index, total, statement, status: 'success' });
      } catch (err) {
        emitProgress(onProgress, { index, total, statement, status: 'error', error: err });

        if (transactional) {
          try {
            await conn.exec('ROLLBACK');
          } catch (_rollbackErr) {
            // Ignore rollback failures: the original error is more useful to the caller.
          }
          const wrapped = new Error(err && err.message ? err.message : String(err));
          wrapped.cause = err;
          wrapped.status = 'ROLLBACK';
          wrapped.applied = applied;
          wrapped.failed = 1;
          wrapped.failedIndex = index;
          wrapped.failedStatement = statement;
          wrapped.durationMs = Date.now() - startedAt;
          throw wrapped;
        }

        return {
          applied,
          failed: 1,
          durationMs: Date.now() - startedAt,
          status: 'PARTIAL',
          failedIndex: index,
          failedStatement: statement,
          error: err
        };
      }
    }

    if (transactional) {
      await conn.exec('COMMIT');
    }

    return {
      applied,
      failed: 0,
      durationMs: Date.now() - startedAt,
      status: 'SUCCESS'
    };
  } finally {
    try {
      await conn.close();
    } catch (_closeErr) {
      // Best-effort close; surfacing this would mask the real failure path.
    }
  }
}

module.exports = { applyStatements };
