import React, { useCallback, useEffect, useState } from 'react'
import Icon from '$core-components/Icon'
import Ajv from 'ajv'
import deepcopy from 'deepcopy'
import features from '$features'

const logger = { warn: () => {}, log: () => {}, error: () => {} }
const getDefinition = (ref, schema) => schema.properties[ref].$ref.substring(2)
  .split('/')
  .reduce((r, k) => r[k], schema)

const hasKeys = (schema, error) => {
  const hasKeyRegex = new RegExp(Object.keys(schema.properties).join('|'), 'gim')
  return hasKeyRegex.test(JSON.stringify(error))
}

const isPropertyError = (id, error) => {
  const re = new RegExp(id, 'igm')
  return re.test(JSON.stringify(error))
}

const cleanOldErrorsForProperty = (id, keyword, errors) => {
  const re = new RegExp(id, 'igm')
  return errors.filter(error => !re.test(JSON.stringify(error) || error.keyword === keyword))
}

const singleDucValidation = (id, value, schema) => {
  const ajv = new Ajv({ errorDataPath: 'property', logger })
  const getPropFromDependencies = (depId) => Object.keys(schema.dependencies)
    .map(dep => schema.dependencies[dep].oneOf
      .map(dep2 => dep2.properties[depId])
      .filter(dp => !!dp)[0])
    .filter(dep => !!dep)[0]

  const partialSchema = {
    type: 'object',
    properties: {
      [id]: schema.properties[id] || getPropFromDependencies(id)
    },
    definitions: {
      [id]: schema.definitions[id]
    },
    required: schema.required.filter(k => k === id)
  }

  const validate = ajv.compile(partialSchema)

  validate({
    [id]: !value ? value === 0 ? value : undefined : value
  })

  return validate
}

const getDependentSchema = (schema, formData) => {
  if (schema.dependencies) {
    const depTree = Object.entries(schema.dependencies).reduce((r, [key, dep]) => {
      const allDeps = dep.oneOf.map(({ properties }) => {
        const parentKey = `${key}|${properties[key].enum.join('|')}`
        const depProps = { ...properties }
        delete depProps[key]

        return { parentKey, depProps }
      }).reduce((acc, deps) => ({ ...acc, [deps.parentKey]: deps.depProps }), {})

      return { ...r, ...allDeps }
    }, {})
    const schemaWithDeps = deepcopy(schema)

    const getProperty = (propSchema) => {
      return Object.keys(propSchema).map(key => {
        if (depTree[`${key}|${formData[key]}`]) {
          return [{ [key]: propSchema[key] }, ...getProperty(depTree[`${key}|${formData[key]}`])]
        } else {
          return { [key]: propSchema[key] }
        }
      }).flat()
    }

    schemaWithDeps.properties = getProperty(schema.properties).reduce((r, prop) => ({ ...r, ...prop }), {})

    const propsMap = Object.keys(schemaWithDeps.properties).reduce((r, d) => ({ ...r, [d]: 1 }), {})
    schemaWithDeps.required = schemaWithDeps.required.filter(d => propsMap[d])
    schemaWithDeps.dependencies = {}

    return schemaWithDeps
  } else {
    return schema
  }
}

const gertPropertiesByOrder = (definitions) => {
  const ordersNKeys = Object.keys(definitions)
    .reduce((acc, next) => ({ ...acc,
      [next]: {
        id: next,
        order: definitions[next].custom.displayOrder
      } }), {})

  return Object.values(ordersNKeys).sort(function (a, b) {
    return a.order - b.order
  })
}

