const db = require('../models');
const Utils = require('../utils');
const { parse } = require('json2csv');

const Sequelize = db.Sequelize;
const Op = Sequelize.Op;

class GenericDBApi {
  static get MODEL() {
    throw new Error('MODEL must be defined in subclass');
  }

  static get TABLE_NAME() {
    return this.MODEL.getTableName();
  }

  static get SEARCHABLE_FIELDS() {
    return [];
  }

  static get RANGE_FIELDS() {
    return [];
  }

  static get ENUM_FIELDS() {
    return [];
  }

  static get RELATION_FILTERS() {
    return [];
  }

  static get CSV_FIELDS() {
    return ['id', 'createdAt'];
  }

  static get AUTOCOMPLETE_FIELD() {
    return 'name';
  }

  static get ASSOCIATIONS() {
    return [];
  }

  static get FIND_BY_INCLUDES() {
    return [];
  }

  static get FIND_ALL_INCLUDES() {
    return [];
  }

  static getFieldMapping(data) {
    return data;
  }

  static async create(data, options = {}) {
    const currentUser = options.currentUser || { id: null };
    const transaction = options.transaction;

    const mappedData = this.getFieldMapping(data);

    const record = await this.MODEL.create(
      {
        ...mappedData,
        importHash: data.importHash || null,
        createdById: currentUser.id,
        updatedById: currentUser.id,
      },
      { transaction }
    );

    for (const assoc of this.ASSOCIATIONS) {
      if (data[assoc.field] !== undefined) {
        await record[assoc.setter](data[assoc.field] || (assoc.isArray ? [] : null), { transaction });
      }
    }

    return record;
  }

  static async bulkImport(data, options = {}) {
    const currentUser = options.currentUser || { id: null };
    const transaction = options.transaction;

    const recordsData = data.map((item, index) => ({
      ...this.getFieldMapping(item),
      importHash: item.importHash || null,
      createdById: currentUser.id,
      updatedById: currentUser.id,
      createdAt: new Date(Date.now() + index * 1000),
    }));

    return this.MODEL.bulkCreate(recordsData, { transaction });
  }

  static async update(id, data, options = {}) {
    const currentUser = options.currentUser || { id: null };
    const transaction = options.transaction;

    const record = await this.MODEL.findByPk(id, { transaction });

    if (!record) {
      throw { status: 404, message: `${this.TABLE_NAME} not found` };
    }

    const updatePayload = { updatedById: currentUser.id };
    const mappedData = this.getFieldMapping(data);

    for (const [key, value] of Object.entries(mappedData)) {
      if (value !== undefined) {
        updatePayload[key] = value;
      }
    }

    await record.update(updatePayload, { transaction });

    for (const assoc of this.ASSOCIATIONS) {
      if (data[assoc.field] !== undefined) {
        await record[assoc.setter](data[assoc.field], { transaction });
      }
    }

    return record;
  }

  static async deleteByIds(ids, options = {}) {
    const currentUser = options.currentUser || { id: null };
    const transaction = options.transaction;

    const records = await this.MODEL.findAll({
      where: { id: { [Op.in]: ids } },
      transaction,
    });

    for (const record of records) {
      await record.update({ deletedBy: currentUser.id }, { transaction });
    }
    for (const record of records) {
      await record.destroy({ transaction });
    }

    return records;
  }

  static async remove(id, options = {}) {
    const currentUser = options.currentUser || { id: null };
    const transaction = options.transaction;

    const record = await this.MODEL.findByPk(id, { transaction });

    if (!record) {
      throw { status: 404, message: `${this.TABLE_NAME} not found` };
    }

    await record.update({ deletedBy: currentUser.id }, { transaction });
    await record.destroy({ transaction });

    return record;
  }

  static async findBy(where, options = {}) {
    const transaction = options.transaction;
    const include = options.include !== undefined
      ? options.include
      : this.FIND_BY_INCLUDES;

    const record = await this.MODEL.findOne({
      where,
      transaction,
      include,
    });

    if (!record) {
      return null;
    }

    return record.get({ plain: true });
  }

  static async findAll(filter = {}, options = {}) {
    filter = filter || {};
    const limit = filter.limit || 0;
    const currentPage = +filter.page || 0;
    const offset = currentPage * limit;

    let where = {};
    let include = [...this.FIND_ALL_INCLUDES];

    if (filter.id) {
      where.id = Utils.uuid(filter.id);
    }

    for (const field of this.SEARCHABLE_FIELDS) {
      if (filter[field]) {
        where[Op.and] = Utils.ilike(this.TABLE_NAME, field, filter[field]);
      }
    }

    for (const field of this.RANGE_FIELDS) {
      const rangeKey = `${field}Range`;
      if (filter[rangeKey]) {
        const [start, end] = filter[rangeKey];
        if (start !== undefined && start !== null && start !== '') {
          where[field] = { ...where[field], [Op.gte]: start };
        }
        if (end !== undefined && end !== null && end !== '') {
          where[field] = { ...where[field], [Op.lte]: end };
        }
      }
    }

    for (const field of this.ENUM_FIELDS) {
      if (filter[field] !== undefined) {
        where[field] = filter[field];
      }
    }

    if (filter.active !== undefined) {
      where.active = filter.active === true || filter.active === 'true';
    }

    if (filter.createdAtRange) {
      const [start, end] = filter.createdAtRange;
      if (start !== undefined && start !== null && start !== '') {
        where.createdAt = { ...where.createdAt, [Op.gte]: start };
      }
      if (end !== undefined && end !== null && end !== '') {
        where.createdAt = { ...where.createdAt, [Op.lte]: end };
      }
    }

    for (const rel of this.RELATION_FILTERS) {
      if (filter[rel.filterKey]) {
        const searchTerms = filter[rel.filterKey].split('|');
        const relInclude = {
          model: rel.model,
          as: rel.as,
          required: searchTerms.length > 0,
          where: searchTerms.length > 0 ? {
            [Op.or]: [
              { id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
              rel.searchField ? {
                [rel.searchField]: {
                  [Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
                }
              } : {}
            ]
          } : undefined
        };
        include = [relInclude, ...include];
      }
    }

    const queryOptions = {
      where,
      include,
      distinct: true,
      order: filter.field && filter.sort
        ? [[filter.field, filter.sort]]
        : [['createdAt', 'desc']],
      transaction: options.transaction,
    };

    if (!options.countOnly) {
      queryOptions.limit = limit ? Number(limit) : undefined;
      queryOptions.offset = offset ? Number(offset) : undefined;
    }

    try {
      const { rows, count } = await this.MODEL.findAndCountAll(queryOptions);
      return {
        rows: options.countOnly ? [] : rows,
        count,
      };
    } catch (error) {
      console.error('Error executing query:', error);
      throw error;
    }
  }

  static async findAllAutocomplete(query, limit, offset) {
    let where = {};

    if (query) {
      where = {
        [Op.or]: [
          { id: Utils.uuid(query) },
          Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query),
        ],
      };
    }

    const records = await this.MODEL.findAll({
      attributes: ['id', this.AUTOCOMPLETE_FIELD],
      where,
      limit: limit ? Number(limit) : undefined,
      offset: offset ? Number(offset) : undefined,
      order: [[this.AUTOCOMPLETE_FIELD, 'ASC']],
    });

    return records.map((record) => ({
      id: record.id,
      label: record[this.AUTOCOMPLETE_FIELD],
    }));
  }

  static toCSV(rows) {
    const opts = { fields: this.CSV_FIELDS };
    return parse(rows, opts);
  }
}

module.exports = GenericDBApi;
