const fs = require('fs')
const path = require('path')
const mariadb = require('mariadb')
require('dotenv').config()

const log = require('./logger')('db')
const { splitStatements } = require('./sqlStatements')

const pool = mariadb.createPool({
  host: process.env.DB_HOST || '127.0.0.1',
  port: Number(process.env.DB_PORT) || 3306,
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || '',
  database: process.env.DB_NAME || 'runic_gateway',
  connectionLimit: 5,
  // Return plain JS numbers, never BigInt — keeps JSON responses clean.
  insertIdAsNumber: true,
  bigIntAsNumber: true,
  decimalAsNumber: true,
  // The driver defaults to 'local' — silently serializing bound JS Date
  // params using the HOST MACHINE's local offset instead of the DB session's
  // timezone (discovered via the Discord bot's temp_roles.expires_at coming
  // back hours off in dev). 'auto' negotiates the actual session timezone so
  // Date round-trips correctly regardless of host TZ — affects any write of
  // a JS Date param, e.g. botConfig.model.js's last_connected_at.
  timezone: 'auto',
})

/**
 * Run a parameterized query and release the connection.
 * @param {string} sql
 * @param {Array} [params]
 */
async function query(sql, params) {
  const conn = await pool.getConnection()
  try {
    return await conn.query(sql, params)
  } finally {
    conn.release()
  }
}

const SCHEMA_PATH = path.join(__dirname, '..', '..', 'db', 'schema.sql')

/**
 * Create tables if they do not exist. Idempotent. Retries while the DB is still
 * coming up (important under docker-compose even with a healthcheck).
 *
 * Once core's schema is in place, every installed module's schema fragment is
 * replayed after it (MODULE_API.md §2.6). That step is deliberately OUTSIDE the
 * retry loop: a fragment that throws is that module's failure, not a signal the
 * database is still coming up, and retrying core's whole schema nine more times
 * because one module shipped bad SQL would turn a 503'd module into a two-minute
 * boot. It is also why this file knows nothing about modules beyond the one call
 * below — the discovery, splitting and per-module failure handling all live in
 * modules/schema.js, required lazily so that requiring the pool never drags the
 * loader in with it.
 *
 * `replayModules: false` is for a caller that has not scanned the volume YET and
 * intends to. server.js is the one: since slice 3 it resolves the declared
 * module set before requiring app.js, which puts core's schema *before* the scan
 * — so it replays the fragments itself, in the one place that knows the scan has
 * happened. Left true everywhere else, so the ordinary caller cannot get module
 * tables by accident and lose them by refactor.
 */
async function ensureSchema({ retries = 10, delayMs = 2000, replayModules = true } = {}) {
  await ensureCoreSchema({ retries, delayMs })
  // eslint-disable-next-line global-require
  if (replayModules) await require('../modules/schema').replayFragments()
}

/** Core's own schema.sql, with the wait-for-the-database retry. */
async function ensureCoreSchema({ retries, delayMs }) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const conn = await pool.getConnection()
      try {
        const sql = fs.readFileSync(SCHEMA_PATH, 'utf8')
        for (const statement of splitStatements(sql)) {
          await conn.query(statement)
        }
        log.info('schema ensured')
        return
      } finally {
        conn.release()
      }
    } catch (err) {
      if (attempt === retries) throw err
      log.warn(`database not ready, retrying (attempt ${attempt}/${retries})`, {
        code: err.code || err.message,
      })
      await new Promise((r) => setTimeout(r, delayMs))
    }
  }
}

// Idempotent: `pool.end()` throws "pool is already closed" on a second call, and
// closing twice is normal rather than exceptional — a SIGINT followed by a
// SIGTERM reaches the shutdown handler twice, and the test harness closes the
// pool for every file on top of the suites that close it themselves. A teardown
// that fails because it had already succeeded is noise.
let closed = false
async function close() {
  if (closed) return
  closed = true
  await pool.end()
}

module.exports = { pool, query, ensureSchema, close }
