'use strict';

/**
 * Sort daftar model berdasarkan FK dependency: parent dulu sebelum child.
 *
 * Menggunakan Kahn's algorithm dengan deterministic tie-break (alphabetical
 * qualifiedName) sehingga output reproducible byte-by-byte.
 *
 * Node ID = qualifiedName (`<schemaName>.<tableName>` atau `tableName` saja
 * bila schemaName null). Edge resolution mengandalkan `rel.target` yang sudah
 * di-resolve oleh cross-model-validator menjadi qualifiedName form.
 *
 * @param {Map<string, object>} models - output loadSchemaFolder() keyed by qualifiedName
 * @returns {string[]} - daftar qualifiedName dalam topological order
 * @throws {Error} bila ada circular dependency
 */
function topologicalSort(models) {
  if (!(models instanceof Map)) {
    throw new Error('topologicalSort: models must be a Map');
  }

  if (models.size === 0) return [];

  const tableNames = Array.from(models.keys());

  // dependents[parent] = Set of child tables that depend on parent
  const dependents = new Map();
  // inDegree[child] = jumlah parent yang dimiliki
  const inDegree = new Map();

  for (const name of tableNames) {
    dependents.set(name, new Set());
    inDegree.set(name, 0);
  }

  for (const tableName of tableNames) {
    const ir = models.get(tableName);
    const relations = ir.relations || {};

    const parents = new Set();
    for (const rel of Object.values(relations)) {
      if (!rel || rel.type !== 'belongsTo') continue;
      const target = rel.target;
      if (typeof target !== 'string') continue;
      // Self-reference: counts as dependency on self → cycle.
      if (!models.has(target)) continue;
      parents.add(target);
    }

    for (const parent of parents) {
      // Only count each parent once even if there are multiple FKs to it
      if (parent === tableName) {
        // Self-loop guarantees a cycle; record it so Kahn detects later
        dependents.get(parent).add(tableName);
        inDegree.set(tableName, inDegree.get(tableName) + 1);
      } else {
        dependents.get(parent).add(tableName);
        inDegree.set(tableName, inDegree.get(tableName) + 1);
      }
    }
  }

  const result = [];
  // Sorted alphabetical seed for determinism
  let queue = tableNames.filter((n) => inDegree.get(n) === 0).sort();

  while (queue.length > 0) {
    const current = queue.shift();
    result.push(current);

    const newlyReady = [];
    const sortedDependents = Array.from(dependents.get(current)).sort();
    for (const child of sortedDependents) {
      const next = inDegree.get(child) - 1;
      inDegree.set(child, next);
      if (next === 0) newlyReady.push(child);
    }

    if (newlyReady.length > 0) {
      queue = queue.concat(newlyReady).sort();
    }
  }

  if (result.length < models.size) {
    const unresolved = tableNames.filter((n) => inDegree.get(n) > 0).sort();
    throw new Error(
      `Circular dependency detected among tables: ${unresolved.join(', ')}`
    );
  }

  return result;
}

module.exports = { topologicalSort };
