'use strict';

const {
  SOFT_DELETE_COLUMNS,
  SOFT_DELETE_COLUMN_TYPES,
  SOFT_DELETE_SUFFIX_LENGTH
} = require('../soft-delete-constants');

const VALID_RELATION_TYPES = new Set(['belongsTo', 'hasOne', 'hasMany']);
const VALID_REFERENTIAL_ACTIONS = new Set(['cascade', 'restrict', 'setNull', 'noAction']);
const COMPARISON_OPS = new Set(['gt', 'gte', 'lt', 'lte']);
const AUTO_UPDATE_VALID_TYPES = new Set(['timestamp', 'date']);
const TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/;
const AUTO_SUFFIX = '_auto';

// --- Soft-delete (Fase 1) -------------------------------------------------
// Nama kolom (R3), kontrak tipe (R5), dan suffix length (R7) berasal dari modul shared
// `soft-delete-constants` (sumber kebenaran tunggal, dipakai juga oleh emitter DDL Phase 02
// dan jalur reverse introspect Phase 03). Kontrak tipe forward (R5) di validator ini dan
// kontrak tipe reverse di introspect-mapper kini membaca satu konstanta yang sama.
// Reusable hanya didukung untuk tipe string (varchar/text) karena value-mutation (R9).
const REUSABLE_STRING_TYPES = new Set(['string', 'text']);

function validateSchema(ir) {
  validateTableLevel(ir);
  validateSchemaName(ir);
  validatePrimaryKeyReferences(ir);
  validateRelations(ir);
  validateIndexAndUniqueReferences(ir);
  validateChecks(ir);
  validateAutoUpdate(ir);
  validateSoftDelete(ir);
}

function prefix(ir, msg) {
  const label = ir.qualifiedName || ir.tableName;
  return `Table '${label}': ${msg}`;
}

function validateTableLevel(ir) {
  const { tableName, fields, primaryKey, _meta } = ir;

  if (!TABLE_NAME_RE.test(tableName)) {
    throw new Error('Table name must be snake_case (lowercase, alphanumeric, underscore, no leading digit)');
  }

  if (_meta.hasSinglePkShorthand && _meta.hasCompositePrimaryKey) {
    throw new Error(prefix(ir, "cannot use 'pk' shorthand together with 'primaryKey' option"));
  }

  if (!Array.isArray(primaryKey) || primaryKey.length === 0) {
    throw new Error(prefix(ir, "must have a primary key (use 'pk' shorthand or 'primaryKey' option)"));
  }

  // Defensive: parser only allows one `pk` token per field, but multiple fields could carry `pk: true`.
  const pkFields = Object.entries(fields).filter(([, f]) => f.pk === true).map(([n]) => n);
  if (pkFields.length > 1) {
    throw new Error(prefix(ir, `only one field can have 'pk' constraint, found: ${pkFields.join(', ')}`));
  }
}

function validateSchemaName(ir) {
  if (ir.schemaName === null || ir.schemaName === undefined) return;
  if (!TABLE_NAME_RE.test(ir.schemaName)) {
    throw new Error(prefix(ir, `invalid schema name '${ir.schemaName}'`));
  }
}

function validatePrimaryKeyReferences(ir) {
  const { fields, primaryKey } = ir;
  for (const fieldName of primaryKey) {
    if (!fields[fieldName]) {
      throw new Error(prefix(ir, `primaryKey references unknown field '${fieldName}'`));
    }
  }
}

