const warehouseIds = [1, 44];

module.exports = Self => {
  Self.remoteMethod('calcCatalog', {
    description: 'Get the available and prices list for an item',
    accessType: 'READ',
    accepts: [
      {
        arg: 'id',
        type: 'Number',
        description: 'The item id'
      }, {
        arg: 'dated',
        type: 'Date',
        description: 'The date'
      }, {
        arg: 'addressFk',
        type: 'Number',
        description: 'The address id'
      }, {
        arg: 'agencyModeFk',
        type: 'Number',
        description: 'The agency id'
      }
    ],
    returns: {
      type: ['Object'],
      description: 'The item available and prices list',
      root: true,
    },
    http: {
      path: `/:id/calcCatalog`,
      verb: 'GET'
    }
  });

  Self.calcCatalog = (id, dated, addressFk, agencyModeFk, cb) => {
    Self.dataSource.connector.query(
      `CALL hedera.item_calcCatalog(?, ?, ?, ?)`,
      [id, dated, addressFk, agencyModeFk],
      (err, res) => {
        if (err) return cb(err)
        return cb(null, res[0])
      }
    );
  };

  Self.remoteMethod('catalog', {
    description: 'Get the catalog',
    accessType: 'READ',
    accepts: [
      {
        arg: 'dated',
        type: 'Date',
        description: 'The available date'
      }, {
        arg: 'typeFk',
        type: 'Number',
        description: 'The item type id'
      }, {
        arg: 'categoryFk',
        type: 'Number',
        description: 'The item category id'
      }, {
        arg: 'search',
        type: 'String',
        description: 'The search string'
      }, {
        arg: 'order',
        type: 'String',
        description: 'The order string'
      }, {
        arg: 'limit',
        type: 'Number',
        description: 'The maximum number of registers'
      }, {
        arg: 'tagFilter',
        type: ['Object'],
        description: 'The tag filter object'
      }
    ],
    returns: {
      type: ['Object'],
      description: 'The item list',
      root: true,
    },
    http: {
      path: `/catalog`,
      verb: 'GET'
    }
  });

  Self.catalog = async (dated, typeFk, categoryFk, search, order, limit, tagFilter) => {
    let $ = Self.app.models;
    let itemIds;

    let inboundWhere = {
      warehouseFk: {inq: warehouseIds},
      available: {gt: 0},
      dated: {lte: dated},
      and: [
        {or: [
          {expired: {gt: dated}},
          {expired: null}
        ]}
      ]
    };

    // Applies base filters

    if (/^[0-9]+$/.test(search)) {
      itemIds = [parseInt(search)];
    } else {
      let inbounds = await $.Inbound.find({
        fields: ['itemFk'],
        where: inboundWhere
      });
      itemIds = toValues(inbounds, 'itemFk');

      if (categoryFk || typeFk || search) {
        let where = {
          id: {inq: itemIds}
        };

        if (typeFk) {
          where.typeFk = typeFk;
        } else if (categoryFk) {
          let types = await $.ItemType.find({
            fields: ['id'],
            where: {categoryFk}
          });
          where.typeFk = {inq: toValues(types, 'id')};
        }

        if (search)
          where.longName = {like: `%${search}%`};

        let filter = {
          fields: ['id'],
          where
        };

        let items = await Self.find(filter);
        itemIds = items.map(i => i.id);
      }
    }

    // Applies tag filters

    let baseItemIds = itemIds;
    let tagItems = [];
    let tagFilterIds = [];

    if (tagFilter && tagFilter.length) {
      for (let filter of tagFilter) {
        let cond;
        let values = filter.values;

        if (values.length)
          cond = {value: {inq: values}};
        else if (values.min && values.max)
          cond = {intValue: {between: [values.min, values.max]}};
        else if (values.min)
          cond = {intValue: {gte: values.min}};
        else if (values.max)
          cond = {intValue: {lte: values.max}};

        let where = {
          itemFk: {inq: itemIds},
          tagFk: filter.tagFk
        };
        Object.assign(where, cond);

        let itemTags = await $.ItemTag.find({
          fields: ['itemFk'],
          where
        });
        tagItems.push(toSet(itemTags, 'itemFk'));
        tagFilterIds.push(filter.tagFk);
      }

      itemIds = intersect(tagItems);
    }

    // Obtains distinct tags and it's distinct values
    
    let tags = [];

    if (typeFk || search) {
      let tagValues = await $.ItemTag.find({
        fields: ['tagFk', 'value', 'intValue', 'priority'],
        where: {
          itemFk: {inq: itemIds},
          tagFk: {nin: tagFilterIds}
        },
        order: 'tagFk, value'
      });
      let tagValueMap = toMultiMap(tagValues, 'tagFk');

      for (let i = 0; i < tagItems.length; i++) {
        let tagFk = tagFilter[i].tagFk;
        let itemIds;

        if (tagItems.length > 1) {
          let siblings = tagItems.filter(v => v != tagItems[i]);
          itemIds = intersect(siblings);
        } else
          itemIds = baseItemIds;

        let tagValues = await $.ItemTag.find({
          fields: ['value', 'intValue', 'priority'],
          where: {
            itemFk: {inq: itemIds},
            tagFk: tagFk
          },
          order: 'value'
        });

        tagValueMap.set(tagFk, tagValues);
      }

      let tagIds = [...tagValueMap.keys()];
      tags = await $.Tag.find({
        fields: ['id', 'name', 'isQuantitative', 'unit'],
        where: {
          id: {inq: tagIds}
        }
      });

      for (let tag of tags) {
        let tagValues = tagValueMap.get(tag.id);

        let filter = tagFilter && tagFilter.find(i => i.tagFk == tag.id);
        filter = filter && filter.values;

        let values = toSet(tagValues, 'value');
        if (Array.isArray(filter))
          values = new Set([...filter, ...values]);

        if (tag.isQuantitative) {
          let intValues = toValues(tagValues, 'intValue');

          if (filter) {
            if (filter.min) intValues.push(filter.min);
            if (filter.max) intValues.push(filter.max);
          }

          let min = Math.min(...intValues);
          let max = Math.max(...intValues);
          let dif = max - min;
          let digits = new String(dif).length;
          let step = Math.pow(10, digits - 1);
          if (digits > 1 && step * 5 > dif) step /= 10;

          Object.assign(tag, {
            step,
            min: Math.floor(min / step) * step,
            max: Math.ceil(max / step) * step
          });
        }

        Object.assign(tag, {
          values: [...values],
          filter
        });
      }
    }

    // Obtains items data

    let items = await Self.find({
      fields: ['id', 'longName', 'subName', 'image'],
      where: {id: {inq: itemIds}},
      limit,
      order,
      include: [
        {
          relation: 'tags',
          scope: {
            fields: ['value', 'tagFk'],
            where: {priority: {gt: 4}},
            order: 'priority',
            include: {
              relation: 'tag',
              scope: {fields: ['name']}
            }
          }
        }, {
          relation: 'inbounds',
          scope: {
            fields: ['available', 'dated', 'tableId'],
            where: inboundWhere,
            order: 'dated DESC',
            include: {
              relation: 'buy',
              scope: {fields: ['id', 'price3']}
            },
          }
        }
      ]
    });

    for (let item of items) {
      item.inbound = item.inbounds()[0];
      item.buy = item.inbound && item.inbound.buy();
      item.available = sum(item.inbounds(), 'available');
    }

    return {items, tags};
  };
};

// Array functions

function sum(array, key) {
  if (!Array.isArray(array)) return 0;
  return array.reduce((a, c) => a + c[key], 0);
}

function toMap(objects, key) {
  let map = new Map();
  for (let object of objects)
    map.set(object[key], object);
  return map;
}

function toMultiMap(objects, key) {
  let map = new Map();
  for (let object of objects) {
    let value = map.get(object[key]);
    if (!value) map.set(object[key], value = []);
    value.push(object);
  }
  return map;
}

function toSet(objects, key) {
  let set = new Set();
  for (let object of objects)
    set.add(object[key]);
  return set;
}

function toValues(objects, key) {
  return [...toSet(objects, key)];
}

function intersect(sets) {
  if (!sets.length) return [];
  let array = [];
  let mySets = sets.slice(0);
  let firstSet = mySets.shift();

  for (let value of firstSet) {
    let isOnAll = true;

    for (let set of mySets)
    if (!set.has(value)) {
      isOnAll = false;
      break;
    }

    if (isOnAll)
      array.push(value);
  }

  return array;
}
