'use strict';

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

/**
 * Check constraint compatibility validator. Verifies that:
 *
 *   1. The operator makes sense for the field type
 *      (e.g., `gt` on a boolean field is meaningless).
 *   2. The value (or each `in`-array item) has a JS type compatible with
 *      the field's declared schema type.
 *
 * Assumes single-model `validateChecks` (in `schema-validator.js`) has
 * already enforced: field reference exists, operator is in the allowed
 * set, `in` value is a non-empty array, comparison ops have numeric
 * values. This validator is purely concerned with the field-type ↔
 * check-shape compatibility that those single-model rules don't cover.
 *
 * @param {Map<string, object>} models
 * @returns {Issue[]}
 */
function validateCheckCompatibility(models) {
  const issues = [];

  for (const [, ir] of models) {
    if (!Array.isArray(ir.checks)) continue;

    for (let i = 0; i < ir.checks.length; i++) {
      const check = ir.checks[i];
      if (!check || !check.field) continue;

      const field = ir.fields[check.field];
      if (!field) continue; // schema-validator already reports unknown field

      const opIssues = validateOpFieldCompat(check, field, ir, i);
      for (const issue of opIssues) issues.push(issue);

      // Only run value-type compat if op is structurally compatible with the
      // field type — otherwise the value-type error adds noise on top of the
      // op-type error.
      if (opIssues.length === 0) {
        const valueIssues = validateValueFieldCompat(check, field, ir, i);
        for (const issue of valueIssues) issues.push(issue);
      }
    }
  }

  return issues;
}

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

// Field types that meaningfully support gt/gte/lt/lte. All other types
// (string, text, uuid, boolean, json) only support equality ops.
const COMPARISON_VALID_TYPES = new Set([
  'integer', 'bigint', 'decimal', 'date', 'timestamp'
]);

function validateOpFieldCompat(check, field, ir, index) {
  if (!COMPARISON_OPS.has(check.op)) return [];
  if (COMPARISON_VALID_TYPES.has(field.type)) return [];

  return [decorate({
    severity: 'error',
    code: 'check-op-incompatible-with-type',
    message: `Table '${qualified(ir)}': check at index ${index} operator '${check.op}' is not compatible with field '${check.field}' of type '${field.type}' (comparison ops only valid for numeric / date / timestamp types)`
  }, ir, check.field)];
}

function validateValueFieldCompat(check, field, ir, index) {
  // For `in`: validate each array element. schema-validator already ensures
  // `value` is a non-empty array at this point.
  if (check.op === 'in') {
    const issues = [];
    for (let i = 0; i < check.value.length; i++) {
      const item = check.value[i];
      if (!isValueCompatibleWithType(item, field.type)) {
        issues.push(decorate({
          severity: 'error',
          code: 'check-value-type-mismatch',
          message: `Table '${qualified(ir)}': check at index ${index} value at position ${i} (${describeJsValue(item)}) is not compatible with field '${check.field}' of type '${field.type}'`
        }, ir, check.field));
      }
    }
    return issues;
  }

  // For eq / neq: validate the single value.
  if (check.op === 'eq' || check.op === 'neq') {
    if (!isValueCompatibleWithType(check.value, field.type)) {
      return [decorate({
        severity: 'error',
        code: 'check-value-type-mismatch',
        message: `Table '${qualified(ir)}': check at index ${index} value ${describeJsValue(check.value)} is not compatible with field '${check.field}' of type '${field.type}'`
      }, ir, check.field)];
    }
    return [];
  }

  // For gt / gte / lt / lte: schema-validator already enforced typeof === 'number'.
  // If the field type is comparison-compatible (numeric/date/timestamp), the
  // numeric value is acceptable across all of them, so no further check is
  // needed here.
  return [];
}

/**
 * @param {*} value
 * @param {string} fieldType
 * @returns {boolean}
 */
function isValueCompatibleWithType(value, fieldType) {
  switch (fieldType) {
    case 'string':
    case 'text':
    case 'uuid':
      return typeof value === 'string';

    case 'integer':
    case 'bigint':
      return typeof value === 'number' && Number.isInteger(value);

    case 'decimal':
      return typeof value === 'number';

    case 'boolean':
      return typeof value === 'boolean';

    case 'date':
    case 'timestamp':
      // Accept string (ISO) or number (epoch). Format validation is runtime concern.
      return typeof value === 'string' || typeof value === 'number';

    case 'json':
      // JSON columns accept any serialisable value.
      return true;

    default:
      // Unknown type — be permissive. Field-type validation happens elsewhere.
      return true;
  }
}

function describeJsValue(value) {
  if (value === null) return 'null';
  if (Array.isArray(value)) return 'array';
  const t = typeof value;
  if (t === 'string') return `string '${value}'`;
  if (t === 'number' || t === 'boolean') return `${t} ${value}`;
  return t;
}

function qualified(ir) {
  return ir.qualifiedName || ir.tableName;
}

function decorate(issue, ir, 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;
  return decorated;
}

module.exports = { validateCheckCompatibility };
