<script setup lang="ts">
/**
 * ObjectBuilder — visually compose a JSON object as a list of typed
 * key / value entries.
 *
 * Three-pane layout:
 *   - LEFT  (Palette):    Buttons for each entry type
 *                         (string / number / boolean / array / object /
 *                         null). Clicking appends a new entry of that
 *                         type.
 *   - MIDDLE (Canvas):    Ordered list of entries. Each row shows the
 *                         key, a type badge, and a value preview.
 *                         Click to select.
 *   - RIGHT (Inspector):  Edits the selected entry's key, type, and
 *                         value. The editor swaps based on `type`.
 *
 * The schema is owned by `objectBuilderStore`. The builder accepts an
 * initial value via `modelValue` (either an `ObjectBuilderSchema` or a
 * plain `Record<string, unknown>`) and emits the current schema
 * through `update:modelValue` on every mutation.
 */
import { computed, onMounted, ref, watch } from "vue"
import { ArrowDown, ArrowUp, Plus, Trash2, X } from "@lucide/vue"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  useObjectBuilder,
  recordToSchema,
  toRecord,
  type ObjectBuilderEntry,
  type ObjectBuilderEntryType,
  type ObjectBuilderSchema,
} from "@/stores/objectBuilderStore"
import { cn } from "@/lib/utils"

export interface ObjectBuilderProps {
  /**
   * Initial / current value. Accepts either an `ObjectBuilderSchema`
   * (preferred) or a plain `Record<string, unknown>` (which is
   * normalised to entries on mount).
   */
  modelValue?: ObjectBuilderSchema | Record<string, unknown>
  /**
   * Identifier used to key the objectBuilderStore. Defaults to a
   * stable auto-generated id so multiple builders on the same page
   * don't collide.
   */
  builderId?: string
  label?: string
  name?: string | null
  classList?: string[] | string
}

const props = withDefaults(defineProps<ObjectBuilderProps>(), {
  label: "Object Builder",
})

const model = defineModel<
  ObjectBuilderSchema | Record<string, unknown> | undefined
>("modelValue")

let nextAutoId = 0
function makeAutoId(): string {
  nextAutoId += 1
  return `object-builder-${Date.now()}-${nextAutoId}`
}
const fallbackId = ref<string>(makeAutoId())

const builderId = computed<string>(() => props.builderId ?? fallbackId.value)

const builder = useObjectBuilder(builderId.value)

const PALETTE_TYPES: Array<{
  type: ObjectBuilderEntryType
  displayLabel: string
  description: string
}> = [
  {
    type: "string",
    displayLabel: "String",
    description: "A text value.",
  },
  {
    type: "number",
    displayLabel: "Number",
    description: "A numeric value.",
  },
  {
    type: "boolean",
    displayLabel: "Boolean",
    description: "true or false.",
  },
  {
    type: "array",
    displayLabel: "Array",
    description: "A JSON array (edit as JSON).",
  },
  {
    type: "object",
    displayLabel: "Object",
    description: "A nested object (edit as JSON).",
  },
  {
    type: "null",
    displayLabel: "Null",
    description: "Explicit null.",
  },
]

function isObjectBuilderSchema(value: unknown): value is ObjectBuilderSchema {
  if (typeof value !== "object" || value === null) {
    return false
  }
  const v = value as Record<string, unknown>
  return Array.isArray(v.entries)
}

/** Seed the store from the initial modelValue. */
onMounted(() => {
  const seed = model.value
  if (seed === undefined || seed === null) {
    return
  }
  if (isObjectBuilderSchema(seed)) {
    builder.loadSchema(seed)
  } else if (typeof seed === "object") {
    builder.loadSchema(recordToSchema(seed as Record<string, unknown>))
  }
})

/** Push schema changes back via v-model. */
watch(
  () => builder.schema.value,
  (next) => {
    model.value = {
      entries: next.entries.map((e) => ({ ...e })),
    }
  },
  { deep: true },
)

const entries = computed<ObjectBuilderEntry[]>(
  () => builder.schema.value.entries,
)

