<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { usePage } from "@inertiajs/vue3";
import {
  ArrowRight,
  ChevronLeft,
  ChevronRight,
  GripVertical,
  Pencil,
  Plus,
  Trash2,
} from "lucide-vue-next";
import Button from "@/components/ui/Button.vue";
import NestedResourceSheet from "./NestedResourceSheet.vue";
import type { FieldDefinition } from "./types";
import type { Language } from "../../types";

export interface PublicResourceItem {
  id: number;
  label: string;
}

export interface PublicResource {
  slug: string;
  label: string;
  items: PublicResourceItem[];
}

export interface NestedRecord {
  id?: number | null;
  _key: string;
  parent_id?: number | null;
  sort_order?: number;
  resource_type?: string | null;
  resource_id?: number | null;
  translations?: Record<string, Record<string, unknown>>;
  [key: string]: unknown;
}

const props = defineProps<{
  field: FieldDefinition;
  modelValue?: unknown;
  error?: string;
  currentLocale?: string | null;
}>();

const emit = defineEmits<{
  "update:modelValue": [value: unknown];
}>();

const page = usePage();
let keyCounter = 0;

// ─── Language helpers ─────────────────────────────────────────────────────────

const activeLocales = computed<Language[]>(() => {
  const languages = (page.props as Record<string, unknown>).languages as
    | Language[]
    | undefined;
  return (languages ?? []).filter((l) => l.is_active);
});

const editorLocale = ref<string>("");

watch(
  () => props.currentLocale,
  (v) => {
    editorLocale.value = v ?? activeLocales.value[0]?.code ?? "en";
  },
  { immediate: true },
);

watch(
  activeLocales,
  (locales) => {
    if (!editorLocale.value && locales.length > 0) {
      editorLocale.value = locales[0].code;
    }
  },
  { immediate: true },
);

const displayLocale = computed<string>(
  () => editorLocale.value || activeLocales.value[0]?.code || "en",
);

// ─── Items state ──────────────────────────────────────────────────────────────

function generateKey(): string {
  return `_new_${++keyCounter}`;
}

function toItems(value: unknown): NestedRecord[] {
  if (!Array.isArray(value)) return [];
  return value.map((item) => {
    const r = item as Record<string, unknown>;
    return {
      ...r,
      _key: (r._key as string) || (r.id ? String(r.id) : generateKey()),
    } as NestedRecord;
  });
}

const items = ref<NestedRecord[]>(toItems(props.modelValue));

watch(
  () => props.modelValue,
  (value) => {
    if (!Array.isArray(value)) {
      items.value = [];
      return;
    }
    const existingById = new Map(
      items.value.filter((i) => i.id).map((i) => [i.id, i._key]),
    );
    items.value = value.map((item) => {
      const r = item as Record<string, unknown>;
      const existingKey = r.id ? existingById.get(r.id as number) : undefined;
      return {
        ...r,
        _key:
          existingKey ??
          (r._key as string) ??
          (r.id ? String(r.id) : generateKey()),
      } as NestedRecord;
    });
  },
);

function emitItems(): void {
  emit(
    "update:modelValue",
    items.value.map(({ _key: _, ...rest }) => rest),
  );
}

// ─── Public resources ─────────────────────────────────────────────────────────

const publicResources = ref<PublicResource[]>([]);

function fetchPublicResources(): void {
  if (!props.field.publicResourcesUrl) return;
  const locale = displayLocale.value;
  const url = `${props.field.publicResourcesUrl}?locale=${encodeURIComponent(locale)}`;
  fetch(url)
    .then((r) => r.json())
    .then((data: PublicResource[]) => {
      publicResources.value = data;
    })
    .catch(() => {});
}

onMounted(fetchPublicResources);
watch(editorLocale, fetchPublicResources);

