<script setup lang="ts">
/**
 * Props-driven barcode scanner input.
 *
 * Wraps `@ericblade/quagga2` to provide a live camera-driven barcode
 * scanner. Renders the last decoded value in a read-only Input plus a
 * "Scan" button that toggles a `<video>` viewport.
 *
 * The quagga library and the `getUserMedia` API are lazy-imported when
 * scanning starts so the component is safe to render in environments
 * without a camera (e.g. jsdom).
 *
 *   <BarcodeScannerInput v-model="ticket" label="Scan ticket" />
 */
import { computed, onBeforeUnmount, ref, useTemplateRef } from "vue"
import { Camera, Loader2, StopCircle } from "@lucide/vue"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { FieldFrame } from "@/components/ui/field-frame"

export interface BarcodeScannerInputProps {
  modelValue?: string | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
  /** Quagga barcode reader symbologies to recognise. */
  scanFormats?: ReadonlyArray<string>
  /** Start scanning automatically when the component mounts. */
  autoStart?: boolean
  /** Preferred camera facing mode. */
  facingMode?: "environment" | "user"
}

const props = withDefaults(defineProps<BarcodeScannerInputProps>(), {
  errors: () => [],
  scanFormats: () => [
    "code_128_reader",
    "ean_reader",
    "ean_8_reader",
    "code_39_reader",
    "upc_reader",
  ],
  autoStart: false,
  facingMode: "environment",
})

const model = defineModel<string | null>("modelValue")

/** Loose Quagga handle — type-definitions ship with the library but we
 *  keep the references local rather than pulling its full type graph. */
type QuaggaResult = { codeResult?: { code?: string | null } }
type QuaggaHandle = {
  init: (config: Record<string, unknown>) => Promise<void>
  start: () => void
  stop: () => Promise<void> | void
  onDetected: (cb: (result: QuaggaResult) => void) => void
  offDetected: (cb: (result: QuaggaResult) => void) => void
}

const videoContainerRef = useTemplateRef<HTMLDivElement>("videoContainerRef")
const scanning = ref<boolean>(false)
const starting = ref<boolean>(false)
const scanError = ref<string>("")

let quagga: QuaggaHandle | null = null
let mediaStream: MediaStream | null = null
let detectedHandler: ((result: QuaggaResult) => void) | null = null

const buttonLabel = computed<string>(() => {
  if (starting.value) {
    return "Starting…"
  }
  return scanning.value ? "Stop scanning" : "Scan"
})

const allErrors = computed<string[]>(() => {
  if (scanError.value) {
    return [...props.errors, scanError.value]
  }
  return props.errors
})

function releaseStream(): void {
  if (mediaStream) {
    for (const track of mediaStream.getTracks()) {
      try {
        track.stop()
      } catch {
        /* ignore */
      }
    }
    mediaStream = null
  }
}

async function stop(): Promise<void> {
  if (quagga) {
    try {
      if (detectedHandler) {
        quagga.offDetected(detectedHandler)
      }
      await quagga.stop()
    } catch {
      /* ignore */
    }
  }
  detectedHandler = null
  quagga = null
  releaseStream()
  scanning.value = false
  starting.value = false
}

async function start(): Promise<void> {
  if (scanning.value || starting.value) {
    return
  }
  scanError.value = ""
  starting.value = true

  try {
    const nav = typeof navigator !== "undefined" ? navigator : undefined
    if (!nav?.mediaDevices?.getUserMedia) {
      throw new Error("Camera access is not available in this environment.")
    }

    mediaStream = await nav.mediaDevices.getUserMedia({
      video: { facingMode: props.facingMode },
      audio: false,
    })

    const mod = await import("@ericblade/quagga2")
    quagga = (mod as unknown as { default: QuaggaHandle }).default

    const target = videoContainerRef.value
    if (!target) {
      throw new Error("Scanner viewport is not mounted.")
    }

    await quagga.init({
      inputStream: {
        type: "LiveStream",
        target,
        constraints: {
          facingMode: props.facingMode,
        },
      },
      decoder: {
        readers: [...props.scanFormats],
      },
      locate: true,
    })

    detectedHandler = (result: QuaggaResult): void => {
      const code = result?.codeResult?.code
      if (typeof code === "string" && code.length > 0) {
        model.value = code
        void stop()
      }
    }
    quagga.onDetected(detectedHandler)

    quagga.start()
    scanning.value = true
  } catch (e) {
    scanError.value = e instanceof Error ? e.message : "Failed to start scanner."
    await stop()
  } finally {
    starting.value = false
  }
}

async function toggleScan(): Promise<void> {
  if (scanning.value) {
    await stop()
  } else {
    await start()
  }
}

/**
 * Test hook — lets tests simulate a successful Quagga detection
 * without spinning up a real camera. Not part of the public contract;
 * exposed only via `wrapper.vm` in unit tests.
 */
function __handleDetectedForTest(code: string): void {
  if (code) {
    model.value = code
  }
}

defineExpose({ __handleDetectedForTest, start, stop })

if (props.autoStart) {
  void start()
}

onBeforeUnmount(() => {
  void stop()
})
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="allErrors"
  >
    <div class="flex flex-col gap-2" :data-element-name="name ?? undefined">
      <div class="flex items-center gap-2">
        <Input
          :id="id"
          :model-value="model ?? undefined"
          :name="name ?? undefined"
          type="text"
          readonly
          :disabled="disabled"
          :required="required"
          placeholder="No code scanned yet"
          :data-element-name="name ?? undefined"
          :class="errors.length ? 'border-destructive ring-destructive' : ''"
        />
        <Button
          type="button"
          :variant="scanning ? 'destructive' : 'default'"
          :disabled="disabled || readonly || starting"
          data-testid="barcode-scan-toggle"
          @click="toggleScan"
        >
          <Loader2 v-if="starting" class="size-4 animate-spin" />
          <StopCircle v-else-if="scanning" class="size-4" />
          <Camera v-else class="size-4" />
          <span>{{ buttonLabel }}</span>
        </Button>
      </div>
      <div
        v-show="scanning || starting"
        ref="videoContainerRef"
        data-testid="barcode-scan-viewport"
        class="relative h-64 w-full overflow-hidden rounded-md border border-input bg-muted"
      />
    </div>
  </FieldFrame>
</template>