const DynamicForm = (props) => {
  const {
    schema,
    uiSchema,
    formData,
    widgets,
    formContext,
    onChange,
    errorSchema,
    transformErrors,
    onFirstValidate,
    onFirstAddToCart,
    translations
  } = props

  const [formDataState, setFormDataState] = useState(formData)

  useEffect(() => {
    setFormDataState(formData)
  }, [formData])

  useEffect(() => {
    const ajv = new Ajv({ allErrors: true, errorDataPath: 'property', logger })
    const schemaWithDeps = getDependentSchema(schema, formData)

    const validate = ajv.compile(schemaWithDeps)

    const valid = validate(formData)
    if (!valid) {
      onFirstValidate && onFirstValidate(formData, formData, validate.errors)
      if (!onFirstAddToCart) {
        const uniqueErrors = getUniqueErrors(validate.errors, errorSchema)

        setErrors(uniqueErrors)
        onChange(formData, [], uniqueErrors, schemaWithDeps)
      }
    }
  }, [onFirstAddToCart])

  useEffect(() => {
    const ajv = new Ajv({ allErrors: true, errorDataPath: 'property', logger })
    const schemaWithDeps = getDependentSchema(schema, formData)

    const validate = ajv.compile(schemaWithDeps)

    const valid = validate(formData)

    if (!valid) {
      if (props.isAddClicked) {
        const uniqueErrors = getUniqueErrors(validate.errors, errorSchema)

        setErrors(uniqueErrors)
        onChange(formData, [], uniqueErrors, schemaWithDeps)
      }
    }
  }, [props.isAddClicked])

  const required = schema.required.reduce((r, k) => ({ ...r, [k]: 1 }), {})
  const [errors, setErrors] = useState(errorSchema || [])
  const checkErrorsForState = (errors, id) => {
    const keyword = errors && errors.length ? errors[0].keyword : ''
    const mergedErrors = [...(errors || []), ...cleanOldErrorsForProperty(id, keyword, (errors || []))]
    return mergedErrors.map(addAffectPriceToPropertyError)
  }

  const addAffectPriceToPropertyError = (error) => {
    const propertyName = error.dataPath
      ? error.dataPath
      : error.schemaPath

    let name = ''
    if (propertyName.includes('dependencies')) {
      const splittedSchema = propertyName.split('/')
      name = splittedSchema[2]
    }
    if (error.dataPath) {
      const re = /[\[\]']+/g
      name = error.dataPath.replaceAll(re, '')
    }

    const schemaByDeps = getDependentSchema(schema, formData)

    return { ...error, affectPrice: schemaByDeps.definitions[name].custom.affectPrice }
  }

  const getUniqueErrors = (errors, errorSchema) => {
    const result = [...new Set([...errors || [], ...errorSchema])]

    const filteredErrors = result.reduce((acc, next) => {
      if (!acc[next.dataPath]) {
        acc[next.dataPath] = { ...next }
      }
      return acc
    }, {})
    return Object.values(filteredErrors).map(addAffectPriceToPropertyError)
  }

  const onBlurValidation = (id, newDucValue) => {
    const validate = singleDucValidation(id, newDucValue, schema)
    const schemaWithDeps = getDependentSchema(schema, formData)

    const newData = {
      ...formData,
      [id]: !newDucValue ? newDucValue === 0 ? newDucValue : undefined : newDucValue
    }

    const newErrors = checkErrorsForState(validate.errors, id)

    setErrors(newErrors)
    onChange(newData, [], newErrors, schemaWithDeps)
  }

  const checkError = useCallback((id) => {
    let errs = []

    errors && errors.filter(err => isPropertyError(id, err)).map((err_, k) => {
      const errorWithPropertyId = [{ ...err_, property: id }]
      errs = transformErrors ? transformErrors(errorWithPropertyId) : err_
    })
    return errs
  }, [errors])

  const handleDucChange = useCallback((id, newDucValue, errSchema) => {
    const newData = {
      ...formData,
      [id]: !newDucValue ? newDucValue === 0 ? newDucValue : undefined : newDucValue
    }

    let newErrors = []

    const validate = singleDucValidation(id, newDucValue, schema)
    newErrors = checkErrorsForState(validate.errors, id)

    const ajvAll = new Ajv({ allErrors: true, errorDataPath: 'property', logger })

    const newSchema = getDependentSchema(schema, newData)

    const validateAll = ajvAll.compile(newSchema)
    validateAll(newData)

    setErrors(newErrors)
    const newDataForChange = {
      ...formData,
      [id]: !newDucValue ? newDucValue === 0 ? newDucValue : '' : newDucValue
    }
    onChange(newDataForChange, validateAll.errors && validateAll.errors.map(addAffectPriceToPropertyError), newErrors, newSchema)
  }, [schema.properties, formData])

  const schemaWithDeps = getDependentSchema(schema, formData)

  return <div>
    {gertPropertiesByOrder(schema.definitions).map(({ id }) => {
      if (schemaWithDeps.properties[id]) {
        const definition = getDefinition(id, schemaWithDeps)

        const { description, title, custom } = definition
        const WidgetComponent = uiSchema[id] && widgets[uiSchema[id]['ui:widget']]
        Object.assign(definition, schemaWithDeps.properties[id])
        delete definition.$ref

        const checkedError = checkError(id)

        return (
          <div key={id} className={`duc-wrapper ${uiSchema[id]['ui:options'].visible
            ? ''
            : 'hidden'} ${checkedError && checkedError.length
            ? 'errors'
            : ''}`}
          >
            <div className="duc-head">
              <label htmlFor={id} className='duc-title'>
                {title}
                {required[id]
                  ? <span className="required-field">*</span>
                  : null
                }
              </label>
              {description &&
                <span className='duc-description'>
                  <Icon name="info.svg" width="16px" height="16px" className="info-icon"/>
                  <div className='duc-description-text'>{description}</div>
                </span>
              }
              {custom.info && <span className='info-icon'>(i)
                <span className='tooltip-text' dangerouslySetInnerHTML={{ __html: custom.info }}/>
              </span>
              }
            </div>
            <WidgetComponent
              id={id}
              readonly={uiSchema[id]['ui:readonly']}
              schema={definition}
              formContext={formContext}
              uiSchema={uiSchema[id]}
              options={uiSchema[id]['ui:options']}
              value={formDataState[id]}
              onChange={handleDucChange}
              onBlur={onBlurValidation}
              t={translations}
              features={features}
              required={required[id]}
            />
            {checkedError && checkedError.length > 0 && <>
              {checkedError.map((errors) => {
                const maskError = custom.maskErrors && custom.maskErrors.includes(errors.keyword)

                return (<div className={`duc-error ${maskError ? 'mask-error' : ''}`} key="err">
                  <div className="error-text">{errors.message}</div>
                </div>)
              })}
            </>
            }
          </div>
        )
      }
    })}
  </div>
}

export default DynamicForm
