import {
  Model as SequelizeModel, Op, Sequelize, ProjectionAlias, Utils, SaveOptions
} from 'sequelize';
import Database from './database';
import {
  getDataTypeKey,
  HASONE,
  HASMANY,
  BELONGSTO,
  BELONGSTOMANY,
} from './fields';
import { toInclude } from './utils';

export interface ApiJsonOptions {

  /**
   * 字段
   * 
   * 数组式:
   * ['col', 'association.col1', 'association_count'],
   * 
   * 白名单:
   * {
   *   only: ['col1'],
   *   appends: ['association_count'],
   * }
   * 
   * 黑名单:
   * {
   *   except: ['col1'],
   *   appends: ['association_count'],
   * }
   */
  fields?: string[] | {
    only?: string[];
    appends?: string[];
  } | {
    except?: string[];
    appends?: string[];
  };

  /**
   * 过滤
   * 
   * 常规用法:
   * {
   *   col1: {
   *     $eq: 'val1'
   *   },
   * }
   * 
   * scope 的用法(如果 scope 与 col 同名,只会执行 scope):
   * {
   *   scope1: value
   * }
   * 
   * json 数据 & 关系数据,可以用点号:
   * {
   *   'association.col1': {
   *     $eq: 'val1'
   *   },
   * }
   *
   * meta 为 json 字段时
   * {
   *   'meta.key': {
   *     $eq: 'val1'
   *   },
   * }
   * 
   * json 数据 & 关系数据的查询也可以不用点号:
   * {
   *   association: {
   *     col1: {
   *       $eq: 'val1'
   *     },
   *   },
   * }
   */
  filter?: any;

  /**
   * 排序
   * 
   * TODO
   * 
   * ['col1', '-col2', 'association.col1', '-association.col2']
   */
  sort?: any;

  /**
   * 页码
   */
  page?: number;
  perPage?: number;

  context?: any;

  [key: string]: any;
}

export interface WithCountAttributeOptions {

  /**
   * 关系名
   */
  association: string;

  /**
   * SourceModel 别名
   * 
   * 在 include 里使用时,需要指定,一般与 include 的 association 同名
   * 
   * include: {
   *   association: 'user', // Post.belongsTo(User)
   *   attributes: [
   *     User.withCountAttribute({
   *       association: 'posts',
   *       sourceAlias: 'user', // 内嵌时,需要指定 source 别名
   *     })
   *   ]
   * }
   */
  sourceAlias?: string;

  where?: any;

  /**
   * 别名,默认为 association_count
   */
  alias?: string;

  [key: string]: any;
}

export const DEFAULT_OFFSET = 0;
export const DEFAULT_LIMIT = 100;
export const MAX_LIMIT = 500;

export interface UpdateAssociationOptions extends SaveOptions {
  context?: any;
}

/**
 * Model 相关
 * 
 * TODO: 自定义 model 时的提示问题
 */
// @ts-ignore
export abstract class Model extends SequelizeModel {

  /**
   * 防止 ts 报错提示
   */
  [key: string]: any;

  /**
   * 当前 Model 的 database
   * 
   * 与 Model.sequelize 对应,database 也用了 public static readonly
   */
  public static database: Database;

  /**
   * 供 model 实例访问的 database
   */
  get database(): Database {
    // @ts-ignore
    return this.constructor.database;
  }

  /**
   * sub query 关联数据的数量
   * 
   * TODO: 关联字段暂不支持主键以外的字段
   * 
   * @param options 
   */
  static withCountAttribute(options?: string | WithCountAttributeOptions): (string | ProjectionAlias) {
    if (typeof options === 'string') {
      options = { association: options };
    }

    const { sourceAlias, association, where = {}, alias, ...restOptions } = options;
    const associator = this.associations[association];
    const table = this.database.getTable(this.name);
    const field = table.getField(association);
    const { targetKey, otherKey, foreignKey, sourceKey } = field.options as any;

    if (associator.associationType === 'HasMany') {
      where[foreignKey as string] = {
        [Op.eq]: Sequelize.col(`${sourceAlias || this.name}.${sourceKey}`),
      };
    } else if (associator.associationType === 'BelongsToMany') {
      where[targetKey] = {
        // @ts-ignore
        [Op.in]: Sequelize.literal(`(${associator.through.model.selectQuery({
          attributes: [otherKey],
          where: {
            [foreignKey]: {
              [Op.eq]: Sequelize.col(`${sourceAlias || this.name}.${sourceKey}`),
            },
            // @ts-ignore
            ...(associator.through.scope || {}),
          },
        })})`),
      };
    }

    let countLiteral = 'count(*)';

    if (this.database.sequelize.getDialect() === 'postgres') {
      countLiteral = 'cast(count(*) as integer)';
    }

    const attribute = [
      Sequelize.literal(
        // @ts-ignore
        `(${associator.target.selectQuery({
          ...restOptions,
          attributes: [[Sequelize.literal(countLiteral), 'count']],
          where: {
            // @ts-ignore
            ...where, ...(associator.scope || {}),
          },
        })})`
      ),
      alias || Utils.underscoredIf(`${association}Count`, this.options.underscored),
    ].filter(Boolean);

    return attribute as unknown as ProjectionAlias;
  }

