'use strict';

const { parseFieldShorthand } = require('./parser/shorthand-parser');

const VALID_CHECK_OPS = new Set(['in', 'gt', 'gte', 'lt', 'lte', 'eq', 'neq']);

// Soft-delete (Fase 1) — blok ini sengaja ketat (R1): key tak dikenal ditolak,
// bukan di-drop diam-diam. Whitelist khusus blok `softDelete`; tidak mengubah
// keleluasaan blok SDF lain (relations/checks/indexes tetap lenient).
const SOFT_DELETE_ALLOWED_KEYS = new Set(['enabled', 'reusable']);
const SOFT_DELETE_REUSABLE_ALLOWED_KEYS = new Set(['field', 'length']);

// Token boundary detection: autoUpdate must stand alone, not part of `default:autoUpdate` or `autoUpdated`
const AUTO_UPDATE_TOKEN_RE = /(?:^|\s)autoUpdate(?=\s|$)/;
const AUTO_UPDATE_TOKEN_RE_G = /(?:^|\s)autoUpdate(?=\s|$)/g;

function buildIR(tableName, options) {
  validateInputs(tableName, options);

  const schemaName = options.schema !== undefined && options.schema !== null ? options.schema : null;
  const qualifiedName = schemaName ? `${schemaName}.${tableName}` : tableName;

  const fields = {};
  for (const [fieldName, shorthand] of Object.entries(options.fields)) {
    fields[fieldName] = parseField(shorthand, fieldName, tableName);
  }

  const hasSinglePkShorthand = Object.values(fields).some((f) => f.pk === true);
  const hasCompositePrimaryKey = Array.isArray(options.primaryKey) && options.primaryKey.length > 0;

  const primaryKey = resolvePrimaryKey(options.primaryKey, fields, hasSinglePkShorthand, hasCompositePrimaryKey);

  const relations = {};
  if (options.relations) {
    for (const [relName, relConfig] of Object.entries(options.relations)) {
      relations[relName] = normalizeRelation(relName, relConfig);
    }
  }

  const autoGeneratedRelations = [];
  for (const [fieldName, field] of Object.entries(fields)) {
    if (!field.fk) continue;
    const autoEntry = autoPromoteFk(fieldName, field.fk, relations);
    relations[autoEntry.name] = autoEntry.relation;
    autoGeneratedRelations.push(autoEntry.name);
  }

  const indexFromFk = collectFkIndexFields(fields, relations);

  const indexes = normalizeIndexes(options.indexes, indexFromFk);
  const uniques = normalizeUniques(options.uniques);
  const checks = normalizeChecks(options.checks);
  const softDelete = normalizeSoftDelete(options.softDelete);

  const ir = {
    schemaName,
    tableName,
    qualifiedName,
    fields,
    primaryKey,
    indexes,
    uniques,
    relations,
    checks,
    _meta: {
      hasSinglePkShorthand,
      hasCompositePrimaryKey,
      autoGeneratedRelations
    }
  };

  // Optional block: only attach when declared so SDF tanpa softDelete tetap byte-identik
  // dengan IR baseline (backward-compat, R1 anti-magic — tidak menambah key diam-diam).
  if (softDelete !== undefined) {
    ir.softDelete = softDelete;
  }

  return ir;
}