function validateRelations(ir) {
  const { fields, relations, _meta } = ir;

  for (const [relName, rel] of Object.entries(relations)) {
    if (!rel.type || !VALID_RELATION_TYPES.has(rel.type)) {
      throw new Error(prefix(ir, `relation '${relName}' has invalid or missing type '${rel.type}'`));
    }
    if (!rel.localKey || typeof rel.localKey !== 'string') {
      throw new Error(prefix(ir, `relation '${relName}' missing 'localKey' property`));
    }
    if (!rel.references || typeof rel.references !== 'string') {
      throw new Error(prefix(ir, `relation '${relName}' missing 'references' property`));
    }
    if (rel.onDelete !== undefined && rel.onDelete !== null && !VALID_REFERENTIAL_ACTIONS.has(rel.onDelete)) {
      throw new Error(prefix(ir, `relation '${relName}' has invalid onDelete '${rel.onDelete}'`));
    }
    if (rel.onUpdate !== undefined && rel.onUpdate !== null && !VALID_REFERENTIAL_ACTIONS.has(rel.onUpdate)) {
      throw new Error(prefix(ir, `relation '${relName}' has invalid onUpdate '${rel.onUpdate}'`));
    }
    if (!fields[rel.localKey]) {
      throw new Error(prefix(ir, `relation '${relName}' references unknown field '${rel.localKey}'`));
    }
  }

  // Mutual exclusivity: field with `fk:` shorthand cannot also be referenced by an explicit relation's localKey.
  for (const [fieldName, field] of Object.entries(fields)) {
    if (!field.fk) continue;
    for (const [relName, rel] of Object.entries(relations)) {
      if (rel._autoGenerated) continue;
      if (rel.localKey === fieldName) {
        throw new Error(prefix(
          ir,
          `field '${fieldName}' has 'fk:' shorthand and is also referenced by relation '${relName}'. Use only one declaration method.`
        ));
      }
    }
  }

  // Auto-generated relation collision: IR builder appends `_auto` suffix when derived name clashes with explicit relation.
  for (const autoName of _meta.autoGeneratedRelations) {
    if (!autoName.endsWith(AUTO_SUFFIX)) continue;
    const baseName = autoName.slice(0, -AUTO_SUFFIX.length);
    const autoRel = relations[autoName];
    const sourceField = autoRel ? autoRel._sourceField : null;
    throw new Error(prefix(
      ir,
      `auto-generated relation name '${baseName}' from field '${sourceField}' conflicts with explicit relation '${baseName}'. Rename one of them.`
    ));
  }

  // Duplicate localKey across multiple relations.
  const byLocalKey = {};
  for (const [relName, rel] of Object.entries(relations)) {
    if (!rel.localKey) continue;
    if (byLocalKey[rel.localKey]) {
      throw new Error(prefix(
        ir,
        `field '${rel.localKey}' is referenced by multiple relations: ${byLocalKey[rel.localKey]}, ${relName}`
      ));
    }
    byLocalKey[rel.localKey] = relName;
  }
}

function validateIndexAndUniqueReferences(ir) {
  const { fields, indexes, uniques } = ir;

  for (const idx of indexes) {
    for (const col of idx.columns) {
      if (!fields[col]) {
        throw new Error(prefix(ir, `index references unknown field '${col}'`));
      }
    }
  }

  for (const uq of uniques) {
    for (const col of uq.columns) {
      if (!fields[col]) {
        throw new Error(prefix(ir, `unique constraint references unknown field '${col}'`));
      }
    }
  }
}

function validateChecks(ir) {
  const { fields, checks } = ir;

  for (let i = 0; i < checks.length; i++) {
    const c = checks[i];

    if (c.field !== undefined && !fields[c.field]) {
      throw new Error(prefix(ir, `check at index ${i} references unknown field '${c.field}'`));
    }

    if (c._invalid) {
      if (c._reason === 'multiple operators') {
        throw new Error(prefix(
          ir,
          `check at index ${i} has multiple operators (use one of: in, gt, gte, lt, lte, eq, neq)`
        ));
      }
      if (c._reason === 'no operator') {
        throw new Error(prefix(
          ir,
          `check at index ${i} must have exactly one operator (use one of: in, gt, gte, lt, lte, eq, neq)`
        ));
      }
      throw new Error(prefix(ir, `check at index ${i} is invalid: ${c._reason}`));
    }

    if (c.op === 'in') {
      if (!Array.isArray(c.value) || c.value.length === 0) {
        throw new Error(prefix(ir, `check at index ${i} operator 'in' requires non-empty array of values`));
      }
      continue;
    }

    if (COMPARISON_OPS.has(c.op)) {
      if (typeof c.value !== 'number') {
        throw new Error(prefix(
          ir,
          `check at index ${i} operator '${c.op}' requires numeric value, got '${typeof c.value}'`
        ));
      }
    }
  }
}

function validateAutoUpdate(ir) {
  const { fields } = ir;
  for (const [fieldName, field] of Object.entries(fields)) {
    if (!field.autoUpdate) continue;
    if (!AUTO_UPDATE_VALID_TYPES.has(field.type)) {
      throw new Error(prefix(
        ir,
        `field '${fieldName}' constraint 'autoUpdate' only valid for type 'timestamp' or 'date', got '${field.type}'`
      ));
    }
    if (field.default !== undefined) {
      throw new Error(prefix(
        ir,
        `field '${fieldName}' constraint 'autoUpdate' cannot be combined with 'default:'`
      ));
    }
  }
}

