import { forIn, isEmpty } from "lodash";
import FieldInterface from "../models/FieldInterface";
import FormInterface from "../models/FormInterface";
import StateInterface from "../models/StateInterface";
import {
  ValidationPlugin,
  ValidationPluginConfig,
  ValidationPluginConstructor,
  ValidationPluginInterface,
} from "../models/ValidatorInterface";

class DVR<TValidator = any>
  implements ValidationPluginInterface<TValidator>
{
  promises: Promise<any>[];
  config: any;
  state: StateInterface | null;
  extend?: (args: { validator: TValidator; form: FormInterface }) => void;
  validator: TValidator;
  schema?: any;

  constructor({
    config,
    state = null,
    promises = [],
  }: ValidationPluginConstructor<TValidator>) {
    this.state = state;
    this.promises = promises;
    this.config = config;
    this.extend = config?.extend;
    this.validator = config.package;
    this.extendValidator();
  }

  extendValidator() {
    if (typeof this.extend === "function") {
      this.extend({
        validator: this.validator,
        form: this.state!.form,
      });
    }
  }

  validate(field: FieldInterface) {
    const data = this.state!.form.flatMapValues;
    this.validateFieldAsync(field, data);
    this.validateFieldSync(field, data);
  }

  makeLabels(validation: any, field: FieldInterface) {
    const labels: Record<string, any> = { [field.path ?? ""]: field.label };
    forIn(validation.rules[field.path ?? ""], (rule) => {
      if (
        typeof rule.value === "string" &&
        rule.name.match(/^(required_|same|different)/)
      ) {
        forIn(rule.value.split(","), (p, i: any) => {
          if (!rule.name.match(/^required_(if|unless)/) || i % 2 === 0) {
            const f = this.state!.form.$(p);
            if (f && f.path && f.label) {
              labels[f.path] = f.label;
            }
          }
        });
      } else if (
        typeof rule.value === "string" &&
        rule.name.match(/^(before|after)/)
      ) {
        const f = this.state!.form.$(rule.value);
        if (f && f.path && f.label) {
          labels[f.path] = f.label;
        }
      }
    });
    validation.setAttributeNames(labels);
  }

  validateFieldSync(field: FieldInterface, data: any) {
    const $rules = this.rules(field.rules, "sync");
    if (isEmpty($rules[0])) return;
    const rules = { [field.path ?? ""]: $rules };
    const validation = new (this.validator as any)(data, rules);
    this.makeLabels(validation, field);
    if (validation.passes()) return;
    field.invalidate((validation.errors.get(field.path) as any[])[0], false);
  }

  validateFieldAsync(field: FieldInterface, data: any) {
    const $rules = this.rules(field.rules, "async");
    if (isEmpty($rules[0])) return;
    const rules = { [field.path ?? ""]: $rules };
    const validation = new (this.validator as any)(data, rules);
    this.makeLabels(validation, field);

    const $p = new Promise((resolve: any) =>
      validation.checkAsync(
        () => this.handleAsyncPasses(field, resolve),
        () => this.handleAsyncFails(field, validation, resolve)
      )
    );

    this.promises.push($p);
  }

  handleAsyncPasses(field: FieldInterface, resolve: () => void) {
    field.setValidationAsyncData(true);
    resolve();
  }

  handleAsyncFails(
    field: FieldInterface,
    validation: any,
    resolve: () => void
  ) {
    field.setValidationAsyncData(
      false,
      (validation.errors.get(field.path) as any[])[0]
    );
    this.executeAsyncValidation(field);
    resolve();
  }

  executeAsyncValidation(field: FieldInterface) {
    if (field.validationAsyncData.valid === false) {
      field.invalidate(field.validationAsyncData.message ?? undefined, false, true);
    }
  }

  rules(rules: any, type: "sync" | "async") {
    const $rules = Array.isArray(rules) ? rules : typeof rules === 'string' ? rules.split("|") : [];
    const v = new (this.validator as any)();
    return $rules.filter(($rule: any) =>
      type === "async"
        ? v.getRule($rule.split(":")[0])?.async
        : !v.getRule($rule.split(":")[0])?.async
    );
  }
}

export default <TValidator = any>(
  config?: ValidationPluginConfig<TValidator>
): ValidationPlugin<TValidator> => ({
  class: DVR<TValidator>,
  config,
});
