/**
 * Cron parser, formatter, and utilities
 * @countermeasure/web-components/form/cron-editor
 */

import { type CronField, type CronFieldConfig, type ParsedCron } from './types'

/** Minimum and maximum values for each cron field */
const FIELD_RANGES: Record<CronField, [number, number]> = {
  minute: [0, 59],
  hour: [0, 23],
  day: [1, 31],
  month: [1, 12],
  weekday: [0, 6],
}

/** Ordered field keys matching the 5-field cron format */
const FIELD_ORDER: CronField[] = ['minute', 'hour', 'day', 'month', 'weekday']

/** Month names for human-readable output */
const MONTH_NAMES = [
  '',
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December',
]

/** Weekday names for human-readable output */
const WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']

/** Parse a single cron field token into a structured config */
function parseField(token: string, field: CronField): CronFieldConfig {
  const [min, max] = FIELD_RANGES[field]

  // Wildcard
  if (token === '*') {
    return { mode: 'every' }
  }

  // Interval: */N or M/N
  if (token.includes('/')) {
    const parts = token.split('/')
    const base = parts[0] ?? '*'
    const step = parseInt(parts[1] ?? '1', 10)
    const start = base === '*' ? min : parseInt(base, 10)
    return {
      mode: 'interval',
      interval: step,
      intervalStart: start,
    }
  }

  // Range: M-N
  if (token.includes('-') && !token.includes(',')) {
    const parts = token.split('-')
    const rangeStart = parseInt(parts[0] ?? String(min), 10)
    const rangeEnd = parseInt(parts[1] ?? String(max), 10)
    return {
      mode: 'range',
      rangeStart,
      rangeEnd,
    }
  }

  // Specific values: N,N,N (also handles single values)
  const values = token
    .split(',')
    .map(v => parseInt(v.trim(), 10))
    .filter(v => !isNaN(v))
  if (values.length > 0) {
    return {
      mode: 'specific',
      specific: values,
    }
  }

  return { mode: 'every' }
}

/** Format a single field config back to a cron token */
function formatField(config: CronFieldConfig, field: CronField): string {
  const [min] = FIELD_RANGES[field]

  switch (config.mode) {
    case 'every':
      return '*'

    case 'specific': {
      const values = config.specific ?? []
      if (values.length === 0) {
        return '*'
      }
      return values.sort((a, b) => a - b).join(',')
    }

    case 'range': {
      const start = config.rangeStart ?? min
      const end = config.rangeEnd ?? min
      return `${String(start)}-${String(end)}`
    }

    case 'interval': {
      const step = config.interval ?? 1
      const start = config.intervalStart ?? min
      const base = start === min ? '*' : String(start)
      return `${base}/${String(step)}`
    }

    default:
      return '*'
  }
}

/** Parse a 5-field cron expression into a structured config */
export function parseCron(expression: string): ParsedCron {
  const tokens = expression.trim().split(/\s+/)
  const fields: CronFieldConfig[] = FIELD_ORDER.map((field, i) => {
    const token = tokens[i] ?? '*'
    return parseField(token, field)
  })

  return {
    minute: fields[0] ?? { mode: 'every' },
    hour: fields[1] ?? { mode: 'every' },
    day: fields[2] ?? { mode: 'every' },
    month: fields[3] ?? { mode: 'every' },
    weekday: fields[4] ?? { mode: 'every' },
  }
}

/** Format a structured config into a 5-field cron expression */
export function formatCron(config: ParsedCron): string {
  return FIELD_ORDER.map(field => formatField(config[field], field)).join(' ')
}

