'use strict';

/**
 * table-order — Topological sort tabel SDF berdasarkan foreign key (parent→child).
 *
 * `data push` bersifat append (INSERT), sehingga tabel CHILD (pemilik kolom FK)
 * wajib di-push SETELAH tabel PARENT (tabel yang direferensikan), agar INSERT child
 * tidak melanggar FK constraint. Helper ini menurunkan urutan tersebut MURNI dari
 * IR SDF, tanpa I/O maupun koneksi DB.
 *
 * Edge dipungut dari `ir.fields[<col>].fk.table` (arah: parent SEBELUM child). FK
 * target selalu nama tabel polos (shorthand `fk:` tidak mendukung lintas-schema —
 * lihat phase-00-discovery Deliverable 1), jadi pemetaan `fk.table → IR` dilakukan
 * via index `tableName → IR`.
 *
 * Penanganan kasus tepi:
 *   - Self-FK (`fk.table === ir.tableName`)          → edge diabaikan (bukan siklus).
 *   - Target tak dikenal (tak ada di `models`, R2)    → edge diabaikan (jangan crash).
 *   - `tableName` duplikat antar-schema (R1)          → ambiguous; edge yang menunjuk
 *     nama itu diabaikan (no-edge). v2 berasumsi single-namespace.
 *   - Siklus FK (R5)                                  → `cycle:true`; `ordered` =
 *     urutan deklarasi (insertion order Map). Runner mencetak warning + fallback.
 *
 * @module generators/lib/data/table-order
 */

/**
 * Identitas node = qualifiedName (kunci unik Map). `tableName` bisa duplikat antar
 * schema, sehingga tidak aman dipakai sebagai identitas node.
 * @param {Object} ir
 * @returns {string}
 */
function nodeKeyOf(ir) {
  return ir.qualifiedName || ir.tableName;
}

/**
 * Urutkan tabel secara topologis berdasarkan FK (Kahn's algorithm).
 *
 * @param {Map<string, Object>} models - Map<qualifiedName, IR> hasil loadSdf.
 * @returns {{ ordered: Object[], cycle: boolean }} `ordered` = IR parent→child
 *   (atau urutan deklarasi bila siklus); `cycle` = true bila siklus terdeteksi.
 */
function topoSortByFk(models) {
  const irs = [...models.values()];

  // Index tableName → IR. Bila >1 IR berbagi tableName (duplikat antar-schema, R1),
  // nama itu ambiguous: edge yang menargetkannya akan diabaikan (no-edge).
  const byTableName = new Map();
  const ambiguous = new Set();
  for (const ir of irs) {
    const tn = ir.tableName;
    if (byTableName.has(tn)) {
      ambiguous.add(tn);
    } else {
      byTableName.set(tn, ir);
    }
  }

  // Graph: node = nodeKey (qualifiedName). `indegree` = jumlah parent; `children`
  // = parentKey → Set<childKey>. Inisialisasi semua node dengan indegree 0.
  const indegree = new Map();
  const children = new Map();
  for (const ir of irs) {
    indegree.set(nodeKeyOf(ir), 0);
    children.set(nodeKeyOf(ir), new Set());
  }

  // Pungut edge unik (parent → child) dari fk.table tiap field.
  for (const childIr of irs) {
    const childKey = nodeKeyOf(childIr);
    for (const fieldName of Object.keys(childIr.fields)) {
      const field = childIr.fields[fieldName];
      const fk = field && field.fk;
      if (!fk || !fk.table) continue;

      const parentName = fk.table; // nama tabel polos

      // Self-FK → bukan siklus, abaikan edge.
      if (parentName === childIr.tableName) continue;
      // Target tak dikenal (SDF parsial, R2) → abaikan edge.
      if (!byTableName.has(parentName)) continue;
      // Duplikat antar-schema (R1) → ambiguous, perlakukan sebagai no-edge.
      if (ambiguous.has(parentName)) continue;

      const parentKey = nodeKeyOf(byTableName.get(parentName));
      // Self lewat qualifiedName (defensif) → abaikan.
      if (parentKey === childKey) continue;

      // Edge ganda (mis. dua kolom FK ke tabel sama) dihitung sekali.
      if (!children.get(parentKey).has(childKey)) {
        children.get(parentKey).add(childKey);
        indegree.set(childKey, indegree.get(childKey) + 1);
      }
    }
  }

  // Kahn: antrekan node indegree 0 dalam URUTAN DEKLARASI (insertion order Map),
  // proses children juga dalam urutan penyisipan (= urutan deklarasi child), agar
  // hasil deterministik dan stabil untuk tabel tanpa FK.
  const irByKey = new Map(irs.map((ir) => [nodeKeyOf(ir), ir]));
  const queue = [];
  for (const ir of irs) {
    if (indegree.get(nodeKeyOf(ir)) === 0) queue.push(nodeKeyOf(ir));
  }

  const orderedKeys = [];
  while (queue.length > 0) {
    const key = queue.shift();
    orderedKeys.push(key);
    for (const childKey of children.get(key)) {
      const next = indegree.get(childKey) - 1;
      indegree.set(childKey, next);
      if (next === 0) queue.push(childKey);
    }
  }

  // Bila tidak semua node ter-emit, ada siklus (R5): fallback urutan deklarasi.
  if (orderedKeys.length !== irs.length) {
    return { ordered: irs.slice(), cycle: true };
  }

  return { ordered: orderedKeys.map((key) => irByKey.get(key)), cycle: false };
}

module.exports = { topoSortByFk };
