import { isSpecialMissing } from '@sasjs/utils/input/validators'
import Handsontable from 'handsontable'
import { CellProperties } from 'handsontable/settings'
import {
  autocompleteValidator,
  intlDateValidator,
  intlTimeValidator
} from 'handsontable/validators'
import { $DataFormats } from 'src/app/models/sas/editors-getdata.model'
import { DQData, SASParam } from '../../models/TableData'
import { Col } from './models/col.model'
import {
  DcValidation,
  DcValidationRuleUpdate
} from './models/dc-validation.model'
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
import { getDqDataCols } from './utils/getDqDataCols'
import { getNotNullDefault } from './utils/getNotNullDefault'
import { mergeColsRules } from './utils/mergeColsRules'
import { parseColType } from './utils/parseColType'
import { dqValidate } from './validations/dq-validation'
import {
  datetimeValidator,
  specialMissingNumericValidator
} from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats'
import { mapIntlCellTypes } from './utils/mapIntlCellTypes'
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
import { registerIntlCellTypes } from './cellTypes/intlCellTypes'
import { makeNumberFormatRenderer } from '../../editor/utils/renderers.utils'

export class DcValidator {
  private rules: DcValidation[] = []
  private dqrules: DQRule[] = []
  private dqdata: DQData[] = []
  private sasparams: SASParam | undefined
  private hiddenColumns: number[] = []
  private primaryKeys: string[] = []
  private hotInstance: Handsontable | undefined

  constructor(
    sasparams: SASParam,
    $dataFormats: $DataFormats,
    cols: Col[],
    dqRules: DQRule[],
    dqData: DQData[],
    hotInstance?: Handsontable
  ) {
    this.registerCustomEditors()

    this.sasparams = sasparams
    this.hotInstance = hotInstance
    this.rules = parseColType(sasparams.COLTYPE)
    this.rules = mergeColsRules(cols, this.rules, $dataFormats)
    this.rules = applyNumericFormats(this.rules)
    this.rules = mapIntlCellTypes(this.rules)
    this.dqrules = dqRules
    this.dqdata = dqData
    this.primaryKeys = sasparams.PK.split(' ')

    this.updateDqData()
    this.setupValidations()
  }

  registerCustomEditors() {
    Handsontable.editors.registerEditor(
      'autocomplete.custom',
      CustomAutocompleteEditor
    )
    registerIntlCellTypes()
  }

  getRules(): DcValidation[] {
    return this.rules
  }

  getRule(selector: string | number): DcValidation | undefined {
    const index = this.getRuleIndex(selector)

    return this.rules[index]
  }

  getHiddenColumns(): number[] {
    return this.hiddenColumns
  }

  updateRule(selector: string | number, value: DcValidationRuleUpdate) {
    let index = this.getRuleIndex(selector)

    this.rules[index] = {
      ...this.rules[index],
      ...value
    }
  }

  removeRule(selector: string | number) {
    const index = this.getRuleIndex(selector)

    this.rules.splice(index, 1)
  }

  /**
   * DQ Rules values comes from MPE_VALIDATIONS table
   * @param col column name (optional) - if not provided all DQ Rules will be returned
   * @returns array of DQRUles for given col name
   */
  getDqDetails(col?: string): DQRule[] {
    if (!col) return this.dqrules

    return this.dqrules.filter((dqrule: DQRule) => dqrule.BASE_COL === col)
  }

  /**
   * Checks if given column is or is not included in the current DATA QUALITY rules
   *
   * @param col column name
   */
  isDqCol(col: string): boolean {
    // My refactored solution
    return !!this.dqrules.find((dqrule: DQRule) => dqrule.BASE_COL === col)
  }

  /**
   * Checks if the given column has the provided dqrules
   * @param dqrules returns true if any of provided rules match
   */
  hasDqRules(
    col: string,
    dqrules: DQRuleTypes[],
    matchAll: boolean = false
  ): boolean {
    if (matchAll) {
      return (
        this.dqrules.filter(
          (x) => x.BASE_COL === col && dqrules.includes(x.RULE_TYPE)
        ).length === dqrules.length
      )
    } else {
      return !!this.dqrules.find(
        (x) => x.BASE_COL === col && dqrules.includes(x.RULE_TYPE)
      )
    }
  }

