"use node";

import { action } from "./_generated/server";
import { v } from "convex/values";

/**
 * GIF search action — backed by the Klipy GIF API (Tenor's
 * Google-shutdown replacement). Klipy embeds the customer id in the
 * URL path and returns results under `data.data[]` with sized
 * variants under `file.{hd|md|sm}.gif.url`. We normalize the response
 * into a flat `{results: [{id, title, url, previewUrl, width, height}]}`
 * shape so callers don't have to know which provider is upstream.
 *
 * Reads `KLIPY_API_KEY` from the Convex environment. Falls back to
 * the legacy `TENOR_API_KEY` env var so existing deployments keep
 * working as soon as the old key is replaced with a Klipy customer
 * id — no `convex env set` rename required.
 */

interface NormalizedGif {
  id: string;
  title: string;
  url: string;
  previewUrl: string;
  width?: number;
  height?: number;
}

interface GifSearchResponse {
  results: NormalizedGif[];
}

/**
 * In-memory TTL cache for Klipy responses. Convex Node actions run on
 * warm container instances, so this survives between invocations on
 * the same worker until it's recycled. Best-effort only — a cold
 * start will re-fetch upstream. Search queries live for 5 minutes,
 * trending / categories for 30 minutes since they change slowly.
 */
interface CacheEntry<T> {
  value: T;
  expiresAt: number;
}

const CACHE = new Map<string, CacheEntry<unknown>>();
const MAX_CACHE_SIZE = 200;
const SEARCH_TTL_MS = 5 * 60 * 1000;
const TRENDING_TTL_MS = 30 * 60 * 1000;
const CATEGORIES_TTL_MS = 30 * 60 * 1000;

function cacheGet<T>(key: string): T | null {
  const entry = CACHE.get(key);
  if (!entry) return null;
  if (entry.expiresAt < Date.now()) {
    CACHE.delete(key);
    return null;
  }
  return entry.value as T;
}

function cacheSet<T>(key: string, value: T, ttlMs: number): void {
  if (CACHE.size >= MAX_CACHE_SIZE) {
    // Drop the oldest insertion — Map iteration order is insertion order.
    const oldest = CACHE.keys().next().value;
    if (oldest !== undefined) CACHE.delete(oldest);
  }
  CACHE.set(key, { value, expiresAt: Date.now() + ttlMs });
}

function normalizeGifItems(items: any[]): NormalizedGif[] {
  return items
    .map((item, idx): NormalizedGif => {
      const file = item?.file ?? {};
      const md = file?.md?.gif ?? file?.sm?.gif ?? file?.hd?.gif ?? {};
      const previewVariant =
        file?.sm?.gif ?? file?.md?.gif ?? file?.hd?.gif ?? {};
      const fullUrl: string = md?.url ?? item?.url ?? "";
      const previewUrl: string = previewVariant?.url ?? fullUrl;
      const width: number | undefined =
        typeof md?.width === "number" ? md.width : undefined;
      const height: number | undefined =
        typeof md?.height === "number" ? md.height : undefined;
      return {
        id: String(item?.slug ?? item?.id ?? `${idx}`),
        title: String(item?.title ?? ""),
        url: fullUrl,
        previewUrl,
        width,
        height,
      };
    })
    .filter((r) => !!r.url);
}

export const search = action({
  args: {
    q: v.string(),
    limit: v.optional(v.number()),
  },
  returns: v.any(),
  handler: async (_ctx, args): Promise<GifSearchResponse> => {
    const apiKey = process.env.KLIPY_API_KEY || process.env.TENOR_API_KEY;
    if (!apiKey) {
      console.warn("KLIPY_API_KEY missing");
      return { results: [] };
    }

    const limit = Math.min(Math.max(args.limit ?? 24, 1), 50);
    const query = args.q.trim().toLowerCase();
    const cacheKey = `search:${query}:${limit}`;
    const cached = cacheGet<GifSearchResponse>(cacheKey);
    if (cached) return cached;

    // Klipy customer id goes in the path; per-page caps the result
    // set without a separate `limit` query param.
    const url = `https://api.klipy.com/api/v1/${encodeURIComponent(apiKey)}/gifs/search?q=${encodeURIComponent(args.q)}&per_page=${limit}&page=1&locale=en`;

    let response: Response;
    try {
      response = await fetch(url, {
        headers: {
          Accept: "application/json",
          "User-Agent":
            "Brycord/1.0 (+https://brycord.com) Klipy-Client",
        },
      });
    } catch (err) {
      console.error("Klipy fetch error:", err);
      return { results: [] };
    }

    if (!response.ok) {
      console.error("Klipy API error:", response.status, response.statusText);
      return { results: [] };
    }

    const json = (await response.json()) as any;
    const items: any[] = Array.isArray(json?.data?.data) ? json.data.data : [];
    const results = normalizeGifItems(items);
    const payload: GifSearchResponse = { results };
    cacheSet(cacheKey, payload, SEARCH_TTL_MS);
    return payload;
  },
});

