<script setup lang="ts">
/**
 * Props-driven integration-connector picker. presented as a button-style
 * picker: the trigger renders the selected connector's icon + label +
 * status dot, and clicking opens a searchable Combobox panel.
 *
 *   <IntegrationConnectorPicker
 *     v-model="connectorId"
 *     label="Connector"
 *     :on-load-connectors="() => fetch('/connectors').then((r) => r.json())"
 *     :on-connect="(id) => beginOAuth(id)"
 *   />
 */
import { computed, ref, watch, type Component } from "vue"
import { ChevronsUpDown, Loader2, Plug, Search } from "@lucide/vue"
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
} from "@/components/ui/combobox"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { FieldFrame } from "@/components/ui/field-frame"
import type { Connector } from "@/definitions/shared/connector"
import { useConnectorLoader } from "@/composables/useConnectorLoader"

export interface IntegrationConnectorPickerProps {
  modelValue?: string | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  placeholder?: string
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
  /** REQUIRED. Returns the list of connectors. */
  onLoadConnectors: () => Promise<Connector[]>
  /** When supplied, surfaces a per-row Connect button on non-connected rows. */
  onConnect?: (id: string) => Promise<void>
  loadingText?: string
  emptyText?: string
  errorText?: string
}

const props = withDefaults(defineProps<IntegrationConnectorPickerProps>(), {
  errors: () => [],
  loadingText: "Loading connectors...",
  emptyText: "No connectors available",
  errorText: "Could not load connectors",
})

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

const { connectors, isLoading, error, refresh, isConnecting, connect } =
  useConnectorLoader(() => props.onLoadConnectors())

const STATUS_DOT_CLASS: Record<NonNullable<Connector["status"]>, string> = {
  connected: "bg-success",
  disconnected: "bg-muted-foreground/40",
  error: "bg-destructive",
}

function statusDotClass(status: Connector["status"]): string {
  if (status && STATUS_DOT_CLASS[status]) {
    return STATUS_DOT_CLASS[status]
  }
  return "bg-muted-foreground/40"
}

function toPascalCase(input: string): string {
  return input
    .replace(/[-_\s]+(.)?/g, (_, ch?: string) => (ch ? ch.toUpperCase() : ""))
    .replace(/^(.)/, (m) => m.toUpperCase())
}

const iconRegistry = ref<Record<string, Component | null>>({})

async function ensureIcon(name: string): Promise<void> {
  if (!name) {
    return
  }
  const pascal = toPascalCase(name)
  if (pascal in iconRegistry.value) {
    return
  }
  const mod = await import("@lucide/vue")
  const registry = mod as unknown as Record<string, Component | undefined>
  iconRegistry.value = {
    ...iconRegistry.value,
    [pascal]: registry[pascal] ?? null,
  }
}

watch(
  connectors,
  (next) => {
    for (const c of next) {
      if (c.icon) {
        void ensureIcon(c.icon)
      }
    }
  },
  { immediate: true },
)

function iconFor(name?: string): Component | null {
  if (!name) {
    return null
  }
  return iconRegistry.value[toPascalCase(name)] ?? null
}

const query = ref<string>("")

const filteredConnectors = computed<Connector[]>(() => {
  const q = query.value.trim().toLowerCase()
  if (!q) {
    return connectors.value
  }
  return connectors.value.filter((c) =>
    [c.label, c.type ?? "", c.id].some((field) =>
      field.toLowerCase().includes(q),
    ),
  )
})

const selectedConnector = computed<Connector | null>(() => {
  if (model.value === undefined || model.value === null) {
    return null
  }
  return connectors.value.find((c) => c.id === model.value) ?? null
})

const currentValue = computed<string | undefined>(() => {
  if (model.value === undefined || model.value === null) {
    return undefined
  }
  return model.value
})

function onInput(event: Event): void {
  query.value = (event.target as HTMLInputElement).value
}

function onUpdate(next: unknown): void {
  if (Array.isArray(next)) {
    model.value = next[0] === undefined ? null : (next[0] as string)
    return
  }
  model.value = next === undefined ? null : (next as string)
}

async function handleConnect(id: string, event?: Event): Promise<void> {
  event?.stopPropagation()
  event?.preventDefault()
  await connect(id, props.onConnect)
}

