import {
  type KeyboardEvent,
  useCallback,
  useEffect,
  useImperativeHandle,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react'
import { defaultTimepickerStrings } from 'shared-types/timepicker'
import { getHourOptions, getMinuteOptions, getMinuteStep } from 'shared-utils/timepicker/options'
import { stepTime } from 'shared-utils/timepicker/stepper'
import { isValidTimeString, timeToMinutes } from 'shared-utils/timepicker/time-utils'

import type { IPktTimepicker } from './types'
import { TPktInputSize } from 'shared-types/size'

/**
 * ## Why this hook exists (React form model)
 *
 * Punkt’s time field is a **custom control** (two visible segments + optional popup), but forms and
 * libraries (e.g. React Hook Form’s `register()`) expect a **single named value**. We therefore keep a
 * real but **visually hidden** `<input type="time">` in the tree with `name` / `value` for serialization
 * and refs — while users interact with the spinbuttons.
 *
 * ## Why we don’t put `required` / `min` / `max` / `step` on that hidden input
 *
 * In HTML, invalid fields trigger the browser’s built‑in validation. The browser then **focuses** the
 * invalid control to show a message. A hidden, non-tabbable time input **cannot be focused**, so you get
 * an error like “invalid form control is not focusable” and **`submit` never fires**. That is a React
 * footgun, not a bug in your form handler.
 *
 * ## What we do instead (deliberate design)
 *
 * - **Value:** The hidden input is still the source of the submitted `HH:MM` string; we **sync** from the
 *   spinbuttons into it before submit (see `applyNativeConstraintAnchorMessage`, form `pointerdown`, Enter).
 * - **Constraints:** We run the same rules as Elements (`validateTimeValue`) and attach messages with
 *   **`setCustomValidity` on the visible hours `<input>`** so the browser can always focus something real.
 *
 * Custom Elements use **`ElementInternals.setValidity(..., anchor)`** for the same “report on a visible
 * node” idea. React components don’t get `attachInternals()` for that API on a `<div>`, so this pattern
 * is the sustainable equivalent in plain React.
 */
const FORM_MESSAGES = {
  required: 'Dette feltet er påkrevd',
  invalid: 'Ugyldig verdi',
  timeStepMismatch: 'Må treffe på et steg, for eksempel 0, {step} minutter',
  timeStepMismatchHour: 'Må treffe på hver hele time',
  timeStepMismatchHalfHour: 'Må treffe på hver hele eller halve time',
  rangeUnderflowMin: 'Verdien må være større enn eller lik {min}.',
  rangeOverflowMax: 'Verdien må være mindre enn eller lik {max}.',
} as const

function splitValid(value: string | undefined): [string, string] {
  if (value && isValidTimeString(value)) {
    const [h, m] = value.split(':')
    return [h, m]
  }
  return ['', '']
}

/** Spinbuttons hold 1–2 digit strings while editing; commits must be zero-padded `HH:MM`. */
function displaySegmentsToCommittedTime(h: string, m: string): string {
  return `${String(parseInt(h, 10)).padStart(2, '0')}:${String(parseInt(m, 10)).padStart(2, '0')}`
}

function validateTimeValue(
  value: string,
  opts: { required?: boolean; min?: string | number; max?: string | number; step?: number },
): { valid: boolean; message?: string } {
  const { required, min, max, step } = opts
  if (required && !value) {
    return { valid: false, message: FORM_MESSAGES.required }
  }
  if (!value) {
    return { valid: true }
  }
  if (!isValidTimeString(value)) {
    return { valid: false, message: FORM_MESSAGES.invalid }
  }
  const totalMinutes = timeToMinutes(value)
  const minuteStep = getMinuteStep(step)
  if (min && totalMinutes < timeToMinutes(String(min))) {
    return {
      valid: false,
      message: FORM_MESSAGES.rangeUnderflowMin.replace('{min}', String(min)),
    }
  }
  if (max && totalMinutes > timeToMinutes(String(max))) {
    return {
      valid: false,
      message: FORM_MESSAGES.rangeOverflowMax.replace('{max}', String(max)),
    }
  }
  if (step && totalMinutes % minuteStep !== 0) {
    const stepMessage =
      minuteStep === 60
        ? FORM_MESSAGES.timeStepMismatchHour
        : minuteStep === 30
          ? FORM_MESSAGES.timeStepMismatchHalfHour
          : FORM_MESSAGES.timeStepMismatch.replace('{step}', `${minuteStep}, ${minuteStep * 2}, ${minuteStep * 3}`)
    return { valid: false, message: stepMessage }
  }
  return { valid: true }
}

function isValidStep(s: number): boolean {
  return s === 3600 || (s < 3600 && 3600 % s === 0 && s % 60 === 0)
}

export function useTimepickerState(props: IPktTimepicker, ref: React.ForwardedRef<HTMLDivElement>) {
  const {
    id,
    label,
    value,
    defaultValue,
    min,
    max,
    step = 60,
    hidePicker = false,
    stepArrows = false,
    fullwidth = false,
    name,
    disabled = false,
    required = false,
    helptext,
    helptextDropdown,
    helptextDropdownButton,
    hasError = false,
    errorMessage,
    optionalTag = false,
    optionalText,
    requiredTag = false,
    requiredText,
    tagText,
    inline = false,
    useWrapper = true,
    ariaDescribedby,
    strings: stringsProp,
    className,
    inputSize = 'medium' as TPktInputSize,
    onValueChange,
    onFocus: onFocusProp,
    onBlur: onBlurProp,
  } = props

  const strings = useMemo(() => stringsProp ?? defaultTimepickerStrings, [stringsProp])

  const isControlled = value !== undefined
  const initialStringValue = value !== undefined ? (value ?? '') : (defaultValue ?? '')
  const initialValuesRef = useRef(initialStringValue)

  const [internalValue, setInternalValue] = useState(() => initialStringValue)

  const effectiveValue = isControlled ? (value ?? '') : internalValue

  const [hours, setHoursState] = useState(() => splitValid(value !== undefined ? value : defaultValue)[0])
  const [minutes, setMinutesState] = useState(() => splitValid(value !== undefined ? value : defaultValue)[1])

  const hoursRef = useRef(hours)
  const minutesRef = useRef(minutes)

  const setHours = useCallback((h: string) => {
    hoursRef.current = h
    setHoursState(h)
  }, [])

  const setMinutes = useCallback((m: string) => {
    minutesRef.current = m
    setMinutesState(m)
  }, [])

  const [isOpen, setIsOpen] = useState(false)
  const [isInvalid, setIsInvalid] = useState(false)

  const prevControlledValueRef = useRef(value)
  useEffect(() => {
    if (!isControlled) return
    if (value === prevControlledValueRef.current) return
    prevControlledValueRef.current = value
    const [h, m] = splitValid(value)
    setHours(h)
    setMinutes(m)
  }, [value, isControlled, setHours, setMinutes])

  useEffect(() => {
    if (step !== null && step !== undefined && !isValidStep(step)) {
      // eslint-disable-next-line no-console
      console.warn(
        `PktTimepicker: step="${step}" er ikke en gyldig verdi. Step må være et multiplum av 60 (hele minutter) eller nøyaktig 3600 (hel time).`,
      )
    }
  }, [step])

  const hoursInputRef = useRef<HTMLInputElement>(null)
  const minutesInputRef = useRef<HTMLInputElement>(null)
  const buttonRef = useRef<HTMLButtonElement>(null)
  const popupRef = useRef<HTMLDivElement>(null)
  const containerRef = useRef<HTMLDivElement | null>(null)
  const changeInputRef = useRef<HTMLInputElement>(null)

  const hoursDigitCountRef = useRef(0)
  const hoursFirstDigitRef = useRef(-1)
  const minutesDigitCountRef = useRef(0)
  const minutesFirstDigitRef = useRef(-1)
  const [touched, setTouched] = useState(false)

  const nativeInputValueSetter = useMemo(
    () => Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set,
    [],
  )

  const minuteStep = useMemo(() => getMinuteStep(step), [step])
  const hourOptions = useMemo(() => getHourOptions(min, max), [min, max])
  const minuteOptions = useMemo(() => getMinuteOptions(step), [step])

  const hoursId = `${id}-hours`
  const minutesId = `${id}-minutes`
  const popupId = `${id}-popup`
  const hiddenInputId = `${id}-input`

  // Sett native constraint validation på det synlige timer-inputfeltet,
  // slik at form.reportValidity() viser feilmelding på riktig element
  useEffect(() => {
    const anchor = hoursInputRef.current
    if (!anchor) return
    const validation = validateTimeValue(effectiveValue, { required, min, max, step })
    anchor.setCustomValidity(validation.valid ? '' : (validation.message ?? ''))
    setIsInvalid(touched && !validation.valid)
  }, [effectiveValue, required, min, max, step, touched])

  const commitValue = useCallback(
    (newValue: string) => {
      if (!isControlled) {
        setInternalValue(newValue)
      }
      const input = changeInputRef.current
      if (!input || !nativeInputValueSetter) return
      nativeInputValueSetter.call(input, newValue)
      input.dispatchEvent(new Event('input', { bubbles: true }))
      onValueChange?.(newValue)
    },
    [isControlled, nativeInputValueSetter, onValueChange],
  )

  const syncValueFromDisplay = useCallback(() => {
    const h = hoursRef.current
    const m = minutesRef.current
    if (h !== '' && m !== '') {
      const newValue = displaySegmentsToCommittedTime(h, m)
      if (newValue !== effectiveValue) {
        commitValue(newValue)
      }
    } else if (effectiveValue !== '') {
      commitValue('')
    }
  }, [effectiveValue, commitValue])

  const syncValueFromDisplayWithInput = useCallback(() => {
    const h = hoursRef.current
    const m = minutesRef.current
    if (h !== '' && m !== '') {
      const newValue = displaySegmentsToCommittedTime(h, m)
      if (newValue !== effectiveValue) {
        commitValue(newValue)
      } else {
        changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
      }
    } else if (effectiveValue !== '') {
      commitValue('')
    } else {
      changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
    }
  }, [effectiveValue, commitValue])

  /** Flush UI → hidden value, then report min/max/step/required on the focusable hours field (form anchor). */
  const applyNativeConstraintAnchorMessage = useCallback(() => {
    setTouched(true)
    syncValueFromDisplay()
    const hoursEl = hoursInputRef.current
    const hidden = changeInputRef.current
    if (!hoursEl) return
    const raw = hidden?.value ?? ''
    const result = validateTimeValue(raw, { required, min, max, step })
    hoursEl.setCustomValidity(result.valid ? '' : (result.message ?? ''))
  }, [syncValueFromDisplay, required, min, max, step])

  useEffect(() => {
    const root = containerRef.current
    const form = root?.closest('form')
    if (!form) return

    const isSubmitActivator = (target: EventTarget | null) => {
      const el = target as HTMLElement | null
      if (!el || !form.contains(el)) return false
      const control = el.closest('button, input[type="submit"], input[type="image"]')
      if (!control || !form.contains(control)) return false
      if (control instanceof HTMLButtonElement) {
        return control.type !== 'button' && control.type !== 'reset'
      }
      return control instanceof HTMLInputElement
    }

    const onPointerDownCapture = (e: PointerEvent) => {
      if (!isSubmitActivator(e.target)) return
      applyNativeConstraintAnchorMessage()
    }

    form.addEventListener('pointerdown', onPointerDownCapture, true)
    return () => form.removeEventListener('pointerdown', onPointerDownCapture, true)
  }, [applyNativeConstraintAnchorMessage])

  useEffect(() => {
    if (changeInputRef.current) {
      changeInputRef.current.value = effectiveValue
    }
  }, [effectiveValue])

  const closePopup = useCallback(() => {
    setIsOpen(false)
    syncValueFromDisplay()
  }, [syncValueFromDisplay])

  useLayoutEffect(() => {
    if (!isOpen) return
    const popup = popupRef.current
    if (!popup) return
    popup.querySelectorAll('.pkt-timepicker-popup__col').forEach((col) => {
      const selected = col.querySelector('.pkt-timepicker-popup__option--selected')
      if (selected) {
        selected.scrollIntoView({ block: 'center' })
      }
    })
    const cols = popup.querySelectorAll('.pkt-timepicker-popup__col')
    const col = cols[0]
    if (!col) return
    const selected = col.querySelector('.pkt-timepicker-popup__option--selected') as HTMLElement | null
    const first = col.querySelector('.pkt-timepicker-popup__option') as HTMLElement | null
    ;(selected || first)?.focus()
  }, [isOpen])

  useEffect(() => {
    if (!isOpen) return
    const handleDocClick = (e: MouseEvent) => {
      const t = e.target as Node
      if (containerRef.current && !containerRef.current.contains(t)) {
        closePopup()
      }
    }
    document.addEventListener('click', handleDocClick, true)
    return () => document.removeEventListener('click', handleDocClick, true)
  }, [isOpen, closePopup])

  const focusSelectedOrFirst = useCallback((type: 'hour' | 'minute') => {
    const popup = popupRef.current
    if (!popup) return
    const cols = popup.querySelectorAll('.pkt-timepicker-popup__col')
    const col = type === 'hour' ? cols[0] : cols[1]
    if (!col) return
    const selected = col.querySelector('.pkt-timepicker-popup__option--selected') as HTMLElement | null
    const first = col.querySelector('.pkt-timepicker-popup__option') as HTMLElement | null
    ;(selected || first)?.focus()
  }, [])

  const handleOptionClick = useCallback(
    (optionValue: number, type: 'hour' | 'minute') => {
      const padded = String(optionValue).padStart(2, '0')
      if (type === 'hour') {
        setHours(padded)
        requestAnimationFrame(() => focusSelectedOrFirst('minute'))
      } else {
        setMinutes(padded)
        setIsOpen(false)
        const h = hoursRef.current
        if (h !== '') {
          commitValue(`${h}:${padded}`)
        }
        requestAnimationFrame(() => buttonRef.current?.focus())
      }
    },
    [commitValue, focusSelectedOrFirst, setHours, setMinutes],
  )

  const stepTimeDelta = useCallback(
    (direction: 1 | -1) => {
      const result = stepTime(hoursRef.current, minutesRef.current, direction, minuteStep)
      setHours(result.hours)
      setMinutes(result.minutes)
      if (`${result.hours}:${result.minutes}` !== effectiveValue) {
        commitValue(`${result.hours}:${result.minutes}`)
      }
    },
    [minuteStep, effectiveValue, commitValue, setHours, setMinutes],
  )

  const handleHoursKeydown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      hoursInputRef.current?.setCustomValidity('')
      const input = e.currentTarget
      const m = minutesRef.current
      switch (e.key) {
        case 'ArrowUp': {
          e.preventDefault()
          const h = hoursRef.current !== '' ? parseInt(hoursRef.current, 10) : 0
          const newH = String((h + 1) % 24).padStart(2, '0')
          setHours(newH)
          if (m !== '') {
            const v = `${newH}:${m}`
            if (v !== effectiveValue) commitValue(v)
            else changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
          }
          break
        }
        case 'ArrowDown': {
          e.preventDefault()
          const h = hoursRef.current !== '' ? parseInt(hoursRef.current, 10) : 0
          const newH = String((h - 1 + 24) % 24).padStart(2, '0')
          setHours(newH)
          if (m !== '') {
            const v = `${newH}:${m}`
            if (v !== effectiveValue) commitValue(v)
            else changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
          }
          break
        }
        case 'ArrowRight':
          e.preventDefault()
          minutesInputRef.current?.focus()
          break
        case 'Backspace':
        case 'Delete': {
          e.preventDefault()
          hoursDigitCountRef.current = 0
          hoursFirstDigitRef.current = -1
          const cur = hoursRef.current
          if (cur.length > 0) {
            setHours(cur.slice(0, -1))
          }
          syncValueFromDisplayWithInput()
          break
        }
        case 'Tab':
          break
        case 'Enter': {
          e.preventDefault()
          applyNativeConstraintAnchorMessage()
          const form = input.form
          if (form) form.requestSubmit()
          else input.blur()
          break
        }
        default:
          if (/^\d$/.test(e.key)) {
            e.preventDefault()
            const digit = parseInt(e.key, 10)
            if (hoursDigitCountRef.current === 0) {
              hoursFirstDigitRef.current = digit
              const newH = String(digit).padStart(2, '0')
              setHours(newH)
              hoursDigitCountRef.current = 1
              changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
              if (digit >= 3) {
                hoursFirstDigitRef.current = -1
                hoursDigitCountRef.current = 0
                syncValueFromDisplayWithInput()
                minutesInputRef.current?.focus()
              }
            } else {
              const combined = hoursFirstDigitRef.current * 10 + digit
              const newH = combined <= 23 ? String(combined).padStart(2, '0') : String(digit).padStart(2, '0')
              setHours(newH)
              hoursFirstDigitRef.current = -1
              hoursDigitCountRef.current = 0
              syncValueFromDisplayWithInput()
              minutesInputRef.current?.focus()
            }
          } else {
            e.preventDefault()
          }
      }
    },
    [effectiveValue, commitValue, setHours, syncValueFromDisplayWithInput, applyNativeConstraintAnchorMessage],
  )

  const handleHoursBlur = useCallback(() => {
    setTouched(true)
    if (hoursRef.current !== '') {
      const newH = String(parseInt(hoursRef.current, 10)).padStart(2, '0')
      setHours(newH)
    }
    hoursDigitCountRef.current = 0
    hoursFirstDigitRef.current = -1
    syncValueFromDisplay()
  }, [setHours, syncValueFromDisplay])

  const handleMinutesKeydown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      hoursInputRef.current?.setCustomValidity('')
      const input = e.currentTarget
      const h = hoursRef.current
      switch (e.key) {
        case 'ArrowUp': {
          e.preventDefault()
          const m = minutesRef.current !== '' ? parseInt(minutesRef.current, 10) : 0
          const newM = String((m + minuteStep) % 60).padStart(2, '0')
          setMinutes(newM)
          if (h !== '') {
            const v = `${h}:${newM}`
            if (v !== effectiveValue) commitValue(v)
            else changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
          }
          break
        }
        case 'ArrowDown': {
          e.preventDefault()
          const m = minutesRef.current !== '' ? parseInt(minutesRef.current, 10) : 0
          const newM = String((m - minuteStep + 60) % 60).padStart(2, '0')
          setMinutes(newM)
          if (h !== '') {
            const v = `${h}:${newM}`
            if (v !== effectiveValue) commitValue(v)
            else changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
          }
          break
        }
        case 'ArrowLeft':
          e.preventDefault()
          hoursInputRef.current?.focus()
          break
        case 'Backspace':
        case 'Delete': {
          e.preventDefault()
          minutesDigitCountRef.current = 0
          minutesFirstDigitRef.current = -1
          const cur = minutesRef.current
          if (cur.length > 0) {
            setMinutes(cur.slice(0, -1))
          }
          syncValueFromDisplayWithInput()
          break
        }
        case 'Tab':
          break
        case 'Enter': {
          e.preventDefault()
          applyNativeConstraintAnchorMessage()
          const form = input.form
          if (form) form.requestSubmit()
          else input.blur()
          break
        }
        default:
          if (/^\d$/.test(e.key)) {
            e.preventDefault()
            const digit = parseInt(e.key, 10)
            if (minutesDigitCountRef.current === 0) {
              minutesFirstDigitRef.current = digit
              const newM = String(digit).padStart(2, '0')
              setMinutes(newM)
              minutesDigitCountRef.current = 1
              changeInputRef.current?.dispatchEvent(new Event('input', { bubbles: true }))
              if (digit >= 6) {
                minutesFirstDigitRef.current = -1
                minutesDigitCountRef.current = 0
                syncValueFromDisplayWithInput()
              }
            } else {
              const combined = minutesFirstDigitRef.current * 10 + digit
              const newM = combined <= 59 ? String(combined).padStart(2, '0') : String(digit).padStart(2, '0')
              setMinutes(newM)
              minutesFirstDigitRef.current = -1
              minutesDigitCountRef.current = 0
              syncValueFromDisplayWithInput()
            }
          } else {
            e.preventDefault()
          }
      }
    },
    [
      minuteStep,
      effectiveValue,
      commitValue,
      setMinutes,
      syncValueFromDisplayWithInput,
      applyNativeConstraintAnchorMessage,
    ],
  )

  const handleMinutesBlur = useCallback(() => {
    setTouched(true)
    if (minutesRef.current !== '') {
      const newM = String(parseInt(minutesRef.current, 10)).padStart(2, '0')
      setMinutes(newM)
    }
    minutesDigitCountRef.current = 0
    minutesFirstDigitRef.current = -1
    syncValueFromDisplay()
  }, [setMinutes, syncValueFromDisplay])

  const handlePopupKeydown = useCallback(
    (e: KeyboardEvent<HTMLDivElement>) => {
      const focused = document.activeElement as HTMLElement
      const col = focused.closest('.pkt-timepicker-popup__col')
      if (!col) return
      const options = Array.from(col.querySelectorAll<HTMLElement>('.pkt-timepicker-popup__option'))
      const currentIdx = options.indexOf(focused)

      const focusOptionAndSync = (option: HTMLElement | undefined) => {
        if (!option) return
        const val = parseInt(option.dataset.value ?? '0', 10)
        const padded = String(val).padStart(2, '0')
        if (focused.dataset.type === 'hour') setHours(padded)
        else setMinutes(padded)
        option.focus()
      }

      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault()
          focusOptionAndSync(options[Math.min(currentIdx + 1, options.length - 1)])
          break
        case 'ArrowUp':
          e.preventDefault()
          focusOptionAndSync(options[Math.max(currentIdx - 1, 0)])
          break
        case 'Home':
          e.preventDefault()
          focusOptionAndSync(options[0])
          break
        case 'End':
          e.preventDefault()
          focusOptionAndSync(options[options.length - 1])
          break
        case 'ArrowRight':
          e.preventDefault()
          if (focused.dataset.type === 'hour') {
            const val = parseInt(focused.dataset.value ?? '0', 10)
            setHours(String(val).padStart(2, '0'))
            requestAnimationFrame(() => {
              popupRef.current?.querySelectorAll('.pkt-timepicker-popup__col').forEach((c) => {
                const sel = c.querySelector('.pkt-timepicker-popup__option--selected')
                sel?.scrollIntoView({ block: 'center' })
              })
              focusSelectedOrFirst('minute')
            })
          }
          break
        case 'ArrowLeft':
          e.preventDefault()
          if (focused.dataset.type === 'minute') {
            const val = parseInt(focused.dataset.value ?? '0', 10)
            setMinutes(String(val).padStart(2, '0'))
            requestAnimationFrame(() => {
              popupRef.current?.querySelectorAll('.pkt-timepicker-popup__col').forEach((c) => {
                const sel = c.querySelector('.pkt-timepicker-popup__option--selected')
                sel?.scrollIntoView({ block: 'center' })
              })
              focusSelectedOrFirst('hour')
            })
          }
          break
        case 'Enter':
        case ' ':
          e.preventDefault()
          focused.click()
          break
        case 'Escape':
          e.preventDefault()
          closePopup()
          buttonRef.current?.focus()
          break
        default:
          break
      }
    },
    [closePopup, focusSelectedOrFirst, setHours, setMinutes],
  )

  const handlePopupFocusOut = useCallback(
    (e: React.FocusEvent<HTMLDivElement>) => {
      const popup = popupRef.current
      if (!popup) return
      const related = e.relatedTarget as Node | null
      if (!related || !popup.contains(related)) {
        closePopup()
      }
    },
    [closePopup],
  )

  const handleFocusIn = useCallback(
    (e: React.FocusEvent<HTMLDivElement>) => {
      const root = containerRef.current
      if (!root) return
      const related = e.relatedTarget as Node | null
      if (related && root.contains(related)) return
      onFocusProp?.(e)
    },
    [onFocusProp],
  )

  const handleFocusOut = useCallback(
    (e: React.FocusEvent<HTMLDivElement>) => {
      const root = containerRef.current
      if (!root) return
      const related = e.relatedTarget as Node | null
      if (related && root.contains(related)) return
      onBlurProp?.(e)
    },
    [onBlurProp],
  )

  const handleClockButtonClick = useCallback(() => {
    if (disabled) return
    if (isOpen) {
      closePopup()
    } else {
      setIsOpen(true)
    }
  }, [disabled, isOpen, closePopup])

  // Expose value getter/setter on ref for React Hook Form register() compatibility (same pattern as PktDatepicker).
  useImperativeHandle(
    ref,
    () =>
      ({
        get value() {
          return changeInputRef.current?.value ?? ''
        },
        set value(newVal: string) {
          const v = newVal ?? ''
          if (!isControlled) {
            setInternalValue(v)
          }
          const [h, m] = splitValid(v)
          setHours(h)
          setMinutes(m)
          if (changeInputRef.current) {
            changeInputRef.current.value = v
          }
        },
        focus() {
          hoursInputRef.current?.focus()
        },
        blur() {
          hoursInputRef.current?.blur()
        },
      }) as unknown as HTMLDivElement,
    [isControlled, setHours, setMinutes],
  )

  useEffect(() => {
    const root = containerRef.current
    const form = root?.closest('form')
    if (!form) return

    const handleReset = () => {
      window.setTimeout(() => {
        const init = initialValuesRef.current
        const [h, m] = splitValid(init)
        setHours(h)
        setMinutes(m)
        if (!isControlled) {
          setInternalValue(init)
        }
        if (changeInputRef.current) {
          changeInputRef.current.value = init
        }
        setIsOpen(false)
        setTouched(false)
      }, 0)
    }

    form.addEventListener('reset', handleReset)
    return () => form.removeEventListener('reset', handleReset)
  }, [isControlled, setHours, setMinutes])

  const outerClasses = [
    'pkt-timepicker',
    `pkt-timepicker--${inputSize}`,
    fullwidth ? 'pkt-timepicker--fullwidth' : '',
    stepArrows ? 'pkt-timepicker--stepper' : '',
    className ?? '',
  ]
    .filter(Boolean)
    .join(' ')

  return {
    id,
    containerRef,
    outerClasses,
    inputSize,
    strings,
    label,
    disabled,
    required,
    helptext,
    helptextDropdown,
    helptextDropdownButton,
    hasError,
    isInvalid,
    errorMessage,
    optionalTag,
    optionalText,
    requiredTag,
    requiredText,
    tagText,
    inline,
    useWrapper,
    ariaDescribedby,
    hidePicker,
    stepArrows,
    min,
    max,
    step,
    hours,
    minutes,
    hoursId,
    minutesId,
    popupId,
    hiddenInputId,
    name: name ?? id,
    isOpen,
    hourOptions,
    minuteOptions,
    hoursInputRef,
    minutesInputRef,
    buttonRef,
    popupRef,
    changeInputRef,
    handleHoursKeydown,
    handleHoursBlur,
    handleMinutesKeydown,
    handleMinutesBlur,
    handlePopupKeydown,
    handlePopupFocusOut,
    handleFocusIn,
    handleFocusOut,
    handleOptionClick,
    handleClockButtonClick,
    closePopup,
    stepTimeDelta,
    effectiveValue,
  }
}
