// @ts-nocheck
import {
  format,
  complementError,
  asyncMap,
  warning,
  deepMerge,
  convertFieldsError,
} from './util'
import validators from './validator/index'
import { messages as defaultMessages, newMessages } from './messages'
import type {
  InternalRuleItem,
  InternalValidateMessages,
  Rule,
  RuleItem,
  Rules,
  ValidateCallback,
  ValidateMessages,
  ValidateOption,
  Values,
  RuleValuePackage,
  ValidateError,
  ValidateFieldsError,
  SyncErrorType,
  ValidateResult,
} from './interface'

export * from './interface'

/**
 *  Encapsulates a validation schema.
 *
 *  @param descriptor An object declaring validation rules
 *  for this schema.
 */
class Schema {
  // ========================= Static =========================
  static register = function register(type: string, validator) {
    if (typeof validator !== 'function') {
      throw new Error(
        'Cannot register a validator by type, validator is not a function'
      )
    }
    validators[type] = validator
  }

  static warning = warning

  static messages = defaultMessages

  static validators = validators

  // ======================== Instance ========================
  rules: Record<string, RuleItem[]> = null
  _messages: InternalValidateMessages = defaultMessages

  constructor(descriptor: Rules) {
    this.define(descriptor)
  }

  define(rules: Rules) {
    if (!rules) {
      throw new Error('Cannot configure a schema with no rules')
    }
    if (typeof rules !== 'object' || Array.isArray(rules)) {
      throw new Error('Rules must be an object')
    }
    this.rules = {}

    Object.keys(rules).forEach(name => {
      const item: Rule = rules[name]
      this.rules[name] = Array.isArray(item) ? item : [item]
    })
  }

  messages(messages?: ValidateMessages) {
    if (messages) {
      this._messages = deepMerge(newMessages(), messages)
    }
    return this._messages
  }

  validate(
    source: Values,
    option?: ValidateOption,
    callback?: ValidateCallback
  ): Promise<Values>
  validate(source: Values, callback: ValidateCallback): Promise<Values>
  validate(source: Values): Promise<Values>