const selectedEntry = computed<ObjectBuilderEntry | null>(() => {
  const sel = builder.selection.value
  if (!sel.id) {
    return null
  }
  return entries.value.find((e) => e.id === sel.id) ?? null
})

const selectedIndex = computed<number>(() => {
  const sel = builder.selection.value
  if (!sel.id) {
    return -1
  }
  return entries.value.findIndex((e) => e.id === sel.id)
})

const canMoveDown = computed<boolean>(() => {
  return (
    selectedIndex.value >= 0 && selectedIndex.value < entries.value.length - 1
  )
})

function onPaletteClick(type: ObjectBuilderEntryType): void {
  const newId = builder.addEntry(type)
  builder.select(newId)
}

function onRowClick(entryId: string): void {
  builder.select(entryId)
}

function onMoveUp(): void {
  if (selectedIndex.value <= 0) {
    return
  }
  builder.reorderEntry(selectedIndex.value, selectedIndex.value - 1)
}

function onMoveDown(): void {
  if (!canMoveDown.value) {
    return
  }
  builder.reorderEntry(selectedIndex.value, selectedIndex.value + 1)
}

function onRemove(): void {
  const sel = builder.selection.value
  if (sel.id) {
    builder.removeEntry(sel.id)
  }
}

function onClearSelection(): void {
  builder.select(null)
}

function patchSelected(patch: Partial<Omit<ObjectBuilderEntry, "id">>): void {
  const sel = builder.selection.value
  if (sel.id) {
    builder.updateEntry(sel.id, patch)
  }
}

function previewValue(entry: ObjectBuilderEntry): string {
  switch (entry.type) {
    case "null":
      return "null"
    case "boolean":
      return entry.value ? "true" : "false"
    case "string":
      return entry.value === "" ? '""' : String(entry.value)
    case "number":
      return String(entry.value)
    case "array":
    case "object":
      try {
        return JSON.stringify(entry.value)
      } catch {
        return "(invalid)"
      }
  }
}

const keyValue = computed<string>(() => selectedEntry.value?.key ?? "")
const stringValue = computed<string>(() =>
  selectedEntry.value && selectedEntry.value.type === "string"
    ? String(selectedEntry.value.value ?? "")
    : "",
)
const numberValue = computed<string>(() =>
  selectedEntry.value && selectedEntry.value.type === "number"
    ? String(selectedEntry.value.value ?? 0)
    : "",
)
const booleanValue = computed<boolean>(() =>
  selectedEntry.value && selectedEntry.value.type === "boolean"
    ? Boolean(selectedEntry.value.value)
    : false,
)

const jsonDraft = ref<string>("")
const jsonError = ref<string>("")

watch(
  () => selectedEntry.value,
  (next) => {
    if (!next) {
      jsonDraft.value = ""
      jsonError.value = ""
      return
    }
    if (next.type === "array" || next.type === "object") {
      try {
        jsonDraft.value = JSON.stringify(next.value, null, 2)
      } catch {
        jsonDraft.value = ""
      }
      jsonError.value = ""
    }
  },
  { immediate: true },
)