/** Convert a cron expression into a human-readable string */
export function cronToHuman(expression: string): string {
  const tokens = expression.trim().split(/\s+/)
  if (tokens.length < 5) {
    return 'Invalid cron expression'
  }

  const [minuteToken, hourToken, dayToken, monthToken, weekdayToken] = tokens as [
    string,
    string,
    string,
    string,
    string,
  ]

  const parts: string[] = []

  // Handle common full-expression patterns first
  if (
    minuteToken === '*' &&
    hourToken === '*' &&
    dayToken === '*' &&
    monthToken === '*' &&
    weekdayToken === '*'
  ) {
    return 'Every minute'
  }

  // Minute patterns
  if (minuteToken.includes('/')) {
    const step = minuteToken.split('/')[1] ?? '1'
    parts.push(`Every ${step} minutes`)
  } else if (minuteToken === '0') {
    // Will be combined with hour
  } else if (minuteToken !== '*') {
    const mins = minuteToken.split(',')
    if (mins.length === 1) {
      parts.push(`At minute ${minuteToken}`)
    } else {
      parts.push(`At minutes ${mins.join(', ')}`)
    }
  }

  // Hour patterns
  if (hourToken.includes('/')) {
    const step = hourToken.split('/')[1] ?? '1'
    parts.push(`every ${step} hours`)
  } else if (hourToken !== '*') {
    const hours = hourToken.split(',')
    if (minuteToken === '0' && hours.length === 1) {
      const h = parseInt(hours[0] ?? '0', 10)
      const ampm = h >= 12 ? 'PM' : 'AM'
      const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h
      parts.push(`At ${String(h12)}:00 ${ampm}`)
    } else if (hours.length === 1 && !minuteToken.includes('/') && minuteToken !== '*') {
      const h = parseInt(hours[0] ?? '0', 10)
      const m = minuteToken.padStart(2, '0')
      const ampm = h >= 12 ? 'PM' : 'AM'
      const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h
      // Replace the minute part with a full time
      parts.length = 0
      parts.push(`At ${String(h12)}:${m} ${ampm}`)
    } else {
      parts.push(`at hours ${hours.join(', ')}`)
    }
  }

  // Day of month
  if (dayToken !== '*') {
    if (dayToken.includes('/')) {
      const step = dayToken.split('/')[1] ?? '1'
      parts.push(`every ${step} days`)
    } else if (dayToken.includes('-')) {
      const [start, end] = dayToken.split('-')
      parts.push(`on days ${start ?? ''} through ${end ?? ''}`)
    } else {
      const days = dayToken.split(',')
      const ordinals = days.map(d => {
        const n = parseInt(d, 10)
        if (n === 1 || n === 21 || n === 31) return `${d}st`
        if (n === 2 || n === 22) return `${d}nd`
        if (n === 3 || n === 23) return `${d}rd`
        return `${d}th`
      })
      parts.push(`on the ${ordinals.join(' and ')}`)
    }
  }

  // Month
  if (monthToken !== '*') {
    if (monthToken.includes('/')) {
      const step = monthToken.split('/')[1] ?? '1'
      parts.push(`every ${step} months`)
    } else {
      const months = monthToken.split(',').map(m => {
        const n = parseInt(m, 10)
        return MONTH_NAMES[n] ?? m
      })
      parts.push(`in ${months.join(', ')}`)
    }
  }

  // Weekday
  if (weekdayToken !== '*') {
    if (weekdayToken.includes('/')) {
      const step = weekdayToken.split('/')[1] ?? '1'
      parts.push(`every ${step} days of the week`)
    } else {
      const days = weekdayToken.split(',').map(d => {
        const n = parseInt(d, 10)
        return WEEKDAY_NAMES[n] ?? d
      })
      parts.push(`on ${days.join(', ')}`)
    }
  }

  if (parts.length === 0) {
    return 'Every minute'
  }

  // Capitalize first letter
  const result = parts.join(', ')
  return result.charAt(0).toUpperCase() + result.slice(1)
}

