/**
 * <BluemojiPicker>: an Expo/React Native-compatible emoji picker backed by
 * an AppView's blue.moji.collection.searchItems query. Debounced
 * search-as-you-type, renders a grid of matches, calls onPick with the
 * chosen emoji's { uri, did, name, alt } — host apps decide what to do with
 * that (typically: insert ":alias:" into a composer's text state).
 *
 * Pure React Native primitives only (View/TextInput/FlatList/Image) — no
 * native modules, so this works in a stock Expo Go app with no config
 * plugins. Mirrors components/webcomponent/picker.ts's behavior (same
 * debounce window, same stale-response guard) for the DOM equivalent.
 */
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
  ActivityIndicator,
  FlatList,
  Image,
  Pressable,
  StyleSheet,
  Text,
  TextInput,
  View,
  type StyleProp,
  type ViewStyle,
} from "react-native";

export interface BluemojiPickResult {
  uri: string;
  did: string;
  name: string;
  alt?: string;
}

interface SearchItem {
  uri: string;
  did: string;
  name: string;
  alt?: string;
  formats?: Record<string, unknown>;
}

export interface BluemojiPickerProps {
  /** AppView origin to query and load blob images from. Defaults to https://moji.blue. */
  apiBase?: string;
  /**
   * Optional at-identifier. When set, searches substring-match within that
   * repo's own collection (suited to "my collection" composer autocomplete).
   * Omit for network-wide whole-word search (suited to a "discover emoji"
   * browse surface).
   */
  repo?: string;
  placeholder?: string;
  /** Max results per search. Defaults to 24. */
  limit?: number;
  /** Debounce window in ms between keystrokes and firing a search. Defaults to 250. */
  debounceMs?: number;
  /** Number of grid columns. Defaults to 5. */
  numColumns?: number;
  onPick: (item: BluemojiPickResult) => void;
  /** Override to customise how a (did, cid) pair becomes an image URL. */
  imgUrl?: (did: string, cid: string) => string;
  style?: StyleProp<ViewStyle>;
}

function blobRefCid(ref: unknown): string | undefined {
  if (typeof ref === "string") return ref;
  return (ref as { ref?: { $link?: string } } | undefined)?.ref?.$link;
}

export function BluemojiPicker({
  apiBase = "https://moji.blue",
  repo,
  placeholder = "Search emoji…",
  limit = 24,
  debounceMs = 250,
  numColumns = 5,
  onPick,
  imgUrl,
  style,
}: BluemojiPickerProps) {
  const [query, setQuery] = useState("");
  const [items, setItems] = useState<SearchItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const requestSeq = useRef(0);

  const resolveImgUrl = useCallback(
    (did: string, cid: string) =>
      imgUrl
        ? imgUrl(did, cid)
        : `${apiBase}/img/${encodeURIComponent(did)}/${encodeURIComponent(cid)}`,
    [apiBase, imgUrl],
  );

  const search = useCallback(
    async (q: string) => {
      const seq = ++requestSeq.current;
      setLoading(true);
      setError(null);

      const url = new URL("/xrpc/blue.moji.collection.searchItems", apiBase);
      url.searchParams.set("q", q);
      url.searchParams.set("limit", String(limit));
      if (repo) url.searchParams.set("repo", repo);

      try {
        const res = await fetch(url.toString());
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const body = (await res.json()) as { items?: SearchItem[] };
        if (seq !== requestSeq.current) return; // superseded by a newer search
        setItems(body.items ?? []);
      } catch (err) {
        if (seq !== requestSeq.current) return;
        setError(err instanceof Error ? err.message : "Search failed.");
        setItems([]);
      } finally {
        if (seq === requestSeq.current) setLoading(false);
      }
    },
    [apiBase, limit, repo],
  );

  useEffect(() => {
    clearTimeout(debounceTimer.current);
    const q = query.trim();
    if (!q) {
      requestSeq.current++; // invalidate any in-flight search
      setItems([]);
      setLoading(false);
      setError(null);
      return;
    }
    debounceTimer.current = setTimeout(() => search(q), debounceMs);
    return () => clearTimeout(debounceTimer.current);
  }, [query, debounceMs, search]);

  return (
    <View style={[styles.container, style]}>
      <TextInput
        style={styles.input}
        value={query}
        onChangeText={setQuery}
        placeholder={placeholder}
        autoCorrect={false}
        autoCapitalize="none"
      />
      {loading && <ActivityIndicator style={styles.status} size="small" />}
      {!loading && error && <Text style={styles.statusText}>{error}</Text>}
      {!loading && !error && query.trim() && items.length === 0 && (
        <Text style={styles.statusText}>No matches.</Text>
      )}
      <FlatList
        data={items}
        key={numColumns}
        numColumns={numColumns}
        keyExtractor={(item) => item.uri}
        renderItem={({ item }) => {
          const cid =
            blobRefCid(item.formats?.png_128) ??
            blobRefCid(item.formats?.webp_128) ??
            blobRefCid(item.formats?.gif_128);
          return (
            <Pressable
              style={styles.item}
              onPress={() =>
                onPick({ uri: item.uri, did: item.did, name: item.name, alt: item.alt })
              }
            >
              {cid ? (
                <Image
                  style={styles.itemImage}
                  source={{ uri: resolveImgUrl(item.did, cid) }}
                  accessibilityLabel={item.alt ?? item.name}
                />
              ) : (
                <View style={styles.itemImage} />
              )}
              <Text style={styles.itemLabel} numberOfLines={1}>
                {item.name}
              </Text>
            </Pressable>
          );
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  input: {
    borderWidth: 1,
    borderColor: "#ccc",
    borderRadius: 4,
    padding: 8,
  },
  status: {
    marginTop: 6,
  },
  statusText: {
    marginTop: 6,
    fontSize: 13,
    color: "#666",
  },
  item: {
    flex: 1,
    alignItems: "center",
    gap: 2,
    padding: 6,
    minWidth: 0,
  },
  itemImage: {
    width: 32,
    height: 32,
  },
  itemLabel: {
    fontSize: 10,
    maxWidth: 48,
  },
});
