import { App, Inject, MidwayWebRouterService } from '@midwayjs/core';
import { Application, Context } from '@midwayjs/koa';
import { compact, flattenDeep, get, head, isArray, isNull, isNumber, isObject, isString, isUndefined, last, uniq } from 'lodash';
import { Opera } from './dbOpera';
import { Opera as operaModel } from '../entityLogs/opera.entity';
import dayjs = require('dayjs');
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { Tags } from '../entity/system/tags.entity';
/**
 * query默认的查询方式(哪个字段 是 = 还是 IN 还是 LIKE)的设置函数为getQueryColumnsOpera,如果有需要重写即可,返回object
 * {
 *    column: Opera.xxx
 * }
 */
export abstract class BaseServiceV2 {
  @App()
  app: Application;

  @Inject()
  ctx: Context;

  @InjectEntityModel(operaModel, 'logs')
  _operaLogsModel: Repository<operaModel>;
  @InjectEntityModel(Tags)
  _tagsModel: Repository<Tags>;

  @Inject()
  private webRouterService: MidwayWebRouterService;

  /**service的model,数据库操作 */
  abstract model: any;
  /**返回{column:查询方式}.特殊的查询需要写入,不写入默认采用 =  */
  getQueryColumnsOpera() {
    return {};
  }
  /** */
  Opera = Opera;
  /**
   * 默认查询列表
   * @param query @Query直接获取的参数
   * @param param @Query和调用前可组织的参数
   * @param {object} operas 指定查询方式
   * @property {number} param.skip 查询数据起始位置
   * @property {number} param.limit 查询数据的数量
   * @property {object} param.order 使用的排序 {${column}:'DESC'/'ASC'}
   * @property {Array<string>} selects 指定显示的字段
   */
  async query(query: object = {}, meta: any = {}, operas?) {
    let skip = get(meta, 'skip', 0);
    let limit = get(meta, 'limit', 0);
    const order = get(meta, 'order', {});
    const selects = get(meta, 'selects', []);
    const builder = await this.model.createQueryBuilder();
    if (selects.length > 0) {
      // 字段是直接传来的,正常限制,需要加上model的name.否则会导致什么字段都没有
      const modelName = this.model.metadata.name;
      builder.select(selects.map(i => `${modelName}.${i}`));
    }
    // 组织查询顺序
    let orderObject: any = {};
    // 如果有自定义顺序,则按照自定义顺序来, 没有自定义顺序,默认按创建时间的desc查询
    if (Object.keys(order).length > 0) {
      for (const column in order) orderObject[column] = order[column];
    } else orderObject = { created_time: 'DESC' };
    // 没有传如何查询,就获取query查询设置的默认查询方式
    if (!operas) operas = this.getQueryColumnsOpera();
    this.completeBuilderCondition(builder, query, operas);
    // 分页
    if (isString(skip)) {
      skip = parseInt(skip);
      if (isNumber(skip)) builder.skip(skip);
    } else if (isNumber(skip)) builder.skip(skip);
    if (isString(limit)) {
      limit = parseInt(limit);
      if (isNumber(limit)) builder.take(limit);
    } else if (isNumber(limit)) builder.take(limit);
    // 排序
    builder.orderBy(orderObject);
    // 执行
    if (this.app.getEnv() !== 'prod' && this.app.getEnv() !== 'production') {
      console.log(builder.getSql());
    }
    const data = await builder.getMany();
    const total = await builder.getCount();
    return { data, total };
  }

