/* ---------------------------------------------------------------------------
 * Form validation derived from a Resource's field definitions.
 *
 * Returns a map of field.key → error message. Empty object = valid.
 * Server-side authoritative; the client may show hints, but every POST
 * must call this before writing to the DB.
 * ------------------------------------------------------------------------- */

import type { Field, FieldContext, Resource } from './resource-types';

export type ValidationErrors = Record<string, string>;

export interface ValidateArgs {
  resource: Resource;
  data: Record<string, unknown>;
  /** Existing item being edited, or null on create. */
  item: Record<string, unknown> | null;
  actingUserId: number;
}

export function validateForResource(args: ValidateArgs): ValidationErrors {
  const { resource, data, item, actingUserId } = args;
  const errors: ValidationErrors = {};

  if (!resource.form) return errors;

  const ctx: FieldContext = { formValues: data, item, actingUserId };

  for (const field of resource.form.fields) {
    if (field.readOnly) continue;
    if (field.visibleWhen && !field.visibleWhen(ctx)) continue;

    const value = data[field.key];
    const error = validateField(field, value);
    if (error) errors[field.key] = error;
  }

  return errors;
}

function validateField(field: Field, value: unknown): string | null {
  const isEmpty =
    value === undefined ||
    value === null ||
    value === '' ||
    (Array.isArray(value) && value.length === 0);

  if (field.required && isEmpty) {
    return `${field.label} is required`;
  }
  if (isEmpty) return null;

  switch (field.kind) {
    case 'text':
    case 'textarea':
    case 'markdown': {
      if (typeof value !== 'string') return `${field.label} must be text`;
      if ('maxLength' in field && field.maxLength && value.length > field.maxLength) {
        return `${field.label} must be ${field.maxLength} characters or fewer`;
      }
      if (field.kind === 'text' && field.pattern && !field.pattern.test(value)) {
        return `${field.label} is not in the expected format`;
      }
      return null;
    }

    case 'select':
    case 'select-async': {
      // Empty already handled. Anything else is accepted at the validate layer;
      // option membership is enforced by the route handler against a fresh option list.
      return null;
    }

    case 'multi-select-async': {
      if (!Array.isArray(value)) return `${field.label} must be a list`;
      return null;
    }

    case 'multi-text': {
      if (!Array.isArray(value)) return `${field.label} must be a list`;
      const filled = value.filter(
        (v) => typeof v === 'string' && v.trim() !== '',
      );
      if (field.minItems !== undefined && filled.length < field.minItems) {
        return `${field.label} requires at least ${field.minItems} entries`;
      }
      if (field.maxItems !== undefined && filled.length > field.maxItems) {
        return `${field.label} allows at most ${field.maxItems} entries`;
      }
      return null;
    }

    case 'number': {
      const n = typeof value === 'number' ? value : Number(value);
      if (!Number.isFinite(n)) return `${field.label} must be a number`;
      if (field.min !== undefined && n < field.min) {
        return `${field.label} must be at least ${field.min}`;
      }
      if (field.max !== undefined && n > field.max) {
        return `${field.label} must be no more than ${field.max}`;
      }
      return null;
    }

    case 'date':
    case 'datetime': {
      if (typeof value !== 'string') return `${field.label} must be a date`;
      const t = Date.parse(value);
      if (Number.isNaN(t)) return `${field.label} is not a valid date`;
      return null;
    }

    case 'readonly':
      return null;
  }
}
