import { FindOptions, Op, OrderItem, Sequelize, WhereOptions } from "sequelize";
import { Criteria } from "./critera";
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types";
import { appendOrder, prependOrder } from "./utils";

export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
  //convert(fieldsToSelect: string[], criteria: Criteria, mappings: Mappings = {}): FindOptions {
  convert(criteria: Criteria, params: ConvertParams = {}): FindOptions {
    const options: FindOptions = {};
    const { mappings = {} } = params;

    // Selección de campos
    /*if (fieldsToSelect.length > 0) {
      options.attributes = fieldsToSelect;
    }*/

    this.applyFilters(options, criteria, mappings);
    this.applyQuickSearch(options, criteria, params);
    this.applyOrder(options, criteria, mappings);
    this.applyPagination(options, criteria);

    return options;
  }

  public applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings) {
    const filterConditions: WhereOptions = {};
    if (criteria.hasFilters()) {
      criteria.filters.value.forEach((filter) => {
        const field = mappings[filter.field.value] || filter.field.value;
        const operator = this.mapOperator(filter.operator.value);
        const value = filter.value.value;

        if (!filterConditions[field]) {
          filterConditions[field] = {};
        }

        filterConditions[field][operator] = this.transformValue(operator, value);
      });

      if (options.where) {
        options.where = { [Op.and]: [options.where, { [Op.or]: filterConditions }] };
      } else {
        options.where = { [Op.or]: filterConditions };
      }
    }
  }

  public applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void {
    const {
      mappings = {},
      searchableFields = [],
      database,
    } = params as ConvertParams & {
      database: Sequelize;
    };

    const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";

    // Si no hay término de búsqueda o no hay campos configurados, no hacemos nada
    if (term === "" || searchableFields.length === 0) {
      return;
    }

    // Construimos query de boolean mode con prefijo
    const booleanTerm = term
      .split(/\s+/)
      .map((w) => `+${w}*`)
      .join(" ");

    // Campos reales (con mappings aplicados)
    const mappedFields = searchableFields.map((field) => mappings[field] || field);

    const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
      booleanTerm
    )} IN BOOLEAN MODE)`;

    const matchLiteral = Sequelize.literal(matchExpr);

    // Añadimos score a los attributes (sin machacar si ya existen)
    if (!options.attributes) {
      options.attributes = { include: [] };
    }

    if (Array.isArray(options.attributes)) {
      options.attributes.push([matchLiteral, "score"]);
    } else {
      options.attributes.include = options.attributes.include || [];
      options.attributes.include.push([matchLiteral, "score"]);
    }

    // WHERE score > 0
    const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
    if (options.where) {
      options.where = { [Op.and]: [options.where, { [Op.or]: scoreCondition }] };
    } else {
      options.where = { [Op.and]: scoreCondition };
    }

    // Ordenar por relevancia (score)
    prependOrder(options, [Sequelize.literal("score"), "DESC"]);
  }

  public applyOrder(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void {
    if (criteria.hasOrder()) {
      const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
      const direction = criteria.order.orderType.value.toUpperCase();

      appendOrder(options, [[field, direction]] as OrderItem[]);
    }
  }

  public applyPagination(options: FindOptions, criteria: Criteria): void {
    if (criteria.pageSize !== null) {
      options.limit = criteria.pageSize;
    }

    if (criteria.pageSize !== null && criteria.pageNumber !== null) {
      options.offset = criteria.pageSize * criteria.pageNumber;
    }
  }

  private mapOperator(operator: string): symbol {
    switch (operator) {
      case "CONTAINS":
        return Op.like;
      case "NOT_CONTAINS":
        return Op.notLike;
      case "NOT_EQUALS":
        return Op.ne;
      case "GREATER_THAN":
        return Op.gt;
      case "GREATER_THAN_OR_EQUAL":
        return Op.gte;
      case "LOWER_THAN":
        return Op.lt;
      case "LOWER_THAN_OR_EQUAL":
        return Op.lte;
      case "EQUALS":
        return Op.eq;
      default:
        return Op.eq;
    }
  }

  private transformValue(operator: symbol, value: any): any {
    if (operator === Op.like || operator === Op.notLike) {
      return `%${value}%`;
    }
    return value;
  }
}
