/**
 * RuleEditor - Visual query/rule builder component
 * @countermeasure/web-components/visualization/rule-editor
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import type { FieldDefinition, RuleCondition, RuleEditorConfig, RuleGroup } from './types'

export type { FieldDefinition, RuleCondition, RuleEditorConfig, RuleGroup } from './types'

/** Default operators for each field type */
const DEFAULT_OPERATORS: Record<string, string[]> = {
  string: [
    'equals',
    'not equals',
    'contains',
    'starts with',
    'ends with',
    'is empty',
    'is not empty',
  ],
  number: [
    'equals',
    'not equals',
    'greater than',
    'less than',
    'greater or equal',
    'less or equal',
    'between',
  ],
  boolean: ['equals', 'not equals'],
  date: ['equals', 'before', 'after', 'between', 'in last', 'is empty'],
}

/** Check whether an item is a RuleGroup (has `logic` and `conditions`) */
function isRuleGroup(item: RuleCondition | RuleGroup): item is RuleGroup {
  return 'logic' in item && 'conditions' in item
}

/** Generate a random short ID */
function randomId(): string {
  return uniqueId('rule')
}

export class RuleEditor {
  private readonly container: HTMLElement
  private readonly config: RuleEditorConfig
  private readonly instanceId: string
  private readonly fields: FieldDefinition[]
  private readonly maxDepth: number
  private rules: RuleGroup
  private wrapperEl: HTMLElement | null = null
  private dragState: { conditionId: string; groupId: string } | null = null

  constructor(config: RuleEditorConfig) {
    this.container = resolveContainer(config.container)
    this.config = config
    this.instanceId = uniqueId('cmm-rule-editor')
    this.fields = config.fields
    this.maxDepth = config.maxDepth ?? 3
    this.rules = this.cloneGroup(config.rules)
    this.render()
  }

  /** Get the current rule group value */
  getValue(): RuleGroup {
    return this.cloneGroup(this.rules)
  }

  /** Set the rules and re-render */
  setValue(rules: RuleGroup): void {
    this.rules = this.cloneGroup(rules)
    this.render()
  }

  /** Remove the component from the DOM and clean up */
  destroy(): void {
    this.container.innerHTML = ''
    this.wrapperEl = null
    this.dragState = null
  }

  /** Deep clone a rule group */
  private cloneGroup(group: RuleGroup): RuleGroup {
    return {
      id: group.id,
      logic: group.logic,
      conditions: group.conditions.map(item => {
        if (isRuleGroup(item)) {
          return this.cloneGroup(item)
        }
        return { ...item }
      }),
    }
  }

  /** Notify the onChange callback */
  private emitChange(): void {
    this.config.onChange?.(this.cloneGroup(this.rules))
  }

  /** Render the entire editor */
  private render(): void {
    this.container.innerHTML = ''

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-rule-editor'
    wrapper.id = this.instanceId

    if (this.config.className !== undefined) {
      wrapper.classList.add(this.config.className)
    }

    wrapper.appendChild(this.renderGroup(this.rules, 0))

    this.wrapperEl = wrapper
    this.container.appendChild(wrapper)
  }