  /**
   * 当前 Model 的 SQL
   * 
   * @param options 
   */
  static selectQuery(options = {}): string {
    // @ts-ignore
    return this.queryGenerator.selectQuery(
      this.getTableName(),
      options,
      this,
    ).replace(/;$/, '');
  }

  static parseApiJson(options: ApiJsonOptions) {
    const { fields, filter, sort, context, page, perPage } = options;
    const data = toInclude({ fields, filter, sort }, {
      model: this,
      associations: this.associations,
      dialect: this.sequelize.getDialect(),
      ctx: context,
      database: this.database,
    });
    if (page || perPage) {
      data.limit = perPage === -1 ? MAX_LIMIT : Math.min(perPage || DEFAULT_LIMIT, MAX_LIMIT);
      data.offset = data.limit * (page > 0 ? page - 1 : DEFAULT_OFFSET);
    }
    if (data.attributes && data.attributes.length === 0) {
      delete data.attributes;
    }
    return data;
  }

  getValuesByFieldNames(scope = []) {
    const table = this.database.getTable(this.constructor.name);
    const Model = table.getModel();
    const associations = table.getAssociations();
    const where = {};
    scope.forEach(col => {
      const association = associations.get(col);
      const dataKey = association && association instanceof BELONGSTO
        ? association.options.foreignKey
        : col;
      if (!Model.rawAttributes[dataKey]) {
        return;
      }
      const value = this.getDataValue(dataKey);
      if (typeof value !== 'undefined') {
        where[dataKey] = value;
      }
    });
    return where;
  }

  async updateSingleAssociation(key: string, data: any, options: UpdateAssociationOptions = {}) {
    const {
      fields,
      transaction = await this.sequelize.transaction(),
      ...opts
    } = options;
    Object.assign(opts, { transaction });

    const table = this.database.getTable(this.constructor.name);
    const association = table.getAssociations().get(key);
    const accessors = association.getAccessors();

    if (data == null) {
      await this[accessors.set](null, opts);
      return;
    }

    if (typeof data === 'number' || typeof data === 'string' || data instanceof SequelizeModel) {
      await this[accessors.set](data, opts);
    } else if (typeof data === 'object') {
      const Target = association.getTargetModel();
      const targetAttribute = association instanceof BELONGSTO
        ? association.options.targetKey
        : association.options.sourceKey;
      if (data[targetAttribute]) {
        await this[accessors.set](data[targetAttribute], opts);
        if (Object.keys(data).length > 1) {
          const target = await Target.findOne({
            where: {
              [targetAttribute]: data[targetAttribute],
            },
            transaction
          });
          await target.update(data, opts);
          // @ts-ignore
          await target.updateAssociations(data, opts);
        }
      } else {
        const t = await this[accessors.create](data, opts);
        await t.updateAssociations(data, opts);
      }
    }
    if (!options.transaction) {
      await transaction.commit();
    }
  }