  /**
   * Returns the RULE_VALUE for a NOTNULL rule on the given column.
   * Used for auto-populating default values when cells are empty.
   * Converts to number for numeric columns.
   *
   * @param col column name
   * @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
   */
  getNotNullDefaultValue(col: string): string | number | undefined {
    const colRule = this.getRule(col)
    return getNotNullDefault(col, this.dqrules, colRule?.type)
  }

  /**
   * Returns the num_digits for a ROUND rule on the given column, used to
   * round edited/pasted values Excel-style. Returns undefined if no ROUND
   * rule exists or its RULE_VALUE is not an integer.
   *
   * @param col column name
   */
  getRoundDigits(col: string): number | undefined {
    const roundRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'ROUND'
    )

    if (!roundRule) return undefined

    const digits = parseInt(roundRule.RULE_VALUE, 10)

    return isNaN(digits) ? undefined : digits
  }

  /**
   * Retrieves dropdown source for given dc validation rule
   * The values comes from MPE_SELECTBOX table
   * @param rule
   */
  getDqDropdownSource(rule: DcValidation): string[] | number[] {
    let details: any[] = []

    this.dqrules.forEach((element: DQRule) => {
      if (
        element.BASE_COL.toString() === rule.data &&
        rule.data.toString() &&
        [
          'HARDSELECT',
          'SOFTSELECT',
          'HARDSELECT_HOOK',
          'SOFTSELECT_HOOK'
        ].includes(element.RULE_TYPE.toString())
      ) {
        this.dqdata
          .filter((dqdata: DQData) => dqdata.BASE_COL === element.BASE_COL)
          .forEach((data: any) => {
            /**
             * If column type is numeric, we want to populate dropdown with numeric array
             * So we will convert it before pushing to array.
             */
            if (rule.type && rule.type === 'numeric') {
              details.push(Number(data['RULE_DATA']))
            } else {
              details.push(data['RULE_DATA'])
            }
          })
      }
    })

    return details
  }

  /**
   * SOFTSELECT is not defined in DQ RULES
   * This function fetches it's values and pushes in the DQ RULES array with SOFTSELECT type
   * Those values are used also for HARDSELECT, only difference is HARDSELECT has strict=true
   */
  updateDqData() {
    if (this.dqdata.length > 0) {
      let dqDataCols: string[] = getDqDataCols(this.dqdata)

      dqDataCols.forEach((dqCol: string) => {
        let rulePresent = false

        if (this.dqrules.length > 0) {
          this.dqrules.forEach((dqRule: DQRule) => {
            if (
              dqRule.BASE_COL === dqCol &&
              (dqRule.RULE_TYPE === 'SOFTSELECT' ||
                dqRule.RULE_TYPE === 'HARDSELECT')
            ) {
              rulePresent = true
            }
          })
        }

        if (!rulePresent) {
          this.dqrules.push({
            BASE_COL: dqCol,
            RULE_TYPE: 'SOFTSELECT',
            RULE_VALUE: dqCol,
            X: 1
          })
        }
      })
    }
  }

  /**
   * It will execute validator, by creating the dummy instance of HOT
   * because validator function requires it in it's context
   * We need this function to be able to REUSE the validation that is done in HOT
   * For exmaple in EDIT RECROD MODAL
   * @param columnRules column rule for which to run validation
   * @param value value against which validation is done
   * @param callback true if valid - false if invalid
   */
  executeHotValidator = (
    columnRules: DcValidation,
    value: any,
    callback?: (valid: boolean) => void
  ) => {
    const cellProperties: CellProperties = {
      ...columnRules,
      validator: undefined,
      row: 0,
      col: 0,
      instance: new Handsontable(document.createElement('div'), {}),
      visualRow: 0,
      visualCol: 0,
      prop: 0
    }

    if (value === undefined || value === null) value = ''

    if (
      !columnRules ||
      !columnRules.validator ||
      typeof columnRules.validator !== 'function'
    ) {
      if (callback) callback(false)
      return
    }

    columnRules.validator.call(cellProperties, value, (valid: boolean) => {
      if (callback) callback(valid)
    })
  }

  /**
   * It will populate columns dropdowns for which data is found
   * Also validator for every column is assigned
   */
  private setupValidations() {
    for (let i = 0; i < this.rules.length; i++) {
      const ruleColName = this.rules[i].data || ''

      if (ruleColName === '_____DELETE__THIS__RECORD_____') {
        continue
      }

      /**
       * Populate dropdowns SOFTSELECT, HARDSELECT
       */
      if (this.isDqCol(ruleColName)) {
        let source: string[] | number[] = this.getDqDropdownSource(
          this.rules[i]
        )

        if (source.length > 0) {
          // For SAS num cols keep type='numeric' so HOT's numericEditor /
          // numericRenderer + numbro coercion stay alive — same per-column
          // model as the per-cell pattern in
          // editor.component.ts:reSetCellValidationValues().
          this.rules[i].source = source
          this.rules[i].editor = 'autocomplete.custom'
          this.rules[i].renderer = 'autocomplete'
          this.rules[i].filter = false

          if (this.rules[i].sasType !== 'num') {
            this.rules[i].type = 'autocomplete'
          }
        }

        if (this.hasDqRules(ruleColName, ['SOFTSELECT'])) {
          this.rules[i].strict = false
        }

        if (this.hasDqRules(ruleColName, ['HARDSELECT'])) {
          this.rules[i].strict = true
        }

        if (this.hasDqRules(ruleColName, ['SOFTSELECT_HOOK'])) {
          this.rules[i].strict = false
        }

        if (this.hasDqRules(ruleColName, ['HARDSELECT_HOOK'])) {
          this.rules[i].strict = true
        }

        if (this.hasDqRules(ruleColName, ['NOTNULL'])) {
          this.rules[i].allowEmpty = false
        }

        // READONLY: render column read-only (default value handled via getColumnDefault on add row)
        if (this.hasDqRules(ruleColName, ['READONLY'])) {
          this.rules[i].readOnly = true
        }

        // HIDDEN: hide column in HOT but keep its data (still submitted via hot.getData())
        if (this.hasDqRules(ruleColName, ['HIDDEN'])) {
          this.hiddenColumns.push(i)
        }

        // NUMBER_FORMAT: display-only Intl.NumberFormat renderer.
        // Set last so it overrides a dropdown's 'autocomplete' renderer (last-wins).
        if (this.hasDqRules(ruleColName, ['NUMBER_FORMAT'])) {
          const fmtRule = this.getDqDetails(ruleColName).find(
            (rule: DQRule) => rule.RULE_TYPE === 'NUMBER_FORMAT'
          )
          this.rules[i].renderer = makeNumberFormatRenderer(fmtRule?.RULE_VALUE)
          // Clear numericFormat (set by applyNumericFormats on every numeric
          // column). On a numeric cell HOT's built-in numeric renderer would
          // re-format via numericFormat, overriding our custom NUMBER_FORMAT
          // Intl renderer and dropping the currency symbol. The numeric
          // editor/validator stay intact via `type`.
          this.rules[i].numericFormat = undefined
        }
      }

      const self = this

      this.setDefaultValidator(self, i)
      this.setColumnLevelSecurity(i)
    }

    // If CLS is true, we hide `delete column`
    if (this.sasparams?.CLS_FLAG) this.hiddenColumns.push(0)
  }

  private setDefaultValidator(self: any, i: number) {
    this.rules[i].validator = function (
      value: any,
      callback: (valid: boolean) => void
    ) {
      const col = self.rules[i].data?.toString()
      const colType = self.rules[i].type || ''
      let handsontableValid = null

      // We call handsontable predefined validators, if it returns false, we validate as FALSE
      // If return is true, we validate with our custom DQ validations

      // Because of dynamic cell validation, that will change the type of cell to dropdown
      // `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
      if (
        this.editor === 'autocomplete' ||
        this.editor === 'autocomplete.custom'
      ) {
        self
          .getHandsontableValidator('autocomplete')
          .call(this, value, (valid: boolean) => {
            handsontableValid = valid
          })

        if (!handsontableValid) {
          console.warn(
            `HOT Native Validation (autocomplete) - invalid (Value: ${value})`
          )
          callback(false)
          return
        }
      }

      // Now the regular type native HOT validation
      self
        .getHandsontableValidator(colType)
        .call(this, value, (valid: boolean) => {
          handsontableValid = valid
        })

      if (!handsontableValid) {
        console.warn(`HOT Native Validation - invalid (Value: ${value})`)
        callback(false)
        return
      }

      if (!self.lengthCheck(value, self.rules[i].length, colType)) {
        console.warn(`Length Validation - invalid (Value: ${value})`)
        callback(false)
        return
      }

      if (self.isDqCol(col || '')) {
        const dqValid = dqValidate(self.getDqDetails(col || ''), value)

        if (!dqValid) {
          console.warn(`DQ Validation - invalid (Value: ${value})`)
          callback(false)
          return
        }
      }

      callback(true)
    }
  }

  // Validation for length or for size in bytes if col type is numeric
  // True means valid
  private lengthCheck(
    value: any,
    ruleLength: number,
    colType: string
  ): boolean {
    if (isSpecialMissing(value)) return true
    if (value === undefined || value === null) return true
    if (ruleLength === undefined || ruleLength === null) return true

    switch (colType) {
      case 'numeric': {
        if (ruleLength === 8) return true

        if (ruleLength < 3 || ruleLength > 8) {
          console.warn(
            'invalid length from SAS, rule length can only be an integer between 3-8'
          )
          return false
        }

        if (!Number.isInteger(ruleLength)) {
          console.warn(
            'invalid length from SAS, rule length can only be an integer between 3-8'
          )
          return false
        }

        const isFloat = Number(value) === value && value % 1 !== 0
        if (ruleLength < 8 && isFloat) {
          console.warn(
            'reduced length numerics cannot contain decimals else precision will be affected'
          )
          return false
        }

        // values for following cases are taken from https://v8doc.sas.com/sashtml/win/numvar.htm
        switch (ruleLength) {
          case 3:
            return value <= 8192 // 2^13
          case 4:
            return value <= 2097152 // 2^21
          case 5:
            return value <= 536870912 // 2^29
          case 6:
            return value <= 137438953472 // 2^37
          case 7:
            return value <= 35184372088832 // 2^45
        }
      }
      default: {
        return value.toString().length <= ruleLength
      }
    }
  }

  private getHandsontableValidator(type: string) {
    switch (type) {
      case 'autocomplete':
        return autocompleteValidator
      case 'numeric':
        return specialMissingNumericValidator
      case 'intl-date':
        return intlDateValidator
      case 'intl-time':
        return intlTimeValidator
      case 'intl-datetime':
        return datetimeValidator
      default:
        return (value: any, callback: any) => (callback ? callback(true) : null)
    }
  }

  private getRuleIndex(col: string | number): number {
    if (typeof col === 'number') return col

    return this.rules.findIndex((rule: DcValidation) => rule.data === col)
  }

  private setColumnLevelSecurity(index: number) {
    if (!this.sasparams?.CLS_FLAG) return

    const rule = this.rules[index]

    // Primary Key variables must always be visible and read-only
    if (this.primaryKeys.includes(rule.data)) rule.clsRule = 'READ'

    if (rule.clsRule === 'HIDE') this.hiddenColumns.push(index)

    if (rule.clsRule === 'READ') rule.readOnly = true

    if (rule.clsRule === 'EDIT') {
      rule.readOnly = false
      const hiddenIndex = this.hiddenColumns.indexOf(index)
      this.hiddenColumns.splice(hiddenIndex, 1)
    }
  }
}
