<script setup lang="ts">
/**
 * GridBuilder — MVP visual grid layout designer.
 *
 * Three-pane layout:
 *   - LEFT  (Palette):    "Add cell" button plus a set of preset size
 *                         buttons (1x1, 2x2, 4x2, 6x4, 12x1). Click any
 *                         to append a new cell of that size.
 *   - MIDDLE (Canvas):    CSS-grid preview of the current layout. Each
 *                         cell is a clickable card; empty rows render a
 *                         dashed placeholder hint.
 *   - RIGHT (Inspector):  Edit the selected cell's label, x/y position
 *                         and w/h span via number inputs. Includes a
 *                         Remove button.
 *
 * Pointer-driven drag-to-reposition and resize-from-corner are wired
 * directly on each cell. Drags snap to the 12-column grid; the
 * resize handle (bottom-right) adjusts `w` / `h`. The inspector
 * remains the canonical numeric editor.
 *
 * State lives in `gridBuilderStore` (Pinia). The component accepts an
 * optional `modelValue` schema (v-modelable) and emits the current
 * schema on every mutation so the host page can persist it.
 */
import { computed, onMounted, onBeforeUnmount, 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 { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  useGridBuilder,
  type GridBuilderItem,
  type GridBuilderSchema,
} from "@/stores/gridBuilderStore"
import { cn } from "@/lib/utils"

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

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

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

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

const builder = useGridBuilder(builderId.value)

function isGridBuilderSchema(value: unknown): value is GridBuilderSchema {
  if (typeof value !== "object" || value === null) {
    return false
  }
  const v = value as Record<string, unknown>
  return (
    typeof v.cols === "number" &&
    typeof v.rowHeight === "number" &&
    Array.isArray(v.items)
  )
}

/** Seed the store from the initial modelValue. */
onMounted(() => {
  const seed = model.value
  if (isGridBuilderSchema(seed)) {
    builder.loadSchema(seed)
  }
})

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

interface CellTemplate {
  label: string
  w: number
  h: number
}

const cellTemplates: CellTemplate[] = [
  { label: "1×1", w: 1, h: 1 },
  { label: "2×2", w: 2, h: 2 },
  { label: "4×2", w: 4, h: 2 },
  { label: "6×4", w: 6, h: 4 },
  { label: "12×1", w: 12, h: 1 },
]

const items = computed<GridBuilderItem[]>(
  () => builder.schema.value.items,
)
const cols = computed<number>(() => builder.schema.value.cols)
const rowHeight = computed<number>(() => builder.schema.value.rowHeight)

const selectedItem = computed<GridBuilderItem | null>(() => {
  const sel = builder.selection.value
  if (!sel.id) {
    return null
  }
  return items.value.find((it) => it.id === sel.id) ?? null
})

const gridStyle = computed(() => ({
  display: "grid",
  gridTemplateColumns: `repeat(${cols.value}, minmax(0, 1fr))`,
  gridAutoRows: `${rowHeight.value}px`,
  gap: "0.5rem",
}))

function cellStyle(it: GridBuilderItem): Record<string, string | number> {
  return {
    gridColumn: `${it.x + 1} / span ${it.w}`,
    gridRow: `${it.y + 1} / span ${it.h}`,
  }
}

function onAddCell(): void {
  const id = builder.addItem()
  builder.select(id)
}

function onAddPresetCell(template: CellTemplate): void {
  const id = builder.addItem({
    w: template.w,
    h: template.h,
    label: `${template.label} cell`,
  })
  builder.select(id)
}

function onCellClick(itemId: string): void {
  builder.select(itemId)
}

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

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

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

function parseInt0(value: string): number {
  const n = Number.parseInt(value, 10)
  return Number.isFinite(n) ? n : 0
}

function parseInt1(value: string): number {
  const n = Number.parseInt(value, 10)
  return Number.isFinite(n) && n >= 1 ? n : 1
}

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

/** Ref to the canvas grid wrapper — used to measure column width. */
const gridEl = ref<HTMLElement | null>(null)

interface DragState {
  mode: "drag" | "resize"
  itemId: string
  pointerId: number
  target: HTMLElement
  startClientX: number
  startClientY: number
  startX: number
  startY: number
  startW: number
  startH: number
  cellWidthPx: number
}

const activeDrag = ref<DragState | null>(null)
/** Id of the cell currently being dragged or resized — drives the
 * `ring-2 ring-primary` visual feedback. */
const activeInteractionId = computed<string | null>(
  () => activeDrag.value?.itemId ?? null,
)

function measureCellWidth(): number {
  const el = gridEl.value
  if (!el) {
    return 0
  }
  const rect = el.getBoundingClientRect()
  // Subtract horizontal padding so we measure the inner track. We
  // assume uniform gaps; column width = (innerWidth - (cols-1)*gap) /
  // cols, but for snap math the simple "width/cols" approximation is
  // accurate enough at the scales we render.
  return rect.width / cols.value
}

function beginDrag(
  it: GridBuilderItem,
  event: PointerEvent,
  mode: "drag" | "resize",
): void {
  if (props.disabled || props.readonly) {
    return
  }
  event.preventDefault()
  event.stopPropagation()
  // Always select the item we're about to manipulate so the inspector
  // matches what the user sees.
  builder.select(it.id)
  const target = event.target as HTMLElement | null
  if (target && typeof target.setPointerCapture === "function") {
    try {
      target.setPointerCapture(event.pointerId)
    } catch {
      // jsdom or capture-disallowed targets: ignore.
    }
  }
  activeDrag.value = {
    mode,
    itemId: it.id,
    pointerId: event.pointerId,
    target: target ?? (event.currentTarget as HTMLElement),
    startClientX: event.clientX,
    startClientY: event.clientY,
    startX: it.x,
    startY: it.y,
    startW: it.w,
    startH: it.h,
    cellWidthPx: measureCellWidth(),
  }
  window.addEventListener("pointermove", onPointerMove)
  window.addEventListener("pointerup", onPointerUp)
  window.addEventListener("pointercancel", onPointerUp)
}

function onCellPointerDown(
  it: GridBuilderItem,
  event: PointerEvent,
): void {
  // Ignore right-click / middle-click; only primary pointer drags.
  if (event.button !== undefined && event.button !== 0) {
    return
  }
  beginDrag(it, event, "drag")
}

function onResizeHandlePointerDown(
  it: GridBuilderItem,
  event: PointerEvent,
): void {
  if (event.button !== undefined && event.button !== 0) {
    return
  }
  beginDrag(it, event, "resize")
}

function onPointerMove(event: PointerEvent): void {
  const state = activeDrag.value
  if (!state) {
    return
  }
  const cellW = state.cellWidthPx > 0 ? state.cellWidthPx : 1
  const rowH = rowHeight.value > 0 ? rowHeight.value : 1
  const dx = event.clientX - state.startClientX
  const dy = event.clientY - state.startClientY
  const colDelta = Math.round(dx / cellW)
  const rowDelta = Math.round(dy / rowH)
  if (state.mode === "drag") {
    const nextX = state.startX + colDelta
    const nextY = state.startY + rowDelta
    builder.updateItem(state.itemId, { x: nextX, y: nextY })
  } else {
    const nextW = Math.max(1, state.startW + colDelta)
    const nextH = Math.max(1, state.startH + rowDelta)
    builder.updateItem(state.itemId, { w: nextW, h: nextH })
  }
}

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)
  // Avoid the unused-param warning while leaving the signature intact.
  void event
}

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

