import * as moment from 'moment';
import * as validator from 'validator';

let models: any = null;

export function ValidateParam(schema: any, value: any, generatedModels: any, name = '') {
  models = generatedModels;

  if (value === undefined || value === null) {
    if (schema.required) {
      throw new InvalidRequestException(`'${name}' is a required ${schema.in} parameter.`);
    } else {
      return undefined;
    }
  }

  switch (schema.typeName) {
    case 'string':
      return validateString(value, name);
    case 'boolean':
      return validateBool(value, name);
    case 'integer':
    case 'long':
      return validateInt(value, name);
    case 'float':
    case 'double':
      return validateFloat(value, name);
    case 'enum':
      return validateEnum(value, name, schema.enumMembers);
    case 'array':
      return validateArray(value, name, schema.array);
    case 'date':
      return validateDate(value, name);
    case 'datetime':
      return validateDateTime(value, name);
    case 'buffer':
      return validateBuffer(value, name);
    default:
      return validateModel(value, schema.typeName);
  }
}

function validateInt(numberValue: string, name: string): number {
  if (!validator.isInt(numberValue + '')) {
    throw new InvalidRequestException(name + ' should be a valid integer.');
  }
  return validator.toInt(numberValue + '', 10);
}

function validateFloat(numberValue: string, name: string): number {
  if (!validator.isFloat(numberValue + '')) {
    throw new InvalidRequestException(name + ' should be a valid float.');
  }

  return validator.toFloat(numberValue + '');
}

function validateEnum(enumValue: string, name: string, members?: string[]): any {
  if (!members) {
    throw new InvalidRequestException(name + ' no member.');
  }
  const existValue = members.filter(m => m === enumValue);
  if (!existValue || !enumValue.length || !existValue.length) {
    throw new InvalidRequestException(name + ' should be one of the following; ' + members.join(', '));
  }
  return existValue[0];
}

function validateDate(dateValue: string, name: string): Date {
  const regEx = /^\d{4}-\d{2}-\d{2}$/;
  if (!dateValue.match(regEx)) {
    throw new InvalidRequestException(name + ' should be a valid date, i.e. YYYY-MM-DD');
  }
  return new Date(dateValue);
}

function validateDateTime(datetimeValue: string, name: string): Date {
  const validatedDate = moment(datetimeValue, moment.ISO_8601, true);
  if (!validatedDate.isValid()) {
    throw new InvalidRequestException(name + ' should be a valid ISO 8601 date, i.e. YYYY-MM-DDTHH:mm:ss');
  }

  return validatedDate.toDate();
}

function validateString(stringValue: string, name: string) {
  if (typeof stringValue !== 'string') {
    throw new InvalidRequestException(name + ' should be a valid string.');
  }

  return stringValue.toString();
}

function validateBool(boolValue: any, typeName: string): boolean {
  if (boolValue === true || boolValue === false) { return boolValue; }
  if (boolValue.toLowerCase() === 'true') { return true; }
  if (boolValue.toLowerCase() === 'false') { return false; }

  throw new InvalidRequestException(name + ' should be valid boolean value.');
}

function validateModel(modelValue: any, typeName: string): any {
  const modelDefinition = models[typeName];

  if (modelDefinition) {
    if (modelDefinition.properties) {
      Object.keys(modelDefinition.properties).forEach((key: string) => {
        const property = modelDefinition.properties[key];
        modelValue[key] = ValidateParam(property, modelValue[key], models, key);
      });
    }
    if (modelDefinition.additionalProperties) {
      Object.keys(modelValue).forEach((key: string) => {
        let validatedValue = null;
        for (const additionalProperty of modelDefinition.additionalProperties) {
          try {
            validatedValue = ValidateParam(additionalProperty, modelValue[key], models, key);
            break;
          } catch (err) {
            continue;
          }
        }
        if (validatedValue) {
          modelValue[key] = validatedValue;
        } else {
          throw new Error(`No matching model found in additionalProperties to validate ${key}`);
        }
      });
    }
  }

  return modelValue;
}

function validateArray(arrayValue: any[], name: string, schema?: any): any[] {
  if (!schema) {
    throw new InvalidRequestException(name + ' array invalid.');
  }
  return arrayValue.map(value => {
    return ValidateParam(schema, value, models, undefined);
  });
}

function validateBuffer(value: string, name: string) {
  return new Buffer(value);
}

interface Exception extends Error {
  status: number;
}

class InvalidRequestException implements Exception {
  public status = 400;
  public name = 'Invalid Request';

  constructor(public message: string) { }
}
