import isEmpty from 'lodash/isEmpty';
import cloneDeep from 'lodash/cloneDeep';
import find from 'lodash/find';
import get from 'lodash/get';
import ConversationBase from './conversation-base';
import {
  decodeDate,
  getTime,
  encode,
  keyRemap,
  union,
  difference,
  internal,
  setValue,
  ensureArray,
} from '../utils';
import {
  GenericCommand,
  ConvCommand,
  JsonObjectMessage,
  BlacklistCommand,
  OpType,
} from '../../proto/message';
import runSignatureFactory from '../signature-factory-runner';
import { createError } from '../error';

/**
 * 部分失败异常
 * @typedef OperationFailureError
 * @type {Error}
 * @property {string} message 异常信息
 * @property {string[]} clientIds 因为该原因失败的 client id 列表
 * @property {number} [code] 错误码
 * @property {string} [detail] 详细信息
 */

/**
 * 部分成功的结果
 * @typedef PartiallySuccess
 * @type {Object}
 * @property {string[]} successfulClientIds 成功的 client id 列表
 * @property {OperationFailureError[]} failures 失败的异常列表
 */

/**
 * 分页查询结果
 * @typedef PagedResults
 * @type {Object}
 * @property {T[]} results 查询结果
 * @property {string} [next] 存在表示还有更多结果,在下次查询中带上可实现翻页。
 */

const createPartiallySuccess = ({ allowedPids, failedPids }) => ({
  successfulClientIds: allowedPids,
  failures: failedPids.map(({ pids, ...error }) =>
    Object.assign(createError(error), { clientIds: pids })
  ),
});

/**
 * @extends ConversationBase
 * @private
 * @abstract
 */
class PersistentConversation extends ConversationBase {
  constructor(
    data,
    {
      creator,
      createdAt,
      updatedAt,
      transient = false,
      system = false,
      muted = false,
      mutedMembers = [],
      ...attributes
    },
    client
  ) {
    super(
      {
        ...data,
        /**
         * 对话创建者
         * @memberof PersistentConversation#
         * @type {String}
         */
        creator,
        /**
         * 对话创建时间
         * @memberof PersistentConversation#
         * @type {Date}
         */
        createdAt,
        /**
         * 对话更新时间
         * @memberof PersistentConversation#
         * @type {Date}
         */
        updatedAt,
        /**
         * 对该对话设置了静音的用户列表
         * @memberof PersistentConversation#
         * @type {?String[]}
         */
        mutedMembers,
        /**
         * 暂态对话标记
         * @memberof PersistentConversation#
         * @type {Boolean}
         */
        transient,
        /**
         * 系统对话标记
         * @memberof PersistentConversation#
         * @type {Boolean}
         * @since 3.3.0
         */
        system,
        /**
         * 当前用户静音该对话标记
         * @memberof PersistentConversation#
         * @type {Boolean}
         */
        muted,
        _attributes: attributes,
      },
      client
    );
    this._reset();
  }

  set createdAt(value) {
    this._createdAt = decodeDate(value);
  }

  get createdAt() {
    return this._createdAt;
  }

  set updatedAt(value) {
    this._updatedAt = decodeDate(value);
  }

  get updatedAt() {
    return this._updatedAt;
  }

  /**
   * 对话名字,对应 _Conversation 表中的 name
   * @type {String}
   */
  get name() {
    return this.get('name');
  }

  set name(value) {
    this.set('name', value);
  }

  /**
   * 获取对话的自定义属性
   * @since 3.2.0
   * @param  {String} key key 属性的键名,'x' 对应 Conversation 表中的 x 列
   * @return {Any} 属性的值
   */
  get(key) {
    return get(internal(this).currentAttributes, key);
  }

  /**
   * 设置对话的自定义属性
   * @since 3.2.0
   * @param {String} key 属性的键名,'x' 对应 Conversation 表中的 x 列,支持使用 'x.y.z' 来修改对象的部分字段。
   * @param {Any} value 属性的值
   * @return {this} self
   * @example
   *
   * // 设置对话的 color 属性
   * conversation.set('color', {
   *   text: '#000',
   *   background: '#DDD',
   * });
   * // 设置对话的 color.text 属性
   * conversation.set('color.text', '#333');
   */
  set(key, value) {
    this._debug(`set [${key}]: ${value}`);
    const { pendingAttributes } = internal(this);
    const pendingKeys = Object.keys(pendingAttributes);
    // suppose pendingAttributes = { 'a.b': {} }
    // set 'a' or 'a.b': delete 'a.b'
    const re = new RegExp(`^${key}`);
    const childKeys = pendingKeys.filter(re.test.bind(re));
    childKeys.forEach(k => {
      delete pendingAttributes[k];
    });
    if (childKeys.length) {
      pendingAttributes[key] = value;
    } else {
      // set 'a.c': nothing to do
      // set 'a.b.c.d': assign c: { d: {} } to 'a.b'
      const parentKey = find(pendingKeys, k => key.indexOf(k) === 0); // 'a.b'
      if (parentKey) {
        setValue(
          pendingAttributes[parentKey],
          key.slice(parentKey.length + 1),
          value
        );
      } else {
        pendingAttributes[key] = value;
      }
    }
    this._buildCurrentAttributes();
    return this;
  }