  /**
   *
   * @param builder model的createQueryBuilder,只有到最后要查数据的时候才是异步的
   * @param {object} query 查询条件
   * @param {object} operas 指定查询方式
   */
  completeBuilderCondition(builder, query = {}, operas = {}) {
    // 组织查询条件
    if (!query) return;
    const searchColumns = Object.keys(query);
    if (searchColumns.length <= 0) return;
    for (let i = 0; i < searchColumns.length; i++) {
      const key = searchColumns[i];
      const value = query[key];
      if (!value) continue;
      /**该字段的查询方式 */
      const opera = get(operas, key);
      /**builder的使用函数名 */
      let method = 'where';
      if (i === 0) method = 'where';
      else method = 'andWhere';
      let str;
      let params;
      // 需要给变量位置重命名,否则多个条件叠加后,都会使用第一个参数
      const valueStr = `value${i}`;
      let valueArr = [];
      const strArr = [];
      switch (opera) {
        case this.Opera.Between:
          str = `"${key}" Between :${valueStr}_1 AND :${valueStr}_2`;
          params = { [`${valueStr}_1`]: head(value), [`${valueStr}_2`]: last(value) };
          break;
        case this.Opera.Not:
          str = `"${key}" != :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.Like:
          str = `"${key}" Like :${valueStr}`;
          params = { [`${valueStr}`]: `%${value}%` };
          break;
        case this.Opera.LikeLeft:
          str = `"${key}" Like :${valueStr}`;
          params = { [`${valueStr}`]: `%${value}` };
          break;
        case this.Opera.LikeRight:
          str = `"${key}" Like :${valueStr}`;
          params = { [`${valueStr}`]: `${value}%` };
          break;
        case this.Opera.ILike:
          str = `"${key}" Not Like :${valueStr}`;
          params = { [`${valueStr}`]: `%${value}%` };
          break;
        case this.Opera.ILikeLeft:
          str = `"${key}" Not Like :${valueStr}`;
          params = { [`${valueStr}`]: `%${value}` };
          break;
        case this.Opera.ILikeRight:
          str = `"${key}" Not Like :${valueStr}`;
          params = { [`${valueStr}`]: `${value}%` };
          break;
        case this.Opera.LessThan:
          str = `"${key}" < :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.LessThanOrEqual:
          str = `"${key}" <= :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.MoreThan:
          str = `"${key}" > :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.MoreThanOrEqual:
          str = `"${key}" >= :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.In:
          if (!isArray(value)) str = `"${key}" IN (:${valueStr})`;
          else str = `"${key}" IN (:...${valueStr})`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.IsNull:
          str = `"${key}" IS NULL`;
          break;
        case this.Opera.IsNotNull:
          str = `"${key}" IS NOT NULL`;
          params = { [`${valueStr}`]: value };
          break;
        case this.Opera.Json:
          params = {};
          if (isArray(value)) valueArr = value;
          else valueArr = [value];
          for (let vi = 0; vi < valueArr.length; vi++) {
            const v = valueArr[vi];
            const mvalKey = `${valueStr}${vi}`;
            const mstr = `JSONB_EXISTS("${key}", :${mvalKey})`;
            strArr.push(mstr);
            params[mvalKey] = v;
          }
          str = `(${strArr.join(' OR ')})`;
          break;
        case this.Opera.Equal:
        default:
          str = `"${key}" = :${valueStr}`;
          params = { [`${valueStr}`]: value };
          break;
      }
      if (!str) continue;
      builder[method](str, params);
    }
  }

  /**
   * 单查询，不止用id还可以根据别的条件,默认全等,可以在调用时,进行查询方式设置
   * @param {object} query 查询条件
   * @param {object} operas 指定查询方式
   */
  async fetch(query: object, operas = {}) {
    const builder = this.model.createQueryBuilder();
    this.completeBuilderCondition(builder, query, operas);
    const result = await builder.getOne();
    return result;
  }

  /**创建 */
  async create(data: object) {
    // 设置 创建数据的人
    const user = get(this.ctx, 'user');
    if (user) {
      // 查询本表是否有data_onwer字段,有再添加.
      const hasColumn = this.checkModelHaveColumn('data_onwer');
      if (hasColumn) Object.assign(data, { data_owner: get(user, 'id') });
    }
    const result = await this.model.insert(data);
    const id = get(result, 'identifiers.0.id');
    // 没有id估计是出错了
    if (!id) return;
    const createData = await this.fetch({ id });
    // 没有查出数据,也是有问题
    if (!createData) return;
    // 处理标签
    await this.dealTags(createData);
    // 添加,没有原数据,只有新数据
    await this.dealLog(null, createData);
    return createData;
  }

  /**修改，单修改/多修改是统一修改为 */
  async update(query: object = {}, data: object) {
    // 没有范围的修改不允许执行
    if (Object.keys(query).length <= 0) return;
    // 处理数据, 只将是本表的字段拿出来保存
    const columns = this.model.metadata.columns;
    /**将array的列设置 转换为object,以便query使用*/
    const columnsObject = {};
    // 整理成object
    for (const c of columns) columnsObject[c.propertyName] = c.type.toString();
    const updateData = {};
    const notDealColumn = ['id', 'created_time', 'update_time', 'data_owner', '__v'];
    for (const column in columnsObject) {
      if (notDealColumn.includes(column)) continue;
      const val = data[column];
      if (isNull(val) || isUndefined(val)) continue;
      updateData[column] = val;
    }
    // 找到原数据
    const originDataBuilder = this.model.createQueryBuilder();
    this.completeBuilderCondition(originDataBuilder, query);
    const origin_data = await originDataBuilder.getMany(query);
    if (origin_data.length <= 0) return;
    await this.model.update(query, updateData);
    const new_data = await originDataBuilder.getMany(query);
    if (new_data.length <= 0) return;
    // 处理标签
    await this.dealTags(new_data);
    // 处理日志
    await this.dealLog(origin_data, new_data);
  }

  /**删除，单删多删都行 */
  async delete(query: object) {
    // 没有范围不能删除,清空表需要特殊处理
    if (query && Object.keys(query).length <= 0) return;
    // 删除前,先查出来数据, 找到原数据
    const originDataBuilder = this.model.createQueryBuilder();
    this.completeBuilderCondition(originDataBuilder, query);
    const origin_data = await originDataBuilder.getMany(query);
    if (origin_data.length <= 0) return;
    const result = await this.model.delete(query);
    // 处理日志
    await this.dealLog(origin_data, null);
    return result;
  }

  /**
   * 整理日志
   * @param origin_data 原数据
   * @param new_data 新数据
   */
  async dealLog(origin_data, new_data) {
    // 添加,没有原数据,只有新数据
    const mn = this.model.metadata.tableName;
    let newObject;
    if (new_data) newObject = { [mn]: new_data };
    let originObject;
    if (origin_data) originObject = { [mn]: origin_data };
    await this.toMakeLogs(originObject, newObject);
  }
  /**
   * 从操作日志服务迁移过来了.不能初始化service,否则会循环引用
   * 生成日志
   * 原数据和新数据格式:
   * {
   *    [表名]:[数据,...数据]
   * }
   * @param origin_data 原数据
   * @param new_data 新数据
   * */
  async toMakeLogs(origin_data?: object, new_data?: object) {
    const user = this.ctx.user;
    if (user && user.id) {
      const req = this.ctx.request;
      const logInfo: any = {
        // 操作人
        operator_id: user.id,
        operator_name: get(user, 'nick_name', get(user, 'name')),
        // ip
        ip: get(req, 'header.x-forwarded-for', req.ip),
        // 设备
        device: get(req, 'header.user-agent'),
        // 操作时间
        time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        // 操作地址
        referer: get(req, 'header.referer'),
        // 请求地址uri
        path: get(req, 'path'),
        // 地址参数
        params: get(req, 'params'),
        // 请求参数
        query: get(req, 'query'),
        // 方法体参数
        body: get(req, 'body'),
        // 操作业务:可以通过路由注解的routerName来获取
        opera: null,
        // 变动值: 表:[数据] 需要自己组织
        origin_data: origin_data,
        new_data: new_data,
      };
      const routeInfo = await this.webRouterService.getMatchedRouterInfo(this.ctx.path, this.ctx.method);
      if (routeInfo) logInfo.opera = get(routeInfo, 'routerName');
      await this._operaLogsModel.insert(logInfo);
    }
  }

  /**
   * 处理标签
   * @param {object|Array} data 操作后的数据
   */
  async dealTags(data) {
    // 处理标签,查询本表是否有标签字段.标签都是tags
    const hasColumn_tags = this.checkModelHaveColumn('tags');
    if (!hasColumn_tags) return;
    let tags;
    // 判断tags数据类型
    if (isObject(data)) {
      tags = get(data, 'tags', []);
      if (!tags) return;
      // 清除空数据
      tags = compact(tags);
    } else if (isArray(data)) {
      // 提取tags
      tags = data.map(i => i.tags);
      // 降至一维
      tags = flattenDeep(tags);
      // 去重
      tags = uniq(tags);
      // 清除空数据
      tags = compact(tags);
    }
    if (!tags || tags.length <= 0) return;
    // 处理标签
    for (const val of tags) {
      const where = { title: val };
      const tag = await this._tagsModel.findOne({ where });
      if (!tag) await this._tagsModel.save(where);
    }
  }

  /**检查有没有指定字段,是从model的映射关系中查询下 */
  private checkModelHaveColumn(columnName: string) {
    const columns = this.model.metadata.columns;
    const has = columns.find(f => get(f, 'propertyName') === columnName);
    return !isUndefined(has);
  }
}
