import type { ColumnType } from 'ant-design-vue/lib/table'
import dayjs from 'dayjs'
import { type FilterType, type LinkToAnotherRecordType, type TableType, UITypes, isDateMonthFormat } from 'nocodb-sdk'

type FormViewColumn = ColumnType & Record<string, any>

export class FormFilters {
  allViewFilters: FilterType[]
  protected groupedFilters: Record<string, FilterType[]>
  nestedGroupedFilters: Record<string, FilterType[]>
  formViewColumns: FormViewColumn[]
  formViewColumnsMapByFkColumnId: Record<string, FormViewColumn>
  formState: Record<string, any>
  value: any
  isSharedForm: boolean
  isMysql?: (sourceId?: string) => boolean
  getMeta?: (baseId: string, tableIdOrTitle: string) => Promise<TableType | null>
  baseId?: string

  constructor({
    data = [],
    nestedGroupedFilters = {},
    formViewColumns = [],
    formViewColumnsMapByFkColumnId = {},
    formState = {},
    isMysql = undefined,
    isSharedForm = false,
    getMeta = undefined,
    baseId = undefined,
  }: {
    data?: FilterType[]
    nestedGroupedFilters?: Record<string, FilterType[]>
    formViewColumns?: FormViewColumn[]
    formViewColumnsMapByFkColumnId?: Record<string, FormViewColumn>
    formState?: Record<string, any>
    isMysql?: (sourceId?: string) => boolean
    isSharedForm?: boolean
    getMeta?: (baseId: string, tableIdOrTitle: string) => Promise<TableType | null>
    baseId?: string
  } = {}) {
    this.allViewFilters = data
    this.groupedFilters = {}
    this.nestedGroupedFilters = nestedGroupedFilters
    this.formViewColumns = formViewColumns
    this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId
    this.formState = formState
    this.isSharedForm = isSharedForm
    this.isMysql = isMysql
    this.getMeta = getMeta
    this.baseId = baseId
  }

  setFilters(filters: FilterType[]) {
    this.allViewFilters = filters
  }

  getRootFilters(parentColId: string) {
    return (this.groupedFilters[parentColId] || [])
      .filter((f) => !f.fk_parent_id)
      .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
  }

  getParentFilters(parentColId: string, parentId: string) {
    return (this.groupedFilters[parentColId] || [])
      .filter((f) => f.fk_parent_id === parentId)
      .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
  }

  getAllChildFilters(filters: FilterType[], parentColId: string): any {
    return filters.map((filter) => {
      if (filter.id && filter.is_group) {
        const childFilters = this.getParentFilters(parentColId, filter.id)
        filter.children = this.getAllChildFilters(childFilters, parentColId)
      }

      return filter
    })
  }

  loadFilters() {
    for (const parentColId in this.groupedFilters) {
      const rootFilters = this.getRootFilters(parentColId)

      this.nestedGroupedFilters[parentColId] = this.getAllChildFilters(rootFilters, parentColId)
    }
    return this.nestedGroupedFilters
  }

  // Method to group filters by fk_parent_column_id
  getNestedGroupedFilters() {
    const groupedFilters = this.allViewFilters.reduce((acc, filter) => {
      const groupingKey = filter.fk_parent_column_id || 'ungrouped'

      if (!acc[groupingKey]) {
        acc[groupingKey] = []
      }

      acc[groupingKey].push(filter)

      return acc
    }, {} as typeof this.groupedFilters)

    this.groupedFilters = groupedFilters

    const nestedGroupedFilters = this.loadFilters()

    return nestedGroupedFilters
  }

  toString(value: any) {
    return `${value || ''}`
  }

  isFieldAboveParentColumn(column: FormViewColumn, parentColumn: FormViewColumn) {
    return column.order < parentColumn.order
  }