  /** Render a rule group */
  private renderGroup(group: RuleGroup, depth: number): HTMLElement {
    const groupEl = document.createElement('div')
    groupEl.className = 'cmm-rule-editor__group'
    groupEl.dataset.groupId = group.id
    groupEl.dataset.depth = String(depth)

    if (depth > 0) {
      groupEl.classList.add('cmm-rule-editor__group--nested')
    }

    // Group header
    const header = document.createElement('div')
    header.className = 'cmm-rule-editor__group-header'

    // Logic toggle
    const logicToggle = document.createElement('button')
    logicToggle.className = 'cmm-rule-editor__logic-toggle'
    logicToggle.classList.add(
      group.logic === 'AND'
        ? 'cmm-rule-editor__logic-toggle--and'
        : 'cmm-rule-editor__logic-toggle--or'
    )
    logicToggle.textContent = group.logic
    logicToggle.addEventListener('click', () => {
      group.logic = group.logic === 'AND' ? 'OR' : 'AND'
      this.emitChange()
      this.render()
    })
    header.appendChild(logicToggle)

    // Add condition button
    const addCondBtn = document.createElement('button')
    addCondBtn.className = 'cmm-rule-editor__btn cmm-rule-editor__btn--add-condition'
    addCondBtn.textContent = '+ Condition'
    addCondBtn.addEventListener('click', () => {
      const firstField = this.fields[0]
      if (firstField === undefined) {
        return
      }
      const operators = this.getOperatorsForField(firstField)
      const firstOp = operators[0]
      const newCondition: RuleCondition = {
        id: randomId(),
        field: firstField.name,
        operator: firstOp ?? 'equals',
        value: this.getDefaultValue(firstField.type),
        type: firstField.type,
      }
      group.conditions.push(newCondition)
      this.emitChange()
      this.render()
    })
    header.appendChild(addCondBtn)

    // Add group button (only if under max depth)
    if (depth < this.maxDepth - 1) {
      const addGroupBtn = document.createElement('button')
      addGroupBtn.className = 'cmm-rule-editor__btn cmm-rule-editor__btn--add-group'
      addGroupBtn.textContent = '+ Group'
      addGroupBtn.addEventListener('click', () => {
        const newGroup: RuleGroup = {
          id: randomId(),
          logic: 'AND',
          conditions: [],
        }
        group.conditions.push(newGroup)
        this.emitChange()
        this.render()
      })
      header.appendChild(addGroupBtn)
    }

    // Remove group button (not for root)
    if (depth > 0) {
      const removeGroupBtn = document.createElement('button')
      removeGroupBtn.className = 'cmm-rule-editor__btn cmm-rule-editor__btn--remove'
      removeGroupBtn.textContent = '\u00D7'
      removeGroupBtn.title = 'Remove group'
      removeGroupBtn.addEventListener('click', () => {
        this.removeGroupById(this.rules, group.id)
        this.emitChange()
        this.render()
      })
      header.appendChild(removeGroupBtn)
    }

    groupEl.appendChild(header)

    // Conditions list
    const conditionsList = document.createElement('div')
    conditionsList.className = 'cmm-rule-editor__conditions'
    conditionsList.dataset.groupId = group.id

    this.setupGroupDropZone(conditionsList, group)

    for (const item of group.conditions) {
      if (isRuleGroup(item)) {
        conditionsList.appendChild(this.renderGroup(item, depth + 1))
      } else {
        conditionsList.appendChild(this.renderCondition(item, group))
      }
    }

    groupEl.appendChild(conditionsList)
    return groupEl
  }

  /** Render a single condition row */
  private renderCondition(condition: RuleCondition, parentGroup: RuleGroup): HTMLElement {
    const rowEl = document.createElement('div')
    rowEl.className = 'cmm-rule-editor__condition'
    rowEl.dataset.conditionId = condition.id
    rowEl.draggable = true

    this.setupConditionDrag(rowEl, condition.id, parentGroup.id)

    // Drag handle
    const handle = document.createElement('span')
    handle.className = 'cmm-rule-editor__drag-handle'
    handle.textContent = '\u2261'
    handle.title = 'Drag to reorder'
    rowEl.appendChild(handle)

    // Field select
    const fieldSelect = document.createElement('select')
    fieldSelect.className = 'cmm-rule-editor__select cmm-rule-editor__field-select'

    for (const field of this.fields) {
      const opt = document.createElement('option')
      opt.value = field.name
      opt.textContent = field.label
      if (field.name === condition.field) {
        opt.selected = true
      }
      fieldSelect.appendChild(opt)
    }

    fieldSelect.addEventListener('change', () => {
      condition.field = fieldSelect.value
      const fieldDef = this.fields.find(f => f.name === fieldSelect.value)
      if (fieldDef !== undefined) {
        condition.type = fieldDef.type
        const ops = this.getOperatorsForField(fieldDef)
        const firstOp = ops[0]
        condition.operator = firstOp ?? 'equals'
        condition.value = this.getDefaultValue(fieldDef.type)
      }
      this.emitChange()
      this.render()
    })
    rowEl.appendChild(fieldSelect)

    // Operator select
    const fieldDef = this.fields.find(f => f.name === condition.field)
    const operators = fieldDef !== undefined ? this.getOperatorsForField(fieldDef) : ['equals']

    const opSelect = document.createElement('select')
    opSelect.className = 'cmm-rule-editor__select cmm-rule-editor__operator-select'

    for (const op of operators) {
      const opt = document.createElement('option')
      opt.value = op
      opt.textContent = op
      if (op === condition.operator) {
        opt.selected = true
      }
      opSelect.appendChild(opt)
    }

    opSelect.addEventListener('change', () => {
      condition.operator = opSelect.value
      this.emitChange()
    })
    rowEl.appendChild(opSelect)

    // Value input
    const valueInput = this.createValueInput(condition, fieldDef)
    rowEl.appendChild(valueInput)

    // Remove button
    const removeBtn = document.createElement('button')
    removeBtn.className = 'cmm-rule-editor__btn cmm-rule-editor__btn--remove'
    removeBtn.textContent = '\u00D7'
    removeBtn.title = 'Remove condition'
    removeBtn.addEventListener('click', () => {
      const idx = parentGroup.conditions.findIndex(c => !isRuleGroup(c) && c.id === condition.id)
      if (idx >= 0) {
        parentGroup.conditions.splice(idx, 1)
        this.emitChange()
        this.render()
      }
    })
    rowEl.appendChild(removeBtn)

    return rowEl
  }

