<script setup lang="ts">
/**
 * ReactiveCanvasBuilder — visual canvas where component-like blocks can
 * be placed and configured.
 *
 * Three-pane layout (matches FormBuilder):
 *   - LEFT (Palette):    a list of built-in block types. Click to add.
 *   - MIDDLE (Canvas):   a fixed-size relative-positioned surface. Each
 *                        block renders as an absolutely-positioned
 *                        placeholder card showing type + size + position.
 *                        Click to select.
 *   - RIGHT (Inspector): edits the selected block's x / y / width /
 *                        height (number inputs), type (Select), and
 *                        props (JSON textarea for now).
 *
 * Blocks may be repositioned by pointer-drag (no snapping, raw pixel
 * delta) and resized via four corner handles. Drag clamps to the
 * canvas bounds; resize clamps each block to a 20x20 minimum.
 *
 * State is owned by `reactiveCanvasBuilderStore`. Two-way binds the
 * full `ReactiveCanvasBuilderSchema` via `defineModel`.
 */
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
import { 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 {
  FieldFrame,
  type FieldFrameProps,
} from "@/components/ui/field-frame"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import {
  REACTIVE_CANVAS_BLOCK_TYPES,
  useReactiveCanvasBuilder,
  type ReactiveCanvasBlock,
  type ReactiveCanvasBuilderSchema,
} from "@/stores/reactiveCanvasBuilderStore"
import { cn } from "@/lib/utils"

export interface ReactiveCanvasBuilderProps
  extends Pick<FieldFrameProps, "label" | "hideLabel" | "helpText" | "errors"> {
  /** Initial / current schema. Two-way bindable via v-model. */
  modelValue?: ReactiveCanvasBuilderSchema
  /**
   * Identifier used to key the reactiveCanvasBuilderStore. Defaults to
   * a stable auto-generated id so multiple builders on the same page
   * don't collide.
   */
  builderId?: string
  name?: string | null
  item?: Record<string, unknown>
  classList?: string[] | string
}

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

const model = defineModel<ReactiveCanvasBuilderSchema | undefined>(
  "modelValue",
)

let nextAutoId = 0
function makeAutoId(): string {
  nextAutoId += 1
  return `reactive-canvas-${Date.now()}-${nextAutoId}`
}
const fallbackId = ref<string>(makeAutoId())
const builderId = computed<string>(() => props.builderId ?? fallbackId.value)

const builder = useReactiveCanvasBuilder(builderId.value)

function isSchema(value: unknown): value is ReactiveCanvasBuilderSchema {
  if (typeof value !== "object" || value === null) {
    return false
  }
  const v = value as Record<string, unknown>
  return (
    typeof v.width === "number" &&
    typeof v.height === "number" &&
    Array.isArray(v.blocks)
  )
}

onMounted(() => {
  const seed = model.value
  if (isSchema(seed)) {
    builder.loadSchema(seed)
  }
})

watch(
  () => builder.schema.value,
  (next) => {
    model.value = next
  },
  { deep: true },
)

const schema = computed<ReactiveCanvasBuilderSchema>(
  () => builder.schema.value,
)
const blocks = computed<ReactiveCanvasBlock[]>(() => schema.value.blocks)
const selectedId = computed<string | null>(() => builder.selection.value.id)
const selectedBlock = computed<ReactiveCanvasBlock | null>(() => {
  const id = selectedId.value
  if (!id) {
    return null
  }
  return blocks.value.find((b) => b.id === id) ?? null
})

/** Local string buffer for the props JSON textarea so users can type
 * freely without us re-parsing on every keystroke. */
const propsJsonDraft = ref<string>("")
const propsJsonError = ref<string | null>(null)

watch(
  () => selectedBlock.value?.id,
  () => {
    const block = selectedBlock.value
    if (!block) {
      propsJsonDraft.value = ""
      propsJsonError.value = null
      return
    }
    propsJsonDraft.value = JSON.stringify(block.props ?? {}, null, 2)
    propsJsonError.value = null
  },
  { immediate: true },
)

function onPaletteAdd(type: string): void {
  const id = builder.addBlock(type)
  builder.select(id)
}

function onBlockClick(blockId: string): void {
  builder.select(blockId)
}

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

function onRemoveSelected(): void {
  const id = selectedId.value
  if (id) {
    builder.removeBlock(id)
  }
}

function patchSelected(
  patch: Partial<Omit<ReactiveCanvasBlock, "id">>,
): void {
  const id = selectedId.value
  if (id) {
    builder.updateBlock(id, patch)
  }
}

function onNumberInput(
  key: "x" | "y" | "width" | "height",
  e: Event,
): void {
  const raw = (e.target as HTMLInputElement).value
  const parsed = Number(raw)
  if (Number.isFinite(parsed)) {
    patchSelected({ [key]: parsed } as Partial<ReactiveCanvasBlock>)
  }
}

function onCanvasSizeInput(key: "width" | "height", e: Event): void {
  const raw = (e.target as HTMLInputElement).value
  const parsed = Number(raw)
  if (Number.isFinite(parsed) && parsed > 0) {
    builder.setCanvasSize({ [key]: parsed })
  }
}

function onTypeChange(v: unknown): void {
  if (typeof v === "string") {
    patchSelected({ type: v })
  }
}

function onPropsJsonInput(e: Event): void {
  const next = (e.target as HTMLTextAreaElement).value
  propsJsonDraft.value = next
  try {
    const parsed = next.trim() === "" ? {} : JSON.parse(next)
    if (
      typeof parsed !== "object" ||
      parsed === null ||
      Array.isArray(parsed)
    ) {
      throw new Error("Props must be a JSON object")
    }
    propsJsonError.value = null
    patchSelected({ props: parsed as Record<string, unknown> })
  } catch (err) {
    propsJsonError.value =
      err instanceof Error ? err.message : "Invalid JSON"
  }
}

const canvasStyle = computed(() => ({
  width: `${schema.value.width}px`,
  height: `${schema.value.height}px`,
  maxWidth: "100%",
}))

function blockStyle(block: ReactiveCanvasBlock): Record<string, string> {
  return {
    left: `${block.x}px`,
    top: `${block.y}px`,
    width: `${block.width}px`,
    height: `${block.height}px`,
  }
}

/* ------------------------------------------------------------------ */
/* Pointer-driven drag + resize                                       */
/* ------------------------------------------------------------------ */

const MIN_BLOCK_SIZE = 20

type ResizeCorner = "tl" | "tr" | "bl" | "br"

interface DragState {
  mode: "drag" | "resize"
  corner: ResizeCorner | null
  blockId: string
  pointerId: number
  target: HTMLElement
  startClientX: number
  startClientY: number
  startX: number
  startY: number
  startWidth: number
  startHeight: number
}

const activeDrag = ref<DragState | null>(null)
const activeInteractionId = computed<string | null>(
  () => activeDrag.value?.blockId ?? null,
)

function clampToCanvas(
  x: number,
  y: number,
  width: number,
  height: number,
): { x: number; y: number } {
  const maxX = Math.max(0, schema.value.width - width)
  const maxY = Math.max(0, schema.value.height - height)
  return {
    x: Math.min(Math.max(0, x), maxX),
    y: Math.min(Math.max(0, y), maxY),
  }
}

function beginInteraction(
  block: ReactiveCanvasBlock,
  event: PointerEvent,
  mode: "drag" | "resize",
  corner: ResizeCorner | null = null,
): void {
  event.preventDefault()
  event.stopPropagation()
  builder.select(block.id)
  const target = event.target as HTMLElement | null
  if (target && typeof target.setPointerCapture === "function") {
    try {
      target.setPointerCapture(event.pointerId)
    } catch {
      // ignore (jsdom or detached node)
    }
  }
  activeDrag.value = {
    mode,
    corner,
    blockId: block.id,
    pointerId: event.pointerId,
    target: target ?? (event.currentTarget as HTMLElement),
    startClientX: event.clientX,
    startClientY: event.clientY,
    startX: block.x,
    startY: block.y,
    startWidth: block.width,
    startHeight: block.height,
  }
  window.addEventListener("pointermove", onPointerMove)
  window.addEventListener("pointerup", onPointerUp)
  window.addEventListener("pointercancel", onPointerUp)
}

function onBlockPointerDown(
  block: ReactiveCanvasBlock,
  event: PointerEvent,
): void {
  if (event.button !== undefined && event.button !== 0) {
    return
  }
  beginInteraction(block, event, "drag")
}

function onResizePointerDown(
  block: ReactiveCanvasBlock,
  corner: ResizeCorner,
  event: PointerEvent,
): void {
  if (event.button !== undefined && event.button !== 0) {
    return
  }
  beginInteraction(block, event, "resize", corner)
}

function applyDrag(state: DragState, dx: number, dy: number): void {
  const nextX = state.startX + dx
  const nextY = state.startY + dy
  const clamped = clampToCanvas(
    nextX,
    nextY,
    state.startWidth,
    state.startHeight,
  )
  builder.updateBlock(state.blockId, { x: clamped.x, y: clamped.y })
}

function applyResize(state: DragState, dx: number, dy: number): void {
  let x = state.startX
  let y = state.startY
  let width = state.startWidth
  let height = state.startHeight
  switch (state.corner) {
    case "br":
      width = state.startWidth + dx
      height = state.startHeight + dy
      break
    case "tr":
      width = state.startWidth + dx
      height = state.startHeight - dy
      y = state.startY + dy
      break
    case "bl":
      width = state.startWidth - dx
      height = state.startHeight + dy
      x = state.startX + dx
      break
    case "tl":
      width = state.startWidth - dx
      height = state.startHeight - dy
      x = state.startX + dx
      y = state.startY + dy
      break
    default:
      return
  }
  // Enforce minimum size: if the user drags past the min on a "leading
  // edge" corner (tl/tr/bl), we need to clamp position too so the
  // block doesn't grow in the wrong direction.
  if (width < MIN_BLOCK_SIZE) {
    if (state.corner === "tl" || state.corner === "bl") {
      x = state.startX + (state.startWidth - MIN_BLOCK_SIZE)
    }
    width = MIN_BLOCK_SIZE
  }
  if (height < MIN_BLOCK_SIZE) {
    if (state.corner === "tl" || state.corner === "tr") {
      y = state.startY + (state.startHeight - MIN_BLOCK_SIZE)
    }
    height = MIN_BLOCK_SIZE
  }
  builder.updateBlock(state.blockId, { x, y, width, height })
}

function onPointerMove(event: PointerEvent): void {
  const state = activeDrag.value
  if (!state) {
    return
  }
  const dx = event.clientX - state.startClientX
  const dy = event.clientY - state.startClientY
  if (state.mode === "drag") {
    applyDrag(state, dx, dy)
  } else {
    applyResize(state, dx, dy)
  }
}

function onPointerUp(event: PointerEvent): void {
  const state = activeDrag.value
  if (state) {
    const target = state.target
    if (target && typeof target.releasePointerCapture === "function") {
      try {
        target.releasePointerCapture(state.pointerId)
      } catch {
        // ignore
      }
    }
  }
  activeDrag.value = null
  window.removeEventListener("pointermove", onPointerMove)
  window.removeEventListener("pointerup", onPointerUp)
  window.removeEventListener("pointercancel", onPointerUp)
  void event
}

onBeforeUnmount(() => {
  window.removeEventListener("pointermove", onPointerMove)
  window.removeEventListener("pointerup", onPointerUp)
  window.removeEventListener("pointercancel", onPointerUp)
})

interface ResizeHandleSpec {
  corner: ResizeCorner
  position: string
  cursor: string
}

const RESIZE_HANDLES: ResizeHandleSpec[] = [
  { corner: "tl", position: "-top-1 -left-1", cursor: "cursor-nwse-resize" },
  { corner: "tr", position: "-top-1 -right-1", cursor: "cursor-nesw-resize" },
  { corner: "bl", position: "-bottom-1 -left-1", cursor: "cursor-nesw-resize" },
  { corner: "br", position: "-bottom-1 -right-1", cursor: "cursor-nwse-resize" },
]
</script>

<template>
  <FieldFrame
    :label="props.label"
    :hide-label="props.hideLabel"
    :help-text="props.helpText"
    :errors="props.errors"
    :class="props.classList"
  >
    <div
      :class="cn('gxp-reactive-canvas-builder grid gap-3 rounded-lg border bg-card')"
      style="grid-template-columns: 220px 1fr 320px; min-height: 24rem"
      data-testid="reactive-canvas-builder"
      :data-element-name="props.name ?? undefined"
    >
      <aside
        class="flex flex-col gap-2 border-r p-3 overflow-y-auto"
        data-testid="reactive-canvas-builder-palette"
      >
        <h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
          Block palette
        </h4>
        <div class="flex flex-col gap-1">
          <button
            v-for="type in REACTIVE_CANVAS_BLOCK_TYPES"
            :key="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="`reactive-canvas-builder-palette-${type}`"
            @click="onPaletteAdd(type)"
          >
            <Plus class="mt-0.5 h-3.5 w-3.5 shrink-0" />
            <span class="font-medium">{{ type }}</span>
          </button>
        </div>
      </aside>

      <section
        class="flex flex-col gap-3 overflow-auto p-3"
        data-testid="reactive-canvas-builder-canvas"
      >
        <header class="flex items-center justify-between gap-2">
          <h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
            Canvas ({{ schema.width }} x {{ schema.height }})
          </h4>
          <Badge variant="outline" class="font-mono text-[10px]">
            builder/reactive-canvas-builder
          </Badge>
        </header>

        <div
          class="relative overflow-auto rounded-md border border-dashed bg-background"
          :style="canvasStyle"
          data-testid="reactive-canvas-builder-surface"
        >
          <p
            v-if="blocks.length === 0"
            class="absolute inset-0 flex items-center justify-center text-center text-xs text-muted-foreground"
            data-testid="reactive-canvas-builder-canvas-empty"
          >
            Click a block type in the palette to add it.
          </p>
          <div
            v-for="block in blocks"
            :key="block.id"
            class="absolute cursor-move touch-none select-none rounded border bg-card p-2 text-xs shadow-sm transition-colors hover:border-primary"
            :class="[
              selectedId === block.id
                ? 'border-primary ring-2 ring-primary/30'
                : '',
              activeInteractionId === block.id ? 'ring-2 ring-primary' : '',
            ]"
            :style="blockStyle(block)"
            :data-testid="`reactive-canvas-builder-block-${block.id}`"
            @click.stop="onBlockClick(block.id)"
            @pointerdown="(e: PointerEvent) => onBlockPointerDown(block, e)"
          >
            <div class="flex items-center justify-between">
              <span class="font-mono text-[10px] text-muted-foreground">
                {{ block.type }}
              </span>
              <span class="font-mono text-[10px] text-muted-foreground">
                {{ block.width }}x{{ block.height }}
              </span>
            </div>
            <div class="mt-1 font-mono text-[10px] text-muted-foreground">
              ({{ block.x }}, {{ block.y }})
            </div>
            <span
              v-for="handle in RESIZE_HANDLES"
              :key="handle.corner"
              :class="
                cn(
                  'absolute h-2 w-2 rounded-sm border border-primary/70 bg-primary/30 hover:bg-primary',
                  handle.position,
                  handle.cursor,
                )
              "
              :data-testid="`reactive-canvas-builder-block-${block.id}-resize-${handle.corner}`"
              @click.stop
              @pointerdown.stop="
                (e: PointerEvent) => onResizePointerDown(block, handle.corner, e)
              "
            />
          </div>
        </div>
      </section>

      <aside
        class="flex flex-col gap-2 overflow-y-auto border-l p-3"
        data-testid="reactive-canvas-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="selectedId"
            variant="ghost"
            size="icon-sm"
            aria-label="Clear selection"
            @click="onClearSelection"
          >
            <X class="h-3.5 w-3.5" />
          </Button>
        </header>

        <Card class="border-dashed">
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              Canvas
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="grid grid-cols-2 gap-2">
              <div class="flex flex-col gap-1">
                <Label for="rcb-canvas-width">Width</Label>
                <Input
                  id="rcb-canvas-width"
                  type="number"
                  :model-value="schema.width"
                  data-testid="reactive-canvas-builder-canvas-width"
                  @input="(e: Event) => onCanvasSizeInput('width', e)"
                />
              </div>
              <div class="flex flex-col gap-1">
                <Label for="rcb-canvas-height">Height</Label>
                <Input
                  id="rcb-canvas-height"
                  type="number"
                  :model-value="schema.height"
                  data-testid="reactive-canvas-builder-canvas-height"
                  @input="(e: Event) => onCanvasSizeInput('height', e)"
                />
              </div>
            </div>
          </CardContent>
        </Card>

        <p
          v-if="!selectedBlock"
          class="py-3 text-xs text-muted-foreground"
          data-testid="reactive-canvas-builder-inspector-empty"
        >
          Select a block on the canvas to edit it.
        </p>

        <div
          v-else
          class="flex flex-col gap-3"
          data-testid="reactive-canvas-builder-inspector-fields"
        >
          <Card class="border-dashed">
            <CardHeader class="py-2">
              <CardTitle class="font-mono text-xs uppercase tracking-wide">
                {{ selectedBlock.type }}
              </CardTitle>
            </CardHeader>
            <CardContent class="flex flex-col gap-2 pb-3">
              <div class="flex flex-col gap-1">
                <Label>Block type</Label>
                <Select
                  :model-value="selectedBlock.type"
                  @update:model-value="onTypeChange"
                >
                  <SelectTrigger
                    data-testid="reactive-canvas-builder-inspector-type"
                  >
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem
                      v-for="t in REACTIVE_CANVAS_BLOCK_TYPES"
                      :key="t"
                      :value="t"
                    >
                      {{ t }}
                    </SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div class="grid grid-cols-2 gap-2">
                <div class="flex flex-col gap-1">
                  <Label for="rcb-block-x">X</Label>
                  <Input
                    id="rcb-block-x"
                    type="number"
                    :model-value="selectedBlock.x"
                    data-testid="reactive-canvas-builder-inspector-x"
                    @input="(e: Event) => onNumberInput('x', e)"
                  />
                </div>
                <div class="flex flex-col gap-1">
                  <Label for="rcb-block-y">Y</Label>
                  <Input
                    id="rcb-block-y"
                    type="number"
                    :model-value="selectedBlock.y"
                    data-testid="reactive-canvas-builder-inspector-y"
                    @input="(e: Event) => onNumberInput('y', e)"
                  />
                </div>
                <div class="flex flex-col gap-1">
                  <Label for="rcb-block-width">Width</Label>
                  <Input
                    id="rcb-block-width"
                    type="number"
                    :model-value="selectedBlock.width"
                    data-testid="reactive-canvas-builder-inspector-width"
                    @input="(e: Event) => onNumberInput('width', e)"
                  />
                </div>
                <div class="flex flex-col gap-1">
                  <Label for="rcb-block-height">Height</Label>
                  <Input
                    id="rcb-block-height"
                    type="number"
                    :model-value="selectedBlock.height"
                    data-testid="reactive-canvas-builder-inspector-height"
                    @input="(e: Event) => onNumberInput('height', e)"
                  />
                </div>
              </div>

              <div class="flex flex-col gap-1">
                <Label for="rcb-block-props">Props (JSON)</Label>
                <Textarea
                  id="rcb-block-props"
                  :model-value="propsJsonDraft"
                  rows="6"
                  class="font-mono text-xs"
                  data-testid="reactive-canvas-builder-inspector-props"
                  @input="onPropsJsonInput"
                />
                <p
                  v-if="propsJsonError"
                  class="text-xs text-destructive"
                  data-testid="reactive-canvas-builder-inspector-props-error"
                >
                  {{ propsJsonError }}
                </p>
              </div>
            </CardContent>
          </Card>

          <div class="flex items-center gap-1">
            <Button
              variant="destructive"
              size="sm"
              class="ml-auto"
              data-testid="reactive-canvas-builder-remove"
              @click="onRemoveSelected"
            >
              <Trash2 class="h-3.5 w-3.5" />
              Remove block
            </Button>
          </div>
        </div>
      </aside>
    </div>
  </FieldFrame>
</template>
