import { sql } from '@vercel/postgres'; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function pokemonHeaders() { const headers = { 'User-Agent': 'Deck-Hearth/1.0', Accept: 'application/json', }; if (process.env.POKEMON_TCG_API_KEY) { headers['X-Api-Key'] = process.env.POKEMON_TCG_API_KEY; } return headers; } export async function fetchWithRetry(url, maxRetries = 3, delayMs = 2000) { for (let attempt = 1; attempt <= maxRetries; attempt += 1) { try { const response = await fetch(url, { headers: pokemonHeaders() }); if (response.ok) { return response; } if (response.status === 404) { throw new Error(`Resource not found: ${response.status}`); } if (response.status === 504 || response.status === 503) { console.log( `[fetchWithRetry] Attempt ${attempt}: ${response.status}, retrying in ${delayMs * attempt}ms` ); await delay(delayMs * attempt); continue; } throw new Error(`Pokemon TCG API error: ${response.status}`); } catch (error) { if (attempt === maxRetries) { throw error; } console.log(`[fetchWithRetry] Attempt ${attempt} failed:`, error.message); await delay(delayMs * attempt); } } throw new Error('fetchWithRetry exhausted retries'); } /** * Import all cards for a Pokémon TCG set id. Skips rows already present by scryfall_id * (legacy column name stores Pokémon TCG API card ids too). */ export async function importPokemonSet(setCode) { const response = await fetchWithRetry( `https://api.pokemontcg.io/v2/cards?q=set.id:${setCode}&pageSize=250` ); const data = await response.json(); const cards = data.data || []; if (cards.length === 0) { return { setCode, imported: 0, skipped: 0, total: 0 }; } let imported = 0; let skipped = 0; for (const card of cards) { try { const existingCard = await sql` SELECT id FROM cards WHERE scryfall_id = ${card.id} `; if (existingCard.rows.length > 0) { skipped += 1; continue; } let currentPrice = null; if (card.tcgplayer?.prices?.normal?.market) { currentPrice = parseFloat(card.tcgplayer.prices.normal.market); } else if (card.tcgplayer?.prices?.holofoil?.market) { currentPrice = parseFloat(card.tcgplayer.prices.holofoil.market); } let rarity = card.rarity || 'Unknown'; if (rarity.includes('Holo')) { rarity = 'Holographic'; } else if (rarity.includes('Secret')) { rarity = 'Secret Rare'; } else if (rarity.includes('Ultra')) { rarity = 'Ultra Rare'; } await sql` INSERT INTO cards ( name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, power, toughness, image_url, stock_image_url, current_price, market_price, scryfall_id, verified ) VALUES ( ${card.name}, ${card.set.name}, ${card.set.id}, ${card.number}, ${rarity}, 'Pokemon', null, null, ${card.supertype || 'Pokemon'}, ${JSON.stringify(card.types || [])}, ${card.flavorText || null}, ${card.attacks?.[0]?.damage || null}, null, ${card.images?.small || null}, ${card.images?.large || null}, ${currentPrice}, null, ${card.id}, true ) `; imported += 1; } catch (error) { console.error(`[importPokemonSet] Error importing card ${card.name}:`, error); skipped += 1; } } return { setCode, imported, skipped, total: cards.length }; } export { pokemonHeaders };