  /** Create an appropriate value input for the condition type */
  private createValueInput(
    condition: RuleCondition,
    fieldDef: FieldDefinition | undefined
  ): HTMLElement {
    const effectiveType = condition.type ?? fieldDef?.type ?? 'string'

    switch (effectiveType) {
      case 'boolean': {
        const select = document.createElement('select')
        select.className = 'cmm-rule-editor__select cmm-rule-editor__value-select'

        const trueOpt = document.createElement('option')
        trueOpt.value = 'true'
        trueOpt.textContent = 'true'
        if (condition.value === true || condition.value === 'true') {
          trueOpt.selected = true
        }
        select.appendChild(trueOpt)

        const falseOpt = document.createElement('option')
        falseOpt.value = 'false'
        falseOpt.textContent = 'false'
        if (condition.value === false || condition.value === 'false') {
          falseOpt.selected = true
        }
        select.appendChild(falseOpt)

        select.addEventListener('change', () => {
          condition.value = select.value === 'true'
          this.emitChange()
        })

        return select
      }
      case 'number': {
        const input = document.createElement('input')
        input.className = 'cmm-rule-editor__input'
        input.type = 'number'
        input.value = String(condition.value)
        input.addEventListener('change', () => {
          const parsed = Number(input.value)
          condition.value = Number.isNaN(parsed) ? 0 : parsed
          this.emitChange()
        })
        return input
      }
      case 'date': {
        const input = document.createElement('input')
        input.className = 'cmm-rule-editor__input'
        input.type = 'date'
        input.value = String(condition.value)
        input.addEventListener('change', () => {
          condition.value = input.value
          this.emitChange()
        })
        return input
      }
      default: {
        const input = document.createElement('input')
        input.className = 'cmm-rule-editor__input'
        input.type = 'text'
        input.value = String(condition.value)
        input.placeholder = 'Enter value...'
        input.addEventListener('input', () => {
          condition.value = input.value
          this.emitChange()
        })
        return input
      }
    }
  }

  /** Get operators for a field definition */
  private getOperatorsForField(field: FieldDefinition): string[] {
    if (field.operators !== undefined && field.operators.length > 0) {
      return field.operators
    }
    const defaults = DEFAULT_OPERATORS[field.type]
    return defaults ?? ['equals']
  }

  /** Get the default value for a field type */
  private getDefaultValue(
    type: 'string' | 'number' | 'boolean' | 'date'
  ): string | number | boolean {
    switch (type) {
      case 'number':
        return 0
      case 'boolean':
        return true
      case 'date':
        return new Date().toISOString().split('T')[0] ?? ''
      default:
        return ''
    }
  }

