export interface CreatePasswordOptions {
  length: number;
  numbers?: boolean;
  symbols?: boolean | string;
  lowercase?: boolean;
  uppercase?: boolean;
  exclude?: string;
  strict?: boolean;
}

const LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz';
const UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const NUMBER_CHARS = '0123456789';
const DEFAULT_SYMBOL_CHARS = '!@#$%^&*()+_-=}{[]|:;"/?.><,`~';

const cryptoRandomIndex = (max: number): number => {
  const buffer = new Uint32Array(1);
  crypto.getRandomValues(buffer);
  return buffer[0] % max;
};

const pickFrom = (chars: string): string => chars.charAt(cryptoRandomIndex(chars.length));

export const createPassword = (options: CreatePasswordOptions): string => {
  const symbolChars: string =
    options.symbols === true
      ? DEFAULT_SYMBOL_CHARS
      : typeof options.symbols === 'string'
        ? options.symbols
        : '';
  const enabledCharsets = [
    options.lowercase ? LOWERCASE_CHARS : '',
    options.uppercase ? UPPERCASE_CHARS : '',
    options.numbers ? NUMBER_CHARS : '',
    symbolChars,
  ];

  const exclude = options.exclude ?? '';
  const filterExcluded = (chars: string): string =>
    chars
      .split('')
      .filter((char) => !exclude.includes(char))
      .join('');

  const filteredCharsets = enabledCharsets.map(filterExcluded).filter((set) => set.length > 0);

  if (filteredCharsets.length === 0) {
    throw new Error('createPassword: no character set enabled');
  }

  const pool = filteredCharsets.join('');

  const generateAttempt = (): string => {
    let password = '';
    for (let i = 0; i < options.length; i++) {
      password += pickFrom(pool);
    }
    return password;
  };

  const meetsStrictness = (password: string): boolean =>
    filteredCharsets.every((charset) => charset.split('').some((char) => password.includes(char)));

  const maxAttempts = 50;
  let attempts = 0;
  let password = generateAttempt();

  if (options.strict) {
    while (!meetsStrictness(password) && attempts < maxAttempts) {
      password = generateAttempt();
      attempts++;
    }
  }

  return password;
};
