/**
 * CronEditor - Visual cron expression builder
 * @countermeasure/web-components/form/cron-editor
 */

import {
  type CronConfig,
  type CronField,
  type CronFieldConfig,
  type CronFieldMode,
  type ParsedCron,
} from './types'
import { cronToHuman, formatCron, getNextRuns, isValidCron, parseCron } from './parser'
import { resolveContainer, uniqueId } from '../../primitives/utils'
import { announce } from '../../utils/a11y'

export type { CronConfig, CronField, CronFieldConfig, CronFieldMode, ParsedCron } from './types'

export { cronToHuman, formatCron, getNextRuns, isValidCron, parseCron } from './parser'

/** Field metadata for building the UI */
interface FieldMeta {
  label: string
  min: number
  max: number
  labels?: string[]
}

const FIELD_META: Record<CronField, FieldMeta> = {
  minute: { label: 'Minute', min: 0, max: 59 },
  hour: { label: 'Hour', min: 0, max: 23 },
  day: { label: 'Day of Month', min: 1, max: 31 },
  month: {
    label: 'Month',
    min: 1,
    max: 12,
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
  },
  weekday: {
    label: 'Weekday',
    min: 0,
    max: 6,
    labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  },
}

const FIELD_ORDER: CronField[] = ['minute', 'hour', 'day', 'month', 'weekday']

const MODE_LABELS: Record<CronFieldMode, string> = {
  every: 'Every',
  specific: 'Specific',
  range: 'Range',
  interval: 'Interval',
}

const MODES: CronFieldMode[] = ['every', 'specific', 'range', 'interval']

export class CronEditor {
  private readonly container: HTMLElement
  private readonly config: CronConfig
  private readonly id: string
  private readonly tabPanelId: string

  private parsed: ParsedCron
  private activeField: CronField = 'minute'

  private tabBarEl: HTMLElement | null = null
  private fieldBodyEl: HTMLElement | null = null
  private expressionInput: HTMLInputElement | null = null
  private previewEl: HTMLElement | null = null
  private nextRunsEl: HTMLElement | null = null

  constructor(config: CronConfig) {
    this.container = resolveContainer(config.container)
    this.config = config
    this.id = uniqueId('cmm-cron-editor')
    this.tabPanelId = uniqueId('cmm-cron-tabpanel')

    const initial = config.value ?? '* * * * *'
    this.parsed = parseCron(initial)

    this.render()
  }

  /** Get the current cron expression */
  getValue(): string {
    return formatCron(this.parsed)
  }

  /** Set the cron expression and update the visual editor */
  setValue(expression: string): void {
    if (!isValidCron(expression)) return
    this.parsed = parseCron(expression)
    this.updateAllUI()
  }

  /** Clean up DOM elements */
  destroy(): void {
    this.container.innerHTML = ''
    this.tabBarEl = null
    this.fieldBodyEl = null
    this.expressionInput = null
    this.previewEl = null
    this.nextRunsEl = null
  }

