import type { NormalizedFeedGame } from "./types";

const CATEGORY_MAP: Record<string, string> = {
  racing: "racing",
  arcade: "arcade",
  shooting: "shooter",
  shooter: "shooter",
  puzzles: "puzzle",
  puzzle: "puzzle",
  action: "action",
  sports: "sports",
  girls: "girls",
  "io games": "io",
  io: "io",
  strategy: "strategy",
  adventure: "adventure",
  hypercasual: "arcade",
  "hyper-casual": "arcade",
  clicker: "arcade",
  fighting: "action",
  junior: "girls",
  multiplayer: "io",
  bejeweled: "puzzle",
  match3: "puzzle",
  cards: "puzzle",
  "3d": "action",
};

export function mapCategory(input: string | undefined): string {
  if (!input) return "arcade";
  const key = input.trim().toLowerCase();
  return CATEGORY_MAP[key] ?? "arcade";
}

export const ALL_MAX = 2_000_000;

function take<T>(items: T[], max: number) {
  return max >= ALL_MAX / 2 ? items : items.slice(0, max);
}

const UA =
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";

async function fetchWithRetry<T>(url: string, timeoutMs = 120_000, retries = 3): Promise<T> {
  let lastError: unknown;
  for (let attempt = 0; attempt < retries; attempt++) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      const res = await fetch(url, {
        signal: ctrl.signal,
        headers: { "user-agent": UA, accept: "application/json,text/plain,*/*" },
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return (await res.json()) as T;
    } catch (err) {
      lastError = err;
      if (attempt < retries - 1) {
        await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
      }
    } finally {
      clearTimeout(timer);
    }
  }
  throw lastError;
}

interface GMItem {
  id?: string;
  title?: string;
  description?: string;
  instructions?: string;
  category?: string;
  thumb?: string;
  width?: string | number;
  height?: string | number;
  url?: string;
}

function normalizeGM(g: GMItem, source: string): NormalizedFeedGame | null {
  if (!g.url || !g.thumb || !g.title || !g.id) return null;
  return {
    source,
    sourceId: g.id,
    title: g.title.trim(),
    description: g.description?.trim() ?? null,
    instructions: g.instructions?.trim() ?? null,
    thumbUrl: g.thumb,
    embedUrl: g.url,
    category: mapCategory(g.category),
    width: Number(g.width) || 800,
    height: Number(g.height) || 600,
  };
}

async function fetchGMJson(
  source: string,
  categories: string[],
  options: { type?: "html5" | "unity"; popularities?: ("" | "newest" | "popular")[] } = {},
): Promise<NormalizedFeedGame[]> {
  const type = options.type ?? "html5";
  const popularities = options.popularities ?? ["", "newest", "popular"];
  const seen = new Set<string>();
  const out: NormalizedFeedGame[] = [];

  for (const category of categories) {
    for (const popularity of popularities) {
      const pop = popularity ? `&popularity=${popularity}` : "";
      const url = `https://rss.gamemonetize.com/rssfeed.php?format=json&amount=1000&type=${type}&category=${encodeURIComponent(category)}${pop}`;
      try {
        const feed = await fetchWithRetry<GMItem[]>(url, 90_000, 2);
        if (!Array.isArray(feed)) continue;
        for (const item of feed) {
          const game = normalizeGM(item, source);
          if (!game || seen.has(game.embedUrl)) continue;
          seen.add(game.embedUrl);
          out.push(game);
        }
      } catch {
        /* next batch */
      }
    }
  }

  return out;
}

export async function fetchGameMonetizeCore(max: number): Promise<NormalizedFeedGame[]> {
  return take(await fetchGMJson("gamemonetize", ["All"], { popularities: ["", "newest", "popular"] }), max);
}

export async function fetchGameMonetizeArcade(max: number): Promise<NormalizedFeedGame[]> {
  return take(
    await fetchGMJson("gamemonetize-arcade", ["Arcade", "Action", "Fighting", "Shooting"], {
      popularities: ["", "newest", "popular"],
    }),
    max,
  );
}

export async function fetchGameMonetizeCasual(max: number): Promise<NormalizedFeedGame[]> {
  return take(
    await fetchGMJson("gamemonetize-casual", ["Hypercasual", "Clicker", "Puzzle", "Bejeweled"], {
      popularities: ["", "newest", "popular"],
    }),
    max,
  );
}

export async function fetchGameMonetizeRacing(max: number): Promise<NormalizedFeedGame[]> {
  return take(
    await fetchGMJson("gamemonetize-racing", ["Racing", "Sports", "3D"], {
      popularities: ["", "newest", "popular"],
    }),
    max,
  );
}

export async function fetchGameMonetizeAdventure(max: number): Promise<NormalizedFeedGame[]> {
  return take(
    await fetchGMJson("gamemonetize-adventure", ["Adventure", "Girls", "Strategy", "IO", "Multiplayer"], {
      popularities: ["", "newest", "popular"],
    }),
    max,
  );
}