// --- Soft-delete invariants (R3, R5, R7, R9) ------------------------------
//
// Predikat tunggal ini dipanggil dari validateSchema, sehingga seluruh command
// schema yang memuat SDF lewat defineModel (validate/migrate/diff/apply) menegakkan
// invariant yang sama tanpa ada jalur yang bisa melewatinya (R4).
//
// Catatan bahasa pesan: mengikuti gaya validator existing yang seluruhnya berbahasa
// Inggris dengan prefix `Table '<name>': ...`. Struktur multi-baris (mis. format
// declared/required pada R7) mengikuti contoh plan master.
function validateSoftDelete(ir) {
  const { fields, softDelete } = ir;

  const enabled = softDelete !== undefined && softDelete !== null && softDelete.enabled === true;
  const presentColumns = SOFT_DELETE_COLUMNS.filter((col) => fields[col] !== undefined);

  // R3 (arah balik): kolom soft-delete ada tetapi enabled bukan true → ERROR.
  if (!enabled) {
    if (presentColumns.length > 0) {
      throw new Error(prefix(
        ir,
        `soft-delete columns ${presentColumns.join(', ')} are declared in 'fields', ` +
        `but 'softDelete.enabled' is not true. ` +
        `Either declare 'softDelete.enabled = true', or remove these columns.`
      ));
    }
    // Tabel normal: tidak ada blok softDelete dan tidak ada kolom soft-delete. OK.
    return;
  }

  // R3 (arah maju): enabled = true tetapi ada kolom yang hilang → ERROR.
  const missing = SOFT_DELETE_COLUMNS.filter((col) => fields[col] === undefined);
  if (missing.length > 0) {
    throw new Error(prefix(
      ir,
      `softDelete.enabled = true requires all soft-delete columns in 'fields', missing: ${missing.join(', ')}. ` +
      `Declare is_deleted (boolean), deleted_at (timestamp), deleted_by (string), or set softDelete.enabled = false.`
    ));
  }

  // R5: validasi tipe kolom (hanya saat soft-delete aktif dan ketiga kolom ada).
  for (const col of SOFT_DELETE_COLUMNS) {
    const expected = SOFT_DELETE_COLUMN_TYPES[col];
    const actual = fields[col].type;
    if (actual !== expected) {
      throw new Error(prefix(
        ir,
        `soft-delete column '${col}' must be type '${expected}', got '${actual}'.`
      ));
    }
  }

  // R7 + R9: validasi tiap field reusable.
  if (softDelete.reusable !== undefined) {
    for (const entry of softDelete.reusable) {
      validateReusableEntry(ir, entry);
    }
  }

  // R21: tolak generate bila ada UNIQUE yang tak punya jalur reusable (composite,
  // atau single-column non-string). Hanya tercapai saat enabled === true.
  validateSoftDeleteUniqueReusability(ir);
}

function validateReusableEntry(ir, entry) {
  const { fields, uniques } = ir;
  const fieldName = entry.field;

  // Entry shape: harus mereferensi field lewat nama.
  if (typeof fieldName !== 'string' || fieldName === '') {
    throw new Error(prefix(
      ir,
      `softDelete.reusable entry must have a 'field' (non-empty string), got '${fieldName}'.`
    ));
  }

  // R9.1: field reusable harus ada di `fields`.
  const field = fields[fieldName];
  if (field === undefined) {
    throw new Error(prefix(
      ir,
      `softDelete.reusable references unknown field '${fieldName}' (not declared in 'fields').`
    ));
  }

  // R7: base length wajib dan harus integer positif.
  const baseLength = entry.length;
  if (baseLength === undefined || baseLength === null) {
    throw new Error(prefix(
      ir,
      `softDelete.reusable field '${fieldName}' is missing required 'length' (the base business-code length).`
    ));
  }
  if (typeof baseLength !== 'number' || !Number.isInteger(baseLength) || baseLength <= 0) {
    throw new Error(prefix(
      ir,
      `softDelete.reusable field '${fieldName}' has invalid 'length' (must be a positive integer), got '${baseLength}'.`
    ));
  }

  // R9.3: reusable harus bertipe string (varchar/text).
  if (!REUSABLE_STRING_TYPES.has(field.type)) {
    throw new Error(prefix(
      ir,
      `softDelete.reusable declares '${fieldName}' as reusable, but its column type is '${field.type}'. ` +
      `Reusable only supports string types (varchar/text) because it uses value-mutation '{code}##{uuidv7}'. ` +
      `Change the column type to string, or remove '${fieldName}' from reusable.`
    ));
  }

  // R9.2: reusable harus punya UNIQUE single-column (composite ditolak).
  const uniqueState = resolveUniqueState(fieldName, field, uniques);
  if (uniqueState === 'none') {
    throw new Error(prefix(
      ir,
      `softDelete.reusable field '${fieldName}' must have a single-column UNIQUE constraint, but none was found.`
    ));
  }
  if (uniqueState === 'composite') {
    throw new Error(prefix(
      ir,
      `softDelete.reusable field '${fieldName}' only participates in a composite UNIQUE constraint. ` +
      `Reusable requires a single-column UNIQUE (a composite UNIQUE is a technical integrity rule, not a reusable business code).`
    ));
  }

  // R7: lebar fisik kolom harus muat base length + suffix.
  // Hanya berlaku untuk `string` (punya length tetap); `text` tidak terbatas panjangnya.
  if (field.type === 'string') {
    const required = baseLength + SOFT_DELETE_SUFFIX_LENGTH;
    if (field.length < required) {
      throw new Error(prefix(
        ir,
        `Reusable unique column '${fieldName}' is too narrow for the soft-delete suffix:\n` +
        `    declared : string:${field.length}\n` +
        `    required : string:${required}   (base ${baseLength} + suffix ${SOFT_DELETE_SUFFIX_LENGTH})`
      ));
    }
  }
}

