<script setup lang="ts">
import draggable from 'vuedraggable'
import { useDisplay } from 'vuetify'
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
import { usePublicFormToken } from '@/composables/publicFormInjection'
import type { PublicFormField, PublicFormSectionOption, SectionPriorityValue } from '@form-schema/types/formBuilder'
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'

const props = defineProps<{
  field: PublicFormField
  modelValue: unknown
  errorMessages?: string[]
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', v: SectionPriorityValue[]): void
  (e: 'blur'): void
}>()

const token = usePublicFormToken()
const { data: sections, isLoading, isError, refetch } = usePublicFormSections(token)
const { mobile } = useDisplay()

const HARD_CAP = 5
const warnedOnMalformedValue = { value: false }

function selfHealIncoming(value: unknown): SectionPriorityValue[] {
  if (!Array.isArray(value)) return []

  const cleaned: SectionPriorityValue[] = []
  for (const entry of value) {
    if (entry === null || typeof entry !== 'object') continue
    const obj = entry as Record<string, unknown>
    const id = obj.section_id
    const prio = obj.priority
    if (typeof id !== 'string' || typeof prio !== 'number') continue
    cleaned.push({ section_id: id, priority: prio })
  }

  // If the inbound array had data but none of it matched the shape,
  // warn once so future misuses are spotted in dev.
  if (import.meta.env.DEV && value.length > 0 && cleaned.length === 0 && !warnedOnMalformedValue.value) {
    console.warn('[FieldSectionPriority] modelValue not in {section_id, priority}[] shape — self-healing to [].')
    warnedOnMalformedValue.value = true
  }

  return cleaned
}

const ranked = ref<SectionPriorityValue[]>(selfHealIncoming(props.modelValue))

watch(() => props.modelValue, v => {
  ranked.value = selfHealIncoming(v)
}, { deep: true })

const maxPriorities = computed(() => {
  // WS-5b canonicalised the legacy `max_priorities` key to `max_selected`;
  // the field's cap (if any) is surfaced under `validation_rules.max_selected`.
  const raw = props.field.validation_rules?.max_selected
  if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
    return Math.min(raw, HARD_CAP)
  }

  return HARD_CAP
})

const sectionsById = computed<Record<string, PublicFormSectionOption>>(() => {
  const list = sections.value ?? []
  const map: Record<string, PublicFormSectionOption> = {}
  for (const s of list) map[s.id] = s

  return map
})

const unrankedPool = computed<PublicFormSectionOption[]>(() => {
  const list = sections.value ?? []
  const rankedIds = new Set(ranked.value.map(r => r.section_id))

  return list.filter(s => !rankedIds.has(s.id))
})

const isEmpty = computed(() => !sections.value || sections.value.length === 0)
const rankedFull = computed(() => ranked.value.length >= maxPriorities.value)

const rules = computed(() => getValidatorsForField(props.field))
const clientError = computed(() => {
  const res = runValidators(rules.value, ranked.value)

  return res === true ? null : res
})
const displayedErrors = computed(() => {
  if (props.errorMessages && props.errorMessages.length > 0) return props.errorMessages
  if (clientError.value) return [clientError.value]

  return []
})

function reassignPriorities(list: SectionPriorityValue[]): SectionPriorityValue[] {
  return list.map((item, index) => ({ section_id: item.section_id, priority: index + 1 }))
}

function emitNow(): void {
  emit('update:modelValue', ranked.value)
  emit('blur')
}

function rankSection(section: PublicFormSectionOption): void {
  if (rankedFull.value) return
  ranked.value = reassignPriorities([
    ...ranked.value,
    { section_id: section.id, priority: ranked.value.length + 1 },
  ])
  emitNow()
}

function unrankAt(index: number): void {
  const next = [...ranked.value]
  next.splice(index, 1)
  ranked.value = reassignPriorities(next)
  emitNow()
}

function onDragEnd(): void {
  // vuedraggable already mutated ranked.value via v-model — we just
  // renumber priorities and emit.
  ranked.value = reassignPriorities(ranked.value)
  emitNow()
}

function sectionNameFor(id: string): string {
  return sectionsById.value[id]?.name ?? ''
}
</script>

