'use strict';

import * as Joi from 'joi';
import {Observable} from 'rxjs';
import {ValidationResult} from './ValidationResult';
import {ValidationError} from './ValidationError';

export interface ValidationSchema {
  [property: string]: {
    [validator: string]: any
  };
}

const JOIValidations = [
  { validation: 'valid' },
  { validation: 'invalid' },
  { validation: 'required' },
  { validation: 'optional' },
  { validation: 'forbidden' },
  { validation: 'min' },
  { validation: 'max' },
  { validation: 'length' },
  { validation: 'unique' },
  { validation: 'pattern' },
  { validation: 'email' },
  { validation: 'string' },
  { validation: 'number' },
  { validation: 'date' },
  { validation: 'array' },
  { validation: 'object' }
];

const JOI_VALIDATION_OPTIONS = {
  abortEarly: false,
  allowUnknown: true
};

function isJOIValidation(name) {
  return JOIValidations.some(v => v['validation'] === name);
}

export class Validator {

  protected _schema: ValidationSchema;
  protected _compiledJoiSchema;
  protected _compiledCustomSchema;

  constructor(schema?: ValidationSchema) {
    this._schema = schema || {};
    this._compile();
  }

  protected _compile() {
    const properties = Object.keys(this._schema);
    this._compiledJoiSchema = {};
    this._compiledCustomSchema = {};

    for (const property of properties) {
      const validations = Object.keys(this._schema[property]);
      let propertyJoiSchema;
      let propertyCustomSchema;

      for (const validation of validations) {
        if (isJOIValidation(validation)) {
          if (!propertyJoiSchema) {
            propertyJoiSchema = Joi;
          }
          let options = this._schema[property][validation];
          if (validation === 'email') {
            options = undefined;
          }
          propertyJoiSchema = propertyJoiSchema[validation](options);
        } else {
          if (!propertyCustomSchema) {
            propertyCustomSchema = [];
          }
          propertyCustomSchema.push({ name: validation, fn: this._schema[property][validation] });
        }
      }

      if (propertyJoiSchema) {
        this._compiledJoiSchema[property] = propertyJoiSchema;
      }
      if (propertyCustomSchema) {
        this._compiledCustomSchema[property] = propertyCustomSchema;
      }
    }
  }

  protected _validateJoi(data: { [property: string]: any }): Observable<ValidationResult> {
    if (!Object.keys(this._compiledJoiSchema).length) {
      return Observable.of(new ValidationResult());
    }

    return Observable.create(subscriber => {
      Joi.validate(data, this._compiledJoiSchema, JOI_VALIDATION_OPTIONS, err => {
        if (err) {
          const createError = e => ValidationError.create({property: e.path, type: e.type.split('.')[1], message: e.message});
          subscriber.next(<any>err.details.map(createError));
        }
        subscriber.complete();
      });
    })
      .defaultIfEmpty()
      .map(errors => new ValidationResult(errors));
  }

  protected _validateCustom(data: { [property: string]: any }): Observable<ValidationResult> {
    const properties = Object.keys(this._compiledCustomSchema);
    if (!properties.length) {
      return Observable.of(ValidationResult.create());
    }
    return Observable.from<string>(properties)
      .map(property => [property, this._compiledCustomSchema[property]])
      .mergeMap(row => Observable.from(row[1]).mergeMap((v: { fn: any }) => v.fn.bind(data)(row[0])))
      .filter(e => !!e)
      .map(e => ValidationError.create({property: (<any>e).path, type: (<any>e).type, message: (<any>e).message}))
      .toArray()
      .defaultIfEmpty()
      .map(ValidationResult.create);
  }

  validate(data: { [property: string]: any }): Observable<ValidationResult> {
    const mapResults = (joiResult, customResult) => joiResult.merge(customResult);
    return Observable.combineLatest(
      this._validateJoi(data),
      this._validateCustom(data),
      mapResults
    );
  }

  static create(schema?: ValidationSchema): Validator {
    return new Validator(schema);
  }
}