import bcrypt from 'bcryptjs';
import get from 'lodash/get';
import omit from 'lodash/omit';
import Debug from 'debug';
import { NotAuthenticated } from '@feathersjs/errors';
import { Query, Params } from '@feathersjs/feathers';
import {
  AuthenticationRequest, AuthenticationBaseStrategy
} from '@feathersjs/authentication';

const debug = Debug('@feathersjs/authentication-local/strategy');

export class LocalStrategy extends AuthenticationBaseStrategy {
  verifyConfiguration () {
    const config = this.configuration;

    [ 'usernameField', 'passwordField' ].forEach(prop => {
      if (typeof config[prop] !== 'string') {
        throw new Error(`'${this.name}' authentication strategy requires a '${prop}' setting`);
      }
    });
  }

  get configuration () {
    const authConfig = this.authentication.configuration;
    const config = super.configuration || {};

    return {
      hashSize: 10,
      service: authConfig.service,
      entity: authConfig.entity,
      entityId: authConfig.entityId,
      errorMessage: 'Invalid login',
      entityPasswordField: config.passwordField,
      entityUsernameField: config.usernameField,
      ...config
    };
  }

  async getEntityQuery (query: Query, _params: Params) {
    return {
      $limit: 1,
      ...query
    };
  }

  async findEntity (username: string, params: Params) {
    const { entityUsernameField, errorMessage } = this.configuration;
    if (!username) { // don't query for users without any condition set.
      throw new NotAuthenticated(errorMessage);
    }

    const query = await this.getEntityQuery({
      [entityUsernameField]: username
    }, params);

    const findParams = Object.assign({}, params, { query });
    const entityService = this.entityService;

    debug('Finding entity with query', params.query);

    const result = await entityService.find(findParams);
    const list = Array.isArray(result) ? result : result.data;

    if (!Array.isArray(list) || list.length === 0) {
      debug(`No entity found`);

      throw new NotAuthenticated(errorMessage);
    }

    const [ entity ] = list;

    return entity;
  }

  async getEntity (result: any, params: Params) {
    const entityService = this.entityService;
    const { entityId = entityService.id, entity } = this.configuration;

    if (!entityId || result[entityId] === undefined) {
      throw new NotAuthenticated('Could not get local entity');
    }

    if (!params.provider) {
      return result;
    }

    return entityService.get(result[entityId], {
      ...params,
      [entity]: result
    });
  }

  async comparePassword (entity: any, password: string) {
    const { entityPasswordField, errorMessage } = this.configuration;
    // find password in entity, this allows for dot notation
    const hash = get(entity, entityPasswordField);

    if (!hash) {
      debug(`Record is missing the '${entityPasswordField}' password field`);

      throw new NotAuthenticated(errorMessage);
    }

    debug('Verifying password');

    const result = await bcrypt.compare(password, hash);

    if (result) {
      return entity;
    }

    throw new NotAuthenticated(errorMessage);
  }

  async hashPassword (password: string, _params: Params) {
    return bcrypt.hash(password, this.configuration.hashSize);
  }

  async authenticate (data: AuthenticationRequest, params: Params) {
    const { passwordField, usernameField, entity } = this.configuration;
    const username = data[usernameField];
    const password = data[passwordField];
    const result = await this.findEntity(username, omit(params, 'provider'));

    await this.comparePassword(result, password);

    return {
      authentication: { strategy: this.name },
      [entity]: await this.getEntity(result, params)
    };
  }
}