<template>
  <div>
    <div class="text-body-2 mb-1 text-high-emphasis">
      {{ field.label }}<span
        v-if="field.is_required"
        class="text-error"
      > *</span>
    </div>
    <p
      v-if="field.help_text"
      class="text-caption text-medium-emphasis mb-2"
    >
      {{ field.help_text }}
    </p>

    <VSkeletonLoader
      v-if="isLoading"
      type="article"
    />

    <VAlert
      v-else-if="isError"
      type="error"
      variant="tonal"
      density="comfortable"
    >
      <div class="d-flex flex-wrap align-center justify-space-between ga-3">
        <span>Kon secties niet laden.</span>
        <VBtn
          size="small"
          variant="outlined"
          @click="refetch()"
        >
          Opnieuw proberen
        </VBtn>
      </div>
    </VAlert>

    <VAlert
      v-else-if="isEmpty"
      type="info"
      variant="tonal"
      density="comfortable"
    >
      Er zijn nog geen secties gepubliceerd voor registratie.
    </VAlert>

    <div v-else>
      <!-- Ranked list -->
      <div class="mb-2 text-caption text-medium-emphasis">
        Jouw voorkeuren ({{ ranked.length }} / {{ maxPriorities }})
      </div>

      <draggable
        v-model="ranked"
        item-key="section_id"
        handle=".section-priority-handle"
        ghost-class="section-priority-ghost"
        chosen-class="section-priority-chosen"
        drag-class="section-priority-drag"
        :animation="150"
        :delay="100"
        :delay-on-touch-only="true"
        class="section-priority-ranked mb-4"
        @end="onDragEnd"
      >
        <template #item="{ element, index }">
          <VCard
            variant="outlined"
            class="section-priority-ranked-item d-flex align-center pa-3 mb-2"
            :aria-label="`Voorkeur ${index + 1}: ${sectionNameFor(element.section_id)}`"
          >
            <div class="section-priority-rank me-3 text-primary font-weight-bold text-h6">
              {{ index + 1 }}
            </div>
            <VIcon
              v-if="sectionsById[element.section_id]?.icon"
              :icon="sectionsById[element.section_id]!.icon!"
              size="18"
              class="me-2 text-medium-emphasis"
            />
            <div class="flex-grow-1">
              <div class="text-body-1">
                {{ sectionNameFor(element.section_id) }}
              </div>
              <div
                v-if="sectionsById[element.section_id]?.registration_description"
                class="text-caption text-medium-emphasis"
              >
                {{ sectionsById[element.section_id]!.registration_description }}
              </div>
            </div>
            <VIcon
              v-if="!mobile"
              class="section-priority-handle me-2 text-disabled"
              style="cursor: grab;"
              icon="tabler-grip-vertical"
              size="18"
            />
            <VBtn
              icon="tabler-x"
              size="small"
              variant="text"
              :aria-label="`Verwijder ${sectionNameFor(element.section_id)} uit je voorkeuren`"
              @click="unrankAt(index)"
            />
          </VCard>
        </template>
      </draggable>

      <div
        v-if="ranked.length === 0"
        class="text-caption text-medium-emphasis mb-4"
      >
        Tik of sleep een sectie hieronder om je eerste voorkeur te kiezen.
      </div>

      <!-- Unranked pool -->
      <div class="mb-2 text-caption text-medium-emphasis">
        Nog te kiezen
      </div>
      <VRow dense>
        <VCol
          v-for="section in unrankedPool"
          :key="section.id"
          cols="12"
          sm="6"
        >
          <VCard
            variant="outlined"
            class="section-priority-unranked-card pa-3"
            :class="{ 'section-priority-unranked-disabled': rankedFull }"
            role="button"
            tabindex="0"
            :aria-label="`Voeg ${section.name} toe aan je voorkeuren`"
            :aria-disabled="rankedFull"
            @click="rankSection(section)"
            @keydown.enter.prevent="rankSection(section)"
            @keydown.space.prevent="rankSection(section)"
          >
            <div class="d-flex align-center">
              <VIcon
                v-if="section.icon"
                :icon="section.icon"
                size="18"
                class="me-2 text-medium-emphasis"
              />
              <div class="flex-grow-1">
                <div class="text-body-1">
                  {{ section.name }}
                </div>
                <div
                  v-if="section.registration_description"
                  class="text-caption text-medium-emphasis"
                >
                  {{ section.registration_description }}
                </div>
              </div>
              <VIcon
                v-if="!rankedFull"
                icon="tabler-plus"
                size="18"
                class="text-primary"
              />
            </div>
            <div
              v-if="rankedFull"
              class="text-caption text-medium-emphasis mt-1"
            >
              Maximaal {{ maxPriorities }} voorkeuren
            </div>
          </VCard>
        </VCol>
      </VRow>

      <div
        v-if="displayedErrors.length"
        class="text-caption text-error mt-2"
      >
        {{ displayedErrors[0] }}
      </div>
    </div>
  </div>
</template>

<style scoped>
/* Vuetify's VCard disabled state isn't contextual enough for an
   "over-cap" affordance — we need it to look dimmed without blocking
   assistive tech, which Vuetify's :disabled would do. */
.section-priority-unranked-card {
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.section-priority-unranked-card:hover:not(.section-priority-unranked-disabled),
.section-priority-unranked-card:focus-visible:not(.section-priority-unranked-disabled) {
  background-color: rgb(var(--v-theme-primary) / 0.04);
  border-color: rgb(var(--v-theme-primary));
}

.section-priority-unranked-disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.section-priority-rank {
  min-inline-size: 1.5rem;
  text-align: center;
}

/* vuedraggable drag states — SortableJS applies these classes. The
   defaults leave the drag-clone semi-transparent while the ghost
   stays solid at the origin, which produces text overlap mid-drag. */
.section-priority-ghost {
  opacity: 0.3;
  background: rgb(var(--v-theme-surface-bright));
}

.section-priority-drag {
  /* !important overrides SortableJS's inline opacity: 0.8 default so
     the drag-clone reads as solid + elevated. */
  opacity: 1 !important;
  background: rgb(var(--v-theme-surface));
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
  cursor: grabbing;
}

.section-priority-chosen {
  cursor: grabbing;
}
</style>