/** Check whether a given value matches a single cron field token */
function matchesField(value: number, token: string, field: CronField): boolean {
  const [min] = FIELD_RANGES[field]

  if (token === '*') {
    return true
  }

  // Handle comma-separated values (must check before range/interval)
  const subTokens = token.split(',')
  if (subTokens.length > 1) {
    return subTokens.some(sub => matchesField(value, sub.trim(), field))
  }

  // Interval
  if (token.includes('/')) {
    const parts = token.split('/')
    const base = parts[0] ?? '*'
    const step = parseInt(parts[1] ?? '1', 10)
    const start = base === '*' ? min : parseInt(base, 10)
    if (step <= 0) return false
    return value >= start && (value - start) % step === 0
  }

  // Range
  if (token.includes('-')) {
    const parts = token.split('-')
    const rangeStart = parseInt(parts[0] ?? '0', 10)
    const rangeEnd = parseInt(parts[1] ?? '0', 10)
    return value >= rangeStart && value <= rangeEnd
  }

  // Exact value
  return value === parseInt(token, 10)
}

/** Compute the next N execution times for a cron expression */
export function getNextRuns(expression: string, count: number, from?: Date): Date[] {
  const tokens = expression.trim().split(/\s+/)
  if (tokens.length < 5) {
    return []
  }

  const [minuteToken, hourToken, dayToken, monthToken, weekdayToken] = tokens as [
    string,
    string,
    string,
    string,
    string,
  ]

  const results: Date[] = []
  const start = from ? new Date(from.getTime()) : new Date()

  // Advance to the next full minute
  start.setSeconds(0, 0)
  start.setMinutes(start.getMinutes() + 1)

  const current = new Date(start.getTime())
  const maxIterations = 525600 // 1 year of minutes

  for (let i = 0; i < maxIterations && results.length < count; i++) {
    const minute = current.getMinutes()
    const hour = current.getHours()
    const dayOfMonth = current.getDate()
    const month = current.getMonth() + 1
    const weekday = current.getDay()

    if (
      matchesField(minute, minuteToken, 'minute') &&
      matchesField(hour, hourToken, 'hour') &&
      matchesField(dayOfMonth, dayToken, 'day') &&
      matchesField(month, monthToken, 'month') &&
      matchesField(weekday, weekdayToken, 'weekday')
    ) {
      results.push(new Date(current.getTime()))
    }

    current.setMinutes(current.getMinutes() + 1)
  }

  return results
}

/** Validate whether a string is a valid 5-field cron expression */
export function isValidCron(expression: string): boolean {
  const tokens = expression.trim().split(/\s+/)
  if (tokens.length !== 5) {
    return false
  }

  for (let i = 0; i < 5; i++) {
    const token = tokens[i] ?? ''
    const field = FIELD_ORDER[i]
    if (field === undefined) return false
    const [min, max] = FIELD_RANGES[field]

    // Split by comma to validate each sub-expression
    const subTokens = token.split(',')
    for (const sub of subTokens) {
      if (!isValidFieldToken(sub.trim(), min, max)) {
        return false
      }
    }
  }

  return true
}

/** Validate a single sub-token within a cron field */
function isValidFieldToken(token: string, min: number, max: number): boolean {
  if (token === '*') {
    return true
  }

  // Interval: */N or M/N
  if (token.includes('/')) {
    const parts = token.split('/')
    if (parts.length !== 2) return false
    const base = parts[0] ?? ''
    const step = parts[1] ?? ''
    if (base !== '*' && !isValidNumber(base, min, max)) return false
    const stepNum = parseInt(step, 10)
    return !isNaN(stepNum) && stepNum > 0
  }

  // Range: M-N
  if (token.includes('-')) {
    const parts = token.split('-')
    if (parts.length !== 2) return false
    return isValidNumber(parts[0] ?? '', min, max) && isValidNumber(parts[1] ?? '', min, max)
  }

  // Simple number
  return isValidNumber(token, min, max)
}

/** Check whether a string represents a valid number within a range */
function isValidNumber(value: string, min: number, max: number): boolean {
  const num = parseInt(value, 10)
  return !isNaN(num) && num >= min && num <= max
}