  /** Recursively find and remove a group by ID */
  private removeGroupById(parent: RuleGroup, groupId: string): boolean {
    for (let i = 0; i < parent.conditions.length; i++) {
      const item = parent.conditions[i]
      if (item === undefined) {
        continue
      }

      if (isRuleGroup(item)) {
        if (item.id === groupId) {
          parent.conditions.splice(i, 1)
          return true
        }
        if (this.removeGroupById(item, groupId)) {
          return true
        }
      }
    }
    return false
  }

  /** Recursively find a group by ID */
  private findGroupById(group: RuleGroup, groupId: string): RuleGroup | undefined {
    if (group.id === groupId) {
      return group
    }

    for (const item of group.conditions) {
      if (isRuleGroup(item)) {
        const found = this.findGroupById(item, groupId)
        if (found !== undefined) {
          return found
        }
      }
    }

    return undefined
  }

  /** Set up drag events on a condition row */
  private setupConditionDrag(el: HTMLElement, conditionId: string, groupId: string): void {
    el.addEventListener('dragstart', (e: DragEvent) => {
      this.dragState = { conditionId, groupId }
      el.classList.add('cmm-rule-editor__condition--dragging')

      if (e.dataTransfer !== null) {
        e.dataTransfer.effectAllowed = 'move'
        e.dataTransfer.setData('text/plain', conditionId)
      }
    })

    el.addEventListener('dragend', () => {
      el.classList.remove('cmm-rule-editor__condition--dragging')
      this.dragState = null

      if (this.wrapperEl !== null) {
        const indicators = this.wrapperEl.querySelectorAll(
          '.cmm-rule-editor__conditions--drag-over'
        )
        for (const ind of indicators) {
          ind.classList.remove('cmm-rule-editor__conditions--drag-over')
        }
      }
    })
  }

  /** Set up drop zone on a conditions list */
  private setupGroupDropZone(el: HTMLElement, group: RuleGroup): void {
    el.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      if (e.dataTransfer !== null) {
        e.dataTransfer.dropEffect = 'move'
      }
      el.classList.add('cmm-rule-editor__conditions--drag-over')
    })

    el.addEventListener('dragleave', (e: DragEvent) => {
      const related = e.relatedTarget
      if (related instanceof Node && el.contains(related)) {
        return
      }
      el.classList.remove('cmm-rule-editor__conditions--drag-over')
    })

    el.addEventListener('drop', (e: DragEvent) => {
      e.preventDefault()
      e.stopPropagation()
      el.classList.remove('cmm-rule-editor__conditions--drag-over')

      if (this.dragState === null) {
        return
      }

      const { conditionId, groupId: sourceGroupId } = this.dragState
      this.dragState = null

      // Find the source group and remove the condition
      const sourceGroup = this.findGroupById(this.rules, sourceGroupId)
      if (sourceGroup === undefined) {
        return
      }

      const condIdx = sourceGroup.conditions.findIndex(c => !isRuleGroup(c) && c.id === conditionId)
      if (condIdx < 0) {
        return
      }

      const removed = sourceGroup.conditions.splice(condIdx, 1)
      const condition = removed[0]
      if (condition === undefined) {
        return
      }

      // Calculate drop index
      const dropIndex = this.calculateDropIndex(el, e.clientY)
      const clampedIndex = Math.min(dropIndex, group.conditions.length)
      group.conditions.splice(clampedIndex, 0, condition)

      this.emitChange()
      this.render()
    })
  }

  /** Calculate the index to drop at based on mouse Y */
  private calculateDropIndex(listEl: HTMLElement, clientY: number): number {
    const children = listEl.querySelectorAll(
      ':scope > .cmm-rule-editor__condition, :scope > .cmm-rule-editor__group'
    )
    let index = children.length

    for (let i = 0; i < children.length; i++) {
      const childEl = children[i]
      if (childEl === undefined) {
        continue
      }
      const rect = childEl.getBoundingClientRect()
      const midY = rect.top + rect.height / 2

      if (clientY < midY) {
        index = i
        break
      }
    }

    return index
  }
}
