class ValidationError extends Error {
  constructor(message, details = []) {
    super(message);
    this.name = "ValidationError";
    this.details = details;
  }
}

function isObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function normalizeString(input, rule = {}) {
  let value = String(input);

  if (rule.trim !== false) {
    value = value.trim();
  }

  if (rule.toLowerCase) {
    value = value.toLowerCase();
  }

  if (rule.toUpperCase) {
    value = value.toUpperCase();
  }

  return value;
}

function validateField(value, key, rule, errors) {
  if (value == null) {
    if (rule.required) {
      errors.push({ field: key, issue: "required" });
    }
    return undefined;
  }

  if (rule.type === "string") {
    const normalized = normalizeString(value, rule);

    if (rule.minLength != null && normalized.length < rule.minLength) {
      errors.push({ field: key, issue: "minLength", expected: rule.minLength });
    }

    if (rule.maxLength != null && normalized.length > rule.maxLength) {
      errors.push({ field: key, issue: "maxLength", expected: rule.maxLength });
    }

    if (rule.pattern && !rule.pattern.test(normalized)) {
      errors.push({ field: key, issue: "pattern" });
    }

    if (rule.enum && !rule.enum.includes(normalized)) {
      errors.push({ field: key, issue: "enum", expected: rule.enum });
    }

    return normalized;
  }

  if (rule.type === "int") {
    const asString = String(value).trim();
    if (!/^-?\d+$/.test(asString)) {
      errors.push({ field: key, issue: "type", expected: "int" });
      return undefined;
    }

    const parsed = Number.parseInt(asString, 10);

    if (!Number.isSafeInteger(parsed)) {
      errors.push({ field: key, issue: "type", expected: "safe-int" });
      return undefined;
    }

    if (rule.min != null && parsed < rule.min) {
      errors.push({ field: key, issue: "min", expected: rule.min });
    }

    if (rule.max != null && parsed > rule.max) {
      errors.push({ field: key, issue: "max", expected: rule.max });
    }

    return parsed;
  }

  errors.push({ field: key, issue: "unsupported-rule" });
  return undefined;
}

function validateObject(payload, schema, locationName) {
  const target = payload == null ? {} : payload;
  const errors = [];

  if (!isObject(target)) {
    throw new ValidationError(`Invalid ${locationName}. Expected an object.`, [
      { field: locationName, issue: "type", expected: "object" },
    ]);
  }

  const output = {};
  const fields = schema.fields || {};
  const allowUnknown = schema.allowUnknown === true;

  if (!allowUnknown) {
    for (const key of Object.keys(target)) {
      if (!Object.prototype.hasOwnProperty.call(fields, key)) {
        errors.push({ field: key, issue: "unknown" });
      }
    }
  }

  for (const [key, rule] of Object.entries(fields)) {
    const validated = validateField(target[key], key, rule, errors);
    if (validated !== undefined) {
      output[key] = validated;
    }
  }

  if (errors.length > 0) {
    throw new ValidationError(`Invalid ${locationName}.`, errors);
  }

  return output;
}

function validateRequest(req, spec = {}) {
  return {
    params: spec.params ? validateObject(req.params, spec.params, "path parameters") : {},
    query: spec.query ? validateObject(req.query, spec.query, "query parameters") : {},
    body: spec.body ? validateObject(req.body, spec.body, "request body") : {},
  };
}

module.exports = {
  ValidationError,
  validateRequest,
};