  /** Build the full DOM structure */
  private render(): void {
    this.container.innerHTML = ''

    const root = document.createElement('div')
    root.className = 'cmm-cron-editor'
    if (this.config.className) {
      root.classList.add(this.config.className)
    }
    root.id = this.id

    // Tab bar for field selection
    const tabBar = document.createElement('div')
    tabBar.className = 'cmm-cron-editor__tabs'
    tabBar.setAttribute('role', 'tablist')
    this.tabBarEl = tabBar

    for (const field of FIELD_ORDER) {
      const meta = FIELD_META[field]
      const tabId = `${this.id}-tab-${field}`
      const tab = document.createElement('button')
      tab.className = 'cmm-cron-editor__tab'
      tab.type = 'button'
      tab.id = tabId
      tab.setAttribute('role', 'tab')
      tab.setAttribute('aria-selected', field === this.activeField ? 'true' : 'false')
      tab.setAttribute('aria-controls', this.tabPanelId)
      tab.dataset.field = field
      tab.textContent = meta.label
      if (field === this.activeField) {
        tab.classList.add('cmm-cron-editor__tab--active')
      }
      tab.addEventListener('click', () => {
        this.setActiveField(field)
      })
      tabBar.appendChild(tab)
    }

    root.appendChild(tabBar)

    // Field body (mode selector + value editors)
    const fieldBody = document.createElement('div')
    fieldBody.id = this.tabPanelId
    fieldBody.className = 'cmm-cron-editor__body'
    fieldBody.setAttribute('role', 'tabpanel')
    fieldBody.setAttribute('aria-labelledby', `${this.id}-tab-${this.activeField}`)
    this.fieldBodyEl = fieldBody
    root.appendChild(fieldBody)

    // Cron expression input
    const exprRow = document.createElement('div')
    exprRow.className = 'cmm-cron-editor__expression'
    const exprLabel = document.createElement('label')
    exprLabel.className = 'cmm-cron-editor__expression-label'
    exprLabel.textContent = 'Cron Expression'
    const exprInput = document.createElement('input')
    exprInput.className = 'cmm-cron-editor__expression-input'
    exprInput.type = 'text'
    exprInput.value = formatCron(this.parsed)
    exprInput.setAttribute('aria-label', 'Cron expression')
    exprInput.addEventListener('change', () => {
      const val = exprInput.value.trim()
      if (isValidCron(val)) {
        this.parsed = parseCron(val)
        this.updateFieldBody()
        this.updatePreview()
        this.updateNextRuns()
        this.updateTabHighlights()
        this.emitChange()
      }
    })
    this.expressionInput = exprInput
    exprRow.appendChild(exprLabel)
    exprRow.appendChild(exprInput)
    root.appendChild(exprRow)

    // Human-readable preview
    if (this.config.showPreview !== false) {
      const preview = document.createElement('div')
      preview.className = 'cmm-cron-editor__preview'
      this.previewEl = preview
      root.appendChild(preview)
    }

    // Next runs preview
    if (this.config.showNextRuns !== false) {
      const nextRuns = document.createElement('div')
      nextRuns.className = 'cmm-cron-editor__next-runs'
      this.nextRunsEl = nextRuns
      root.appendChild(nextRuns)
    }

    this.container.appendChild(root)

    // Render initial state
    this.updateFieldBody()
    this.updatePreview()
    this.updateNextRuns()
  }

  /** Switch to a different field tab */
  private setActiveField(field: CronField): void {
    this.activeField = field
    this.updateTabHighlights()
    this.updateFieldBody()

    // Update tabpanel aria-labelledby
    if (this.fieldBodyEl !== null) {
      this.fieldBodyEl.setAttribute('aria-labelledby', `${this.id}-tab-${field}`)
    }

    // Announce the field change
    const meta = FIELD_META[field]
    announce(`${meta.label} field selected`, 'polite')
  }

  /** Update tab bar active states */
  private updateTabHighlights(): void {
    if (this.tabBarEl === null) return
    const tabs = this.tabBarEl.querySelectorAll<HTMLElement>('.cmm-cron-editor__tab')
    tabs.forEach(tab => {
      const f = tab.dataset.field
      if (f === this.activeField) {
        tab.classList.add('cmm-cron-editor__tab--active')
        tab.setAttribute('aria-selected', 'true')
      } else {
        tab.classList.remove('cmm-cron-editor__tab--active')
        tab.setAttribute('aria-selected', 'false')
      }
    })
  }

  /** Render the body for the currently active field */
  private updateFieldBody(): void {
    const body = this.fieldBodyEl
    if (body === null) return
    body.innerHTML = ''

    const fieldConfig = this.parsed[this.activeField]
    const meta = FIELD_META[this.activeField]

    // Mode selector
    const modeRow = document.createElement('div')
    modeRow.className = 'cmm-cron-editor__modes'

    for (const mode of MODES) {
      const btn = document.createElement('button')
      btn.className = 'cmm-cron-editor__mode'
      btn.type = 'button'
      btn.textContent = MODE_LABELS[mode]
      if (mode === fieldConfig.mode) {
        btn.classList.add('cmm-cron-editor__mode--active')
      }
      btn.addEventListener('click', () => {
        this.setFieldMode(mode)
      })
      modeRow.appendChild(btn)
    }

    body.appendChild(modeRow)

    // Value editor based on mode
    const editorEl = document.createElement('div')
    editorEl.className = 'cmm-cron-editor__editor'

    switch (fieldConfig.mode) {
      case 'every':
        this.renderEveryEditor(editorEl, meta)
        break
      case 'specific':
        this.renderSpecificEditor(editorEl, meta, fieldConfig)
        break
      case 'range':
        this.renderRangeEditor(editorEl, meta, fieldConfig)
        break
      case 'interval':
        this.renderIntervalEditor(editorEl, meta, fieldConfig)
        break
    }

    body.appendChild(editorEl)
  }

