<template>
  <div>
    <div :class="mergedGroupClass">
      <label
        v-if="label"
        :for="hoursInputId"
        :class="mergedLabelClass"
      >
        {{ label }}
      </label>

      <div class="flex items-center gap-2">
        <input
          :id="hoursInputId"
          :name="hoursName"
          autocomplete="off"
          :class="mergedInputClass('hours')"
          :required="required"
          :disabled="disabled"
          :readonly="readonly"
          :aria-invalid="!!error"
          :aria-describedby="describedBy"
          :value="hoursValue"
          v-bind="attrs"
          type="text"
          inputmode="numeric"
          placeholder="00"
          maxlength="2"
          @input="onHoursInput"
          @focus="activeField = 'hours'"
          @blur="onHoursBlur"
        >

        <span class="text-[18px] text-black">:</span>

        <input
          ref="minutesInputRef"
          :id="minutesInputId"
          :name="minutesName"
          autocomplete="off"
          :class="mergedInputClass('minutes')"
          :required="required"
          :disabled="disabled"
          :readonly="readonly"
          :aria-invalid="!!error"
          :aria-describedby="describedBy"
          :value="minutesValue"
          v-bind="attrs"
          type="text"
          inputmode="numeric"
          placeholder="00"
          maxlength="2"
          @input="onMinutesInput"
          @focus="activeField = 'minutes'"
          @blur="onMinutesBlur"
        >
      </div>
    </div>

    <p
      v-if="hint || hasError || hasSuccess"
      :id="`${inputId}-describedby`"
      :class="[
        hasError
          ? 'text-m-danger'
          : hasSuccess
            ? 'text-m-success'
            : 'text-m-muted',
        'mt-1 ml-[2px] text-xs',
      ]"
    >
      {{ error || success || hint }}
    </p>
  </div>
</template>

<script setup lang="ts">
import {computed, nextTick, ref, useAttrs, useId, watch} from 'vue'
import {twMerge} from 'tailwind-merge'

defineOptions({name: 'MalioTime', inheritAttrs: false})

const props = withDefaults(
  defineProps<{
    id?: string
    label?: string
    name?: string
    modelValue?: string | null | undefined
    inputClass?: string
    labelClass?: string
    groupClass?: string
    required?: boolean
    disabled?: boolean
    readonly?: boolean
    hint?: string
    error?: string
    success?: string
  }>(),
  {
    id: '',
    name: '',
    modelValue: undefined,
    label: '',
    inputClass: '',
    labelClass: '',
    groupClass: '',
    required: false,
    readonly: false,
    disabled: false,
    hint: '',
    error: '',
    success: '',
  },
)

const attrs = useAttrs()
const generatedId = useId()
const hoursValue = ref('')
const minutesValue = ref('')
const activeField = ref<'hours' | 'minutes' | null>(null)
const minutesInputRef = ref<HTMLInputElement | null>(null)

const inputId = computed(() => props.id?.toString() || `malio-time-${generatedId}`)
const hoursInputId = computed(() => `${inputId.value}-hours`)
const minutesInputId = computed(() => `${inputId.value}-minutes`)
const hoursName = computed(() => (props.name ? `${props.name}-hours` : ''))
const minutesName = computed(() => (props.name ? `${props.name}-minutes` : ''))
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value)

const emit = defineEmits<{
  (event: 'update:modelValue', value: string): void
}>()

const padSegment = (value: string) => (value === '' ? '' : value.padStart(2, '0'))

const sanitizeDigits = (value: string) =>
  value.replace(/\D/g, '').slice(0, 2)

const normalizeHours = (value: string) => {
  const digits = sanitizeDigits(value)
  if (digits === '') return ''
  if (Number.parseInt(digits, 10) > 23) return '23'
  return digits
}

const normalizeMinutes = (value: string) => {
  const digits = sanitizeDigits(value)
  if (digits === '') return ''
  if (Number.parseInt(digits, 10) > 59) return '59'
  return digits
}

const parseTimeValue = (value: string | null | undefined) => {
  if (!value) return {hours: '', minutes: ''}

  const [rawHours = '', rawMinutes = ''] = value.split(':')

  return {
    hours: normalizeHours(rawHours),
    minutes: normalizeMinutes(rawMinutes),
  }
}

const syncFromModelValue = (value: string | null | undefined) => {
  if (activeField.value) return
  const parsedValue = parseTimeValue(value)
  hoursValue.value = parsedValue.hours
  minutesValue.value = parsedValue.minutes
}

watch(() => props.modelValue, syncFromModelValue, {immediate: true})

const describedBy = computed(() =>
  (props.hint || hasError.value || hasSuccess.value) ? `${inputId.value}-describedby` : undefined,
)

const mergedGroupClass = computed(() =>
  twMerge(
    'relative mt-4 flex w-full items-center',
    props.groupClass,
  ),
)

const mergedLabelClass = computed(() =>
  twMerge(
    'mt-px mr-4 cursor-pointer text-black text-[18px]',
    hasError.value ? 'text-m-danger' : '',
    hasSuccess.value ? 'text-m-success' : '',
    props.disabled ? 'cursor-not-allowed text-black/60' : '',
    props.labelClass
  ),
)

const mergedInputClass = (field: 'hours' | 'minutes') =>
  twMerge(
    'h-[30px] w-10 border bg-white text-center text-[18px] outline-none rounded-md placeholder:text-m-muted',
    props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
    hasError.value
      ? 'border-m-danger focus:border-m-danger'
      : hasSuccess.value
        ? 'border-m-success focus:border-m-success'
        : activeField.value === field
          ? 'border-m-primary text-m-primary'
          : 'border-black text-black',
    props.inputClass,
  )

const emitCurrentValue = (pad = false) => {
  if (!hoursValue.value && !minutesValue.value) {
    emit('update:modelValue', '')
    return
  }

  const h = pad ? padSegment(hoursValue.value || '0') : (hoursValue.value || '00')
  const m = pad ? padSegment(minutesValue.value || '0') : (minutesValue.value || '00')

  emit('update:modelValue', `${h}:${m}`)
}

const onHoursInput = (event: Event) => {
  const target = event.target as HTMLInputElement
  const normalizedValue = normalizeHours(target.value)

  hoursValue.value = normalizedValue
  target.value = normalizedValue
  emitCurrentValue()

  if (normalizedValue.length === 2) {
    nextTick(() => minutesInputRef.value?.focus())
  }
}

const onMinutesInput = (event: Event) => {
  const target = event.target as HTMLInputElement
  const normalizedValue = normalizeMinutes(target.value)

  minutesValue.value = normalizedValue
  target.value = normalizedValue
  emitCurrentValue()
}

const formatFieldOnBlur = (field: 'hours' | 'minutes') => {
  if (field === 'hours' && hoursValue.value) {
    hoursValue.value = padSegment(hoursValue.value)
  }

  if (field === 'minutes' && minutesValue.value) {
    minutesValue.value = padSegment(minutesValue.value)
  }

  emitCurrentValue(true)
}

const onHoursBlur = () => {
  formatFieldOnBlur('hours')
  activeField.value = null
}

const onMinutesBlur = () => {
  formatFieldOnBlur('minutes')
  activeField.value = null
}
</script>