function validateInputs(tableName, options) {
  if (typeof tableName !== 'string' || tableName === '') {
    throw new Error('Table name must be a non-empty string');
  }
  if (!isPlainObject(options)) {
    throw new Error(`Table '${tableName}': options must be a plain object`);
  }
  if (!isPlainObject(options.fields)) {
    throw new Error(`Table '${tableName}': options.fields must be a plain object`);
  }
  if (Object.keys(options.fields).length === 0) {
    throw new Error(`Table '${tableName}': options.fields must have at least one field`);
  }
  if (options.indexes !== undefined && !Array.isArray(options.indexes)) {
    throw new Error(`Table '${tableName}': options.indexes must be an array`);
  }
  if (options.uniques !== undefined && !Array.isArray(options.uniques)) {
    throw new Error(`Table '${tableName}': options.uniques must be an array`);
  }
  if (options.primaryKey !== undefined && !Array.isArray(options.primaryKey)) {
    throw new Error(`Table '${tableName}': options.primaryKey must be an array`);
  }
  if (options.relations !== undefined && !isPlainObject(options.relations)) {
    throw new Error(`Table '${tableName}': options.relations must be a plain object`);
  }
  if (options.checks !== undefined && !Array.isArray(options.checks)) {
    throw new Error(`Table '${tableName}': options.checks must be an array`);
  }
  if (options.schema !== undefined && options.schema !== null && typeof options.schema !== 'string') {
    throw new Error(`Table '${tableName}': options.schema must be a string or null`);
  }
  if (options.softDelete !== undefined) {
    if (!isPlainObject(options.softDelete)) {
      throw new Error(`Table '${tableName}': options.softDelete must be a plain object`);
    }
    if (typeof options.softDelete.enabled !== 'boolean') {
      throw new Error(`Table '${tableName}': options.softDelete.enabled must be a boolean`);
    }
    // R1 (strict): tolak key tak dikenal di level blok `softDelete` (selain whitelist),
    // alih-alih membiarkan normalizeSoftDelete membuangnya diam-diam.
    for (const key of Object.keys(options.softDelete)) {
      if (!SOFT_DELETE_ALLOWED_KEYS.has(key)) {
        throw new Error(unknownSoftDeleteKeyError(tableName, key));
      }
    }
    if (options.softDelete.reusable !== undefined) {
      if (!Array.isArray(options.softDelete.reusable)) {
        throw new Error(`Table '${tableName}': options.softDelete.reusable must be an array`);
      }
      for (let i = 0; i < options.softDelete.reusable.length; i++) {
        const entry = options.softDelete.reusable[i];
        if (!isPlainObject(entry)) {
          throw new Error(`Table '${tableName}': options.softDelete.reusable[${i}] must be a plain object`);
        }
        // R1 (strict): tiap entri reusable hanya boleh `field` dan `length`.
        for (const key of Object.keys(entry)) {
          if (!SOFT_DELETE_REUSABLE_ALLOWED_KEYS.has(key)) {
            throw new Error(
              `Table '${tableName}': options.softDelete.reusable[${i}] has unknown key '${key}'; ` +
              `only 'field' and 'length' are allowed.`
            );
          }
        }
      }
    }
  }
}

function parseField(shorthand, fieldName, tableName) {
  let cleaned = shorthand;
  let hasAutoUpdate = false;

  // Pre-process: strip `autoUpdate` token (parser Phase 1 belum mengenal token ini, sesuai aturan jangan modifikasi parser)
  if (typeof shorthand === 'string' && AUTO_UPDATE_TOKEN_RE.test(shorthand)) {
    hasAutoUpdate = true;
    cleaned = shorthand.replace(AUTO_UPDATE_TOKEN_RE_G, ' ').replace(/\s+/g, ' ').trim();
  }

  let parsed;
  try {
    parsed = parseFieldShorthand(cleaned, fieldName);
  } catch (err) {
    err.message = `Table '${tableName}': ${err.message}`;
    throw err;
  }

  if (hasAutoUpdate) {
    parsed.autoUpdate = true;
  }

  return parsed;
}

function resolvePrimaryKey(optionPk, fields, hasSinglePkShorthand, hasCompositePrimaryKey) {
  let primaryKey = [];

  if (hasCompositePrimaryKey) {
    primaryKey = optionPk.slice();
    for (const pkField of optionPk) {
      if (fields[pkField]) fields[pkField].notnull = true;
    }
  }

  if (hasSinglePkShorthand) {
    for (const [fieldName, field] of Object.entries(fields)) {
      if (field.pk !== true) continue;
      if (!hasCompositePrimaryKey) primaryKey.push(fieldName);
      field.notnull = true;
    }
  }

  return primaryKey;
}

function normalizeRelation(relName, relConfig) {
  return {
    type: relConfig.type,
    target: relConfig.target ?? relName,
    localKey: relConfig.localKey,
    references: relConfig.references,
    onDelete: relConfig.onDelete,
    onUpdate: relConfig.onUpdate,
    _autoGenerated: false,
    _sourceField: null
  };
}

function autoPromoteFk(fieldName, fk, existingRelations) {
  // Auto-name collision strategy: keep auto entry with `_auto` suffix so validator dapat deteksi conflict
  let name = deriveRelationName(fieldName, fk.table);
  if (existingRelations[name] && existingRelations[name]._autoGenerated === false) {
    name = `${name}_auto`;
  }
  return {
    name,
    relation: {
      type: 'belongsTo',
      target: fk.table,
      localKey: fieldName,
      references: fk.column,
      _autoGenerated: true,
      _sourceField: fieldName
    }
  };
}