  /** Set the mode for the current field */
  private setFieldMode(mode: CronFieldMode): void {
    const meta = FIELD_META[this.activeField]
    const config: CronFieldConfig = { mode }

    switch (mode) {
      case 'specific':
        config.specific = []
        break
      case 'range':
        config.rangeStart = meta.min
        config.rangeEnd = meta.max
        break
      case 'interval':
        config.interval = 1
        config.intervalStart = meta.min
        break
    }

    this.parsed[this.activeField] = config
    this.onFieldChanged()
  }

  /** Render the "every" mode editor (just informational text) */
  private renderEveryEditor(container: HTMLElement, meta: FieldMeta): void {
    const text = document.createElement('p')
    text.className = 'cmm-cron-editor__hint'
    text.textContent = `Every ${meta.label.toLowerCase()} (* wildcard)`
    container.appendChild(text)
  }

  /** Render the "specific" mode editor with a clickable value grid */
  private renderSpecificEditor(
    container: HTMLElement,
    meta: FieldMeta,
    fieldConfig: CronFieldConfig
  ): void {
    const grid = document.createElement('div')
    grid.className = 'cmm-cron-editor__grid'

    const selected = new Set(fieldConfig.specific ?? [])

    for (let i = meta.min; i <= meta.max; i++) {
      const btn = document.createElement('button')
      btn.className = 'cmm-cron-editor__grid-item'
      btn.type = 'button'

      if (meta.labels !== undefined) {
        const labelIndex = i - meta.min
        btn.textContent = meta.labels[labelIndex] ?? String(i)
      } else {
        btn.textContent = String(i)
      }

      if (selected.has(i)) {
        btn.classList.add('cmm-cron-editor__grid-item--active')
      }

      const value = i
      btn.addEventListener('click', () => {
        this.toggleSpecificValue(value)
      })

      grid.appendChild(btn)
    }

    container.appendChild(grid)
  }

  /** Toggle a specific value in the current field */
  private toggleSpecificValue(value: number): void {
    const config = this.parsed[this.activeField]
    if (config.mode !== 'specific') return

    const values = config.specific ?? []
    const index = values.indexOf(value)
    if (index >= 0) {
      values.splice(index, 1)
    } else {
      values.push(value)
    }
    config.specific = values
    this.onFieldChanged()
  }

  /** Render the "range" mode editor with from/to inputs */
  private renderRangeEditor(
    container: HTMLElement,
    meta: FieldMeta,
    fieldConfig: CronFieldConfig
  ): void {
    const row = document.createElement('div')
    row.className = 'cmm-cron-editor__range-row'

    const fromLabel = document.createElement('label')
    fromLabel.className = 'cmm-cron-editor__field-label'
    fromLabel.textContent = 'From'

    const fromInput = document.createElement('input')
    fromInput.className = 'cmm-cron-editor__field-input'
    fromInput.type = 'number'
    fromInput.min = String(meta.min)
    fromInput.max = String(meta.max)
    fromInput.value = String(fieldConfig.rangeStart ?? meta.min)
    fromInput.addEventListener('change', () => {
      const v = parseInt(fromInput.value, 10)
      if (!isNaN(v) && v >= meta.min && v <= meta.max) {
        this.parsed[this.activeField].rangeStart = v
        this.onFieldChanged()
      }
    })

    const toLabel = document.createElement('label')
    toLabel.className = 'cmm-cron-editor__field-label'
    toLabel.textContent = 'To'

    const toInput = document.createElement('input')
    toInput.className = 'cmm-cron-editor__field-input'
    toInput.type = 'number'
    toInput.min = String(meta.min)
    toInput.max = String(meta.max)
    toInput.value = String(fieldConfig.rangeEnd ?? meta.max)
    toInput.addEventListener('change', () => {
      const v = parseInt(toInput.value, 10)
      if (!isNaN(v) && v >= meta.min && v <= meta.max) {
        this.parsed[this.activeField].rangeEnd = v
        this.onFieldChanged()
      }
    })

    row.appendChild(fromLabel)
    row.appendChild(fromInput)
    row.appendChild(toLabel)
    row.appendChild(toInput)
    container.appendChild(row)
  }

