'use strict';

/**
 * value-codec — Encoding nilai canonical ↔ representasi text untuk envelope.
 *
 * Mengacu pada docs/plan/data-pull-push/data-pull-push-03-data-format.md.
 *
 * - `encodeValue`: nilai mentah hasil SELECT (driver) → representasi envelope.
 *   Mayoritas scalar menjadi string ("nilai text") agar portable; kolom `json`
 *   adalah pengecualian (disimpan sebagai nilai JSON nested asli).
 * - `decodeValue`: representasi envelope → nilai bindable untuk driver target,
 *   dibedakan per dialect untuk boolean dan json.
 *
 * Sistem tipe: 10 canonical (shorthand-parser): string, text, integer, bigint,
 * decimal, boolean, date, timestamp, uuid, json.
 *
 * @module generators/lib/data/value-codec
 */

/**
 * Normalisasi argumen dialect menjadi nama kanonik lowercase.
 * Menerima string ('postgres'/'postgresql'/'mysql'/'oracle'/'sqlite') atau
 * instance dialect (punya properti `.name`).
 *
 * @param {string|Object} dialect
 * @returns {string} 'postgresql' | 'mysql' | 'oracle' | 'sqlite' | string apa adanya
 */
function dialectName(dialect) {
  const raw = typeof dialect === 'string' ? dialect : (dialect && dialect.name);
  const d = (raw || '').toLowerCase();
  if (d === 'postgres' || d === 'pg') return 'postgresql';
  return d;
}

/**
 * Format objek Date menjadi 'YYYY-MM-DD' memakai komponen lokal.
 * Driver pg mengembalikan kolom DATE sebagai Date pada tengah malam lokal,
 * sehingga komponen lokal memberi tanggal yang benar tanpa pergeseran zona.
 *
 * @param {Date} d
 * @returns {string}
 */
function formatDateOnly(d) {
  const yyyy = String(d.getFullYear()).padStart(4, '0');
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

/**
 * Normalisasi nilai boolean mentah menjadi 'true' / 'false'.
 * Menangani boolean asli, string ('true'/'false'/'t'/'f', case-insensitive),
 * dan angka (1/0) yang mungkin dikembalikan beberapa driver.
 *
 * @param {*} raw
 * @returns {string} 'true' | 'false'
 */
function normalizeBoolean(raw) {
  if (typeof raw === 'boolean') return raw ? 'true' : 'false';
  if (typeof raw === 'number') return raw !== 0 ? 'true' : 'false';
  if (typeof raw === 'string') {
    const s = raw.trim().toLowerCase();
    if (s === 'true' || s === 't' || s === '1') return 'true';
    if (s === 'false' || s === 'f' || s === '0') return 'false';
  }
  // Fallback: pertahankan determinisme via truthiness JS.
  return raw ? 'true' : 'false';
}

/**
 * Encode nilai mentah dari driver menjadi representasi envelope.
 *
 * @param {*} raw - Nilai hasil SELECT
 * @param {string} canonicalType - Salah satu dari 10 tipe canonical
 * @returns {*} Representasi envelope (string, null, atau nilai JSON nested untuk json)
 */
function encodeValue(raw, canonicalType) {
  if (raw === null || raw === undefined) return null;

  switch (canonicalType) {
    case 'boolean':
      return normalizeBoolean(raw);

    case 'integer':
    case 'bigint':
    case 'decimal':
      // String menjaga presisi (bigint di luar batas Number, scale decimal).
      // bigint JS → String tanpa suffix 'n'.
      return String(raw);

    case 'date':
      if (raw instanceof Date) return formatDateOnly(raw);
      // String dari driver (MySQL dateStrings / Oracle NLS / SQLite text).
      // Ambil porsi tanggal bila ada komponen waktu.
      return String(raw).slice(0, 10);

    case 'timestamp':
      // ISO-8601, zona dipertahankan bila ada. Date JS → toISOString (UTC 'Z').
      if (raw instanceof Date) return raw.toISOString();
      // String dari driver: normalisasi SATU separator tanggal-waktu spasi → 'T'
      // (mis. MySQL dateStrings '2026-01-02 08:00:00' → '2026-01-02T08:00:00').
      // Hanya separator yang diubah; nilai waktu dan zona/offset dipertahankan apa
      // adanya. String yang sudah ber-'T' (tanpa spasi separator) tidak terpengaruh.
      return String(raw).replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/, '$1T$2');

    case 'json':
      // Nilai JSON nested asli (object/array), BUKAN string ter-escape.
      // Driver yang menyimpan JSON sebagai TEXT/CLOB (SQLite/Oracle) mengembalikan
      // string → parse kembali ke nilai nested. PG/MySQL sudah object.
      if (typeof raw === 'string') {
        try {
          return JSON.parse(raw);
        } catch (_e) {
          // String yang bukan JSON valid: pertahankan apa adanya agar tidak
          // menyembunyikan data secara diam-diam.
          return raw;
        }
      }
      return raw;

    case 'uuid':
    case 'string':
    case 'text':
    default:
      return String(raw);
  }
}

/**
 * Decode representasi envelope menjadi nilai bindable untuk driver target.
 *
 * @param {*} text - Nilai dari envelope (string, null, atau nilai JSON nested)
 * @param {string} canonicalType - Salah satu dari 10 tipe canonical
 * @param {string|Object} dialect - Nama dialect atau instance dialect
 * @returns {*} Nilai siap di-bind ke driver
 */
function decodeValue(text, canonicalType, dialect) {
  if (text === null || text === undefined) return null;

  const d = dialectName(dialect);

  switch (canonicalType) {
    case 'boolean': {
      const norm = normalizeBoolean(text);
      // PostgreSQL: kolom boolean native → bind JS boolean.
      // MySQL/Oracle/SQLite: kolom VARCHAR 'true'/'false' → bind string.
      if (d === 'postgresql') return norm === 'true';
      return norm;
    }

    case 'integer':
    case 'bigint':
    case 'decimal':
      // Boleh string sesuai driver; bigint/decimal tetap string menjaga presisi.
      // Driver/DB melakukan cast text → numeric pada kolom target.
      return String(text);

    case 'date':
    case 'timestamp':
      // String; driver/NLS menangani konversi. Oracle via ALTER SESSION NLS.
      return String(text);

    case 'json':
      // Stringify untuk dialect yang menerima JSON sebagai TEXT:
      //   - SQLite/Oracle menyimpan JSON sebagai TEXT/CLOB.
      //   - PostgreSQL: cast text→jsonb. WAJIB string karena driver `pg`
      //     menserialisasi JS array sebagai PostgreSQL array literal ('{...}'),
      //     bukan JSON, sehingga bind array ke kolom JSONB gagal (phase 05b).
      // MySQL: tetap object native (driver men-serialize ke JSON); sudah PASS.
      if (d === 'sqlite' || d === 'oracle' || d === 'postgresql') {
        return JSON.stringify(text);
      }
      return text;

    case 'uuid':
    case 'string':
    case 'text':
    default:
      return String(text);
  }
}

module.exports = {
  encodeValue,
  decodeValue,
  // Diekspor untuk reuse/test internal.
  _internal: { dialectName, formatDateOnly, normalizeBoolean }
};