defineExpose({ refresh, handleConnect })
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="errors"
  >
    <Combobox
      :model-value="currentValue"
      :disabled="disabled || readonly"
      ignore-filter
      @update:model-value="onUpdate"
    >
      <ComboboxAnchor
        :data-element-name="name ?? undefined"
        data-testid="integration-connector-picker-anchor"
        :class="[
          'justify-between text-left',
          errors.length ? 'border-destructive ring-destructive' : '',
        ]"
      >
        <div class="flex min-w-0 flex-1 items-center gap-2">
          <component
            :is="iconFor(selectedConnector?.icon)"
            v-if="selectedConnector && iconFor(selectedConnector.icon)"
            class="size-4 shrink-0"
          />
          <Plug
            v-else-if="!selectedConnector"
            class="size-4 shrink-0 text-muted-foreground/60"
          />
          <span
            v-if="selectedConnector"
            class="truncate text-sm"
            data-testid="integration-connector-picker-label"
          >
            {{ selectedConnector.label }}
          </span>
          <span
            v-else
            class="truncate text-sm text-muted-foreground"
            data-testid="integration-connector-picker-placeholder"
          >
            {{ placeholder ?? "Select connector…" }}
          </span>
          <span
            v-if="selectedConnector"
            :class="[
              'inline-block size-2 shrink-0 rounded-full',
              statusDotClass(selectedConnector.status),
            ]"
            :data-status="selectedConnector.status ?? 'unknown'"
            data-testid="integration-connector-picker-status"
          />
        </div>
        <ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
      </ComboboxAnchor>
      <ComboboxContent>
        <div class="flex items-center border-b px-2 py-1.5">
          <Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
          <ComboboxInput
            :id="id"
            :model-value="query"
            placeholder="Search connectors…"
            class="h-8"
            data-testid="integration-connector-picker-input"
            @input="onInput"
          />
          <Loader2
            v-if="isLoading"
            data-testid="integration-connector-picker-loader"
            class="ml-2 h-4 w-4 shrink-0 animate-spin text-muted-foreground"
          />
        </div>
        <div
          v-if="isLoading"
          class="px-2 py-2 text-sm text-muted-foreground"
          data-testid="integration-connector-picker-loading-text"
        >
          {{ loadingText }}
        </div>
        <div
          v-else-if="error"
          class="flex flex-col gap-2 px-2 py-2"
          data-testid="integration-connector-picker-error"
        >
          <p class="text-sm text-destructive">{{ errorText }}</p>
          <Button
            type="button"
            size="sm"
            variant="outline"
            data-testid="integration-connector-picker-retry"
            @click="refresh"
          >
            Retry
          </Button>
        </div>
        <ComboboxEmpty v-else-if="filteredConnectors.length === 0">
          {{ emptyText }}
        </ComboboxEmpty>
        <ComboboxItem
          v-for="conn in filteredConnectors"
          :key="conn.id"
          :value="conn.id"
          :data-testid="`integration-connector-picker-item-${conn.id}`"
        >
          <ComboboxItemIndicator />
          <div class="flex flex-1 items-center gap-2">
            <component
              :is="iconFor(conn.icon)"
              v-if="iconFor(conn.icon)"
              class="size-4 shrink-0"
            />
            <span class="truncate text-sm">{{ conn.label }}</span>
            <Badge v-if="conn.type" variant="outline" size="sm">
              {{ conn.type }}
            </Badge>
            <span
              :class="['ml-auto inline-block size-2 rounded-full', statusDotClass(conn.status)]"
              :data-status="conn.status ?? 'unknown'"
              :data-testid="`integration-connector-picker-status-${conn.id}`"
            />
            <Button
              v-if="onConnect && conn.status !== 'connected'"
              type="button"
              size="sm"
              variant="outline"
              class="ml-2"
              :disabled="Boolean(isConnecting[conn.id])"
              :data-testid="`integration-connector-picker-connect-${conn.id}`"
              @click.stop="handleConnect(conn.id, $event)"
            >
              <Loader2
                v-if="isConnecting[conn.id]"
                class="size-3 animate-spin"
              />
              <span>{{
                conn.status === "error" ? "Reconnect" : "Connect"
              }}</span>
            </Button>
          </div>
        </ComboboxItem>
      </ComboboxContent>
    </Combobox>
  </FieldFrame>
</template>
