import { toZonedTime, fromZonedTime } from 'date-fns-tz'
import { Cron } from 'croner'
import type {
  TaskScheduleConfig,
  RelativeScheduleConfig,
  AbsoluteScheduleConfig,
  RecurringScheduleConfig
} from './types'

/**
 * Parsed offset result
 */
export interface ParsedOffset {
  /** Can be negative for "before" offsets */
  value: number
  unit: 'd' | 'h' | 'm'
}

/**
 * Parse offset strings like '+3d', '-2h', '+30m', '-14d'
 *
 * @param offset - Offset string (e.g., '+3d', '-2h', '+30m')
 * @returns Parsed offset with value and unit
 * @throws Error if format is invalid
 *
 * @example
 * parseOffset('+3d')  // { value: 3, unit: 'd' }
 * parseOffset('-2h')  // { value: -2, unit: 'h' }
 * parseOffset('+30m') // { value: 30, unit: 'm' }
 */
export function parseOffset(offset: string): ParsedOffset {
  const match = offset.match(/^([+-])(\d+)([dhm])$/)
  if (!match) {
    throw new Error(`Invalid offset format: "${offset}". Expected format: +3d, -2h, +30m, etc.`)
  }

  const sign = match[1] === '+' ? 1 : -1
  const value = parseInt(match[2], 10) * sign
  const unit = match[3] as 'd' | 'h' | 'm'

  return { value, unit }
}

/**
 * Apply an offset to an anchor date
 *
 * @param anchor - The anchor date to apply offset to
 * @param offset - Offset string (e.g., '+3d', '-2h')
 * @returns New Date with offset applied
 *
 * @example
 * const anchor = new Date('2026-01-15T10:00:00Z')
 * applyOffset(anchor, '+3d')  // 2026-01-18T10:00:00Z
 * applyOffset(anchor, '-2h')  // 2026-01-15T08:00:00Z
 */
export function applyOffset(anchor: Date, offset: string): Date {
  const { value, unit } = parseOffset(offset)
  const result = new Date(anchor)

  switch (unit) {
    case 'd':
      result.setDate(result.getDate() + value)
      break
    case 'h':
      result.setHours(result.getHours() + value)
      break
    case 'm':
      result.setMinutes(result.getMinutes() + value)
      break
  }

  return result
}

/**
 * Calculate next retry time using exponential backoff.
 * Formula: (retryCount + 1)^2 minutes
 * Results: 1 min (retry 0), 4 min (retry 1), 9 min (retry 2), 16 min (retry 3)
 *
 * @param retryCount - Current retry attempt (0-based)
 * @returns Date for when the next retry should occur
 *
 * @example
 * computeRetryNextRunAt(0) // 1 minute from now
 * computeRetryNextRunAt(1) // 4 minutes from now
 * computeRetryNextRunAt(2) // 9 minutes from now
 * computeRetryNextRunAt(3) // 16 minutes from now
 */
export function computeRetryNextRunAt(retryCount: number): Date {
  const delayMinutes = Math.pow(retryCount + 1, 2)
  const nextRun = new Date()
  nextRun.setMinutes(nextRun.getMinutes() + delayMinutes)
  return nextRun
}

/**
 * Compute next run for recurring schedule.
 * Supports both cron expressions (via croner) and interval-based (daily/weekly/monthly).
 *
 * Timezone handling:
 * - config.time / config.cron are interpreted in config.timezone
 * - fromTime is UTC, converted to target timezone for comparison
 * - Returns UTC Date for storage
 */
function computeRecurringNextRun(config: RecurringScheduleConfig, fromTime: Date): Date | null {
  const timezone = config.timezone || 'UTC'

  // Check if schedule has ended
  if (config.endAt) {
    const endDate = new Date(config.endAt)
    if (fromTime >= endDate) {
      return null
    }
  }

  // Cron-based schedules
  if (config.cron) {
    const job = new Cron(config.cron, { timezone })
    const next = job.nextRun(fromTime)

    if (!next) return null

    // Check end date
    if (config.endAt && next > new Date(config.endAt)) {
      return null
    }

    return next
  }

  // Interval-based schedules
  if (config.interval && config.time) {
    const [hours, minutes] = config.time.split(':').map(Number)

    // Convert fromTime (UTC) to the target timezone for comparison
    const fromTimeInTz = toZonedTime(fromTime, timezone)

    // Create next run time in target timezone
    const nextInTz = new Date(fromTimeInTz)

    // Set the time in target timezone
    nextInTz.setHours(hours, minutes, 0, 0)

    // If that time has passed today in target timezone, move to next interval
    if (nextInTz <= fromTimeInTz) {
      switch (config.interval) {
        case 'daily':
          nextInTz.setDate(nextInTz.getDate() + 1)
          break
        case 'weekly':
          nextInTz.setDate(nextInTz.getDate() + 7)
          break
        case 'monthly':
          nextInTz.setMonth(nextInTz.getMonth() + 1)
          break
      }
    }

    // Convert back to UTC for storage
    const nextUtc = fromZonedTime(nextInTz, timezone)

    // Check end date again (compare UTC times)
    if (config.endAt && nextUtc > new Date(config.endAt)) {
      return null
    }

    return nextUtc
  }

  throw new Error('Invalid recurring schedule: must have either cron or interval+time')
}

/**
 * Compute next run for relative schedule (anchor + offset)
 */
function computeRelativeNextRun(config: RelativeScheduleConfig, currentStep: number): Date | null {
  if (currentStep >= config.items.length) {
    return null // Sequence complete
  }

  const item = config.items[currentStep]
  const anchor = new Date(config.anchorAt)

  return applyOffset(anchor, item.offset)
}

