'use strict';

/**
 * data-scope — Helper bersama untuk scope flags & derivasi path file data.
 *
 * Dipakai oleh `data pull` dan `data push` agar kedua command berbagi satu-satunya
 * sumber kebenaran untuk: (a) parsing `--schema` comma-separated, (b) validasi
 * "tepat satu" dari `{--table, --schema, --all-schemas}`, (c) derivasi path file
 * relatif (bersarang per schema vs flat), dan (d) resolusi scope flag → daftar IR.
 *
 * Model scope (mirror `schema introspect`, namun "tepat satu" alih-alih kombinasi):
 *   --table=<name>        → satu IR via findTableOrThrow (qualified/unqualified).
 *   --schema=<LIST>       → semua IR ber-schemaName ∈ LIST; 0 match = exit 2 (anti-typo).
 *   --all-schemas         → seluruh IR (boleh kosong → caller tangani sebagai 0 tabel).
 *
 * Aturan path seragam (Q2): layout ditentukan MURNI oleh schema tabel di IR.
 *   ber-schema  → `<schemaName>/<tableName>.json`
 *   tanpa schema → `<tableName>.json` (flat root)
 *
 * Murni in-memory, tanpa I/O maupun koneksi DB.
 *
 * @module generators/lib/data/data-scope
 */

const path = require('path');
const { findTableOrThrow, getNamespace } = require('./sdf-reader');

/**
 * Buat Error dengan exitCode terlampir (precondition/usage = 2).
 * @param {string} message
 * @param {number} exitCode
 * @returns {Error}
 */
function failWith(message, exitCode) {
    const err = new Error(message);
    err.exitCode = exitCode;
    return err;
}

/**
 * Parse flag `--schema` (single atau comma-separated) → array nama schema.
 *
 * Selaras pola `schema introspect` (introspect.js:175-180): split koma, trim tiap
 * elemen, buang elemen kosong. Mengembalikan `null` bila bukan string atau hasil
 * akhir kosong (mis. `--schema=` atau `--schema=,,`), sehingga caller dapat
 * memperlakukan "schema tidak disebut" secara seragam.
 *
 * @param {*} value - Nilai flag --schema
 * @returns {string[]|null}
 */
function parseSchemaFlag(value) {
    if (typeof value !== 'string') return null;
    const parts = value.split(',').map((s) => s.trim()).filter(Boolean);
    if (parts.length === 0) return null;
    return parts;
}

/**
 * Path file data relatif terhadap storage dir, diturunkan MURNI dari schema IR.
 *
 * Satu-satunya titik derivasi nama file untuk pull & push (menghindari drift tiga
 * titik edit lama). Tabel ber-schema → bersarang `<schemaName>/<tableName>.json`;
 * tabel tanpa schema → flat `<tableName>.json`. Pemanggil cukup
 * `path.join(storageDir, relDataFilePath(ir))`.
 *
 * @param {Object} ir - IR model SDF satu tabel
 * @returns {string} Path relatif (memakai separator OS via path.join)
 */
function relDataFilePath(ir) {
    const { schemaName } = getNamespace(ir);
    return schemaName
        ? path.join(schemaName, `${ir.tableName}.json`)
        : `${ir.tableName}.json`;
}

/**
 * Tegakkan "tepat satu" dari `{--table, --schema, --all-schemas}`.
 *
 * Arg-parser tidak punya mekanisme XOR lintas-flag (hanya `required` per-flag),
 * sehingga validasi tiga-arah ditegakkan di sini. Dipanggil SEBELUM load SDF /
 * koneksi DB. Keduanya/lebih maupun tidak ada satupun → exit 2 dengan pesan sama.
 *
 * @param {Object} args - argumen ter-parse (args.table, args['all-schemas'])
 * @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
 * @throws {Error} exitCode=2 bila BUKAN tepat satu yang aktif
 */
function validateScopeFlags(args, schemaList) {
    const hasTable = args.table != null && args.table !== '';
    const hasSchema = Array.isArray(schemaList) && schemaList.length > 0;
    const hasAll = args['all-schemas'] === true;

    const count = (hasTable ? 1 : 0) + (hasSchema ? 1 : 0) + (hasAll ? 1 : 0);
    if (count !== 1) {
        throw failWith('Specify exactly one of --table, --schema, or --all-schemas.', 2);
    }
}

/**
 * Resolusi scope flag → daftar IR yang akan diproses (urutan deklarasi Map).
 *
 * Mengasumsikan validateScopeFlags() sudah lolos (tepat satu flag aktif).
 *   --table        → [findTableOrThrow(models, args.table)] (exit 2 bila tak terdaftar/ambigu).
 *   --schema=<L>   → subset IR ber-schemaName ∈ L; 0 match → exit 2 (sinyal typo).
 *   --all-schemas  → seluruh IR (boleh kosong → caller perlakukan sebagai 0 tabel, exit 0).
 *
 * @param {Map<string, Object>} models - hasil loadSdf
 * @param {Object} args - argumen ter-parse
 * @param {string[]|null} schemaList - hasil parseSchemaFlag(args.schema)
 * @returns {Object[]} array IR sesuai scope
 * @throws {Error} exitCode=2 (table tak terdaftar/ambigu, atau --schema 0-match)
 */
function resolveScope(models, args, schemaList) {
    const hasTable = args.table != null && args.table !== '';
    if (hasTable) {
        return [findTableOrThrow(models, args.table)];
    }

    if (Array.isArray(schemaList) && schemaList.length > 0) {
        const set = new Set(schemaList);
        const subset = [...models.values()].filter((ir) => set.has(getNamespace(ir).schemaName));
        if (subset.length === 0) {
            throw failWith(`No tables found for schema(s): ${schemaList.join(', ')}.`, 2);
        }
        return subset;
    }

    // --all-schemas: seluruh tabel (semua schema, termasuk schemaless). Boleh kosong.
    return [...models.values()];
}

module.exports = {
    parseSchemaFlag,
    relDataFilePath,
    validateScopeFlags,
    resolveScope,
    // Diekspor untuk reuse/test internal.
    _internal: { failWith }
};
