const _ = require("lodash");

function _debugModelInfo(model) {
  if (!model.name) return;

  console.log("\n\n----------------------------------\n", model.name, "\n----------------------------------");

  console.log("\nAttributes");
  if (model.rawAttributes) {
    for (let attr of Object.keys(model.rawAttributes)) {
      console.log(model.name + "." + attr);
    }
  }

  console.log("\nAssociations");
  if (model.associations) {
    for (let assoc of Object.keys(model.associations)) {
      console.log("as: ", model.associations[assoc].as, "type: ", model.associations[assoc].associationType);
      for (let accessor of Object.keys(model.associations[assoc].accessors)) {
        console.log(accessor);
        console.log(model.name + "." + model.associations[assoc].accessors[accessor] + "()");
      }
    }
  }

  if (model.Instance && model.Instance.super_) {
    console.log("\nCommon");
    for (let func of Object.keys(model.Instance.super_.prototype)) {
      if (func === "constructor" || func === "sequelize") continue;
      console.log(model.name + "." + func + "()");
    }
  }

  console.log("\n\n----------------------------------\n", "END", "\n----------------------------------");

  return;
}

function foundModelAssociation(model, associationName) {
  let result = false;

  if (typeof model !== "function" || typeof model.associations !== "object") {
    throw new Error("Model should be an object with the 'associations' property.");
  }

  Object.keys(model.associations).forEach((key) => {
    const nameAs = model.associations[key].as;
    if (nameAs.toUpperCase() == associationName.toUpperCase()) {
      const item = model.associations[key];
      const accessors = item.accessors;
      result = {
        name: item.as,
        type: item.associationType,
        accessors: accessors,
        countFunc: accessors["count"],
        getFunc: accessors["get"],
      };
    }
  });
  return result;
}

const parseParamsToFindOptions = (params) => {
  const result = {};

  // Include All
  result.include = params.includeAll ? [{ all: true }] : [];

  // Query & params
  result.where = {};
  if (params.query) {
    result.where = params.query;
  }
  if (params.params) {
    result.where = Object.assign(result.where, params.params);
  }

  // Paginate
  if (params.paginate) {
    result.offset = params.paginate.limit * (params.paginate.page - 1);
    result.limit = params.paginate.limit; // result.offset + params.paginate.limit;
  }

  // Order
  result.order = [];
  if (params.sort) {
    Object.keys(params.sort).forEach((key) => {
      let dir = params.sort[key] ? "ASC" : "DESC";
      result.order.push([key, dir]);
    });
  }

  // Attributes
  if (params.fields) {
    if (params.fields.validFields) {
      result.attributes = params.fields.validFields;
    }
    if (params.fields.invalidFields && Array.isArray(params.fields.invalidFields)) {
      result.attributes = {
        ...result.attributes,
        exclude: params.fields.invalidFields,
      };
    }
  }

  return result;
};

function hasAssociation(params) {
  return params ? (params.params ? (params.params.association ? params.params.association : false) : false) : false;
}

const defaultOptions = {};