function getResourceLabel(item: NestedRecord): string | null {
  if (!item.resource_type || !item.resource_id) return null;
  const resource = publicResources.value.find(
    (r) => r.slug === item.resource_type,
  );
  if (!resource) return `${item.resource_type} #${item.resource_id}`;
  const record = resource.items.find((r) => r.id === item.resource_id);
  return record
    ? `${resource.label}: ${record.label}`
    : `${resource.label} #${item.resource_id}`;
}

// ─── Display helpers ──────────────────────────────────────────────────────────

function getDisplayLabel(item: NestedRecord): string {
  const translations = item.translations as
    | Record<string, Record<string, string>>
    | undefined;
  if (translations) {
    const label =
      translations[displayLocale.value]?.label ??
      Object.values(translations)[0]?.label;
    if (label) return label;
  }
  return `#${item.id ?? "nieuw"}`;
}

function getDisplaySub(item: NestedRecord): string {
  const resourceLabel = getResourceLabel(item);
  if (resourceLabel) return resourceLabel;
  const translations = item.translations as
    | Record<string, Record<string, string>>
    | undefined;
  if (!translations) return "";
  return (
    translations[displayLocale.value]?.url ??
    Object.values(translations)[0]?.url ??
    ""
  );
}

// ─── Tree helpers ─────────────────────────────────────────────────────────────

interface DisplayRow {
  record: NestedRecord;
  depth: number;
}

const INDENT_PX = 32;

const displayRows = computed<DisplayRow[]>(() => buildDisplayRows(items.value));

function buildDisplayRows(records: NestedRecord[]): DisplayRow[] {
  const result: DisplayRow[] = [];

  function walk(parentId: number | null, depth: number): void {
    records
      .filter((r) => ((r.parent_id as number | null) ?? null) === parentId)
      .sort(
        (a, b) =>
          ((a.sort_order as number) ?? 0) - ((b.sort_order as number) ?? 0),
      )
      .forEach((r) => {
        result.push({ record: r, depth });
        if (r.id) walk(r.id as number, depth + 1);
      });
  }

  walk(null, 0);

  const shown = new Set(result.map((r) => r.record._key));
  records
    .filter((r) => !shown.has(r._key))
    .forEach((r) => result.push({ record: r, depth: 0 }));

  return result;
}

function rebuildItems(flat: DisplayRow[]): NestedRecord[] {
  const counters = new Map<number | null, number>();
  return flat.map(({ record }) => {
    const pid = (record.parent_id as number | null) ?? null;
    const order = counters.get(pid) ?? 0;
    counters.set(pid, order + 1);
    return { ...record, sort_order: order };
  });
}

function getSubtreeKeys(key: string, flat: DisplayRow[]): Set<string> {
  const idx = flat.findIndex((r) => r.record._key === key);
  if (idx === -1) return new Set([key]);
  const dragDepth = flat[idx].depth;
  const keys = new Set<string>([key]);
  for (let i = idx + 1; i < flat.length; i++) {
    if (flat[i].depth > dragDepth) keys.add(flat[i].record._key);
    else break;
  }
  return keys;
}

// ─── Drag & drop (pointer events) ────────────────────────────────────────────

interface Placeholder {
  afterIndex: number; // -1 = before all
  depth: number;
}

const editorRef = ref<HTMLElement | null>(null);
const draggingKey = ref<string | null>(null);
const placeholder = ref<Placeholder | null>(null);

let ghostEl: HTMLElement | null = null;
let ghostOffsetX = 0;
let ghostOffsetY = 0;

const dragCursorX = ref(0);
const dragCursorY = ref(0);

