"use client";

import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Palette, Check, X, Loader2 } from "lucide-react";
import { builtInThemes, showTheme } from "@/lib/theme-registry";
import clsx from "clsx";

type Available = {
  active: string | null;
  chosen: string | null;
  siteDefault: string | null;
  selectable: string[];
  hidden?: string[];
};

/**
 * The picture next to a theme's name.
 *
 * The preview if the theme ships one, the old gradient swatch if it
 * doesn't -- and the swatch again if the file 404s or is corrupt, since a
 * broken-image icon reads as a broken theme. Sits behind the picture
 * rather than replacing it, so there is never a blank gap while it loads.
 */
/**
 * One theme, as a picture you can actually judge.
 *
 * This was a 40px swatch beside a line of text, which is a list of names
 * with decoration — you cannot choose a *look* from that. Now the preview
 * is the option, at a size where the layout is legible, with the name
 * across the bottom of it.
 *
 * The gradient swatch is still there underneath for a theme that ships no
 * picture, and for one whose picture fails to load: a broken-image icon
 * reads as a broken theme.
 */
function ThemeTile({
  slug,
  src,
  name,
  note,
  chosen,
  busy,
  onPick,
}: {
  slug: string;
  src?: string;
  name: string;
  note?: string;
  chosen: boolean;
  busy: boolean;
  onPick: () => void;
}) {
  const [failed, setFailed] = useState(false);
  const showImage = Boolean(src) && !failed;

  return (
    <button
      type="button"
      onClick={onPick}
      className={clsx("ts-tile", chosen && "on")}
      aria-pressed={chosen}
    >
      <span className={clsx("ts-shot", !showImage && "noshot")} data-theme={slug}>
        {showImage && (
          // Plain img, not next/image: the file is whatever a theme author
          // shipped, at whatever size.
          // eslint-disable-next-line @next/next/no-img-element
          <img src={src} alt="" onError={() => setFailed(true)} />
        )}

        <span className="ts-plate">
          <b>{name}</b>
          {note && <em>{note}</em>}
        </span>

        {busy ? (
          <span className="ts-mark busy">
            <Loader2 size={14} className="animate-spin" />
          </span>
        ) : chosen ? (
          <span className="ts-mark">
            <Check size={14} />
          </span>
        ) : null}
      </span>
    </button>
  );
}

/**
 * Choosing a theme.
 *
 * Only what an admin offered, plus the site's own. Someone already using
 * a theme that's since been unmarked keeps it — taking away something
 * they chose would be a surprise, and no harm comes of leaving it.
 */
export function ThemeSwitcher({ onClose }: { onClose: () => void }) {
  const [state, setState] = useState<Available | null>(null);
  const [saving, setSaving] = useState<string | null>(null);
  /**
   * slug -> preview picture, written by npm run theme:use.
   *
   * A flat file rather than something generated into the bundle: a theme
   * bought and dropped in only needs a rebuild to be offered, and a
   * missing or unreadable index here just means the gradient swatches
   * come back -- never a switcher that fails to open.
   */
  const [previews, setPreviews] = useState<Record<string, string>>({});

  const load = useCallback(() => {
    fetch("/api/themes/available")
      .then((r) => (r.ok ? r.json() : null))
      .then(setState)
      .catch(() => {});

    fetch("/theme-previews/index.json")
      .then((r) => (r.ok ? r.json() : null))
      .then((m) => m && typeof m === "object" && setPreviews(m))
      .catch(() => {});
  }, []);

  useEffect(load, [load]);

  // Only themes this build actually compiled in. One that's installed but
  // not yet built would be offered and then do nothing.
  const built = builtInThemes();

  // Everything compiled in, minus anything an admin switched off. The
  // other way round — nothing until it's marked — meant installing a
  // theme and seeing no change, with nothing to say a second step
  // existed.
  const offered = built.filter(
    (t) =>
      !state?.hidden?.includes(t.slug) ||
      // Whatever they're already using stays, so they can switch away
      // from it deliberately rather than being moved without warning.
      state?.chosen === t.slug
  );

  async function choose(slug: string | null) {
    setSaving(slug ?? "default");

    const res = await fetch("/api/me/theme", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ theme: slug ?? "default" }),
    }).catch(() => null);

    setSaving(null);
    if (!res || !res.ok) return;

    // Applied at once rather than after a reload.
    showTheme(slug);
    window.dispatchEvent(new Event("xr-theme-changed"));
    onClose();
  }

  if (typeof document === "undefined") return null;

  return createPortal(
    <div className="ts-back" onClick={onClose}>
      <div className="ts-panel" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose} className="ts-close" aria-label="Close">
          <X size={18} />
        </button>

        <b>
          <Palette size={18} /> Choose a theme
        </b>
        <em>How the site looks, for you. Nobody else is affected.</em>

        <div className="ts-grid">
          <ThemeTile
            slug="default"
            src={previews.default}
            name="Twitter"
            note="The one this site ships with"
            chosen={!state?.chosen}
            busy={saving === "default"}
            onPick={() => choose(null)}
          />

          {offered.map((t) => (
            <ThemeTile
              key={t.slug}
              slug={t.slug}
              src={previews[t.slug]}
              name={t.name}
              note={[t.author && `by ${t.author}`, t.version && `v${t.version}`]
                .filter(Boolean)
                .join(" · ")}
              chosen={state?.chosen === t.slug}
              busy={saving === t.slug}
              onPick={() => choose(t.slug)}
            />
          ))}
        </div>

        {offered.length === 0 && (
          <p className="ts-none">
            {built.length === 0
              ? "No themes are installed on this site yet."
              : "An admin has switched off the other themes here."}
          </p>
        )}
      </div>
    </div>,
    document.body
  );
}