function onJsonInput(raw: string): void {
  jsonDraft.value = raw
  try {
    const parsed = JSON.parse(raw)
    const entry = selectedEntry.value
    if (!entry) {
      return
    }
    if (entry.type === "array" && !Array.isArray(parsed)) {
      jsonError.value = "Value must be a JSON array."
      return
    }
    if (
      entry.type === "object" &&
      (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
    ) {
      jsonError.value = "Value must be a JSON object."
      return
    }
    jsonError.value = ""
    patchSelected({ value: parsed })
  } catch (err) {
    jsonError.value = err instanceof Error ? err.message : "Invalid JSON."
  }
}

function onTypeChange(next: string): void {
  const t = next as ObjectBuilderEntryType
  patchSelected({ type: t })
}

/** Re-export the flattening helper so consumers can `toRecord(schema)`. */
defineExpose({ toRecord })
</script>

<template>
  <div
    :class="
      cn(
        'gxp-object-builder grid gap-3 rounded-lg border bg-card',
        props.classList,
      )
    "
    style="grid-template-columns: 200px 1fr 300px; min-height: 22rem"
    data-testid="object-builder"
    :data-element-name="props.name ?? undefined"
  >
    <aside
      class="flex flex-col gap-2 border-r p-3 overflow-y-auto"
      data-testid="object-builder-palette"
    >
      <h4
        class="text-xs font-semibold uppercase tracking-wide text-muted-foreground"
      >
        {{ props.label }}
      </h4>
      <div class="flex flex-col gap-1">
        <button
          v-for="entry in PALETTE_TYPES"
          :key="entry.type"
          type="button"
          class="flex items-start gap-2 rounded-md border bg-background px-2 py-1.5 text-left text-xs transition-colors hover:border-primary hover:bg-accent/40"
          :data-testid="`object-builder-palette-${entry.type}`"
          @click="onPaletteClick(entry.type)"
        >
          <Plus class="mt-0.5 h-3.5 w-3.5 shrink-0" />
          <span class="flex min-w-0 flex-col gap-0.5">
            <span class="font-medium">{{ entry.displayLabel }}</span>
            <span class="line-clamp-2 text-[10px] text-muted-foreground">
              {{ entry.description }}
            </span>
          </span>
        </button>
      </div>
    </aside>

    <section
      class="flex flex-col gap-3 overflow-y-auto p-3"
      data-testid="object-builder-canvas"
    >
      <header class="flex items-center justify-between">
        <h4
          class="text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          Entries
        </h4>
        <Badge variant="outline" class="font-mono text-[10px]">
          builder/object-builder
        </Badge>
      </header>

      <p
        v-if="entries.length === 0"
        class="rounded-md border border-dashed bg-background py-6 text-center text-xs text-muted-foreground"
        data-testid="object-builder-canvas-empty"
      >
        Click an entry type in the palette to add one.
      </p>

      <ul
        v-else
        class="flex flex-col gap-1 text-xs"
        data-testid="object-builder-canvas-list"
      >
        <li
          v-for="(entry, index) in entries"
          :key="entry.id"
          class="flex cursor-pointer items-center gap-2 rounded border bg-background px-2 py-1.5 transition-colors"
          :class="
            builder.selection.value.id === entry.id
              ? 'border-primary bg-accent/30'
              : ''
          "
          :data-testid="`object-builder-row-${entry.id}`"
          @click="onRowClick(entry.id)"
        >
          <span class="w-6 text-right tabular-nums text-muted-foreground">
            {{ index + 1 }}.
          </span>
          <span class="font-medium">
            {{ entry.key || "(unnamed)" }}
          </span>
          <Badge variant="secondary" class="font-mono text-[9px]">
            {{ entry.type }}
          </Badge>
          <span
            class="ml-auto max-w-[12rem] truncate font-mono text-[10px] text-muted-foreground"
          >
            {{ previewValue(entry) }}
          </span>
        </li>
      </ul>
    </section>

    <aside
      class="flex flex-col gap-2 overflow-y-auto border-l p-3"
      data-testid="object-builder-inspector"
    >
      <header class="flex items-center justify-between">
        <h4
          class="text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          Inspector
        </h4>
        <Button
          v-if="builder.selection.value.id"
          variant="ghost"
          size="icon-sm"
          aria-label="Clear selection"
          @click="onClearSelection"
        >
          <X class="h-3.5 w-3.5" />
        </Button>
      </header>

      <p
        v-if="!selectedEntry"
        class="py-3 text-xs text-muted-foreground"
        data-testid="object-builder-inspector-empty"
      >
        Select an entry to edit its key, type, and value.
      </p>

      <div
        v-else
        class="flex flex-col gap-3"
        data-testid="object-builder-inspector-fields"
      >
        <Card class="border-dashed">
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              {{ selectedEntry.type }}
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="flex flex-col gap-1">
              <Label for="ob-key">Key</Label>
              <Input
                id="ob-key"
                :model-value="keyValue"
                data-testid="object-builder-inspector-key"
                @input="
                  (e: Event) =>
                    patchSelected({
                      key: (e.target as HTMLInputElement).value,
                    })
                "
              />
            </div>

            <div class="flex flex-col gap-1">
              <Label for="ob-type">Type</Label>
              <Select
                :model-value="selectedEntry.type"
                @update:model-value="(v) => onTypeChange(v as string)"
              >
                <SelectTrigger
                  id="ob-type"
                  data-testid="object-builder-inspector-type"
                >
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem
                    v-for="opt in PALETTE_TYPES"
                    :key="opt.type"
                    :value="opt.type"
                  >
                    {{ opt.displayLabel }}
                  </SelectItem>
                </SelectContent>
              </Select>
            </div>

            <div
              v-if="selectedEntry.type === 'string'"
              class="flex flex-col gap-1"
            >
              <Label for="ob-value-string">Value</Label>
              <Input
                id="ob-value-string"
                :model-value="stringValue"
                data-testid="object-builder-inspector-value-string"
                @input="
                  (e: Event) =>
                    patchSelected({
                      value: (e.target as HTMLInputElement).value,
                    })
                "
              />
            </div>

            <div
              v-else-if="selectedEntry.type === 'number'"
              class="flex flex-col gap-1"
            >
              <Label for="ob-value-number">Value</Label>
              <Input
                id="ob-value-number"
                type="number"
                :model-value="numberValue"
                data-testid="object-builder-inspector-value-number"
                @input="
                  (e: Event) => {
                    const v = (e.target as HTMLInputElement).value
                    const parsed = v === '' ? 0 : Number(v)
                    patchSelected({
                      value: Number.isNaN(parsed) ? 0 : parsed,
                    })
                  }
                "
              />
            </div>

            <div
              v-else-if="selectedEntry.type === 'boolean'"
              class="flex items-center justify-between rounded border bg-background px-2 py-1"
            >
              <Label for="ob-value-boolean">Value</Label>
              <Switch
                id="ob-value-boolean"
                :model-value="booleanValue"
                data-testid="object-builder-inspector-value-boolean"
                @update:model-value="(v: boolean) => patchSelected({ value: v })"
              />
            </div>

            <div
              v-else-if="
                selectedEntry.type === 'array' ||
                selectedEntry.type === 'object'
              "
              class="flex flex-col gap-1"
            >
              <Label for="ob-value-json">Value (JSON)</Label>
              <Textarea
                id="ob-value-json"
                :model-value="jsonDraft"
                rows="6"
                data-testid="object-builder-inspector-value-json"
                @input="
                  (e: Event) =>
                    onJsonInput((e.target as HTMLTextAreaElement).value)
                "
              />
              <p
                v-if="jsonError"
                class="text-[11px] text-destructive"
                data-testid="object-builder-inspector-value-json-error"
              >
                {{ jsonError }}
              </p>
            </div>

            <div
              v-else-if="selectedEntry.type === 'null'"
              class="rounded border bg-background px-2 py-1 text-xs text-muted-foreground"
              data-testid="object-builder-inspector-value-null"
            >
              Value is fixed to <code class="font-mono">null</code>.
            </div>
          </CardContent>
        </Card>

        <div class="flex items-center gap-1">
          <Button
            variant="outline"
            size="sm"
            :disabled="selectedIndex <= 0"
            data-testid="object-builder-move-up"
            @click="onMoveUp"
          >
            <ArrowUp class="h-3.5 w-3.5" />
          </Button>
          <Button
            variant="outline"
            size="sm"
            :disabled="!canMoveDown"
            data-testid="object-builder-move-down"
            @click="onMoveDown"
          >
            <ArrowDown class="h-3.5 w-3.5" />
          </Button>
          <Button
            variant="destructive"
            size="sm"
            class="ml-auto"
            data-testid="object-builder-remove"
            @click="onRemove"
          >
            <Trash2 class="h-3.5 w-3.5" />
            Remove
          </Button>
        </div>
      </div>
    </aside>
  </div>
</template>