function startDrag(e: PointerEvent, key: string): void {
  if (e.button !== undefined && e.button > 0) return;
  e.preventDefault();

  const rowEl = (e.currentTarget as HTMLElement).closest(
    "[data-drag-row]",
  ) as HTMLElement | null;
  if (!rowEl) return;

  draggingKey.value = key;

  const rect = rowEl.getBoundingClientRect();
  ghostOffsetX = e.clientX - rect.left;
  ghostOffsetY = e.clientY - rect.top;

  // Build ghost: clone the row, strip indent margin, position at cursor
  ghostEl = rowEl.cloneNode(true) as HTMLElement;
  Object.assign(ghostEl.style, {
    position: "fixed",
    pointerEvents: "none",
    zIndex: "9999",
    width: `${rect.width}px`,
    left: `${e.clientX - ghostOffsetX}px`,
    top: `${e.clientY - ghostOffsetY}px`,
    opacity: "0.92",
    boxShadow: "0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08)",
    transform: "rotate(0.8deg) scale(1.01)",
    marginLeft: "0",
    transition: "none",
  });
  document.body.appendChild(ghostEl);

  document.addEventListener("pointermove", onPointerMove);
  document.addEventListener("pointerup", onPointerUp, { once: true });
  document.addEventListener("pointercancel", onPointerUp, { once: true });
}

function onPointerMove(e: PointerEvent): void {
  if (!ghostEl) return;
  e.preventDefault();
  dragCursorX.value = e.clientX;
  dragCursorY.value = e.clientY;
  ghostEl.style.left = `${e.clientX - ghostOffsetX}px`;
  ghostEl.style.top = `${e.clientY - ghostOffsetY}px`;
  updateDropTarget(e.clientX, e.clientY);
}

function updateDropTarget(clientX: number, clientY: number): void {
  if (!editorRef.value || !draggingKey.value) return;

  const editorRect = editorRef.value.getBoundingClientRect();
  const relX = clientX - editorRect.left;

  const rowEls = Array.from(
    editorRef.value.querySelectorAll("[data-drag-row]"),
  ) as HTMLElement[];

  if (rowEls.length === 0) {
    placeholder.value = { afterIndex: -1, depth: 0 };
    return;
  }

  // afterIndex: last row whose vertical midpoint the cursor has passed
  let afterIndex = -1;
  for (let i = 0; i < rowEls.length; i++) {
    const r = rowEls[i].getBoundingClientRect();
    if (clientY > r.top + r.height / 2) afterIndex = i;
  }

  // Maximum allowed depth at this position: depth of the nearest
  // non-dragged item above + 1. Can only nest under a saved item (has id).
  const flat = displayRows.value;
  const subtreeKeys = getSubtreeKeys(draggingKey.value, flat);

  let effectiveAboveDepth = -1;
  for (let i = afterIndex; i >= 0; i--) {
    const rowKey = rowEls[i]?.dataset.dragRow;
    const rowData = rowKey
      ? flat.find((r) => r.record._key === rowKey)
      : undefined;
    if (rowData && !subtreeKeys.has(rowData.record._key)) {
      effectiveAboveDepth = rowData.record.id
        ? rowData.depth
        : rowData.depth - 1;
      break;
    }
  }
  const maxDepth = Math.max(0, effectiveAboveDepth + 1);

  // Depth snaps to INDENT_PX steps based on cursor X
  const rawDepth = Math.floor(Math.max(0, relX) / INDENT_PX);
  const depth = Math.max(0, Math.min(rawDepth, maxDepth));

  placeholder.value = { afterIndex, depth };
}

function onPointerUp(_e: PointerEvent): void {
  document.removeEventListener("pointermove", onPointerMove);
  document.removeEventListener("pointercancel", onPointerUp);

  if (ghostEl) {
    ghostEl.remove();
    ghostEl = null;
  }

  if (placeholder.value && draggingKey.value) {
    applyMove(draggingKey.value, placeholder.value);
  }

  draggingKey.value = null;
  placeholder.value = null;
  emitItems();
}

// Clean up if component is destroyed mid-drag
onUnmounted(() => {
  document.removeEventListener("pointermove", onPointerMove);
  document.removeEventListener("pointerup", onPointerUp);
  document.removeEventListener("pointercancel", onPointerUp);
  ghostEl?.remove();
});