/**
 * Trending GIFs — used for the picker's home feed when no query is
 * typed. Returns the same normalized shape as `search` so callers
 * can use a single render path for both.
 */
export const trending = action({
  args: {
    limit: v.optional(v.number()),
  },
  returns: v.any(),
  handler: async (_ctx, args): Promise<GifSearchResponse> => {
    const apiKey = process.env.KLIPY_API_KEY || process.env.TENOR_API_KEY;
    if (!apiKey) return { results: [] };

    const limit = Math.min(Math.max(args.limit ?? 24, 1), 50);
    const cacheKey = `trending:${limit}`;
    const cached = cacheGet<GifSearchResponse>(cacheKey);
    if (cached) return cached;

    const url = `https://api.klipy.com/api/v1/${encodeURIComponent(apiKey)}/gifs/trending?per_page=${limit}&page=1&locale=en`;

    let response: Response;
    try {
      response = await fetch(url, {
        headers: {
          Accept: "application/json",
          "User-Agent": "Brycord/1.0 (+https://brycord.com) Klipy-Client",
        },
      });
    } catch (err) {
      console.error("Klipy trending fetch error:", err);
      return { results: [] };
    }

    if (!response.ok) {
      console.error(
        "Klipy trending API error:",
        response.status,
        response.statusText,
      );
      return { results: [] };
    }

    const json = (await response.json()) as any;
    const items: any[] = Array.isArray(json?.data?.data) ? json.data.data : [];
    const results = normalizeGifItems(items);
    const payload: GifSearchResponse = { results };
    cacheSet(cacheKey, payload, TRENDING_TTL_MS);
    return payload;
  },
});

/**
 * Trending categories — Klipy exposes `/categories` returning a list
 * of slugs the picker can show as quick-search chips. Normalized
 * into `{categories: [{name, image, query}]}` so the consumer
 * doesn't depend on the upstream shape.
 */
interface NormalizedCategory {
  name: string;
  image: string;
  query: string;
}

export const categories = action({
  args: {},
  returns: v.any(),
  handler: async (): Promise<{ categories: NormalizedCategory[] }> => {
    const apiKey = process.env.KLIPY_API_KEY || process.env.TENOR_API_KEY;
    if (!apiKey) {
      return { categories: [] };
    }

    const cacheKey = `categories`;
    const cached = cacheGet<{ categories: NormalizedCategory[] }>(cacheKey);
    if (cached) return cached;

    const url = `https://api.klipy.com/api/v1/${encodeURIComponent(apiKey)}/gifs/categories?locale=en`;

    let response: Response;
    try {
      response = await fetch(url, {
        headers: {
          Accept: "application/json",
          "User-Agent":
            "Brycord/1.0 (+https://brycord.com) Klipy-Client",
        },
      });
    } catch (err) {
      console.error("Klipy categories fetch error:", err);
      return { categories: [] };
    }

    if (!response.ok) {
      console.error(
        "Klipy categories API error:",
        response.status,
        response.statusText,
      );
      return { categories: [] };
    }

    // Klipy's actual shape (verified against /api/v1/.../gifs/categories):
    //   { result: true, data: { locale: "en_US", categories: [
    //       { category: "hello", query: "hello", preview_url: "..." },
    //       ...
    //   ]}}
    // Older / parallel paths (data.data[] from the search+trending
    // endpoints, or a flat data[] array) are accepted as fallbacks so
    // a future upstream change doesn't silently break the picker.
    const json = (await response.json()) as any;
    const items: any[] = Array.isArray(json?.data?.categories)
      ? json.data.categories
      : Array.isArray(json?.data?.data)
        ? json.data.data
        : Array.isArray(json?.data)
          ? json.data
          : [];

    const categories: NormalizedCategory[] = items
      .map((item) => ({
        name: String(
          item?.category ?? item?.name ?? item?.title ?? item?.query ?? "",
        ),
        image: String(
          item?.preview_url ?? item?.image ?? item?.preview ?? "",
        ),
        query: String(
          item?.query ?? item?.search_term ?? item?.category ?? item?.name ?? "",
        ),
      }))
      .filter((c) => !!c.query);

    const payload = { categories };
    cacheSet(cacheKey, payload, CATEGORIES_TTL_MS);
    return payload;
  },
});