export async function fetchGameMonetizeUnity(max: number): Promise<NormalizedFeedGame[]> {
  const cats = [
    "All",
    "Arcade",
    "Racing",
    "Shooting",
    "Action",
    "Adventure",
    "Hypercasual",
    "Clicker",
    "Fighting",
    "3D",
    "Multiplayer",
  ];
  return take(await fetchGMJson("gamemonetize-unity", cats, { type: "unity", popularities: ["", "newest"] }), max);
}

function parseRssItem(block: string): GMItem | null {
  const pick = (tag: string) => {
    const m = block.match(new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${tag}>|<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
    return (m?.[1] ?? m?.[2] ?? "").trim();
  };
  const id = pick("id") || pick("guid");
  const title = pick("title");
  const url = pick("url");
  const thumb = pick("thumb");
  if (!id || !title || !url || !thumb) return null;
  return {
    id,
    title,
    description: pick("description") || undefined,
    instructions: pick("instructions") || undefined,
    category: pick("category") || undefined,
    thumb,
    url,
    width: pick("width") || undefined,
    height: pick("height") || undefined,
  };
}

export async function fetchGameMonetizeRss(max: number): Promise<NormalizedFeedGame[]> {
  const out: NormalizedFeedGame[] = [];
  const seen = new Set<string>();
  const nums = [500, 1000, 2000, 3000, 5000, 8000, 10000];

  for (const num of nums) {
    if (out.length >= max) break;
    try {
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 90_000);
      const res = await fetch(`https://gamemonetize.com/feed.php?num=${num}`, {
        signal: ctrl.signal,
        headers: { "user-agent": UA, accept: "application/xml,text/xml,*/*" },
      });
      clearTimeout(timer);
      if (!res.ok) continue;
      const xml = await res.text();
      const items = xml.match(/<item>[\s\S]*?<\/item>/gi) ?? [];
      for (const block of items) {
        const game = normalizeGM(parseRssItem(block) ?? {}, "gamemonetize-rss");
        if (!game || seen.has(game.embedUrl)) continue;
        seen.add(game.embedUrl);
        out.push(game);
        if (out.length >= max) break;
      }
    } catch {
      /* try next size */
    }
  }

  return take(out, max);
}

interface GPGame {
  id?: string;
  title?: string;
  description?: string;
  desc_en?: string;
  instructions?: string;
  controls?: string;
  howToPlay?: string;
  how_to_play?: string;
  thumbnailUrl?: string;
  url?: string;
  width?: number;
  height?: number;
  category?: string;
  categories?: string[];
}

interface GPResponse {
  data?: GPGame[];
}

export async function fetchGamePix(max: number): Promise<NormalizedFeedGame[]> {
  const out: NormalizedFeedGame[] = [];
  const pageSize = 100;
  let offset = 0;

  while (out.length < max) {
    const limit = max >= ALL_MAX / 2 ? pageSize : Math.min(pageSize, max - out.length);
    const url = `https://games.gamepix.com/games?sid=00000&limit=${limit}&offset=${offset}`;
    let page: GPResponse;
    try {
      page = await fetchWithRetry<GPResponse>(url, 120_000, 3);
    } catch {
      break;
    }
    const items = page.data ?? [];
    if (items.length === 0) break;

    for (const g of items) {
      if (!g.id || !g.url || !g.thumbnailUrl || !g.title) continue;
      const cat = g.categories?.[0] || g.category;
      out.push({
        source: "gamepix",
        sourceId: g.id,
        title: g.title.trim(),
        description: (g.desc_en || g.description || "").trim() || null,
        instructions: (g.instructions || g.howToPlay || g.how_to_play || g.controls || "").trim() || null,
        thumbUrl: g.thumbnailUrl,
        embedUrl: g.url,
        category: mapCategory(cat),
        width: g.width || 800,
        height: g.height || 600,
      });
    }

    offset += items.length;
    if (items.length < limit) break;
  }

  return take(out, max);
}

export const ALL_SOURCES = [
  { id: "gamemonetize", name: "GameMonetize (основной)", fetch: fetchGameMonetizeCore },
  { id: "gamemonetize-arcade", name: "GameMonetize Аркады", fetch: fetchGameMonetizeArcade },
  { id: "gamemonetize-casual", name: "GameMonetize Казуальные", fetch: fetchGameMonetizeCasual },
  { id: "gamemonetize-racing", name: "GameMonetize Гонки", fetch: fetchGameMonetizeRacing },
  { id: "gamemonetize-adventure", name: "GameMonetize Приключения", fetch: fetchGameMonetizeAdventure },
  { id: "gamemonetize-unity", name: "GameMonetize Unity", fetch: fetchGameMonetizeUnity },
  { id: "gamemonetize-rss", name: "GameMonetize RSS", fetch: fetchGameMonetizeRss },
] as const;
