'use strict';

/** @typedef {import('./validator-types').Issue} Issue */

/**
 * Type compatibility validator. For every `belongsTo` relation, compares the
 * source localKey field type with the target references field type. Emits:
 *
 *   - `fk-type-mismatch` (error)        when base types differ
 *   - `fk-length-mismatch` (warning)    when both are strings with different lengths
 *   - `fk-precision-mismatch` (warning) when both are decimals with different precision
 *   - `fk-scale-mismatch` (warning)     when both are decimals with different scale
 *
 * MUST run after `validateCrossModel` because it relies on `rel.target` being
 * resolved to a fully-qualified name and on the augmentation that cross-model
 * performs even on partial failures.
 *
 * @param {Map<string, object>} models
 * @returns {Issue[]}
 */
function validateTypeCompatibility(models) {
  const issues = [];

  for (const [sourceQualified, ir] of models) {
    for (const [relName, rel] of Object.entries(ir.relations)) {
      // hasOne / hasMany are inverse relations: the FK column lives on the
      // child table, not on `ir`. Type compatibility for those is implicitly
      // checked when the child's belongsTo is validated.
      if (rel.type !== 'belongsTo') continue;

      const sourceField = ir.fields[rel.localKey];
      if (!sourceField) continue; // cross-model already reports unresolved localKey scenarios indirectly

      const targetQualified = rel.target;
      if (typeof targetQualified !== 'string') continue;

      const targetModel = models.get(targetQualified);
      if (!targetModel) continue; // cross-model already reported unknown-target

      const targetField = targetModel.fields[rel.references];
      if (!targetField) continue; // cross-model already reported unknown-references-field

      const fieldIssues = compareFieldTypes(sourceField, targetField, ir, relName, rel.localKey, sourceQualified);
      for (const issue of fieldIssues) issues.push(issue);
    }
  }

  return issues;
}

/**
 * Compare two field definitions and return any compatibility issues.
 *
 * @param {object} sourceField   Source localKey field def
 * @param {object} targetField   Target references field def
 * @param {object} ir            Source model IR (for decorating Issue.file / table)
 * @param {string} relName
 * @param {string} localKeyName  Source field name (for Issue.field)
 * @param {string} sourceQualified  Source table qualified name (for Issue.message)
 * @returns {Issue[]}
 */
function compareFieldTypes(sourceField, targetField, ir, relName, localKeyName, sourceQualified) {
  const issues = [];

  if (sourceField.type !== targetField.type) {
    issues.push(decorate({
      severity: 'error',
      code: 'fk-type-mismatch',
      message: `Table '${sourceQualified}': relation '${relName}' localKey '${localKeyName}' type '${sourceField.type}' does not match target field type '${targetField.type}'`
    }, ir, relName, localKeyName));
    // Length / precision checks are meaningless when base types already differ.
    return issues;
  }

  if (sourceField.type === 'string' || sourceField.type === 'text') {
    if (definedAndDifferent(sourceField.length, targetField.length)) {
      issues.push(decorate({
        severity: 'warning',
        code: 'fk-length-mismatch',
        message: `Table '${sourceQualified}': relation '${relName}' localKey '${localKeyName}' length ${describeLen(sourceField.length)} does not match target length ${describeLen(targetField.length)}`
      }, ir, relName, localKeyName));
    }
  }

  if (sourceField.type === 'decimal') {
    if (definedAndDifferent(sourceField.precision, targetField.precision)) {
      issues.push(decorate({
        severity: 'warning',
        code: 'fk-precision-mismatch',
        message: `Table '${sourceQualified}': relation '${relName}' localKey '${localKeyName}' precision ${describeLen(sourceField.precision)} does not match target precision ${describeLen(targetField.precision)}`
      }, ir, relName, localKeyName));
    }
    if (definedAndDifferent(sourceField.scale, targetField.scale)) {
      issues.push(decorate({
        severity: 'warning',
        code: 'fk-scale-mismatch',
        message: `Table '${sourceQualified}': relation '${relName}' localKey '${localKeyName}' scale ${describeLen(sourceField.scale)} does not match target scale ${describeLen(targetField.scale)}`
      }, ir, relName, localKeyName));
    }
  }

  return issues;
}

function definedAndDifferent(a, b) {
  // Treat undefined as "no modifier specified". If both sides omit the
  // modifier we consider them equal (no warning). If one specifies and the
  // other does not, that's a meaningful divergence the developer should see.
  if (a === undefined && b === undefined) return false;
  return a !== b;
}

function describeLen(v) {
  return v === undefined ? '(unspecified)' : String(v);
}

function decorate(issue, ir, relName, fieldName) {
  const decorated = {
    severity: issue.severity,
    code: issue.code,
    message: issue.message
  };
  if (ir._filePath) decorated.file = ir._filePath;
  if (ir.qualifiedName) decorated.table = ir.qualifiedName;
  if (fieldName) decorated.field = fieldName;
  if (relName) decorated.relation = relName;
  return decorated;
}

module.exports = { validateTypeCompatibility };