  validate(source_: Values, o: any = {}, oc: any = () => {}): Promise<Values> {
    let source: Values = source_
    let options: ValidateOption = o
    let callback: ValidateCallback = oc
    if (typeof options === 'function') {
      callback = options
      options = {}
    }
    if (!this.rules || Object.keys(this.rules).length === 0) {
      if (callback) {
        callback(null, source)
      }
      return Promise.resolve(source)
    }

    function complete(results: (ValidateError | ValidateError[])[]) {
      let errors: ValidateError[] = []
      let fields: ValidateFieldsError = {}

      function add(e: ValidateError | ValidateError[]) {
        if (Array.isArray(e)) {
          errors = errors.concat(...e)
        } else {
          errors.push(e)
        }
      }

      for (let i = 0; i < results.length; i++) {
        add(results[i])
      }
      if (!errors.length) {
        callback(null, source)
      } else {
        fields = convertFieldsError(errors)
        ;(callback as (errors: ValidateError[], fields: ValidateFieldsError) => void)(
          errors,
          fields
        )
      }
    }

    if (options.messages) {
      let messages = this.messages()
      if (messages === defaultMessages) {
        messages = newMessages()
      }
      deepMerge(messages, options.messages)
      options.messages = messages
    } else {
      options.messages = this.messages()
    }

    const series: Record<string, RuleValuePackage[]> = {}
    const keys = options.keys || Object.keys(this.rules)
    keys.forEach(z => {
      const arr = this.rules[z]
      let value = source[z]
      arr.forEach(r => {
        let rule: InternalRuleItem = r
        if (typeof rule.transform === 'function') {
          if (source === source_) {
            source = { ...source }
          }
          value = source[z] = rule.transform(value)
        }
        if (typeof rule === 'function') {
          rule = {
            validator: rule,
          }
        } else {
          rule = { ...rule }
        }

        // Fill validator. Skip if nothing need to validate
        rule.validator = this.getValidationMethod(rule)
        if (!rule.validator) {
          return
        }

        rule.field = z
        rule.fullField = rule.fullField || z
        rule.type = this.getType(rule)
        series[z] = series[z] || []
        series[z].push({
          rule,
          value,
          source,
          field: z,
        })
      })
    })
    const errorFields = {}
    return asyncMap(
      series,
      options,
      (data, doIt) => {
        const rule = data.rule
        let deep =
          (rule.type === 'object' || rule.type === 'array') &&
          (typeof rule.fields === 'object' || typeof rule.defaultField === 'object')
        deep = deep && (rule.required || (!rule.required && data.value))
        rule.field = data.field

        function addFullField(key: string, schema: RuleItem) {
          return {
            ...schema,
            fullField: `${rule.fullField}.${key}`,
            fullFields: rule.fullFields ? [...rule.fullFields, key] : [key],
          }
        }

        function cb(e: SyncErrorType | SyncErrorType[] = []) {
          let errorList = Array.isArray(e) ? e : [e]
          if (!options.suppressWarning && errorList.length) {
            Schema.warning('async-validator:', errorList)
          }
          if (errorList.length && rule.message !== undefined) {
            errorList = [].concat(rule.message)
          }

          // Fill error info
          let filledErrors = errorList.map(complementError(rule, source))

          if (options.first && filledErrors.length) {
            errorFields[rule.field] = 1
            return doIt(filledErrors)
          }
          if (!deep) {
            doIt(filledErrors)
          } else {
            // if rule is required but the target object
            // does not exist fail at the rule level and don't
            // go deeper
            if (rule.required && !data.value) {
              if (rule.message !== undefined) {
                filledErrors = []
                  .concat(rule.message)
                  .map(complementError(rule, source))
              } else if (options.error) {
                filledErrors = [
                  options.error(rule, format(options.messages.required, rule.field)),
                ]
              }
              return doIt(filledErrors)
            }

            let fieldsSchema: Record<string, Rule> = {}
            if (rule.defaultField) {
              Object.keys(data.value).map(key => {
                fieldsSchema[key] = rule.defaultField
              })
            }
            fieldsSchema = {
              ...fieldsSchema,
              ...data.rule.fields,
            }

            const paredFieldsSchema: Record<string, RuleItem[]> = {}

            Object.keys(fieldsSchema).forEach(field => {
              const fieldSchema = fieldsSchema[field]
              const fieldSchemaList = Array.isArray(fieldSchema)
                ? fieldSchema
                : [fieldSchema]
              paredFieldsSchema[field] = fieldSchemaList.map(
                addFullField.bind(null, field)
              )
            })
            const schema = new Schema(paredFieldsSchema)
            schema.messages(options.messages)
            if (data.rule.options) {
              data.rule.options.messages = options.messages
              data.rule.options.error = options.error
            }
            schema.validate(data.value, data.rule.options || options, errs => {
              const finalErrors = []
              if (filledErrors && filledErrors.length) {
                finalErrors.push(...filledErrors)
              }
              if (errs && errs.length) {
                finalErrors.push(...errs)
              }
              doIt(finalErrors.length ? finalErrors : null)
            })
          }
        }

        let res: ValidateResult
        if (rule.asyncValidator) {
          res = rule.asyncValidator(rule, data.value, cb, data.source, options)
        } else if (rule.validator) {
          try {
            res = rule.validator(rule, data.value, cb, data.source, options)
          } catch (error) {
            console.error?.(error)
            // rethrow to report error
            if (!options.suppressValidatorError) {
              setTimeout(() => {
                throw error
              }, 0)
            }
            cb(error.message)
          }
          if (res === true) {
            cb()
          } else if (res === false) {
            cb(
              typeof rule.message === 'function'
                ? rule.message(rule.fullField || rule.field)
                : rule.message || `${rule.fullField || rule.field} fails`
            )
          } else if (res instanceof Array) {
            cb(res)
          } else if (res instanceof Error) {
            cb(res.message)
          }
        }
        if (res && (res as Promise<void>).then) {
          ;(res as Promise<void>).then(
            () => cb(),
            e => cb(e)
          )
        }
      },
      results => {
        complete(results)
      },
      source
    )
  }

  getType(rule: InternalRuleItem) {
    if (rule.type === undefined && rule.pattern instanceof RegExp) {
      rule.type = 'pattern'
    }
    if (
      typeof rule.validator !== 'function' &&
      rule.type &&
      !validators.hasOwnProperty(rule.type)
    ) {
      throw new Error(format('Unknown rule type %s', rule.type))
    }
    return rule.type || 'string'
  }

  getValidationMethod(rule: InternalRuleItem) {
    if (typeof rule.validator === 'function') {
      return rule.validator
    }
    const keys = Object.keys(rule)
    const messageIndex = keys.indexOf('message')
    if (messageIndex !== -1) {
      keys.splice(messageIndex, 1)
    }
    if (keys.length === 1 && keys[0] === 'required') {
      return validators.required
    }
    return validators[this.getType(rule)] || undefined
  }
}

export default Schema
