"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import {
  Crop, Sparkles, Smile, Type, AtSign, Music, X, Check,
  RotateCw, Loader2, Search, Trash2, Play, Pause,
} from "lucide-react";
import { FILTERS, filterCss, type FilterId } from "@/lib/photo-filters";
import { PhotoCrop, boxForRatio, type CropBox } from "./PhotoCrop";
import type { User } from "@/lib/types";
import clsx from "clsx";

/** Anything laid over the photo: a sticker, some words, or a mention. */
type Overlay = {
  id: string;
  kind: "sticker" | "text" | "mention";
  content: string;
  /** As a fraction of the photo, so it survives resizing. */
  x: number;
  y: number;
  scale: number;
  colour?: string;
};

/** Matches what /api/music/search returns, so results actually render. */
type Track = {
  trackName: string;
  artistName: string;
  artworkUrl?: string;
  previewUrl: string;
};

const CROPS = [
  { id: "free", label: "Free", ratio: null },
  { id: "original", label: "Original", ratio: null },
  { id: "square", label: "1:1", ratio: 1 },
  { id: "portrait", label: "4:5", ratio: 4 / 5 },
  { id: "landscape", label: "16:9", ratio: 16 / 9 },
];

const STICKERS = [
  "😀", "😂", "🥰", "😎", "🤔", "🎉", "🔥", "💯",
  "❤️", "👏", "🙌", "✨", "🌟", "🎂", "🍕", "☕",
  "🐶", "🐱", "🌈", "⚡", "💡", "📍", "🎵", "👀",
];

const TEXT_COLOURS = ["#ffffff", "#000000", "#e63946", "#f59e0b", "#10b981", "#3b82f6", "#8b5cf6"];

type Tool = "crop" | "filter" | "sticker" | "text" | "mention" | "music" | null;

/**
 * Editing a photo before it goes on a post — crop, filters, stickers, text,
 * mentions and a track.
 *
 * Everything is applied to a canvas when the person is done, so what they
 * see is what gets uploaded rather than the original with instructions
 * attached.
 */
/** Everything the editor holds, so a second pass can carry on where the
 *  first left off rather than starting from nothing. */
export type EditState = {
  filter: FilterId;
  crop: string;
  cropBox: CropBox;
  rotation: number;
  overlays: Overlay[];
  track: Track | null;
};

