import { FieldType, type BlueprintFieldDefinition } from "@nexus/shared";

export interface CustomFieldValidationError {
  key: string;
  message: string;
}

// ReDoS hardening: the blueprint field `pattern` is admin-editable. A
// catastrophic-backtracking pattern like `^(a+)+$` against a crafted input
// can freeze the event loop for multiple seconds per request. We bound the
// attack surface on both axes:
//
// 1. Pattern length capped at 200 chars (see blueprint.schema.ts too).
// 2. Input length capped at 4096 chars before regex.test() — even a bad
//    pattern on a short input completes in < 50 ms.
// 3. A cheap heuristic rejects obvious nested-quantifier shapes at
//    validation time so malicious patterns simply don't match.
const MAX_PATTERN_LENGTH = 200;
const MAX_REGEX_INPUT_LENGTH = 4_096;

// Heuristic: reject grouped subexpressions that contain a quantifier AND
// are themselves wrapped in an outer quantifier — that's the shape of
// every classical ReDoS pattern ((a+)+, (a|a)*, (.*?)+ etc.). This
// over-approximates: it may reject some benign patterns that happen to
// look this way, which is acceptable for admin-side form validation.
export function isSuspectRegexPattern(pattern: string): boolean {
  if (pattern.length > MAX_PATTERN_LENGTH) return true;
  // Match: open paren, any non-close-paren chars containing an unbounded
  // quantifier (+, *, or {n,}), then close paren, then an outer quantifier
  // (+, *, ?, or {).
  const nestedQuantifier = /\([^)]*(?:[+*]|\{\d+,\d*\})[^)]*\)[+*?{]/;
  return nestedQuantifier.test(pattern);
}

export { MAX_PATTERN_LENGTH, MAX_REGEX_INPUT_LENGTH };

/**
 * Validates a `dynamicFields` record against an array of BlueprintFieldDefinitions.
 * Returns an array of errors (empty = valid).
 */
export function validateCustomFields(
  fieldDefs: BlueprintFieldDefinition[],
  dynamicFields: Record<string, unknown>,
): CustomFieldValidationError[] {
  const errors: CustomFieldValidationError[] = [];

  for (const def of fieldDefs) {
    const value = dynamicFields[def.key];
    const isEmpty = value === undefined || value === null || value === "";

    if (def.required && isEmpty) {
      errors.push({ key: def.key, message: `${def.label} is required` });
      continue;
    }

    if (isEmpty) continue;

    switch (def.type) {
      case FieldType.NUMBER: {
        if (typeof value !== "number" && isNaN(Number(value))) {
          errors.push({ key: def.key, message: `${def.label} must be a number` });
        }
        const validation = def.validation;
        if (validation) {
          const num = Number(value);
          if (validation.min !== undefined && num < validation.min) {
            errors.push({
              key: def.key,
              message: `${def.label} must be at least ${validation.min}`,
            });
          }
          if (validation.max !== undefined && num > validation.max) {
            errors.push({
              key: def.key,
              message: `${def.label} must be at most ${validation.max}`,
            });
          }
        }
        break;
      }

      case FieldType.BOOLEAN:
        if (value !== true && value !== false && value !== "true" && value !== "false") {
          errors.push({ key: def.key, message: `${def.label} must be a boolean` });
        }
        break;

      case FieldType.SELECT:
        if (def.options && def.options.length > 0) {
          const valid = def.options.some((o) => o.value === value);
          if (!valid) {
            const allowed = def.options.map((o) => o.label || o.value).join(", ");
            errors.push({ key: def.key, message: `${def.label} must be one of: ${allowed}` });
          }
        }
        break;

      case FieldType.MULTI_SELECT:
        if (Array.isArray(value) && def.options && def.options.length > 0) {
          const validSet = new Set(def.options.map((o) => o.value));
          const invalid = (value as string[]).filter((v) => !validSet.has(v));
          if (invalid.length > 0) {
            errors.push({
              key: def.key,
              message: `${def.label} contains invalid values: ${invalid.join(", ")}`,
            });
          }
        }
        break;

      case FieldType.URL:
        try {
          new URL(String(value));
        } catch {
          errors.push({ key: def.key, message: `${def.label} must be a valid URL` });
        }
        break;

      case FieldType.EMAIL:
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
          errors.push({ key: def.key, message: `${def.label} must be a valid email address` });
        }
        break;

      case FieldType.TEXT:
      case FieldType.TEXTAREA: {
        const strVal = String(value);
        const v = def.validation;
        if (v) {
          if (v.minLength !== undefined && strVal.length < v.minLength) {
            errors.push({
              key: def.key,
              message: v.message ?? `${def.label} must be at least ${v.minLength} characters`,
            });
          }
          if (v.maxLength !== undefined && strVal.length > v.maxLength) {
            errors.push({
              key: def.key,
              message: v.message ?? `${def.label} must be at most ${v.maxLength} characters`,
            });
          }
          if (v.pattern !== undefined) {
            // ReDoS defence: reject suspect patterns OUTRIGHT (counts as
            // validation failure so the admin sees a clear error) and cap
            // the input before regex.test() to bound runtime even if an
            // unsafe pattern somehow slipped through save-time validation.
            if (isSuspectRegexPattern(v.pattern)) {
              errors.push({
                key: def.key,
                message: v.message ?? `${def.label} pattern rejected (unsafe)`,
              });
            } else {
              const capped =
                strVal.length > MAX_REGEX_INPUT_LENGTH
                  ? strVal.slice(0, MAX_REGEX_INPUT_LENGTH)
                  : strVal;
              let matched = false;
              try {
                matched = new RegExp(v.pattern).test(capped);
              } catch {
                // Invalid regex syntax — treat as validation failure.
                matched = false;
              }
              if (!matched) {
                errors.push({
                  key: def.key,
                  message: v.message ?? `${def.label} has an invalid format`,
                });
              }
            }
          }
        }
        break;
      }

      case FieldType.DATE: {
        const dateVal = new Date(String(value));
        if (isNaN(dateVal.getTime())) {
          errors.push({ key: def.key, message: `${def.label} must be a valid date` });
        } else {
          const v = def.validation;
          if (v) {
            if (v.min !== undefined && dateVal.getTime() < new Date(v.min).getTime()) {
              errors.push({
                key: def.key,
                message:
                  v.message ??
                  `${def.label} must not be before ${new Date(v.min).toLocaleDateString()}`,
              });
            }
            if (v.max !== undefined && dateVal.getTime() > new Date(v.max).getTime()) {
              errors.push({
                key: def.key,
                message:
                  v.message ??
                  `${def.label} must not be after ${new Date(v.max).toLocaleDateString()}`,
              });
            }
          }
        }
        break;
      }
    }
  }

  return errors;
}