// Tentukan status UNIQUE sebuah field: 'single' (single-column unique, valid untuk
// reusable), 'composite' (hanya bagian composite unique → ditolak R9.2), atau 'none'.
// Sumber unique: shorthand `unique` (field.unique === true) maupun ir.uniques.
function resolveUniqueState(fieldName, field, uniques) {
  if (field.unique === true) return 'single';
  let hasComposite = false;
  for (const uq of uniques) {
    if (!Array.isArray(uq.columns) || !uq.columns.includes(fieldName)) continue;
    if (uq.columns.length === 1) return 'single';
    hasComposite = true;
  }
  return hasComposite ? 'composite' : 'none';
}

// R21: gate pra-generate untuk soft-delete dengan UNIQUE tak ter-reusable.
//
// Mutasi nilai '{code}##{uuidv7}' yang membebaskan UNIQUE dari bentrok saat re-create
// baris HANYA berlaku untuk UNIQUE single-column bertipe string (R9.2 + R9.3). UNIQUE
// jenis lain tetap PENUH/non-partial (emitter create-index.js R10/R11 sengaja tidak
// mem-partial-kan UNIQUE: uniqueness tetap penuh), sehingga membuat ulang baris dengan
// kunci sama setelah soft-delete akan melanggar UNIQUE tanpa jalur pembebasan. Dua bentuk
// tanpa jalur reusable:
//   (1) composite UNIQUE (lebih dari satu kolom);
//   (2) single-column UNIQUE bertipe non-string (date/number/uuid/...).
// Keduanya ditolak di generate-time. Gate murni JS pada IR → identik untuk semua dialect
// (agnostic, deterministik); tidak menyentuh DDL/index/tipe kolom.
//
// Hanya dipanggil dari validateSoftDelete setelah blok enabled (jadi hanya saat
// softDelete.enabled === true). Throw pada pelanggaran pertama dengan urutan stabil:
// sumber A (shorthand `unique`, urut Object.keys(fields)) lalu sumber B (ir.uniques).
function validateSoftDeleteUniqueReusability(ir) {
  const { fields, uniques } = ir;

  // Sumber A: UNIQUE single-column via shorthand `unique` (field.unique === true).
  for (const fieldName of Object.keys(fields)) {
    const field = fields[fieldName];
    if (field.unique !== true) continue;
    if (!REUSABLE_STRING_TYPES.has(field.type)) {
      throw new Error(softDeleteNonStringUniqueMessage(ir, fieldName, field.type));
    }
  }

  // Sumber B: UNIQUE dari ir.uniques (composite maupun single-column).
  for (const uq of uniques) {
    const columns = Array.isArray(uq.columns) ? uq.columns : [];
    if (columns.length > 1) {
      throw new Error(prefix(
        ir,
        `softDelete.enabled = true, but the table has a composite UNIQUE (${columns.join(', ')}) ` +
        `that cannot be freed by reusable value-mutation on soft-delete. ` +
        `Re-creating a row with the same key after soft-delete would violate this UNIQUE. ` +
        `Use hard-delete for process-driven tables (balances/ledgers/snapshots/period-close), ` +
        `or make the only UNIQUE a single-column string business code.`
      ));
    }
    if (columns.length === 1) {
      const col = columns[0];
      const field = fields[col];
      // Eksistensi kolom sudah dijamin validateIndexAndUniqueReferences; cek defensif.
      if (field !== undefined && !REUSABLE_STRING_TYPES.has(field.type)) {
        throw new Error(softDeleteNonStringUniqueMessage(ir, col, field.type));
      }
    }
  }
}

function softDeleteNonStringUniqueMessage(ir, column, type) {
  return prefix(
    ir,
    `softDelete.enabled = true, but single-column UNIQUE '${column}' is type '${type}', ` +
    `which cannot be freed by reusable value-mutation on soft-delete (reusable requires string/text). ` +
    `Re-creating a row with the same value after soft-delete would violate this UNIQUE. ` +
    `Use hard-delete, or change the unique key to a single-column string business code.`
  );
}

module.exports = { validateSchema };
