import { BadRequestException } from '@nestjs/common';
import { randomBytes } from 'node:crypto';

export function record(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new BadRequestException('Request body must be an object.');
  }
  return value as Record<string, unknown>;
}

export function text(
  value: unknown,
  field: string,
  maxLength: number,
): string {
  if (
    typeof value !== 'string' ||
    value.trim().length === 0 ||
    value.trim().length > maxLength
  ) {
    throw new BadRequestException(`${field} is required (max ${maxLength}).`);
  }
  return value.trim();
}

/** Empty strings and null clear the field. */
export function optionalText(
  value: unknown,
  field: string,
  maxLength: number,
): string | null {
  if (value === undefined || value === null || value === '') return null;
  if (typeof value !== 'string' || value.trim().length > maxLength) {
    throw new BadRequestException(`${field} must be at most ${maxLength} characters.`);
  }
  return value.trim() || null;
}

export function integer(
  value: unknown,
  field: string,
  min: number,
  max = 2_147_483_647,
): number {
  if (
    typeof value !== 'number' ||
    !Number.isInteger(value) ||
    value < min ||
    value > max
  ) {
    throw new BadRequestException(`${field} must be a whole number from ${min} to ${max}.`);
  }
  return value;
}

export function boolean(value: unknown, field: string): boolean {
  if (typeof value !== 'boolean') {
    throw new BadRequestException(`${field} must be true or false.`);
  }
  return value;
}

export function idList(value: unknown, field: string): number[] {
  if (value === undefined || value === null) return [];
  if (
    !Array.isArray(value) ||
    !value.every(
      (id): id is number => typeof id === 'number' && Number.isInteger(id) && id > 0,
    )
  ) {
    throw new BadRequestException(`Choose valid ${field}.`);
  }
  return [...new Set(value)];
}

export function oneOf<T extends string>(
  value: unknown,
  options: readonly T[],
  field: string,
): T {
  if (typeof value !== 'string' || !(options as readonly string[]).includes(value)) {
    throw new BadRequestException(`Choose a valid ${field}.`);
  }
  return value as T;
}

export function date(value: unknown, field: string): Date {
  if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) {
    throw new BadRequestException(`${field} must be a valid date.`);
  }
  return new Date(value);
}

const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function email(value: unknown, field = 'Email'): string {
  const result = text(value, field, 254).toLowerCase();
  if (!EMAIL.test(result)) throw new BadRequestException(`Enter a valid ${field.toLowerCase()}.`);
  return result;
}

export function phone(value: unknown, field = 'Phone number'): string {
  const result = text(value, field, 32);
  if (result.replace(/\D/g, '').length < 7) {
    throw new BadRequestException(`Enter a valid ${field.toLowerCase()}.`);
  }
  return result;
}

/**
 * Site links and uploaded asset paths. Uploads are stored as `/uploads/...`;
 * everything else must be an http(s) URL so a `javascript:` link can never
 * reach the storefront.
 */
export function optionalUrl(value: unknown, field: string): string | null {
  const result = optionalText(value, field, 500);
  if (result === null) return null;
  if (result.startsWith('/') && !result.startsWith('//')) return result;
  try {
    const url = new URL(result);
    if (url.protocol === 'http:' || url.protocol === 'https:') return url.toString();
  } catch {
    // fall through
  }
  throw new BadRequestException(`${field} must be a web address starting with https://.`);
}

export function password(value: unknown): string {
  if (typeof value !== 'string') {
    throw new BadRequestException('Password is required.');
  }
  const bytes = Buffer.byteLength(value, 'utf8');
  if (bytes < 8 || bytes > 4096) {
    throw new BadRequestException('Password must be at least 8 characters.');
  }
  return value;
}

/** Names written only in Bangla have no Latin letters, so they get a generated slug. */
export function slugify(value: string, fallbackPrefix = 'item'): string {
  const slug = value
    .normalize('NFKD')
    .replace(/[̀-ͯ]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
    .slice(0, 160)
    .replace(/-$/, '');
  return slug || `${fallbackPrefix}-${randomBytes(3).toString('hex')}`;
}
