<script setup lang="ts">
/**
 * Props-driven dropzone composition.
 *
 * Drag-and-drop target backed by a hidden `<input type="file">` for
 * click-to-browse. Same value contract as `UploadFile` — single `File`
 * or `File[]` when `multiple` is true.
 *
 *   <DropzoneFile v-model="doc" label="Document" accept="application/pdf" />
 */
import { computed, ref, watch } from "vue"
import { UploadCloud, X } from "@lucide/vue"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { FieldFrame } from "@/components/ui/field-frame"
import { cn } from "@/lib/utils"

export interface DropzoneFileProps {
  modelValue?: File | File[] | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  placeholder?: string
  accept?: string
  acceptedFileTypes?: string[]
  multiple?: boolean
  maxFileSize?: number
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
}

const props = withDefaults(defineProps<DropzoneFileProps>(), {
  errors: () => [],
  acceptedFileTypes: () => [],
})

const model = defineModel<File | File[] | null>("modelValue")

const fileInput = ref<HTMLInputElement | null>(null)
const dragOver = ref<boolean>(false)
const files = ref<File[]>([])

watch(
  () => model.value,
  (next) => {
    if (next instanceof File) {
      files.value = [next]
    } else if (Array.isArray(next) && next.every((f) => f instanceof File)) {
      files.value = next as File[]
    } else {
      files.value = []
    }
  },
  { immediate: true },
)

const accept = computed<string>(() => {
  if (props.accept) {
    return props.accept
  }
  const list = props.acceptedFileTypes
  return Array.isArray(list) && list.length ? list.join(",") : ""
})

const maxBytes = computed<number | undefined>(() => props.maxFileSize)

function emitFiles(): void {
  if (props.multiple) {
    model.value = files.value.slice()
  } else {
    model.value = files.value[0] ?? null
  }
}

function ingest(incoming: File[]): void {
  if (!incoming.length) {
    return
  }
  const tooLarge = maxBytes.value
    ? incoming.find((f) => f.size > (maxBytes.value as number))
    : undefined
  if (tooLarge) {
    return
  }
  if (props.multiple) {
    files.value = [...files.value, ...incoming]
  } else {
    files.value = [incoming[0]!]
  }
  emitFiles()
}

function onChange(event: Event): void {
  const target = event.target as HTMLInputElement
  const list = target.files ? Array.from(target.files) : []
  ingest(list)
  target.value = ""
}

function onDrop(event: DragEvent): void {
  event.preventDefault()
  dragOver.value = false
  if (props.disabled || props.readonly) {
    return
  }
  const list = event.dataTransfer ? Array.from(event.dataTransfer.files) : []
  ingest(list)
}

function onDragOver(event: DragEvent): void {
  event.preventDefault()
  if (!props.disabled && !props.readonly) {
    dragOver.value = true
  }
}

function onDragLeave(event: DragEvent): void {
  event.preventDefault()
  dragOver.value = false
}

function openPicker(): void {
  fileInput.value?.click()
}

function removeFile(index: number): void {
  files.value.splice(index, 1)
  emitFiles()
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) {
    return `${bytes} B`
  }
  if (bytes < 1024 * 1024) {
    return `${(bytes / 1024).toFixed(1)} KB`
  }
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :help-text="helpText"
    :errors="errors"
  >
    <div
      :class="
        cn(
          'flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed border-input bg-background px-6 py-10 text-center transition-colors',
          dragOver && 'border-primary bg-primary/5',
          (disabled || readonly) && 'cursor-not-allowed opacity-50',
        )
      "
      role="button"
      tabindex="0"
      data-testid="dropzone-target"
      :data-drag-over="dragOver || undefined"
      :data-element-name="name ?? undefined"
      @click="openPicker"
      @keydown.enter.prevent="openPicker"
      @keydown.space.prevent="openPicker"
      @drop="onDrop"
      @dragover="onDragOver"
      @dragleave="onDragLeave"
    >
      <UploadCloud class="size-8 text-muted-foreground" />
      <p class="text-sm font-medium">
        {{ placeholder ?? "Drop files here, or click to browse" }}
      </p>
      <p v-if="accept" class="text-xs text-muted-foreground">
        {{ accept }}
      </p>
      <input
        ref="fileInput"
        type="file"
        class="sr-only"
        :accept="accept || undefined"
        :multiple="multiple"
        :disabled="disabled || readonly"
        :required="required"
        :name="name ?? undefined"
        data-testid="dropzone-input"
        @change="onChange"
      >
    </div>

    <div v-if="files.length" class="flex flex-col gap-2">
      <Card
        v-for="(file, i) in files"
        :key="`${file.name}-${i}`"
        class="border"
      >
        <CardContent class="flex items-center justify-between gap-3 py-3">
          <div class="flex flex-col">
            <span class="text-sm font-medium">{{ file.name }}</span>
            <span class="text-xs text-muted-foreground">{{ formatBytes(file.size) }}</span>
          </div>
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            aria-label="Remove file"
            :disabled="disabled || readonly"
            data-testid="dropzone-remove"
            @click.stop="removeFile(i)"
          >
            <X class="size-4" />
          </Button>
        </CardContent>
      </Card>
    </div>
  </FieldFrame>
</template>
