'use strict';

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';

function validateSchema(ir) {
  validateTableLevel(ir);
  validateSchemaName(ir);
  validatePrimaryKeyReferences(ir);
  validateRelations(ir);
  validateIndexAndUniqueReferences(ir);
  validateChecks(ir);
  validateAutoUpdate(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:'`
      ));
    }
  }
}

module.exports = { validateSchema };