  _buildCurrentAttributes() {
    const { pendingAttributes } = internal(this);
    internal(this).currentAttributes = Object.keys(pendingAttributes).reduce(
      (target, k) => setValue(target, k, pendingAttributes[k]),
      cloneDeep(this._attributes)
    );
  }

  _updateServerAttributes(attributes) {
    Object.keys(attributes).forEach(key =>
      setValue(this._attributes, key, attributes[key])
    );
    this._buildCurrentAttributes();
  }

  _reset() {
    Object.assign(internal(this), {
      pendingAttributes: {},
      currentAttributes: this._attributes,
    });
  }

  /**
   * 保存当前对话的属性至服务器
   * @return {Promise.<this>} self
   */
  async save() {
    this._debug('save');
    const attr = internal(this).pendingAttributes;
    if (isEmpty(attr)) {
      this._debug('nothing touched, resolve with self');
      return this;
    }
    this._debug('attr: %O', attr);
    const convMessage = new ConvCommand({
      attr: new JsonObjectMessage({
        data: JSON.stringify(encode(attr)),
      }),
    });
    const resCommand = await this._send(
      new GenericCommand({
        op: 'update',
        convMessage,
      })
    );
    this.updatedAt = resCommand.convMessage.udate;
    this._attributes = internal(this).currentAttributes;
    internal(this).pendingAttributes = {};
    return this;
  }

  /**
   * 从服务器更新对话的属性
   * @return {Promise.<this>} self
   */
  async fetch() {
    const query = this._client.getQuery().equalTo('objectId', this.id);
    await query.find();
    return this;
  }

  /**
   * 静音,客户端拒绝收到服务器端的离线推送通知
   * @return {Promise.<this>} self
   */
  async mute() {
    this._debug('mute');
    await this._send(
      new GenericCommand({
        op: 'mute',
      })
    );
    if (!this.transient) {
      this.muted = true;
      this.mutedMembers = union(this.mutedMembers, [this._client.id]);
    }
    return this;
  }

  /**
   * 取消静音
   * @return {Promise.<this>} self
   */
  async unmute() {
    this._debug('unmute');
    await this._send(
      new GenericCommand({
        op: 'unmute',
      })
    );
    if (!this.transient) {
      this.muted = false;
      this.mutedMembers = difference(this.mutedMembers, [this._client.id]);
    }
    return this;
  }

  async _appendConversationSignature(command, action, clientIds) {
    if (this._client.options.conversationSignatureFactory) {
      const params = [this.id, this._client.id, clientIds.sort(), action];
      const signatureResult = await runSignatureFactory(
        this._client.options.conversationSignatureFactory,
        params
      );
      Object.assign(
        command.convMessage,
        keyRemap(
          {
            signature: 's',
            timestamp: 't',
            nonce: 'n',
          },
          signatureResult
        )
      );
    }
  }

  async _appendBlacklistSignature(command, action, clientIds) {
    if (this._client.options.blacklistSignatureFactory) {
      const params = [this.id, this._client.id, clientIds.sort(), action];
      const signatureResult = await runSignatureFactory(
        this._client.options.blacklistSignatureFactory,
        params
      );
      Object.assign(
        command.blacklistMessage,
        keyRemap(
          {
            signature: 's',
            timestamp: 't',
            nonce: 'n',
          },
          signatureResult
        )
      );
    }
  }

  /**
   * 增加成员
   * @param {String|String[]} clientIds 新增成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async add(clientIds) {
    this._debug('add', clientIds);
    if (typeof clientIds === 'string') {
      clientIds = [clientIds]; // eslint-disable-line no-param-reassign
    }
    const command = new GenericCommand({
      op: 'add',
      convMessage: new ConvCommand({
        m: clientIds,
      }),
    });
    await this._appendConversationSignature(command, 'invite', clientIds);
    const {
      convMessage,
      convMessage: { allowedPids },
    } = await this._send(command);
    this._addMembers(allowedPids);
    return createPartiallySuccess(convMessage);
  }

  /**
   * 剔除成员
   * @param {String|String[]} clientIds 成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async remove(clientIds) {
    this._debug('remove', clientIds);
    if (typeof clientIds === 'string') {
      clientIds = [clientIds]; // eslint-disable-line no-param-reassign
    }
    const command = new GenericCommand({
      op: 'remove',
      convMessage: new ConvCommand({
        m: clientIds,
      }),
    });
    await this._appendConversationSignature(command, 'kick', clientIds);
    const {
      convMessage,
      convMessage: { allowedPids },
    } = await this._send(command);
    this._removeMembers(allowedPids);
    return createPartiallySuccess(convMessage);
  }

  /**
   * (当前用户)加入该对话
   * @return {Promise.<this>} self
   */
  async join() {
    this._debug('join');
    return this.add(this._client.id).then(({ failures }) => {
      if (failures[0]) throw failures[0];
      return this;
    });
  }