  async updateMultipleAssociation(associationName: string, data: any, options: UpdateAssociationOptions = {}) {
    const items = Array.isArray(data) ? data : data == null ? [] : [data];

    const {
      fields,
      transaction = await this.sequelize.transaction(),
      ...opts
    } = options;
    Object.assign(opts, { transaction });

    const table = this.database.getTable(this.constructor.name);
    const association = table.getAssociations().get(associationName);
    const accessors = association.getAccessors();

    if (!items.length) {
      await this[accessors.set](null, opts);
      return;
    }

    const Target = association.getTargetModel();
    // 当前表关联 target 表的外键(大部分情况与 target 表主键相同,但可以设置为不同的,要考虑)
    const { targetKey = Target.primaryKeyAttribute } = association.options;
    // target 表的主键
    const targetPk = Target.primaryKeyAttribute;
    const targetKeyIsPk = targetKey === targetPk;
    // 准备设置的关联主键
    const toSetPks = new Set();
    const toSetUks = new Set();
    // 筛选后准备设置的关联主键
    const toSetItems = new Set();
    // 准备添加的关联对象
    const toUpsertObjects = [];

    // 遍历所有值成员准备数据
    items.forEach(item => {
      if (item instanceof SequelizeModel) {
        if (targetKeyIsPk) {
          toSetPks.add(item.getDataValue(targetPk));
        } else {
          toSetUks.add(item.getDataValue(targetKey));
        }
        return;
      }
      if (typeof item === 'number' || typeof item === 'string') {
        let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
        if (targetKeyType === 'integer') {
          targetKeyType = 'number';
        }
        // 如果传值类型与之前在 Model 上定义的 targetKey 不同,则报错。
        // 不应兼容定义的 targetKey 不是 primaryKey 却传了 primaryKey 的值的情况。
        if (typeof item !== targetKeyType) {
          throw new Error(`target key type [${typeof item}] does not match to [${targetKeyType}]`);
        }
        if (targetKeyIsPk) {
          toSetPks.add(item);
        } else {
          toSetUks.add(item);
        }
        return;
      }
      if (typeof item === 'object') {
        toUpsertObjects.push(item);
      }
    });

    /* 仅传关联键处理开始 */
    // 查找已存在的数据
    const byPkExistItems = toSetPks.size ? await Target.findAll({
      ...opts,
      // @ts-ignore
      where: {
        [targetPk]: {
          [Op.in]: Array.from(toSetPks)
        }
      },
      attributes: [targetPk]
    }) : [];
    byPkExistItems.forEach(item => {
      toSetItems.add(item);
    });

    const byUkExistItems = toSetUks.size ? await Target.findAll({
      ...opts,
      // @ts-ignore
      where: {
        [targetKey]: {
          [Op.in]: Array.from(toSetUks)
        }
      },
      attributes: [targetPk, targetKey]
    }) : [];
    byUkExistItems.forEach(item => {
      toSetItems.add(item);
    });
    /* 仅传关联键处理结束 */

    const belongsToManyList = [];
    /* 值为对象处理开始 */
    for (const item of toUpsertObjects) {
      let target;
      if (typeof item[targetKey] === 'undefined') {
        target = await this[accessors.create](item, opts);
      } else {
        target = await Target.findOne({
          ...opts,
          where: { [targetKey]: item[targetKey] },
        });
        if (!target) {
          target = await this[accessors.create](item, opts);
        } else {
          await target.update(item, opts);
        }
      }
      // TODO(optimize): 此处添加的对象其实已经创建了关联,
      // 但考虑到单条 create 的 hook 要求带上关联键,且后面的 set,
      // 所以仍然交给 set 再调用关联一次。
      toSetItems.add(target);

      if (association instanceof BELONGSTOMANY) {
        belongsToManyList.push({
          item,
          target
        });
      }

      await target.updateAssociations(item, opts);
    }
    /* 值为对象处理结束 */

    // 添加所有计算后的关联
    await this[accessors.set](Array.from(toSetItems), opts);

    // 后处理 belongsToMany 的更新内容
    if (belongsToManyList.length) {
      const ThroughModel = (association as BELONGSTOMANY).getThroughModel();
      const throughName = (association as BELONGSTOMANY).getThroughName();


      for (const { item, target } of belongsToManyList) {
        const throughValues = item[throughName];
        if (throughValues && typeof throughValues === 'object') {
          const { foreignKey, sourceKey, otherKey } = association.options;
          const through = await ThroughModel.findOne({
            where: {
              [foreignKey]: this.get(sourceKey),
              [otherKey]: target.get(targetKey),
            },
            transaction
          });
          await through.update(throughValues, opts);
          // TODO:有 BUG,未知
          // await through.updateAssociations(throughValues, opts);
        }
      }
    }

    if (!options.transaction) {
      await transaction.commit();
    }
  }

  async updateAssociation(key: string, data: any, options: UpdateAssociationOptions = {}) {
    const table = this.database.getTable(this.constructor.name);
    const association = table.getAssociations().get(key);
    switch (true) {
      case association instanceof BELONGSTO:
      case association instanceof HASONE:
        return this.updateSingleAssociation(key, data, options);
      case association instanceof HASMANY:
      case association instanceof BELONGSTOMANY:
        return this.updateMultipleAssociation(key, data, options);
    }
  }

  /**
   * 关联数据的更新
   * 
   * @param data
   */
  async updateAssociations(data: any, options: UpdateAssociationOptions = {}) {
    const { transaction = await this.sequelize.transaction() } = options;
    const table = this.database.getTable(this.constructor.name);
    for (const key of table.getAssociations().keys()) {
      // 如果 key 不存在才跳过
      if (!Object.keys(data).includes(key)) {
        continue;
      }
      await this.updateAssociation(key, data[key], {
        ...options,
        transaction
      });
    }

    await this.database.runHooks('afterUpdateAssociations', this, {
      ...options,
      transaction,
    });
    // @ts-ignore
    // await this.sequelize.runHooks('afterUpdateAssociations', this, options);

    if (!options.transaction) {
      await transaction.commit();
    }
  }
}

/**
 * ModelCtor 需要为当前 Model 的
 */
export type ModelCtor<M extends Model> = typeof Model & { new(): M } & { [key: string]: any };

export default Model;