const generateService = (model, extraMethods = {}, options = defaultOptions) => {
  const defaultService = {
    fetchAssociation: async (params, context) => {
      const _fetchOne = async (params, context) => {
        const findOptions = parseParamsToFindOptions(params);
        return await model.scope(context.scopes).findOne(findOptions);
      };

      const associationName = hasAssociation(params);
      console.log("associationName => ", associationName);
      delete params.params.association;

      const associationInfo = foundModelAssociation(model, associationName);
      if (associationInfo) {
        console.log(associationInfo);
        const master = await _fetchOne({ params: params.params }, context);
        console.log(master);
        const detailRows = await master[associationInfo.getFunc]();

        const details = {
          rows: detailRows,
          count: detailRows.length,
        };
        console.log(details);
        return details;
      } else {
        return {
          rows: ["Association not exists"],
          count: 0,
        };
      }
    },

    fetchAll: async (params, context) => {
      if (hasAssociation(params)) {
        return defaultService.fetchAssociation(params, context);
      } else {
        const findOptions = parseParamsToFindOptions(params);

        // Necesario para el cálculo del count
        // https://github.com/sequelize/sequelize/issues/10557
        findOptions.distinct = true;

        const result = await model.scope(context.scopes).findAndCountAll(findOptions);

        if (extraMethods.afterFetchAll) {
          return extraMethods.afterFetchAll(result, params, context);
        } else {
          return result;
        }
      }
    },

    fetchOne: async (params, context) => {
      const findOptions = parseParamsToFindOptions(params);
      const result = await model.scope(context.scopes).findOne(findOptions);
      if (extraMethods.afterFetchOne) {
        return extraMethods.afterFetchOne(result, params, context);
      } else {
        return result;
      }
    },

    count: async (params, context) => {
      const findOptions = parseParamsToFindOptions(params);
      const result = await model.scope(context.scopes).count(findOptions);

      if (extraMethods.afterCount) {
        return extraMethods.afterCount(result, params, context);
      } else {
        return result;
      }
    },

    create: async (values, context) => {
      if (extraMethods.beforeCreate) {
        values = await extraMethods.beforeCreate(values, context);
      }
      var result = await model.scope(context.scopes).create(values);
      if (extraMethods.afterCreate) {
        result = extraMethods.afterCreate(result, values, context);
      }
      return result;
    },

    update: async (params, values, context) => {
      const findOptions = parseParamsToFindOptions(params);
      if (extraMethods.beforeUpdate) {
        values = extraMethods.beforeUpdate(values, findOptions, context);
      }
      var result = await model.scope(context.scopes).update(values, findOptions);
      var row = await defaultService.fetchOne(params, context);

      if (extraMethods.afterUpdate) {
        row = extraMethods.afterUpdate(row, values, context);
      }
      return result;
    },

    delete: async (params, context) => {
      const findOptions = parseParamsToFindOptions(params);
      const numAffectedRows = await model.scope(context.scopes).destroy(findOptions);

      var result = numAffectedRows > 0;

      if (extraMethods.afterDelete) {
        extraMethods.afterDelete(result, findOptions, context);
      }
      return result;
    },

    search: async (params, context) => {
      /* 
            // Convert `params` object to filters compatible with Bookshelf.
            const filters = strapi.utils.models.convertParams('post', params);
            // Select field to populate.
            const populate = Post.associations
                .filter(ast => ast.autoPopulate !== false)
                .map(ast => ast.alias);

            const associations = Post.associations.map(x => x.alias);
            const searchText = Object.keys(Post._attributes)
                .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
                .filter(attribute => ['string', 'text'].includes(Post._attributes[attribute].type));

            const searchNoText = Object.keys(Post._attributes)
                .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
                .filter(attribute => !['string', 'text', 'boolean', 'integer', 'decimal', 'float'].includes(Post._attributes[attribute].type));

            const searchInt = Object.keys(Post._attributes)
                .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
                .filter(attribute => ['integer', 'decimal', 'float'].includes(Post._attributes[attribute].type));

            const searchBool = Object.keys(Post._attributes)
                .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
                .filter(attribute => ['boolean'].includes(Post._attributes[attribute].type));

            const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, '');

            return Post.query(qb => {
                // Search in columns which are not text value.
                searchNoText.forEach(attribute => {
                    qb.orWhereRaw(`LOWER(${attribute}) LIKE '%${_.toLower(query)}%'`);
                });

                if (!_.isNaN(_.toNumber(query))) {
                    searchInt.forEach(attribute => {
                        qb.orWhereRaw(`${attribute} = ${_.toNumber(query)}`);
                    });
                }

                if (query === 'true' || query === 'false') {
                    searchBool.forEach(attribute => {
                        qb.orWhereRaw(`${attribute} = ${_.toNumber(query === 'true')}`);
                    });
                }

                // Search in columns with text using index.
                switch (Post.client) {
                    case 'mysql':
                        qb.orWhereRaw(`MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`, `*${query}*`);
                        break;
                    case 'pg':
                        {
                            const searchQuery = searchText.map(attribute =>
                                _.toLower(attribute) === attribute ?
                                    `to_tsvector(${attribute})` :
                                    `to_tsvector('${attribute}')`
                            );

                            qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query);
                            break;
                        }
                }

                if (filters.sort) {
                    qb.orderBy(filters.sort.key, filters.sort.order);
                }

                if (filters.skip) {
                    qb.offset(_.toNumber(filters.skip));
                }

                if (filters.limit) {
                    qb.limit(_.toNumber(filters.limit));
                }
            }).fetchAll({
                withRelated: populate
            }); */
    },
  };

  return {
    ...defaultService,
    //...associationControllers
    ...extraMethods,
  };
};

module.exports = {
  _debugModelInfo,
  generateService,
  parseParamsToFindOptions,
  defaultOptions,
};