export function PhotoEditor({
  src,
  previous,
  onCancel,
  onDone,
}: {
  src: string;
  /** What was done last time, when reopening. */
  previous?: EditState | null;
  onCancel: () => void;
  /** The edited image, plus anything that travels with the post. */
  onDone: (result: {
    dataUrl: string;
    mentions: string[];
    track?: Track;
    state: EditState;
  }) => void;
}) {
  const [tool, setTool] = useState<Tool>(null);
  const [filter, setFilter] = useState<FilterId>(previous?.filter ?? "normal");
  const [crop, setCrop] = useState(previous?.crop ?? "free");
  /** Where the crop box sits, as fractions of the photo. */
  // The whole photo, so nothing is trimmed unless someone actually crops.
  const [cropBox, setCropBox] = useState<CropBox>(
    previous?.cropBox ?? { x: 0, y: 0, w: 1, h: 1 }
  );
  const [rotation, setRotation] = useState(previous?.rotation ?? 0);
  const [overlays, setOverlays] = useState<Overlay[]>(previous?.overlays ?? []);
  const [selected, setSelected] = useState<string | null>(null);
  const [track, setTrack] = useState<Track | null>(previous?.track ?? null);
  const [working, setWorking] = useState(false);
  /** The photo's width ÷ height. */
  const [photoShape, setPhotoShape] = useState<number | null>(null);
  /** The size the photo is actually drawn at, worked out from the stage. */
  const [fitted, setFitted] = useState<{ w: number; h: number } | null>(null);
  /** Shown on the photo while it's being typed, before it's added. */
  // Fits the photo inside whatever room the stage has, in pixels. Doing it
  // here rather than in CSS avoids percentage heights resolving against the
  // wrong box, which is what clipped tall photos.
  useEffect(() => {
    if (!photoShape) return;

    const measure = () => {
      const stage = stageRef.current;
      if (!stage) return;

      const room = { w: stage.clientWidth, h: stage.clientHeight };
      if (!room.w || !room.h) return;

      const byWidth = { w: room.w, h: room.w / photoShape };
      setFitted(
        byWidth.h <= room.h ? byWidth : { w: room.h * photoShape, h: room.h }
      );
    };

    measure();

    // The stage changes size when a tool panel opens, and when the window
    // does, so the photo has to be re-fitted both times.
    const observer = new ResizeObserver(measure);
    if (stageRef.current) observer.observe(stageRef.current);
    return () => observer.disconnect();
  }, [photoShape]);

  const [draft, setDraft] = useState<
    { kind: "text" | "mention"; content: string; colour: string } | null
  >(null);

  const stageRef = useRef<HTMLDivElement | null>(null);
  const imageRef = useRef<HTMLImageElement | null>(null);

  /**
   * The photo's own rectangle, which is smaller than the stage whenever the
   * picture doesn't fill it. Every position is measured against this.
   */
  const photoRect = () =>
    imageRef.current?.getBoundingClientRect() ??
    stageRef.current?.getBoundingClientRect() ??
    null;
  const dragging = useRef<{ id: string; dx: number; dy: number } | null>(null);
  /** Live pointers, so two fingers can resize what one finger drags. */
  const pointers = useRef(new Map<number, { x: number; y: number }>());
  const pinch = useRef<{ id: string; distance: number; scale: number } | null>(null);

  // Dragging an overlay around the photo.
  const onPointerDown = (e: React.PointerEvent, overlay: Overlay) => {
    e.stopPropagation();
    pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });

    // A second finger switches from dragging to resizing.
    if (pointers.current.size === 2) {
      const [a, b] = [...pointers.current.values()];
      pinch.current = {
        id: overlay.id,
        distance: Math.hypot(a.x - b.x, a.y - b.y),
        scale: overlay.scale,
      };
      dragging.current = null;
      return;
    }

    const photo = photoRect();
    if (!photo) return;
    dragging.current = {
      id: overlay.id,
      dx: (e.clientX - photo.left) / photo.width - overlay.x,
      dy: (e.clientY - photo.top) / photo.height - overlay.y,
    };
    setSelected(overlay.id);
  };

  useEffect(() => {
    const move = (e: PointerEvent) => {
      if (pointers.current.has(e.pointerId)) {
        pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
      }

      // Two fingers: resize rather than move.
      const p = pinch.current;
      if (p && pointers.current.size === 2) {
        const [a, b] = [...pointers.current.values()];
        const scale = Math.min(
          4,
          Math.max(0.4, (p.scale * Math.hypot(a.x - b.x, a.y - b.y)) / p.distance)
        );
        setOverlays((prev) =>
          prev.map((o) => (o.id === p.id ? { ...o, scale } : o))
        );
        return;
      }

      const drag = dragging.current;
      const photo = photoRect();
      if (!drag || !photo) return;

      // 7% in, not 3%: an overlay is centred on its point, so any closer
      // and half of it hangs off the picture.
      const x = Math.min(0.93, Math.max(0.07, (e.clientX - photo.left) / photo.width - drag.dx));
      const y = Math.min(0.93, Math.max(0.07, (e.clientY - photo.top) / photo.height - drag.dy));

      setOverlays((prev) =>
        prev.map((o) => (o.id === drag.id ? { ...o, x, y } : o))
      );
    };
    const up = (e: PointerEvent) => {
      pointers.current.delete(e.pointerId);
      // Both fingers must be down for a pinch; one lifted ends it.
      if (pointers.current.size < 2) pinch.current = null;
      if (pointers.current.size === 0) dragging.current = null;
    };

    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    // A cancelled pointer (a browser gesture taking over, say) would
    // otherwise leave an entry behind and the next drag would misbehave.
    window.addEventListener("pointercancel", up);
    return () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      window.removeEventListener("pointercancel", up);
    };
  }, []);

  function add(kind: Overlay["kind"], content: string, colour?: string) {
    const id = Math.random().toString(36).slice(2, 8);
    // Selected on arrival, so the size slider is there without hunting.
    setSelected(id);

    setOverlays((prev) => [
      ...prev,
      {
        id,
        kind,
        content,
        // Placed in the upper area rather than dead centre: with a tool
        // panel open the middle of a short photo sits behind it, so a new
        // sticker looked like it hadn't been added at all. Each one is
        // nudged down a little so a run of them doesn't stack.
        x: 0.34 + (prev.length % 3) * 0.16,
        // The tool panel covers the lower part of the photo, so new
        // pieces land in the clear top half, each nudged along so a run of
        // them doesn't pile up in one spot.
        y: 0.18 + (prev.length % 4) * 0.08,
        scale: 1,
        colour: kind === "sticker" ? undefined : colour ?? "#ffffff",
      },
    ]);
  }

  /** Draws everything onto a canvas, so the upload is the finished picture. */
  const render = useCallback(async () => {
    setWorking(true);

    const image = new Image();
    image.crossOrigin = "anonymous";
    await new Promise((resolve, reject) => {
      image.onload = resolve;
      image.onerror = reject;
      image.src = src;
    }).catch(() => {});

    const turned = rotation % 180 !== 0;
    const sourceW = turned ? image.naturalHeight : image.naturalWidth;
    const sourceH = turned ? image.naturalWidth : image.naturalHeight;

    // Exactly what the crop box covers, in the photo's own pixels.
    const sx = cropBox.x * sourceW;
    const sy = cropBox.y * sourceH;
    const sw = cropBox.w * sourceW;
    const sh = cropBox.h * sourceH;

    const canvas = document.createElement("canvas");
    canvas.width = Math.round(sw);
    canvas.height = Math.round(sh);
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      setWorking(false);
      return;
    }

    ctx.filter = filterCss(filter);
    ctx.save();
    // Rotate about the middle of the crop, then take the region.
    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.rotate((rotation * Math.PI) / 180);
    ctx.translate(-sw / 2, -sh / 2);
    ctx.drawImage(image, sx, sy, sw, sh, 0, 0, sw, sh);
    ctx.restore();
    ctx.filter = "none";

    // Overlays sit on top, at the same relative positions.
    for (const o of overlays) {
      // Positions are against the whole photo, so they're shifted into the
      // cropped region rather than drifting when someone crops.
      const inCropX = (o.x - cropBox.x) / cropBox.w;
      const inCropY = (o.y - cropBox.y) / cropBox.h;
      // Clamped rather than skipped — dropping one silently was why
      // stickers and text vanished from the posted photo.
      const clampedX = Math.min(1, Math.max(0, inCropX));
      const clampedY = Math.min(1, Math.max(0, inCropY));

      const size = (o.kind === "sticker" ? 0.14 : 0.07) * canvas.width * o.scale;
      ctx.font = `700 ${size}px Inter, system-ui, sans-serif`;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";

      // Text is drawn from its middle, so something placed near an edge
      // would have half of itself off the canvas. Measuring it and keeping
      // it a half-width inside means a corner sticker is drawn whole.
      const label = o.kind === "mention" ? `@${o.content}` : o.content;
      const halfWide = ctx.measureText(label).width / 2 + 4;
      const halfTall = size * 0.62;

      const x = Math.min(
        canvas.width - halfWide,
        Math.max(halfWide, clampedX * canvas.width)
      );
      const y = Math.min(
        canvas.height - halfTall,
        Math.max(halfTall, clampedY * canvas.height)
      );

      if (o.kind !== "sticker") {
        // A shadow keeps words readable over a busy photo.
        ctx.shadowColor = "rgba(0,0,0,.55)";
        ctx.shadowBlur = size * 0.3;
      }
      ctx.fillStyle = o.colour ?? "#ffffff";
      ctx.fillText(label, x, y);
      ctx.shadowBlur = 0;
    }

    const dataUrl = canvas.toDataURL("image/jpeg", 0.9);
    setWorking(false);

    onDone({
      dataUrl,
      mentions: overlays.filter((o) => o.kind === "mention").map((o) => o.content),
      track: track ?? undefined,
      // Handed back so reopening picks up where this left off.
      state: { filter, crop, cropBox, rotation, overlays, track },
    });
  }, [src, crop, cropBox, rotation, filter, overlays, track, onDone]);

  const chosenCrop = CROPS.find((c) => c.id === crop);

  return (
    <div className="pe-back" onClick={onCancel}>
      {/* Clicks inside stay inside — the editor may sit within another
          overlay that closes on a stray click. */}
      <div
        className={clsx("pe-shell", tool === "crop" && "cropping")}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="pe-bar">
          <button onClick={onCancel} className="pe-x">
            <X size={20} />
          </button>
          <span>Edit photo</span>
          <button onClick={render} disabled={working} className="pe-done">
            {working ? <Loader2 size={15} className="animate-spin" /> : <Check size={15} />}
            Done
          </button>
        </div>

        <div
          ref={stageRef}
          className="pe-stage"
          onClick={(e) => {
            // Only a click on the empty photo clears the selection — a click
            // on an overlay is handled by the overlay itself. Without this
            // the slider appeared and vanished immediately.
            if (e.target === e.currentTarget) setSelected(null);
          }}
        >
          {/* The photo and everything on it share one box, so what's on
              screen and what gets drawn agree. */}
          <span
            className="pe-photo"
            style={fitted ? { width: fitted.w, height: fitted.h } : undefined}
          >
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              ref={imageRef}
              src={src}
              alt=""
              onLoad={(e) => {
                const img = e.currentTarget;
                if (img.naturalHeight) {
                  setPhotoShape(img.naturalWidth / img.naturalHeight);
                }
              }}
              style={{
                filter: filterCss(filter),
                transform: `rotate(${rotation}deg)`,
              }}
            />






{draft && (
            <span
              className={clsx("pe-overlay draft", draft.kind)}
              style={{ left: "50%", top: "24%", color: draft.colour }}
            >
              {draft.kind === "mention" ? `@${draft.content}` : draft.content}
            </span>
          )}

          {overlays.map((o) => (
            <button
              key={o.id}
              onPointerDown={(e) => onPointerDown(e, o)}
              onWheel={(e) => {
                // A wheel is the mouse equivalent of a pinch. React attaches
                // this passively, so preventDefault would only throw.
                setOverlays((prev) =>
                  prev.map((x) =>
                    x.id === o.id
                      ? {
                          ...x,
                          scale: Math.min(
                            4,
                            Math.max(0.4, x.scale - e.deltaY * 0.002)
                          ),
                        }
                      : x
                  )
                );
              }}
              onClick={(e) => {
                // Selecting brings up the size slider, so a plain tap has to
                // work as well as a drag. Stopping here keeps the stage's own
                // handler from clearing it again straight away.
                e.stopPropagation();
                e.preventDefault();
                setSelected(o.id);
              }}
              className={clsx("pe-overlay", selected === o.id && "on", o.kind)}
              style={{
                left: `${o.x * 100}%`,
                top: `${o.y * 100}%`,
                fontSize: o.kind === "sticker" ? `${38 * o.scale}px` : `${20 * o.scale}px`,
                color: o.colour,
              }}
            >
              {o.kind === "mention" ? `@${o.content}` : o.content}
            </button>
          ))}

          {/* The crop box only shows while cropping, so it doesn't get in
              the way of placing a sticker. */}
          {tool === "crop" && (
            <PhotoCrop
              box={cropBox}
              ratio={chosenCrop?.ratio ?? null}
              onChange={setCropBox}
            />
          )}
          </span>

          {track && (
            <span className="pe-track">
              <Music size={13} /> {track.trackName} — {track.artistName}
            </span>
          )}
        </div>

        {/* Whatever the chosen tool needs. */}
        {tool && (
          <div className="pe-panel">
            {tool === "crop" && (
              <>
                <div className="pe-chips">
                  {CROPS.map((c) => (
                    <button
                      key={c.id}
                      onClick={() => {
                        setCrop(c.id);
                        if (c.id === "original") {
                          setCropBox({ x: 0, y: 0, w: 1, h: 1 });
                          return;
                        }
                        const frame = stageRef.current?.getBoundingClientRect();
                        setCropBox(
                          c.id === "free"
                            ? { x: 0, y: 0, w: 1, h: 1 }
                            : boxForRatio(c.ratio, frame ? frame.width / frame.height : 1)
                        );
                      }}
                      className={clsx("pe-chip", crop === c.id && "on")}
                    >
                      {c.label}
                    </button>
                  ))}
                </div>
                <button
                  onClick={() => setRotation((r) => (r + 90) % 360)}
                  className="pe-chip"
                >
                  <RotateCw size={14} /> Rotate
                </button>
              </>
            )}

            {tool === "filter" && (
              <div className="pe-filters">
                {FILTERS.map((f) => (
                  <button
                    key={f.id}
                    onClick={() => setFilter(f.id)}
                    className={clsx("pe-filter", filter === f.id && "on")}
                  >
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img src={src} alt="" style={{ filter: f.css }} />
                    <em>{f.label}</em>
                  </button>
                ))}
              </div>
            )}

            {tool === "sticker" && (
              <div className="pe-stickers">
                {STICKERS.map((s) => (
                  <button key={s} onClick={() => add("sticker", s)}>
                    {s}
                  </button>
                ))}
              </div>
            )}

            {tool === "text" && (
              <TextTool
                selectedColour={
                  overlays.find((o) => o.id === selected && o.kind !== "sticker")
                    ?.colour
                }
                onColour={(colour) => {
                  // Recolours whatever is selected, so a colour can be
                  // changed after the fact rather than only before adding.
                  if (!selected) return;
                  setOverlays((prev) =>
                    prev.map((o) =>
                      o.id === selected && o.kind !== "sticker" ? { ...o, colour } : o
                    )
                  );
                }}
                onAdd={(text, colour) => {
                  setDraft(null);
                  add("text", text, colour);
                }}
                onPreview={(text, colour) =>
                  setDraft(text.trim() ? { kind: "text", content: text, colour } : null)
                }
              />
            )}

            {tool === "mention" && (
              <MentionTool
                onAdd={(username) => {
                  setDraft(null);
                  add("mention", username);
                }}
                onPreview={(username) =>
                  setDraft(
                    username
                      ? { kind: "mention", content: username, colour: "#ffffff" }
                      : null
                  )
                }
                // Once placed, the tag on the photo is the real thing —
                // drag it, size it, delete it.
                onPlaced={() => setDraft(null)}
              />
            )}

            {tool === "music" && (
              <MusicTool current={track} onPick={(t) => setTrack(t)} />
            )}
          </div>
        )}

        {selected && (
          <div className="pe-selected">
            <span className="pe-selected-label">Size</span>
            <input
              type="range"
              min="0.5"
              max="3"
              step="0.1"
              value={overlays.find((o) => o.id === selected)?.scale ?? 1}
              onChange={(e) =>
                setOverlays((prev) =>
                  prev.map((o) =>
                    o.id === selected ? { ...o, scale: Number(e.target.value) } : o
                  )
                )
              }
            />
            <button
              onClick={() => {
                setOverlays((prev) => prev.filter((o) => o.id !== selected));
                setSelected(null);
              }}
            >
              <Trash2 size={15} />
            </button>
          </div>
        )}

        <div className="pe-tools">
          {[
            { id: "crop" as const, icon: Crop, label: "Crop" },
            { id: "filter" as const, icon: Sparkles, label: "Effects" },
            { id: "sticker" as const, icon: Smile, label: "Stickers" },
            { id: "text" as const, icon: Type, label: "Text" },
            { id: "mention" as const, icon: AtSign, label: "Mention" },
            { id: "music" as const, icon: Music, label: "Music" },
          ].map((t) => (
            <button
              key={t.id}
              onClick={() => {
                const next = tool === t.id ? null : t.id;
                setTool(next);
                // Switching away from text or mentions clears whatever was
                // being previewed, so it can't linger over the photo.
                setDraft(null);
                // Opening Crop used to inset the box, which trimmed every
                // photo even if you only glanced at the tool. It stays on
                // the whole picture until you actually drag it.
              }}
              className={clsx("pe-tool", tool === t.id && "on")}
            >
              <t.icon size={19} />
              <em>{t.label}</em>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

/**
 * Typing words onto the photo. What's typed appears on the picture straight
 * away, so it can be positioned while it's being written rather than only
 * after it's added.
 */
function TextTool({
  onAdd,
  onPreview,
  onColour,
  selectedColour,
}: {
  onAdd: (text: string, colour: string) => void;
  onPreview: (text: string, colour: string) => void;
  /** Recolours whatever is selected on the photo. */
  onColour: (colour: string) => void;
  selectedColour?: string;
}) {
  const [text, setText] = useState("");
  const [colour, setColour] = useState("#ffffff");

  // The swatches follow the selection, so it's clear what a colour applies to.
  const shown = selectedColour ?? colour;

  useEffect(() => {
    onPreview(text, colour);
  }, [text, colour, onPreview]);

  return (
    <div className="pe-text">
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type something"
        onKeyDown={(e) => {
          if (e.key === "Enter" && text.trim()) {
            onAdd(text.trim(), colour);
            setText("");
          }
        }}
      />

      <div className="pe-colours">
        {TEXT_COLOURS.map((c) => (
          <button
            key={c}
            onClick={() => {
              setColour(c);
              onColour(c);
            }}
            className={clsx("pe-colour", shown === c && "on")}
            style={{ background: c }}
          />
        ))}
      </div>

      <button
        onClick={() => {
          if (!text.trim()) return;
          onAdd(text.trim(), colour);
          setText("");
        }}
        disabled={!text.trim()}
        className="pe-add"
      >
        Add
      </button>
    </div>
  );
}

/** Tagging someone in the photo itself. */
function MentionTool({
  onAdd,
  onPreview,
  onPlaced,
}: {
  onAdd: (username: string) => void;
  /** Shows the tag on the photo while it's being chosen. */
  onPreview: (username: string) => void;
  /** Clears that preview once a real tag has been placed. */
  onPlaced: () => void;
}) {
  const [query, setQuery] = useState("");
  const [people, setPeople] = useState<User[]>([]);

  // What's typed shows on the photo straight away, so the tag can be
  // positioned while it's being written rather than only once picked.
  useEffect(() => {
    onPreview(query.trim());
  }, [query, onPreview]);

  /** Puts the tag on the photo, where it can then be dragged and sized. */
  function place(username: string) {
    onAdd(username.replace(/^@/, ""));
    setQuery("");
    setPeople([]);
    onPlaced();
  }

  useEffect(() => {
    if (query.trim().length < 1) {
      setPeople([]);
      return;
    }
    const handle = setTimeout(() => {
      fetch(`/api/search?q=${encodeURIComponent(query)}&type=people`)
        .then((r) => (r.ok ? r.json() : null))
        .then((d) => setPeople((d?.users ?? d ?? []).slice(0, 6)))
        .catch(() => {});
    }, 250);
    return () => clearTimeout(handle);
  }, [query]);

  return (
    <div className="pe-mention">
      <span className="pe-search">
        <Search size={14} />
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Who's in this photo?"
          onKeyDown={(e) => {
            if (e.key === "Enter" && query.trim()) place(query.trim());
          }}
        />
        {query.trim() && (
          <button onClick={() => place(query.trim())} className="pe-add-tag">
            Add
          </button>
        )}
      </span>

      {query.trim() && people.length === 0 && (
        <p className="pe-none">
          Press Add to tag @{query.trim()} anyway.
        </p>
      )}

      {people.map((p) => (
        <button
          key={p.id}
          onPointerEnter={() => onPreview(p.username)}
          onPointerLeave={() => onPreview(query.trim())}
          onPointerCancel={() => onPreview("")}
          onClick={() => place(p.username)}
          className="pe-person"
        >
          <b>{p.name}</b>
          <em>@{p.username}</em>
        </button>
      ))}
    </div>
  );
}

/** Choosing a track to go with the post. */
function MusicTool({
  current,
  onPick,
}: {
  current: Track | null;
  onPick: (t: Track | null) => void;
}) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<Track[]>([]);
  const [searching, setSearching] = useState(false);
  /** Which track is playing in the list, if any. */
  const [previewing, setPreviewing] = useState<string | null>(null);

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }
    setSearching(true);
    const handle = setTimeout(() => {
      fetch(`/api/music/search?q=${encodeURIComponent(query)}`)
        .then((r) => (r.ok ? r.json() : null))
        .then((d) => {
          // The endpoint wraps its results, so a bare array check found
          // nothing and the list always looked empty.
          const list = Array.isArray(d) ? d : (d?.tracks ?? d?.results ?? []);
          setResults(list.slice(0, 8));
        })
        .catch(() => {})
        .finally(() => setSearching(false));
    }, 300);
    return () => clearTimeout(handle);
  }, [query]);

  return (
    <div className="pe-music">
      <span className="pe-search">
        {searching ? <Loader2 size={14} className="animate-spin" /> : <Search size={14} />}
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search for a song"
        />
      </span>

      {current && (
        <button onClick={() => onPick(null)} className="pe-track-current">
          <Music size={13} /> {current.trackName} — {current.artistName}
          <X size={13} />
        </button>
      )}

      {results.map((t) => (
        <div key={t.previewUrl} className="pe-song">
          {/* Listening before choosing, so a track isn't picked blind. */}
          <button
            onClick={() => setPreviewing(previewing === t.previewUrl ? null : t.previewUrl)}
            className="pe-play"
            aria-label={previewing === t.previewUrl ? "Stop" : "Play"}
          >
            {previewing === t.previewUrl ? <Pause size={14} /> : <Play size={14} />}
          </button>

          {t.artworkUrl && (
            // eslint-disable-next-line @next/next/no-img-element
            <img src={t.artworkUrl} alt="" />
          )}

          <button onClick={() => onPick(t)} className="min-w-0 flex-1 text-left">
            <b>{t.trackName}</b>
            <em>{t.artistName}</em>
          </button>

          <button onClick={() => onPick(t)} className="pe-use">
            Use
          </button>
        </div>
      ))}

      {previewing && (
        <audio src={previewing} autoPlay onEnded={() => setPreviewing(null)} />
      )}

      {!searching && query.trim() && results.length === 0 && (
        <p className="pe-none">Nothing found for that.</p>
      )}
    </div>
  );
}