/**
 * Compute next run for absolute schedule (specific datetimes)
 */
function computeAbsoluteNextRun(config: AbsoluteScheduleConfig, currentStep: number): Date | null {
  if (currentStep >= config.items.length) {
    return null // Sequence complete
  }

  return new Date(config.items[currentStep].runAt)
}

/**
 * Compute the next run time for a schedule based on its config and current step
 *
 * @param config - The schedule configuration
 * @param currentStep - Current step index (0-based, for sequences)
 * @param fromTime - Optional reference time for recurring (defaults to now)
 * @returns Next run Date, or null if schedule is complete
 *
 * @example
 * // Relative schedule
 * const config = { type: 'relative', anchorAt: '2026-01-15T10:00:00Z', items: [...] }
 * computeNextRunAt(config, 0) // Returns anchor + items[0].offset
 *
 * // Absolute schedule
 * const config = { type: 'absolute', items: [{ runAt: '2026-01-20T09:00:00Z', ... }] }
 * computeNextRunAt(config, 0) // Returns items[0].runAt as Date
 *
 * // Returns null when sequence is complete
 * computeNextRunAt(config, items.length) // null
 */
export function computeNextRunAt(config: TaskScheduleConfig, currentStep: number, fromTime?: Date): Date | null {
  switch (config.type) {
    case 'recurring':
      return computeRecurringNextRun(config, fromTime || new Date())

    case 'relative':
      return computeRelativeNextRun(config, currentStep)

    case 'absolute':
      return computeAbsoluteNextRun(config, currentStep)

    default: {
      // TypeScript exhaustiveness check
      const _exhaustive: never = config
      throw new Error(`Unknown schedule type: ${(_exhaustive as { type: string }).type}`)
    }
  }
}

/**
 * Get the payload for current step
 *
 * @param config - The schedule configuration
 * @param currentStep - Current step index (0-based)
 * @returns Payload for the current step, or null if step is out of range
 *
 * @example
 * const config = { type: 'relative', items: [{ offset: '+1d', payload: { tone: 'grateful' } }] }
 * getCurrentPayload(config, 0) // { tone: 'grateful' }
 * getCurrentPayload(config, 1) // null (out of range)
 */
export function getCurrentPayload(config: TaskScheduleConfig, currentStep: number): Record<string, unknown> | null {
  switch (config.type) {
    case 'recurring':
      return config.payload

    case 'relative':
      if (currentStep >= config.items.length) return null
      return config.items[currentStep].payload

    case 'absolute':
      if (currentStep >= config.items.length) return null
      return config.items[currentStep].payload
  }
}

/**
 * Get the label for current step (if any)
 *
 * @param config - The schedule configuration
 * @param currentStep - Current step index (0-based)
 * @returns Label for the current step, or undefined if none
 *
 * @example
 * const config = { type: 'relative', items: [{ offset: '+1d', label: 'Thank you', payload: {} }] }
 * getCurrentItemLabel(config, 0) // 'Thank you'
 * getCurrentItemLabel(config, 1) // undefined (out of range)
 */
export function getCurrentItemLabel(config: TaskScheduleConfig, currentStep: number): string | undefined {
  switch (config.type) {
    case 'recurring':
      return undefined // Recurring schedules don't have step labels

    case 'relative':
      if (currentStep >= config.items.length) return undefined
      return config.items[currentStep].label

    case 'absolute':
      if (currentStep >= config.items.length) return undefined
      return config.items[currentStep].label
  }
}

/**
 * Check if a schedule is complete (all items executed)
 *
 * @param config - The schedule configuration
 * @param currentStep - Current step index (0-based)
 * @returns true if schedule is complete, false otherwise
 *
 * @example
 * const config = { type: 'relative', items: [{ offset: '+1d', payload: {} }] }
 * isScheduleComplete(config, 0) // false
 * isScheduleComplete(config, 1) // true (past all items)
 *
 * // Recurring with end date
 * const recurring = { type: 'recurring', interval: 'daily', time: '09:00', endAt: '2026-01-15' }
 * isScheduleComplete(recurring, 0) // false (before end date)
 * // Returns true after endAt has passed
 */
export function isScheduleComplete(config: TaskScheduleConfig, currentStep: number): boolean {
  switch (config.type) {
    case 'recurring':
      // Recurring schedules are only complete if endAt has passed
      if (config.endAt) {
        return new Date() >= new Date(config.endAt)
      }
      return false // Indefinite recurring schedules never complete

    case 'relative':
    case 'absolute':
      return currentStep >= config.items.length
  }
}

/**
 * Get the total number of steps in a schedule
 *
 * @param config - The schedule configuration
 * @returns Number of steps, or null for indefinite recurring schedules
 *
 * @example
 * const relative = { type: 'relative', items: [{...}, {...}, {...}] }
 * getTotalSteps(relative) // 3
 *
 * const recurring = { type: 'recurring', interval: 'daily', endAt: null }
 * getTotalSteps(recurring) // null (indefinite)
 */
export function getTotalSteps(config: TaskScheduleConfig): number | null {
  switch (config.type) {
    case 'recurring':
      // Recurring schedules don't have a fixed number of steps
      return null

    case 'relative':
      return config.items.length

    case 'absolute':
      return config.items.length
  }
}

/**
 * Validate that an offset string is properly formatted
 *
 * @param offset - Offset string to validate
 * @returns true if valid, false otherwise
 *
 * @example
 * isValidOffset('+3d')  // true
 * isValidOffset('-2h')  // true
 * isValidOffset('3d')   // false (missing sign)
 * isValidOffset('+3x')  // false (invalid unit)
 */
export function isValidOffset(offset: string): boolean {
  return /^[+-]\d+[dhm]$/.test(offset)
}
