"use client";

import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVerticalIcon, XIcon } from "lucide-react";
import { useId, useState, type ReactNode } from "react";

import { FieldError } from "@/components/ui/field";
import { slugify } from "@/lib/slug";
import { cn } from "@/lib/utils";

/**
 * "42 / 60" counter. Turns amber past `recommended` and red past `max`, and
 * reads out to screen readers only when it crosses a limit.
 */
export function CharCounter({
  value,
  recommended,
  max,
  min,
  className,
}: {
  value: string;
  recommended?: number;
  max?: number;
  min?: number;
  className?: string;
}) {
  const length = value.length;
  const limit = recommended ?? max;
  const over = max !== undefined && length > max;
  const warn = !over && ((recommended !== undefined && length > recommended) || (min !== undefined && length > 0 && length < min));
  return (
    <span
      className={cn(
        "text-xs tabular-nums",
        over ? "text-destructive" : warn ? "text-warning" : length > 0 ? "text-success" : "text-muted-foreground",
        className,
      )}
      aria-live={over || warn ? "polite" : "off"}
    >
      {length}
      {limit !== undefined && ` / ${limit}`}
    </span>
  );
}

export function ErrorText({ message }: { message?: string }) {
  if (!message) return null;
  return <FieldError>{message}</FieldError>;
}

/** Chips with free-text entry and suggestions. Enter or comma adds; Backspace on empty removes the last. */
export function TagInput({
  value,
  onChange,
  suggestions,
  max = 20,
  placeholder = "Add a tag…",
}: {
  value: string[];
  onChange: (tags: string[]) => void;
  suggestions: string[];
  max?: number;
  placeholder?: string;
}) {
  const [draft, setDraft] = useState("");
  const listId = useId();
  const existing = new Set(value.map((tag) => slugify(tag)));

  const add = (raw: string) => {
    const tag = raw.trim().replace(/,+$/, "").slice(0, 40);
    if (!tag || existing.has(slugify(tag)) || value.length >= max) {
      setDraft("");
      return;
    }
    onChange([...value, tag]);
    setDraft("");
  };

  const matches = draft
    ? suggestions
        .filter((tag) => tag.toLowerCase().includes(draft.toLowerCase()) && !existing.has(slugify(tag)))
        .slice(0, 6)
    : [];

  return (
    <div>
      <div className="flex min-h-9 flex-wrap items-center gap-1.5 rounded-lg border border-input bg-background px-2 py-1.5 focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50">
        {value.map((tag) => (
          <span
            key={tag}
            className="inline-flex h-6 items-center gap-1 rounded-md bg-secondary pr-1 pl-2 text-xs font-medium"
          >
            {tag}
            <button
              type="button"
              aria-label={`Remove tag ${tag}`}
              className="grid size-4 place-items-center rounded text-muted-foreground hover:bg-background hover:text-foreground"
              onClick={() => onChange(value.filter((item) => item !== tag))}
            >
              <XIcon className="size-3" />
            </button>
          </span>
        ))}
        <input
          value={draft}
          list={listId}
          placeholder={value.length ? "" : placeholder}
          aria-label="Add tag"
          disabled={value.length >= max}
          onChange={(event) => {
            const next = event.target.value;
            if (next.endsWith(",")) add(next);
            else setDraft(next);
          }}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.preventDefault();
              add(draft);
            } else if (event.key === "Backspace" && !draft && value.length) {
              onChange(value.slice(0, -1));
            }
          }}
          onBlur={() => draft && add(draft)}
          className="h-6 min-w-24 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
        />
      </div>
      <datalist id={listId}>
        {matches.map((tag) => (
          <option key={tag} value={tag} />
        ))}
      </datalist>
    </div>
  );
}

/** Vertical drag-and-drop list. Also sortable from the keyboard: focus a handle, Space, arrows, Space. */
export function SortableList<T extends { id: string }>({
  items,
  onReorder,
  renderItem,
  className,
}: {
  items: T[];
  onReorder: (items: T[]) => void;
  renderItem: (item: T, index: number, handle: ReactNode) => ReactNode;
  className?: string;
}) {
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (!over || active.id === over.id) return;
    const from = items.findIndex((item) => item.id === active.id);
    const to = items.findIndex((item) => item.id === over.id);
    if (from >= 0 && to >= 0) onReorder(arrayMove(items, from, to));
  };

  return (
    <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
      <SortableContext items={items.map((item) => item.id)} strategy={verticalListSortingStrategy}>
        <ul className={className}>
          {items.map((item, index) => (
            <SortableRow key={item.id} id={item.id}>
              {(handle) => renderItem(item, index, handle)}
            </SortableRow>
          ))}
        </ul>
      </SortableContext>
    </DndContext>
  );
}

function SortableRow({ id, children }: { id: string; children: (handle: ReactNode) => ReactNode }) {
  const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({ id });

  const handle = (
    <button
      type="button"
      ref={setActivatorNodeRef}
      {...attributes}
      {...listeners}
      aria-label="Drag to reorder"
      className="grid size-7 shrink-0 cursor-grab touch-none place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none active:cursor-grabbing"
    >
      <GripVerticalIcon className="size-4" />
    </button>
  );

  return (
    <li
      ref={setNodeRef}
      style={{ transform: CSS.Translate.toString(transform), transition }}
      className={cn("relative", isDragging && "z-10 opacity-90 shadow-lg")}
    >
      {children(handle)}
    </li>
  );
}