// ─── Apply move ───────────────────────────────────────────────────────────────

function applyMove(key: string, target: Placeholder): void {
  const flat = displayRows.value;
  const dragIdx = flat.findIndex((r) => r.record._key === key);
  if (dragIdx === -1) return;

  const subtreeKeys = getSubtreeKeys(key, flat);
  const subtreeItems = flat.filter((r) => subtreeKeys.has(r.record._key));
  const withoutSubtree = flat.filter((r) => !subtreeKeys.has(r.record._key));

  const removedBefore = flat
    .slice(0, target.afterIndex + 1)
    .filter((r) => subtreeKeys.has(r.record._key)).length;
  let insertAfter = target.afterIndex - removedBefore;
  insertAfter = Math.max(-1, Math.min(insertAfter, withoutSubtree.length - 1));

  // Find new parent_id for the dragged root
  let newParentId: number | null = null;
  if (target.depth > 0) {
    for (let i = insertAfter; i >= 0; i--) {
      if (
        withoutSubtree[i].depth === target.depth - 1 &&
        withoutSubtree[i].record.id
      ) {
        newParentId = withoutSubtree[i].record.id as number;
        break;
      }
    }
    if (newParentId === null) target.depth = 0;
  }

  const depthDelta = target.depth - flat[dragIdx].depth;
  const updatedSubtree: DisplayRow[] = subtreeItems.map((row, i) => ({
    ...row,
    depth: row.depth + depthDelta,
    record: i === 0 ? { ...row.record, parent_id: newParentId } : row.record,
  }));

  const newFlat: DisplayRow[] = [
    ...withoutSubtree.slice(0, insertAfter + 1),
    ...updatedSubtree,
    ...withoutSubtree.slice(insertAfter + 1),
  ];

  items.value = rebuildItems(newFlat);
}

// ─── Placeholder visibility + parent highlight ────────────────────────────────

function shouldShowPlaceholder(afterIndex: number): boolean {
  if (!placeholder.value || !draggingKey.value) return false;
  if (placeholder.value.afterIndex !== afterIndex) return false;
  const flat = displayRows.value;
  const subtreeKeys = getSubtreeKeys(draggingKey.value, flat);
  const firstIdx = flat.findIndex((r) => subtreeKeys.has(r.record._key));
  const lastIdx = flat.findLastIndex((r) => subtreeKeys.has(r.record._key));
  // Suppress no-op: inserting right before or inside the dragged block
  if (afterIndex >= firstIdx - 1 && afterIndex <= lastIdx) return false;
  return true;
}

/** The key of the item that will become the parent when dropped. */
const placeholderParentKey = computed<string | null>(() => {
  if (!placeholder.value || placeholder.value.depth === 0 || !draggingKey.value)
    return null;

  const flat = displayRows.value;
  const subtreeKeys = getSubtreeKeys(draggingKey.value, flat);
  const without = flat.filter((r) => !subtreeKeys.has(r.record._key));

  const removedBefore = flat
    .slice(0, placeholder.value.afterIndex + 1)
    .filter((r) => subtreeKeys.has(r.record._key)).length;
  const insertAfter = Math.min(
    placeholder.value.afterIndex - removedBefore,
    without.length - 1,
  );

  for (let i = insertAfter; i >= 0; i--) {
    if (
      without[i].depth === placeholder.value.depth - 1 &&
      without[i].record.id
    ) {
      return without[i].record._key;
    }
  }
  return null;
});

const placeholderLabel = computed<string>(() => {
  if (!placeholder.value || !draggingKey.value) return "";

  if (placeholder.value.depth === 0) {
    return props.field.topLevelLabel ?? "Top-level item";
  }

  const subItemOf = props.field.subItemOfLabel ?? "Sub-item of";

  if (placeholderParentKey.value) {
    const parentRow = displayRows.value.find(
      (r) => r.record._key === placeholderParentKey.value,
    );
    if (parentRow) {
      return `${subItemOf}: ${getDisplayLabel(parentRow.record)}`;
    }
  }

  return subItemOf;
});

