'use strict';

import * as Joi from 'joi';
import {isFunction} from 'util';
import {Observable} from 'rxjs';
import {ValidationResult} from './ValidationResult';
import {IValidator, IValidationResult} from './interfaces';

export class Validator implements IValidator {
  
  protected _syncSchema;
  protected _asyncSchema;
  
  constructor(syncSchema, asyncSchema?) {
    this._syncSchema = syncSchema;
    this._asyncSchema = asyncSchema;
  }
  
  protected _syncValidate(data) {
    if(!this._syncSchema) {
      return Observable.of([]);
    }
    const validationOptions = {
      abortEarly: false,
      allowUnknown: true
    };
    const result = Joi.validate(data, this._syncSchema, validationOptions);
      if(!result.error) {
        return Observable.of([]);
      }      
      return Observable.of(result.error.details.map(r => {        
        r['property'] = r['path'];
        const type = r['type'].split('.');
        if(type.length > 1) {
          type.shift();
        }
        r['type'] = type.join('.');
        delete r['context'];
        delete r['path'];
        return r;
      }));
  }
  
  protected _asyncValidate(data) {
    if(!this._asyncSchema) {
      return Observable.of([]);
    }
    return Observable.from(Object.keys(this._asyncSchema))
        .mergeMap(propertyName => {
          return Observable.from(this._asyncSchema[<string>propertyName])
            .mergeMap(validator => {              
              const result = (<Function>validator).bind(data)(propertyName);
              if(isFunction(result.subscribe)) {
                return result;
              }
              return Observable.of(result);
            })
            .map(errorMessage => {              
              if(errorMessage) {
                return {
                  property: propertyName,
                  type: 'custom',
                  message: errorMessage
                };
              }
              return null;
            });
        })          
        .toArray()
        .map(result => result.reduce((aggr, curr) => {
          if(curr) {
            (<[]>aggr).push(curr);
          }
          return aggr;
        }, []));
  }
  
  validate(data): Observable<IValidationResult> {
    const mapResults = (sync, async) => sync.concat(async);
    return Observable.combineLatest(this._syncValidate(data), this._asyncValidate(data), mapResults)
      .map(resultRaw => new ValidationResult(resultRaw));
  }
}