'use strict';

import {Query} from 'blow-query';
import {Observable} from 'rxjs';
import * as mongodb from 'mongodb';
import {DataConnector} from './DataConnector';
import {Entity} from '../Entity';

export interface ConnectOptions {
  uriDecodeAuth: boolean;
  db: Object;
  server: Object;
  replSet: Object;
  mongos: Object;
}

export class MongoClient {

  static connect(url: string, options?: ConnectOptions): Observable<Db> {
    return Observable.from<mongodb.Db>(mongodb.MongoClient.connect(url, options))
      .map(db => new Db(db));
  }
}

export class Db {

  protected _db: mongodb.Db;

  constructor(db: mongodb.Db) {
    this._db = db;
  }

  collection<T>(name: string): Collection<T> {
    return new Collection(this._db.collection(name));
  }

  delete() {
    return Observable.from(this._db.dropDatabase())
      .mergeMap(() => this.close());
  }

  close(): Observable<boolean> {
    return Observable.from(this._db.close());
  }
}

export class Collection<T> {

  protected _collection: mongodb.Collection;

  constructor(collection: mongodb.Collection) {
    this._collection = collection;
  }

  find(query?): Observable<T> {
    query = Object.assign({}, { where: {} }, query || {});
    let cursor = this._collection.find(query.where);

    Object.keys(query).forEach(key => {
      if (key !== 'where') {
        cursor = cursor[key](query[key]);
      }
    });

    return Observable.create(subscriber => {
      cursor.forEach(document => {
        subscriber.next(document);
      }, error => {
        if (error) {
          subscriber.error(error);
        }
        subscriber.complete();
      });
    });
  }

  count(query?): Observable<number> {
    query = query || {};
    return Observable.from(this._collection.count(query));
  }

  delete(query?): Observable<number> {
    query = query || {};
    return Observable.from(this._collection.deleteMany(query));
  }

  insert(doc): Observable<T> {
    return Observable.from(this._collection.insertOne(doc))
      .map(response => response['ops'][0]);
  }

  update(query, doc, options?): Observable<T> {
    return Observable.from(this._collection.updateOne(query, { $set: doc }, options))
      .map(response => {
        if (response['modifiedCount']) {
          doc._id = query._id.toString();
          return doc;
        } else {
          return null;
        }
      });
  }
}

export class MongoDBConnector extends DataConnector {

  protected _db: Db;

  protected _buildQueryWhereForId(id: string) {
    return this._prepareQueryWhere({
      _id: id
    });
  }

  protected _normalizeId(value) {
    let id;
    try {
      id = new mongodb.ObjectID(value);
    } catch (e) {
      id = value;
    }
    return id;
  }

  protected _prepareQueryWhere(queryWhere: { [key: string]: any }): { [key: string]: any } {
    const where = {};
    Object.keys(queryWhere).forEach(key => {
      let value = queryWhere[key];
      if (key === '_id') {
        value = this._normalizeId(value);
      }
      where[key] = value;
    });
    return where;
  }

  protected _collection<T>(collectionName: string): Collection<T> {
    return this._db.collection<T>(collectionName);
  }

  connect(): Observable<MongoDBConnector> {
    this._state.connecting();
    return Observable.from<Db>(MongoClient.connect(<string>this._settings['url']))
      .do(db => {
        this._db = db;
        this._state.connected();
      })
      .mapTo(this);
  }

  disconnect(): Observable<MongoDBConnector> {
    this._state.disconnecting();
    return Observable.from(this._db.close())
      .do(() => this._state.disconnected())
      .mapTo(this);
  }

  destroyDb(): Observable<MongoDBConnector> {
    return this._db.delete();
  }

  find<T>(collectionName: string, query?: Query): Observable<T> {
    return this._collection(collectionName)
      .find(this._prepareQuery(query));
  }

  count(collectionName: string, query?: Query): Observable<number> {
    return this._collection(collectionName)
      .count(this._prepareQuery(query).where);
  }

  delete(collectionName: string, query?: Query): Observable<number> {
    return this._collection(collectionName)
      .delete(this._prepareQuery(query).where)
      .map(response => response['result'].n);
  }

  deleteById(collectionName: string, id: string): Observable<boolean> {
    return this._collection(collectionName)
      .delete(this._buildQueryWhereForId(id))
      .map(response => response['result'].n === 1);
  }

  get<T>(collectionName: string, id: string): Observable<T> {
    return this._collection(collectionName)
      .find({ where: this._buildQueryWhereForId(id) });
  }

  save<T>(collectionName: string, doc: Entity): Observable<T> {
    const hasId = Object.keys(doc).indexOf('_id') > -1 && doc['_id'];
    if (!hasId) {
      return this._collection(collectionName).insert(doc);
    } else {
      return this._collection(collectionName).update(this._buildQueryWhereForId(doc['_id']), doc, {upsert: true});
    }
  }

  updateAttributes<T>(collectionName: string, id: string, attributes: Entity): Observable<T> {
    delete attributes['_id'];
    return this.get<T>(collectionName, id)
      .map(result => Object.assign(result, attributes))
      .mergeMap(doc => this.save<T>(collectionName, doc));
  }
}