// ─── Indent / unindent buttons ────────────────────────────────────────────────

function canIndent(record: NestedRecord): boolean {
  const idx = displayRows.value.findIndex((r) => r.record._key === record._key);
  if (idx <= 0) return false;
  return !!displayRows.value[idx - 1].record.id;
}

function canUnindent(record: NestedRecord): boolean {
  return !!record.parent_id;
}

function indent(record: NestedRecord): void {
  const idx = displayRows.value.findIndex((r) => r.record._key === record._key);
  if (idx <= 0) return;
  const prev = displayRows.value[idx - 1];
  if (!prev.record.id) return;
  items.value = items.value.map((item) =>
    item._key === record._key
      ? { ...item, parent_id: prev.record.id as number }
      : item,
  );
  emitItems();
}

function unindent(record: NestedRecord): void {
  if (!record.parent_id) return;
  const parent = items.value.find((i) => i.id === record.parent_id);
  const newParentId = (parent?.parent_id as number | null) ?? null;
  items.value = items.value.map((item) =>
    item._key === record._key ? { ...item, parent_id: newParentId } : item,
  );
  emitItems();
}

// ─── CRUD ─────────────────────────────────────────────────────────────────────

function deleteItem(key: string): void {
  const toDelete = getSubtreeKeys(key, displayRows.value);
  items.value = items.value.filter((i) => !toDelete.has(i._key));
  emitItems();
}

// ─── Sheet ────────────────────────────────────────────────────────────────────

const sheetOpen = ref(false);
const editingItem = ref<NestedRecord | null>(null);

function openAdd(): void {
  editingItem.value = null;
  sheetOpen.value = true;
}

function openEdit(item: NestedRecord): void {
  editingItem.value = { ...item };
  sheetOpen.value = true;
}

function onSave(record: NestedRecord): void {
  if (editingItem.value) {
    const key = editingItem.value._key;
    items.value = items.value.map((i) =>
      i._key === key ? { ...record, _key: key } : i,
    );
  } else {
    items.value = [...items.value, { ...record, _key: generateKey() }];
  }
  emitItems();
}

const schema = computed<FieldDefinition[]>(
  () => (props.field.schema as FieldDefinition[] | null) ?? [],
);
</script>