function deriveRelationName(fieldName, targetTable) {
  if (fieldName.endsWith('_id')) {
    return fieldName.slice(0, -3);
  }
  return `${fieldName}_${targetTable}`;
}

function collectFkIndexFields(fields, relations) {
  const result = new Set();

  for (const [fieldName, field] of Object.entries(fields)) {
    if (field.fk) {
      field.index = true;
      result.add(fieldName);
    }
  }

  for (const rel of Object.values(relations)) {
    if (rel.type !== 'belongsTo') continue;
    if (!rel.localKey) continue;
    if (fields[rel.localKey]) fields[rel.localKey].index = true;
    result.add(rel.localKey);
  }

  return result;
}

function normalizeIndexes(input, fkFields) {
  const result = [];
  const seen = new Set();

  if (Array.isArray(input)) {
    for (const entry of input) {
      let columns;
      if (typeof entry === 'string') {
        columns = [entry];
      } else if (Array.isArray(entry)) {
        columns = entry.slice();
      } else {
        continue;
      }
      const key = columns.join('|');
      if (seen.has(key)) continue;
      seen.add(key);
      result.push({ columns, composite: columns.length > 1 });
    }
  }

  for (const fkField of fkFields) {
    if (seen.has(fkField)) continue;
    seen.add(fkField);
    result.push({ columns: [fkField], composite: false });
  }

  return result;
}

function normalizeUniques(input) {
  if (!Array.isArray(input)) return [];
  const result = [];
  for (const entry of input) {
    if (Array.isArray(entry)) {
      result.push({ columns: entry.slice() });
    }
  }
  return result;
}

function normalizeChecks(input) {
  if (!Array.isArray(input)) return [];
  const result = [];
  for (const entry of input) {
    if (!isPlainObject(entry)) {
      result.push({ _invalid: true, _reason: 'check entry is not a plain object' });
      continue;
    }

    const operators = Object.keys(entry).filter((k) => VALID_CHECK_OPS.has(k));

    if (operators.length === 0) {
      const out = { field: entry.field, _invalid: true, _reason: 'no operator' };
      if (entry.name !== undefined) out.name = entry.name;
      result.push(out);
      continue;
    }

    if (operators.length > 1) {
      const out = { field: entry.field, _invalid: true, _reason: 'multiple operators' };
      if (entry.name !== undefined) out.name = entry.name;
      result.push(out);
      continue;
    }

    const op = operators[0];
    const out = { field: entry.field, op, value: entry[op] };
    if (entry.name !== undefined) out.name = entry.name;
    result.push(out);
  }
  return result;
}

// Pesan ERROR unknown-key di level blok `softDelete`. Untuk `visibility` diberi
// petunjuk khusus: itu config endpoint di RDF/payload (R13/R15), bukan SDF, supaya
// developer memindahkannya, bukan sekadar menghapus.
function unknownSoftDeleteKeyError(tableName, key) {
  let msg =
    `Table '${tableName}': options.softDelete has unknown key '${key}'; ` +
    `only 'enabled' and 'reusable' are allowed.`;
  if (key === 'visibility') {
    msg +=
      ` 'visibility' is an endpoint configuration declared in the RDF/payload ` +
      `(one of active_only/deleted_only/include_deleted, see R13/R15), not in the SDF; ` +
      `move it to the payload's softDelete block instead of declaring it here.`;
  }
  return msg;
}

function normalizeSoftDelete(input) {
  if (input === undefined) return undefined;
  // validateInputs sudah menjamin: input plain object, enabled boolean,
  // reusable (bila ada) array of object, DAN tidak ada key tak dikenal — baik di
  // level blok (hanya enabled/reusable) maupun di tiap entri reusable (hanya
  // field/length). Jadi key seperti `visibility` (R13/R15, milik RDF) sudah ditolak
  // ERROR di validateInputs, bukan di-drop diam-diam di sini (R1, anti-magic).
  // Di sini hanya normalisasi minimal: simpan enabled apa adanya, dan proyeksikan
  // tiap entri reusable ke { field, length }. Validasi semantik (kolom ada, tipe,
  // unique, panjang) ditegakkan di validateSoftDelete.
  const out = { enabled: input.enabled };
  if (input.reusable !== undefined) {
    out.reusable = input.reusable.map((entry) => ({ field: entry.field, length: entry.length }));
  }
  return out;
}

function isPlainObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

module.exports = { buildIR };