  /** Render the "interval" mode editor with step and start inputs */
  private renderIntervalEditor(
    container: HTMLElement,
    meta: FieldMeta,
    fieldConfig: CronFieldConfig
  ): void {
    const row = document.createElement('div')
    row.className = 'cmm-cron-editor__interval-row'

    const everyLabel = document.createElement('label')
    everyLabel.className = 'cmm-cron-editor__field-label'
    everyLabel.textContent = 'Every'

    const stepInput = document.createElement('input')
    stepInput.className = 'cmm-cron-editor__field-input'
    stepInput.type = 'number'
    stepInput.min = '1'
    stepInput.max = String(meta.max - meta.min + 1)
    stepInput.value = String(fieldConfig.interval ?? 1)
    stepInput.addEventListener('change', () => {
      const v = parseInt(stepInput.value, 10)
      if (!isNaN(v) && v > 0) {
        this.parsed[this.activeField].interval = v
        this.onFieldChanged()
      }
    })

    const startLabel = document.createElement('label')
    startLabel.className = 'cmm-cron-editor__field-label'
    startLabel.textContent = 'Starting at'

    const startInput = document.createElement('input')
    startInput.className = 'cmm-cron-editor__field-input'
    startInput.type = 'number'
    startInput.min = String(meta.min)
    startInput.max = String(meta.max)
    startInput.value = String(fieldConfig.intervalStart ?? meta.min)
    startInput.addEventListener('change', () => {
      const v = parseInt(startInput.value, 10)
      if (!isNaN(v) && v >= meta.min && v <= meta.max) {
        this.parsed[this.activeField].intervalStart = v
        this.onFieldChanged()
      }
    })

    row.appendChild(everyLabel)
    row.appendChild(stepInput)
    row.appendChild(startLabel)
    row.appendChild(startInput)
    container.appendChild(row)
  }

  /** Called whenever a field value changes - update expression, preview, etc. */
  private onFieldChanged(): void {
    this.updateFieldBody()
    this.updateExpressionInput()
    this.updatePreview()
    this.updateNextRuns()
    this.emitChange()
  }

  /** Update the expression text input */
  private updateExpressionInput(): void {
    if (this.expressionInput !== null) {
      this.expressionInput.value = formatCron(this.parsed)
    }
  }

  /** Update the human-readable preview text */
  private updatePreview(): void {
    if (this.previewEl === null) return
    const expr = formatCron(this.parsed)
    this.previewEl.textContent = cronToHuman(expr)
  }

  /** Update the next-runs list */
  private updateNextRuns(): void {
    if (this.nextRunsEl === null) return
    this.nextRunsEl.innerHTML = ''

    const count = this.config.nextRunCount ?? 5
    const expr = formatCron(this.parsed)
    const runs = getNextRuns(expr, count)

    if (runs.length === 0) {
      const emptyText = document.createElement('p')
      emptyText.className = 'cmm-cron-editor__no-runs'
      emptyText.textContent = 'No upcoming runs found'
      this.nextRunsEl.appendChild(emptyText)
      return
    }

    const heading = document.createElement('div')
    heading.className = 'cmm-cron-editor__next-runs-heading'
    heading.textContent = `Next ${String(runs.length)} runs`
    this.nextRunsEl.appendChild(heading)

    const list = document.createElement('ul')
    list.className = 'cmm-cron-editor__runs-list'

    for (const run of runs) {
      const li = document.createElement('li')
      li.className = 'cmm-cron-editor__run'
      li.textContent = formatRunDate(run)
      list.appendChild(li)
    }

    this.nextRunsEl.appendChild(list)
  }

  /** Update all UI elements to match current state */
  private updateAllUI(): void {
    this.updateTabHighlights()
    this.updateFieldBody()
    this.updateExpressionInput()
    this.updatePreview()
    this.updateNextRuns()
  }

  /** Emit the onChange callback */
  private emitChange(): void {
    const expr = formatCron(this.parsed)
    this.config.onChange?.(expr)

    // Announce the human-readable schedule
    const human = cronToHuman(expr)
    announce(`Schedule: ${human}`, 'polite')
  }
}

/** Format a Date as a human-readable string for the next runs list */
function formatRunDate(date: Date): string {
  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
  const months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ]
  const dayName = days[date.getDay()] ?? ''
  const monthName = months[date.getMonth()] ?? ''
  const day = date.getDate()
  const year = date.getFullYear()
  const h = date.getHours()
  const m = String(date.getMinutes()).padStart(2, '0')
  const ampm = h >= 12 ? 'PM' : 'AM'
  const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h
  return `${dayName}, ${monthName} ${String(day)}, ${String(year)} at ${String(h12)}:${m} ${ampm}`
}
