import { UndefinedOr } from "../../../../../../utilities";
import { RuleValidator } from "../../../RuleValidator";
import { Result } from "../../Result";
import { ResultCollection } from "../../ResultCollection";
import { StringValueObject } from "../../StringValueObject";
import { Filter, IFilter } from "./Filter";

export interface IFilterCriteria {
  getFilterRoot(): any;
  toJSON(): string;
  toString(): string;
  toObject(): Record<string, any>;
}

interface IFilterNode {
  connection: string | undefined;
  filter: Filter;
}

export class FilterCriteria extends StringValueObject implements IFilterCriteria {
  protected _root: IFilterNode;

  protected static parseFilterString = (filterString: string): string[][] => {
    // Ejemplo: lang_code[eq]pt|AND|dealer_id[eq]2222222|and|pepe[eq]assaas

    // eslint-disable-next-line no-useless-escape
    const regex = /(?:\|([^\]]+)(?:\|))*([^\[|]+)\[([^\]]+)\]([^\[|]+)/gi;
    const result: any[] = [];
    let matches: any;

    while ((matches = regex.exec(filterString)) !== null) {
      result.push([...matches]);
    }

    return result;
  };

  public static create(value: UndefinedOr<string>) {
    const validatedProps = this.validate(value);

    if (validatedProps.isFailure) {
      return Result.fail(validatedProps.error);
    }

    return Result.ok<FilterCriteria>(new FilterCriteria(String(value)));
  }

  protected static validate(value: UndefinedOr<string>) {
    if (RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, value).isSuccess) {
      const stringOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_STRING, value);

      if (stringOrError.isFailure) {
        return stringOrError;
      }
    }

    const filterString = String(value);

    const filtersOrErrors = new ResultCollection();

    this.parseFilterString(filterString).forEach((token: string[]) => {
      const filterOrError = Filter.create({
        field: token[2],
        operator: String(token[3]).toUpperCase(),
        value: token[4],
      });

      filtersOrErrors.add(filterOrError);
    });

    if (filtersOrErrors.hasSomeFaultyResult()) {
      return filtersOrErrors.getFirstFaultyResult() as Result<never, Error>;
    }

    return Result.ok(filterString);
  }

  constructor(value: string) {
    super(value);
    this._root = this.buildFilterRoot(value);
  }

  public getFilterRoot(): any {
    return this._root;
  }

  public toJSON(): string {
    return JSON.stringify(this.toObject());
  }

  public toPrimitive(): string {
    throw new Error("NOT IMPLEMENT FilterCriteria.toPrimitive()");
  }

  public toObject(): Record<string, any> {
    return this.getFilterRoot();
  }

  protected buildFilterRoot(filterString: string): IFilterNode {
    const __processNodes: any = (nodes: any[], prevFilter?: IFilter) => {
      const _node: any = nodes.shift();

      if (!_node) {
        return prevFilter;
      }

      if (!_node.connection) {
        return __processNodes(nodes, _node.filter);
      }

      return {
        operator: _node.connection,
        value: [prevFilter, __processNodes(nodes, _node.filter)],
      };
    };

    const filterNodes = FilterCriteria.parseFilterString(filterString).map((token: string[]) => {
      /** TOKEN
       * [1] => and / or (optional)
       * [2] => field
       * [3] => operator
       * [4] => value
       */
      const connection = token[1] ? String(token[1]).toUpperCase() : undefined;

      const filterOrError = Filter.create({
        field: token[2],
        operator: String(token[3]).toUpperCase(),
        value: token[4],
      });

      if (filterOrError.isFailure) {
        throw new Error(`Filter '${token.join()}' is not valid`);
      }

      return {
        connection,
        filter: filterOrError.object,
      };
    });

    return __processNodes(filterNodes, null);
  }

  public add(filter: Filter, connection: string = "AND"): void {
    const newFilterString = `${connection.toLowerCase()}|${
      filter.field
    }[${filter.operator.toLowerCase()}]${filter.value}`;

    this._root = this.buildFilterRoot(`${this.props}|${newFilterString}`);
  }
}