<template>
  <div ref="editorRef" class="space-y-1.5">
    <!-- Header -->
    <div class="flex items-center justify-between gap-2 pb-0.5">
      <label class="text-sm font-medium text-foreground">
        {{ field.label }}
      </label>
      <Button
        type="button"
        variant="outline"
        size="sm"
        class="gap-1.5"
        @click="openAdd"
      >
        <Plus class="h-3.5 w-3.5" />
        {{ field.addItemLabel ?? "Item toevoegen" }}
      </Button>
    </div>

    <!-- Error -->
    <p v-if="error" class="text-sm text-destructive">{{ error }}</p>

    <!-- Empty state -->
    <p
      v-if="displayRows.length === 0"
      class="rounded-md border border-dashed border-border py-6 text-center text-sm text-muted-foreground"
    >
      {{ field.noItemsLabel ?? "Nog geen items." }}
    </p>

    <!-- Placeholder before all items -->
    <div
      v-if="shouldShowPlaceholder(-1)"
      :style="{ marginLeft: `${placeholder!.depth * INDENT_PX}px` }"
      class="pointer-events-none flex h-9 items-center gap-1.5 rounded-md border-2 border-dashed border-primary bg-primary/8 px-3 text-xs font-medium text-primary"
    >
      <ArrowRight class="h-3.5 w-3.5 shrink-0" />
      {{ placeholderLabel }}
    </div>

    <template v-for="(row, displayIndex) in displayRows" :key="row.record._key">
      <!-- Item row -->
      <div
        :data-drag-row="row.record._key"
        :style="{
          marginLeft: row.depth > 0 ? `${row.depth * INDENT_PX}px` : undefined,
        }"
        :class="[
          'flex items-center gap-2 rounded-md border bg-card px-2.5 py-2 select-none transition-colors',
          draggingKey === row.record._key
            ? 'opacity-30'
            : placeholderParentKey === row.record._key
              ? 'border-primary/50 bg-primary/5'
              : 'border-border',
        ]"
      >
        <!-- Drag handle -->
        <span
          class="shrink-0 cursor-grab text-muted-foreground/40 transition-colors hover:text-muted-foreground active:cursor-grabbing"
          style="touch-action: none"
          @pointerdown="startDrag($event, row.record._key)"
        >
          <GripVertical class="h-4 w-4" />
        </span>

        <!-- Labels -->
        <div class="min-w-0 flex-1">
          <p class="truncate text-sm font-medium">
            {{ getDisplayLabel(row.record) }}
          </p>
          <p
            v-if="getDisplaySub(row.record)"
            class="truncate text-xs text-muted-foreground"
          >
            {{ getDisplaySub(row.record) }}
          </p>
        </div>

        <!-- Actions -->
        <div class="flex shrink-0 items-center gap-0.5">
          <button
            v-if="canUnindent(row.record)"
            type="button"
            class="inline-flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            title="Niveau omhoog"
            @click="unindent(row.record)"
          >
            <ChevronLeft class="h-3.5 w-3.5" />
          </button>
          <button
            v-if="canIndent(row.record)"
            type="button"
            class="inline-flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            title="Inspringen"
            @click="indent(row.record)"
          >
            <ChevronRight class="h-3.5 w-3.5" />
          </button>
          <button
            type="button"
            class="inline-flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            @click="openEdit(row.record)"
          >
            <Pencil class="h-3.5 w-3.5" />
          </button>
          <button
            type="button"
            class="inline-flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
            @click="deleteItem(row.record._key)"
          >
            <Trash2 class="h-3.5 w-3.5" />
          </button>
        </div>
      </div>

      <!-- Drop placeholder after this row -->
      <div
        v-if="shouldShowPlaceholder(displayIndex)"
        :style="{
          marginLeft: `${placeholder!.depth * INDENT_PX}px`,
        }"
        class="pointer-events-none flex h-9 items-center gap-1.5 rounded-md border-2 border-dashed border-primary bg-primary/8 px-3 text-xs font-medium text-primary"
      >
        <ArrowRight class="h-3.5 w-3.5 shrink-0" />
        {{ placeholderLabel }}
      </div>
    </template>

    <!-- Floating cursor badge during drag -->
    <Teleport to="body">
      <div
        v-if="draggingKey && placeholder"
        class="pointer-events-none fixed z-[10000] max-w-56 truncate rounded-full bg-primary px-2.5 py-1 text-xs font-semibold text-primary-foreground shadow-lg"
        :style="{
          left: `${dragCursorX + 14}px`,
          top: `${dragCursorY - 10}px`,
        }"
      >
        {{ placeholderLabel }}
      </div>
    </Teleport>

    <!-- Sheet -->
    <NestedResourceSheet
      v-model="sheetOpen"
      :record="editingItem"
      :schema="schema"
      :current-locale="editorLocale"
      :available-locales="activeLocales"
      :public-resources="publicResources"
      :add-title="field.addItemLabel ?? 'Item toevoegen'"
      :edit-title="field.editItemLabel ?? 'Item bewerken'"
      :link-type-url-label="field.linkTypeUrlLabel ?? 'Handmatige URL'"
      :link-type-resource-label="
        field.linkTypeResourceLabel ?? 'Koppelen aan pagina'
      "
      :select-type-label="field.selectTypeLabel ?? '— Selecteer type —'"
      :select-record-label="field.selectRecordLabel ?? '— Selecteer pagina —'"
      @save="onSave"
    />
  </div>
</template>