<template>
  <div
    :class="
      cn(
        'gxp-grid-builder grid gap-3 rounded-lg border bg-card',
        props.classList,
      )
    "
    style="grid-template-columns: 220px 1fr 280px; min-height: 24rem"
    data-testid="grid-builder"
    :data-element-name="props.name ?? undefined"
  >
    <aside
      class="flex flex-col gap-2 border-r p-3 overflow-y-auto"
      data-testid="grid-builder-palette"
    >
      <h4
        class="text-xs font-semibold uppercase tracking-wide text-muted-foreground"
      >
        {{ props.label ?? "Grid palette" }}
      </h4>
      <Button
        type="button"
        variant="outline"
        size="sm"
        class="justify-start gap-2"
        data-testid="grid-builder-palette-add"
        :disabled="props.disabled || props.readonly"
        @click="onAddCell"
      >
        <Plus class="h-3.5 w-3.5" />
        Add cell
      </Button>
      <div class="flex flex-col gap-1">
        <button
          v-for="tpl in cellTemplates"
          :key="tpl.label"
          type="button"
          class="flex items-center 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 disabled:opacity-50"
          :data-testid="`grid-builder-palette-${tpl.w}x${tpl.h}`"
          :disabled="props.disabled || props.readonly"
          @click="onAddPresetCell(tpl)"
        >
          <Plus class="h-3.5 w-3.5 shrink-0" />
          <span class="font-medium">{{ tpl.label }} cell</span>
        </button>
      </div>
      <p v-if="props.helpText" class="text-xs text-muted-foreground">
        {{ props.helpText }}
      </p>
      <ul
        v-if="props.errors.length"
        class="flex flex-col gap-0.5 text-xs text-destructive"
        role="alert"
      >
        <li v-for="(err, i) in props.errors" :key="i">{{ err }}</li>
      </ul>
    </aside>

    <section
      class="flex flex-col gap-3 overflow-y-auto p-3"
      data-testid="grid-builder-canvas"
    >
      <header class="flex items-center justify-between">
        <h4
          class="text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          Preview
        </h4>
        <Badge variant="outline" class="font-mono text-[10px]">
          builder/grid-builder
        </Badge>
      </header>
      <div
        v-if="items.length > 0"
        ref="gridEl"
        class="rounded-md border border-dashed bg-background p-2"
        :style="gridStyle"
        data-testid="grid-builder-canvas-grid"
      >
        <div
          v-for="it in items"
          :key="it.id"
          :style="cellStyle(it)"
          :data-testid="`grid-builder-cell-${it.id}`"
          class="relative flex cursor-move touch-none select-none flex-col items-center justify-center gap-1 rounded border bg-card px-2 py-1 text-center text-xs transition-colors hover:border-primary"
          :class="[
            builder.selection.value.id === it.id
              ? 'border-primary bg-accent/30 ring-2 ring-primary/30'
              : '',
            activeInteractionId === it.id ? 'ring-2 ring-primary' : '',
          ]"
          @click="onCellClick(it.id)"
          @pointerdown="(e: PointerEvent) => onCellPointerDown(it, e)"
        >
          <span class="font-medium">{{ it.label }}</span>
          <span class="font-mono text-[10px] text-muted-foreground">
            {{ it.w }}×{{ it.h }} @ ({{ it.x }}, {{ it.y }})
          </span>
          <span
            class="absolute bottom-0.5 right-0.5 h-2.5 w-2.5 cursor-nwse-resize rounded-sm border border-primary/70 bg-primary/30 hover:bg-primary"
            :data-testid="`grid-builder-resize-${it.id}`"
            @click.stop
            @pointerdown.stop="(e: PointerEvent) => onResizeHandlePointerDown(it, e)"
          />
        </div>
      </div>
      <p
        v-else
        class="py-6 text-center text-xs text-muted-foreground"
        data-testid="grid-builder-canvas-empty"
      >
        Click "Add cell" or a preset to start the layout.
      </p>
    </section>

    <aside
      class="flex flex-col gap-2 overflow-y-auto border-l p-3"
      data-testid="grid-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="!selectedItem"
        class="py-3 text-xs text-muted-foreground"
        data-testid="grid-builder-inspector-empty"
      >
        Select a cell on the canvas to edit its size and position.
      </p>

      <div
        v-else
        class="flex flex-col gap-3"
        data-testid="grid-builder-inspector-fields"
      >
        <Card class="border-dashed">
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              cell
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="flex flex-col gap-1">
              <Label for="gb-label">Label</Label>
              <Input
                id="gb-label"
                :model-value="selectedItem.label"
                data-testid="grid-builder-inspector-label"
                @input="
                  (e: Event) =>
                    patchSelected({
                      label: (e.target as HTMLInputElement).value,
                    })
                "
              />
            </div>
            <div class="grid grid-cols-2 gap-2">
              <div class="flex flex-col gap-1">
                <Label for="gb-x">X (col)</Label>
                <Input
                  id="gb-x"
                  type="number"
                  min="0"
                  :model-value="selectedItem.x"
                  data-testid="grid-builder-inspector-x"
                  @input="
                    (e: Event) =>
                      patchSelected({
                        x: parseInt0((e.target as HTMLInputElement).value),
                      })
                  "
                />
              </div>
              <div class="flex flex-col gap-1">
                <Label for="gb-y">Y (row)</Label>
                <Input
                  id="gb-y"
                  type="number"
                  min="0"
                  :model-value="selectedItem.y"
                  data-testid="grid-builder-inspector-y"
                  @input="
                    (e: Event) =>
                      patchSelected({
                        y: parseInt0((e.target as HTMLInputElement).value),
                      })
                  "
                />
              </div>
              <div class="flex flex-col gap-1">
                <Label for="gb-w">Width</Label>
                <Input
                  id="gb-w"
                  type="number"
                  min="1"
                  :model-value="selectedItem.w"
                  data-testid="grid-builder-inspector-w"
                  @input="
                    (e: Event) =>
                      patchSelected({
                        w: parseInt1((e.target as HTMLInputElement).value),
                      })
                  "
                />
              </div>
              <div class="flex flex-col gap-1">
                <Label for="gb-h">Height</Label>
                <Input
                  id="gb-h"
                  type="number"
                  min="1"
                  :model-value="selectedItem.h"
                  data-testid="grid-builder-inspector-h"
                  @input="
                    (e: Event) =>
                      patchSelected({
                        h: parseInt1((e.target as HTMLInputElement).value),
                      })
                  "
                />
              </div>
            </div>
          </CardContent>
        </Card>

        <Button
          variant="destructive"
          size="sm"
          class="self-end"
          data-testid="grid-builder-remove"
          @click="onRemove"
        >
          <Trash2 class="h-3.5 w-3.5" />
          Remove
        </Button>
      </div>
    </aside>
  </div>
</template>