  /**
   * (当前用户)退出该对话
   * @return {Promise.<this>} self
   */
  async quit() {
    this._debug('quit');
    return this.remove(this._client.id).then(({ failures }) => {
      if (failures[0]) throw failures[0];
      return this;
    });
  }

  /**
   * 在该对话中禁言成员
   * @param {String|String[]} clientIds 成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async muteMembers(clientIds) {
    this._debug('mute', clientIds);
    clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign
    const command = new GenericCommand({
      op: OpType.add_shutup,
      convMessage: new ConvCommand({
        m: clientIds,
      }),
    });
    const { convMessage } = await this._send(command);
    return createPartiallySuccess(convMessage);
  }

  /**
   * 在该对话中解除成员禁言
   * @param {String|String[]} clientIds 成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async unmuteMembers(clientIds) {
    this._debug('unmute', clientIds);
    clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign
    const command = new GenericCommand({
      op: OpType.remove_shutup,
      convMessage: new ConvCommand({
        m: clientIds,
      }),
    });
    const { convMessage } = await this._send(command);
    return createPartiallySuccess(convMessage);
  }

  /**
   * 查询该对话禁言成员列表
   * @param {Object} [options]
   * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10
   * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页。
   * @return {PagedResults.<string>} 查询结果。其中的 cureser 存在表示还有更多结果。
   */
  async queryMutedMembers({ limit, next } = {}) {
    this._debug('query muted: limit %O, next: %O', limit, next);
    const command = new GenericCommand({
      op: OpType.query_shutup,
      convMessage: new ConvCommand({
        limit,
        next,
      }),
    });
    const {
      convMessage: { m, next: newNext },
    } = await this._send(command);
    return {
      results: m,
      next: newNext,
    };
  }

  /**
   * 将用户加入该对话黑名单
   * @param {String|String[]} clientIds 成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async blockMembers(clientIds) {
    this._debug('block', clientIds);
    clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign
    const command = new GenericCommand({
      cmd: 'blacklist',
      op: OpType.block,
      blacklistMessage: new BlacklistCommand({
        srcCid: this.id,
        toPids: clientIds,
      }),
    });
    await this._appendBlacklistSignature(
      command,
      'conversation-block-clients',
      clientIds
    );
    const { blacklistMessage } = await this._send(command);
    return createPartiallySuccess(blacklistMessage);
  }

  /**
   * 将用户移出该对话黑名单
   * @param {String|String[]} clientIds 成员 client id
   * @return {Promise.<PartiallySuccess>} 部分成功结果,包含了成功的 id 列表、失败原因与对应的 id 列表
   */
  async unblockMembers(clientIds) {
    this._debug('unblock', clientIds);
    clientIds = ensureArray(clientIds); // eslint-disable-line no-param-reassign
    const command = new GenericCommand({
      cmd: 'blacklist',
      op: OpType.unblock,
      blacklistMessage: new BlacklistCommand({
        srcCid: this.id,
        toPids: clientIds,
      }),
    });
    await this._appendBlacklistSignature(
      command,
      'conversation-unblock-clients',
      clientIds
    );
    const { blacklistMessage } = await this._send(command);
    return createPartiallySuccess(blacklistMessage);
  }

  /**
   * 查询该对话黑名单
   * @param {Object} [options]
   * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10
   * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页
   * @return {PagedResults.<string>} 查询结果。其中的 cureser 存在表示还有更多结果。
   */
  async queryBlockedMembers({ limit, next } = {}) {
    this._debug('query blocked: limit %O, next: %O', limit, next);
    const command = new GenericCommand({
      cmd: 'blacklist',
      op: OpType.query,
      blacklistMessage: new BlacklistCommand({
        srcCid: this.id,
        limit,
        next,
      }),
    });
    const {
      blacklistMessage: { blockedPids, next: newNext },
    } = await this._send(command);
    return {
      results: blockedPids,
      next: newNext,
    };
  }

  toFullJSON() {
    const {
      creator,
      system,
      transient,
      createdAt,
      updatedAt,
      _attributes,
    } = this;
    return {
      ...super.toFullJSON(),
      creator,
      system,
      transient,
      createdAt: getTime(createdAt),
      updatedAt: getTime(updatedAt),
      ..._attributes,
    };
  }

  toJSON() {
    const {
      creator,
      system,
      transient,
      muted,
      mutedMembers,
      createdAt,
      updatedAt,
      _attributes,
    } = this;
    return {
      ...super.toJSON(),
      creator,
      system,
      transient,
      muted,
      mutedMembers,
      createdAt,
      updatedAt,
      ..._attributes,
    };
  }
}

export default PersistentConversation;