<template>
  <div :class="mergedGroupClass" >
    <label
      v-if="label"
      :for="inputId"
      :class="mergedLabelClass"
    >
      {{ label }}
    </label>
    <button
      type="button"
      :disabled="isMinusDisabled"
      @click="decrement"
    >
      <IconifyIcon
        icon="mdi:minus"
        :class="mergedButtonMinusClass"
      />
    </button>
    <input
      :id="inputId"
      :name="name"
      autocomplete="off"
      :class="mergedInputClass"
      :style="inputWidthStyle"
      :value="displayedValue"
      :required="required"
      :disabled="disabled"
      :readonly="readonly"
      :aria-invalid="!!error"
      :aria-describedby="describedBy"
      v-bind="attrs"
      type="text"
      inputmode="numeric"
      placeholder="_"
      @input="onInput"
      @focus="isFocused = true"
      @blur="isFocused = false"
    >
    <button
      type="button"
      :disabled="isPlusDisabled"
      @click="increment"
    >
      <IconifyIcon
        icon="mdi:plus"
        :class="mergedButtonPlusClass"
      />
    </button>

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

<script setup lang="ts">
import {computed, ref, useAttrs, useId} from 'vue'
import {Icon as IconifyIcon} from '@iconify/vue'
import {twMerge} from 'tailwind-merge'

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

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

const attrs = useAttrs()
const generatedId = useId()
const localValue = ref('')
const isFocused = ref(false)

const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`)
const isControlled = computed(() => props.modelValue !== undefined)
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success)

// Ajoute un separateur de milliers pour l'affichage dans le champ.
const formatDisplayValue = (value: string) => {
  if (!value) return ''
  const [integerPart = '', decimalPart] = value.split('.')
  const formattedIntegerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')

  if (decimalPart !== undefined) {
    return `${formattedIntegerPart}.${decimalPart}`
  }

  return formattedIntegerPart
}

// Valeur visible dans l'input, avec formatage des milliers.
const displayedValue = computed(() => formatDisplayValue(currentValue.value))
const inputCharacterWidth = computed(() => Math.max(displayedValue.value.length, 1))

// Transforme min/max en nombres utilisables.
const parseBound = (value: number | string | undefined) => {
  if (value === undefined || value === '') return undefined
  const parsedValue = Number.parseFloat(String(value).replace(',', '.'))
  return Number.isNaN(parsedValue) ? undefined : parsedValue
}

const minValue = computed(() => parseBound(props.min))
const maxValue = computed(() => parseBound(props.max))

// Recupere la valeur numerique brute actuellement saisie.
const currentNumericValue = computed(() => {
  if (currentValue.value === '') return undefined
  const parsedValue = Number.parseFloat(currentValue.value)
  return Number.isNaN(parsedValue) ? undefined : parsedValue
})

const inputWidthStyle = computed(() => ({
  width: `calc(${inputCharacterWidth.value}ch + 30px)`,
  maxWidth: '100%',
}))


const isMinusDisabled = computed(() =>
  props.disabled || currentNumericValue.value <= minValue.value,
)

const isPlusDisabled = computed(() =>
  props.disabled || currentNumericValue.value >= maxValue.value,

)
const mergedGroupClass = computed(() =>
  twMerge(
    'relative mt-4 flex h-12 w-full items-center',
    props.groupClass,
  ),
)
const mergedInputClass = computed(() =>
  twMerge(
    ' peer h-[20px] min-w-0 border bg-white text-center outline-none placeholder:text-transparent text-lg border-x-0 border-black',
    props.disabled ? 'cursor-not-allowed text-black/60' : 'cursor-text',
    hasError.value
      ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
      : hasSuccess.value
        ? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
        : '',
    props.inputClass,
  ),
)
const mergedLabelClass = computed(() =>
  twMerge(
    'radio-text mt-px cursor-pointer text-black mr-3',
    hasError.value ? 'text-m-error' : '',
    hasSuccess.value ? 'text-m-success' : '',
    props.disabled ? 'cursor-not-allowed text-black/60' : '',
    props.labelClass,
  ),
)

const mergedButtonMinusClass = computed(() =>
  twMerge(
    'h-[20px] w-[30px] border border-black rounded-s-[3px]',
    isMinusDisabled.value ? 'cursor-not-allowed text-black/60' : 'cursor-pointer',
    hasError.value
      ? 'border-m-error'
      : hasSuccess.value
        ? 'border-m-success'
        : '',
  ),
)
const mergedButtonPlusClass = computed(() =>
  twMerge(
    'h-[20px] w-[30px] border border-black rounded-e-[3px]',
    isPlusDisabled.value ? 'cursor-not-allowed text-black/60' : 'cursor-pointer',
    hasError.value
      ? 'border-m-error'
      : hasSuccess.value
        ? 'border-m-success'
        : '',
  ),
)

const describedBy = computed(() => {
  const ids: string[] = []
  if (props.hint && !hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-hint`)
  if (hasError.value) ids.push(`${inputId.value}-error`)
  if (hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-success`)
  return ids.length ? ids.join(' ') : undefined
})

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

// Met a jour l'etat local si besoin puis emet la valeur brute.
const updateValue = (value: string) => {
  if (!isControlled.value) {
    localValue.value = value
  }
  emit('update:modelValue', value)
}

// Force la valeur a rester entre les bornes min et max.
const clampValue = (value: number) => {
  if (minValue.value !== undefined && value < minValue.value) return minValue.value
  if (maxValue.value !== undefined && value > maxValue.value) return maxValue.value
  return value
}

// Garde uniquement les chiffres et la virgule puis applique les bornes.
const normalizeValue = (value: string) => {
  const sanitizedValue = value
    .replace(/[^\d,.]/g, '')
    .replace(/,/g, '.')

  const [integerPart = '', ...decimalParts] = sanitizedValue.split('.')
  const decimalPart = decimalParts.join('')
  const hasDecimalSeparator = sanitizedValue.includes('.')

  if (hasDecimalSeparator) {
    const normalizedValue = `${integerPart || '0'}.${decimalPart}`
    const parsedValue = Number.parseFloat(normalizedValue)

    if (Number.isNaN(parsedValue)) return ''

    const clampedValue = clampValue(parsedValue)
    if (clampedValue !== parsedValue) return String(clampedValue)

    return decimalPart === '' ? `${integerPart || '0'}.` : normalizedValue
  }

  return integerPart === '' ? '' : String(clampValue(Number.parseFloat(integerPart)))
}

// Reformate l'affichage dans le champ tout en conservant une valeur brute pour le v-model.
const onInput = (event: Event) => {
  const target = event.target as HTMLInputElement
  const normalizedValue = normalizeValue(target.value)

  target.value = formatDisplayValue(normalizedValue)
  updateValue(normalizedValue)
}

// Retourne la valeur numerique courante, ou 0 si le champ est vide.
const getNumericValue = () => {
  const parsedValue = Number.parseFloat(currentValue.value || '0')
  return Number.isNaN(parsedValue) ? 0 : parsedValue
}

// Retire 1 a la valeur si l'action est autorisee.
const decrement = () => {
  if (props.disabled || props.readonly || isMinusDisabled.value) return
  updateValue(String(clampValue(getNumericValue() - 1)))
}

// Ajoute 1 a la valeur si l'action est autorisee.
const increment = () => {
  if (props.disabled || props.readonly || isPlusDisabled.value) return
  updateValue(String(clampValue(getNumericValue() + 1)))
}
</script>