  async getOoOrBtColVal(column: FormViewColumn) {
    const fk_related_model_id = (column?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id

    if (!fk_related_model_id || typeof this.getMeta !== 'function' || !this.baseId) return null

    const relatedTableMeta = await this.getMeta(this.baseId, fk_related_model_id)

    if (!relatedTableMeta || !Array.isArray(relatedTableMeta?.columns)) return null

    const displayValTitle = (relatedTableMeta.columns.find((c) => c.pv) || relatedTableMeta.columns?.[0])?.title || ''

    if (
      !displayValTitle ||
      !this.formState[column.title] ||
      !ncIsObject(this.formState[column.title]) ||
      this.formState[column.title][displayValTitle] === undefined
    ) {
      return null
    }

    return this.formState[column.title][displayValTitle]
  }

  async validateCondition(
    filters: FilterType[] = [],
    parentCol: FormViewColumn,
    errors: Record<string, string>,
  ): Promise<boolean | undefined> {
    if (!filters.length) {
      return true
    }

    let isValid

    for (const filter of filters) {
      let res

      if (filter.is_group) {
        res = await this.validateCondition(filter.children, parentCol, errors)
      } else {
        if (!filter.fk_column_id || !this.formViewColumnsMapByFkColumnId[filter.fk_column_id]) {
          res = false
        }

        const column = this.formViewColumnsMapByFkColumnId[filter.fk_column_id]
        // If the filter condition col is below parent column then this will be invalid condition so return false
        if (!this.isFieldAboveParentColumn(column, parentCol)) {
          errors[column.fk_column_id] = `Condition references a field (${column.title}) that comes later in the form.`
          res = true
        }

        if (!column.show) {
          errors[column.fk_column_id] = `Condition references a field (${column.title}) that was removed from the form.`

          res = true
        }

        if (!column.permissions?.isAllowedToEdit) {
          errors[
            column.fk_column_id
          ] = `Condition references a field (${column.title}) that is not editable and will be hidden in shared form.`

          res = true
        }

        if (!column?.visible) {
          res = false
        }

        const field = column.title

        let val = this.formState[field]

        if (res === undefined) {
          if (
            [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(column.uidt) &&
            !['empty', 'blank', 'notempty', 'notblank'].includes(filter.comparison_op)
          ) {
            const dateFormat = this.isMysql?.(column.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'

            let now = dayjs(new Date())
            const dateFormatFromMeta = column?.meta?.date_format
            const dataVal: any = val
            let filterVal: any = filter.value
            if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
              // reset to 1st
              now = dayjs(now).date(1)
              if (val) val = dayjs(val).date(1)
            }
            if (filterVal) res = dayjs(filterVal).isSame(dataVal, 'day')

            // handle sub operation
            switch (filter.comparison_sub_op) {
              case 'today':
                filterVal = now
                break
              case 'tomorrow':
                filterVal = now.add(1, 'day')
                break
              case 'yesterday':
                filterVal = now.add(-1, 'day')
                break
              case 'oneWeekAgo':
                filterVal = now.add(-1, 'week')
                break
              case 'oneWeekFromNow':
                filterVal = now.add(1, 'week')
                break
              case 'oneMonthAgo':
                filterVal = now.add(-1, 'month')
                break
              case 'oneMonthFromNow':
                filterVal = now.add(1, 'month')
                break
              case 'daysAgo':
                if (!filterVal) return
                filterVal = now.add(-filterVal, 'day')
                break
              case 'daysFromNow':
                if (!filterVal) return
                filterVal = now.add(filterVal, 'day')
                break
              case 'exactDate':
                if (!filterVal) return
                break
              // sub-ops for `isWithin` comparison
              case 'pastWeek':
                filterVal = now.add(-1, 'week')
                break
              case 'pastMonth':
                filterVal = now.add(-1, 'month')
                break
              case 'pastYear':
                filterVal = now.add(-1, 'year')
                break
              case 'nextWeek':
                filterVal = now.add(1, 'week')
                break
              case 'nextMonth':
                filterVal = now.add(1, 'month')
                break
              case 'nextYear':
                filterVal = now.add(1, 'year')
                break
              case 'pastNumberOfDays':
                if (!filterVal) return
                filterVal = now.add(-filterVal, 'day')
                break
              case 'nextNumberOfDays':
                if (!filterVal) return
                filterVal = now.add(filterVal, 'day')
                break
            }

            if (dataVal) {
              switch (filter.comparison_op) {
                case 'eq':
                  res = dayjs(dataVal).isSame(filterVal, 'day')
                  break
                case 'neq':
                  res = !dayjs(dataVal).isSame(filterVal, 'day')
                  break
                case 'gt':
                  res = dayjs(dataVal).isAfter(filterVal, 'day')
                  break
                case 'lt':
                  res = dayjs(dataVal).isBefore(filterVal, 'day')
                  break
                case 'lte':
                case 'le':
                  res = dayjs(dataVal).isSameOrBefore(filterVal, 'day')
                  break
                case 'gte':
                case 'ge':
                  res = dayjs(dataVal).isSameOrAfter(filterVal, 'day')
                  break
                case 'empty':
                case 'blank':
                  res = dataVal === '' || dataVal === null || dataVal === undefined
                  break
                case 'notempty':
                case 'notblank':
                  res = !(dataVal === '' || dataVal === null || dataVal === undefined)
                  break
                case 'isWithin': {
                  let now = dayjs(new Date()).format(dateFormat).toString()
                  now = column.uidt === UITypes.Date ? now.substring(0, 10) : now
                  switch (filter.comparison_sub_op) {
                    case 'pastWeek':
                    case 'pastMonth':
                    case 'pastYear':
                    case 'pastNumberOfDays':
                      res = dayjs(dataVal).isBetween(filterVal, now, 'day')
                      break
                    case 'nextWeek':
                    case 'nextMonth':
                    case 'nextYear':
                    case 'nextNumberOfDays':
                      res = dayjs(dataVal).isBetween(now, filterVal, 'day')
                      break
                  }
                }
              }
            }
          } else {
            switch (typeof filter.value) {
              case 'boolean':
                val = !!this.formState[field]
                break
              case 'number':
                val = Number.isNaN(parseFloat(this.formState[field])) ? this.formState[field] : +this.formState[field]
                break
            }

            switch (column.uidt) {
              case UITypes.Links:
                if (isMm(column) || isHm(column)) {
                  val = (this.formState[field] ?? []).length
                }
                break
              case UITypes.LinkToAnotherRecord:
                if (isOo(column) || isBt(column)) {
                  val = await this.getOoOrBtColVal(column)
                }
                break
            }

            switch (filter.comparison_op) {
              case 'eq':
                // eslint-disable-next-line eqeqeq
                res = val == filter.value
                break
              case 'neq':
                // eslint-disable-next-line eqeqeq
                res = val != filter.value
                break
              case 'like':
                res = this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
                break
              case 'nlike':
                res = !this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
                break
              case 'empty':
              case 'blank':
                res = val === '' || val === null || val === undefined
                break
              case 'notempty':
              case 'notblank':
                res = !(val === '' || val === null || val === undefined)
                break
              case 'checked':
                res = !!val
                break
              case 'notchecked':
                res = !val
                break
              case 'null':
                res = val === null
                break
              case 'notnull':
                res = val !== null
                break
              case 'allof':
                res = (
                  this.toString(filter.value)
                    .split(',')
                    .map((item) => item.trim()) ?? []
                ).every((item) => (this.toString(val).split(',') ?? []).includes(item))
                break
              case 'anyof':
                res = (
                  this.toString(filter.value)
                    .split(',')
                    .map((item) => item.trim()) ?? []
                ).some((item) => (this.toString(val).split(',') ?? []).includes(item))
                break
              case 'nallof':
                res = !(
                  this.toString(filter.value)
                    .split(',')
                    .map((item) => item.trim()) ?? []
                ).every((item) => (this.toString(val).split(',') ?? []).includes(item))
                break
              case 'nanyof':
                res = !(
                  this.toString(filter.value)
                    .split(',')
                    .map((item) => item.trim()) ?? []
                ).some((item) => (this.toString(val).split(',') ?? []).includes(item))
                break
              case 'lt':
                res = parseFloat(val) < +filter.value
                break
              case 'lte':
              case 'le':
                res = parseFloat(val) <= +filter.value
                break
              case 'gt':
                res = parseFloat(val) > +filter.value
                break
              case 'gte':
              case 'ge':
                res = parseFloat(val) >= +filter.value
                break
            }
          }
        }
      }

      switch (filter.logical_op) {
        case 'or':
          isValid = isValid || !!res
          break
        case 'not':
          isValid = isValid && !res
          break
        case 'and':
        default:
          isValid = (isValid ?? true) && res
          break
      }
    }
    return isValid
  }

  async validateVisibility() {
    const res: Record<string, boolean> = {}

    for (const column of this.formViewColumns) {
      const columnFilters = this.nestedGroupedFilters[column.fk_column_id] ?? []

      const errors: Record<string, string> = {}

      const isValid = await this.validateCondition(columnFilters, column, errors)

      if (this.isSharedForm) {
        if (!column.meta?.preFilledHiddenField) {
          column.visible = !!isValid
        }
      } else {
        column.visible = !!isValid

        column.meta = {
          ...parseProp(column.meta),
          visibility: {
            errors,
          },
        }
      }
    }

    return res
  }

  validateErrors() {}
}
