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 [];
  }

  /**
   * UUID fields that require validation before querying.
   * These are typically foreign key fields like 'projectId'.
   * Invalid UUIDs will return empty results instead of causing DB errors.
   * Override in subclass to specify fields.
   * Example: return ['projectId', 'userId'];
   */
  static get UUID_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 [];
  }

  /**
   * Fields that should be automatically JSON-stringified
   * Override in subclass to specify fields.
   * Example: return ['settings_json', 'metadata_json'];
   */
  static get JSON_FIELDS() {
    return [];
  }

  /**
   * Custom field transformers for data mapping.
   * Override in subclass to add custom transformations.
   * Example:
   *   return {
   *     email: (value) => value?.toLowerCase().trim(),
   *     slug: (value) => value?.toLowerCase().replace(/\s+/g, '-'),
   *   };
   */
  static get FIELD_TRANSFORMERS() {
    return {};
  }

  /**
   * Field mapping configuration for declarative field handling.
   * Override in subclass to specify how fields should be mapped.
   * Example:
   *   return {
   *     name: { default: null },
   *     sort_order: { default: 0 },
   *     is_active: { default: true },
   *   };
   */
  static get FIELD_DEFAULTS() {
    return {};
  }

  /**
   * Transform input data for database operations.
   * Template Method Pattern: Uses JSON_FIELDS, FIELD_TRANSFORMERS, and FIELD_DEFAULTS
   * to declaratively transform data, reducing boilerplate in subclasses.
   *
   * Override this method for complex custom transformations that can't be
   * expressed declaratively.
   *
   * @param {Object} data - Input data to transform
   * @returns {Object} - Transformed data ready for database
   */
  static getFieldMapping(data) {
    if (!data) return data;
    const mapped = { ...data };

    // Apply field defaults
    for (const [field, config] of Object.entries(this.FIELD_DEFAULTS)) {
      if (mapped[field] === undefined) {
        mapped[field] = config.default;
      } else if (mapped[field] === null && config.nullDefault !== undefined) {
        mapped[field] = config.nullDefault;
      }
    }

    // Auto-stringify JSON fields
    for (const field of this.JSON_FIELDS) {
      if (mapped[field] !== undefined && mapped[field] !== null) {
        if (typeof mapped[field] !== 'string') {
          mapped[field] = JSON.stringify(mapped[field]);
        }
      }
    }

    // Apply custom transformers
    for (const [field, transformer] of Object.entries(this.FIELD_TRANSFORMERS)) {
      if (mapped[field] !== undefined) {
        mapped[field] = transformer(mapped[field]);
      }
    }

    return mapped;
  }

  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) {
      if (!Utils.isValidUuid(filter.id)) {
        return { rows: [], count: 0 };
      }
      where.id = 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];
      }
    }

    // Validate UUID fields - return empty results for invalid UUIDs
    for (const field of this.UUID_FIELDS) {
      if (filter[field] !== undefined) {
        if (!Utils.isValidUuid(filter[field])) {
          return { rows: [], count: 0 };
        }
        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 validUuids = Utils.filterValidUuids(searchTerms);

        // Build OR conditions array
        const orConditions = [];

        // Add UUID condition only if there are valid UUIDs
        if (validUuids.length > 0) {
          orConditions.push({ id: { [Op.in]: validUuids } });
        }

        // Add text search condition if searchField is defined
        if (rel.searchField) {
          orConditions.push({
            [rel.searchField]: {
              [Op.or]: searchTerms.map((term) => ({
                [Op.iLike]: `%${term}%`,
              })),
            },
          });
        }

        const relInclude = {
          model: rel.model,
          as: rel.as,
          required: orConditions.length > 0,
          where:
            orConditions.length > 0 ? { [Op.or]: orConditions } : 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) {
      const orConditions = [
        Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query),
      ];

      if (Utils.isValidUuid(query)) {
        orConditions.unshift({ id: query });
      }

      where = { [Op.or]: orConditions };
    }

    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;
