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";

/**
 * Conversor optimizado Criteria → Sequelize FindOptions
 */
export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
  convert(criteria: Criteria, params: ConvertParams = {}): FindOptions {
    const options: FindOptions = params.baseOptions ? { ...params.baseOptions } : {};
    const { mappings = {} } = params;

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

    return options;
  }

  /** Filtros simples (sin anidaciones complejas) */
  public applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings) {
    if (!criteria.hasFilters()) return;

    const filters: WhereOptions[] = [];

    for (const filter of criteria.filters.value) {
      const field = mappings[filter.field.value] || filter.field.value;
      const operator = this.mapOperator(filter.operator.value);
      const value = this.transformValue(operator, filter.value.value);
      filters.push({ [field]: { [operator]: value } });
    }

    options.where = options.where
      ? { [Op.and]: [options.where, ...filters] }
      : { [Op.and]: filters };
  }

  /** QuickSearch seguro con validación FULLTEXT */
  public applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void {
    const {
      mappings = {},
      searchableFields = [],
      database,
      enableFullText = false,
    } = params as ConvertParams & { database?: Sequelize };

    const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
    if (!term || searchableFields.length === 0 || !enableFullText) return;

    // Validación defensiva
    if (!database) {
      const msg = `[CriteriaToSequelizeConverter] enableFullText=true pero falta 'database' en params.`;
      if (params.strictMode) throw new Error(msg);
      console.warn(msg);
      return;
    }

    const booleanTerm = term
      .split(/\s+/)
      .map((w) => `+${w}*`)
      .join(" ");

    const mappedFields = searchableFields.map((f) => mappings[f] || f);
    const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
      booleanTerm
    )} IN BOOLEAN MODE)`;
    const matchLiteral = Sequelize.literal(matchExpr);

    // Añadir campo virtual "score"
    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 });
    options.where = options.where
      ? { [Op.and]: [options.where, scoreCondition] }
      : { [Op.and]: [scoreCondition] };

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

  /** Ordenación validada y parametrizable */
  public applyOrder(
    options: FindOptions,
    criteria: Criteria,
    mappings: CriteriaMappings,
    params: ConvertParams
  ): void {
    if (!criteria.hasOrder()) return;

    const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
    const direction = criteria.order.orderType.value.toUpperCase();

    const allowedFields = params.allowedFields ?? ["id", "created_at"];
    const strict = params.strictMode ?? false;

    if (!allowedFields.includes(field)) {
      const msg = `[CriteriaToSequelizeConverter] Ignored ORDER BY '${field}' (not in allowedFields).`;
      if (strict) throw new Error(msg);
      console.warn(msg);
      return;
    }

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

  /** Paginación estándar */
  public applyPagination(options: FindOptions, criteria: Criteria): void {
    if (criteria.pageSize != null) {
      options.limit = criteria.pageSize;
      if (criteria.pageNumber != null) {
        options.offset = criteria.pageSize * criteria.pageNumber;
      }
    }
  }

  /** Mapeo de operadores Criteria → Sequelize */
  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;
  }
}
