'use strict'; const fs = require('fs'); const path = require('path'); const RATINGS = Object.freeze(['flame', 'good', 'neutral', 'skip']); const WEIGHT = Object.freeze({ flame: 3, good: 1, neutral: 0, skip: -2 }); const normalize = value => String(value || '').normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, ''); function loadCatalog(file = path.join(__dirname, 'catalog.json')) { const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); if (parsed.schema !== 1 || !Array.isArray(parsed.entries)) throw new Error('Invalid Greatest of All Time catalog'); const ids = new Set(); for (const entry of parsed.entries) { if (!/^[a-z0-9-]+$/.test(entry.id) || ids.has(entry.id) || !entry.title || !entry.period || !Number.isInteger(entry.rank)) throw new Error(`Invalid or duplicate Greatest of All Time entry: ${entry.id}`); if (entry.cover && !/^https:\/\//.test(entry.cover)) throw new Error(`Invalid cover URL for ${entry.id}`); if (entry.source?.url && !/^https:\/\//.test(entry.source.url)) throw new Error(`Invalid source URL for ${entry.id}`); ids.add(entry.id); } return parsed; } function findOwned(entry, mangas) { const names = new Set([entry.title, ...(entry.aliases || [])].map(normalize)); return mangas.find(m => { const alt = m.meta?.altTitles; return [m.title, ...(Array.isArray(alt) ? alt : alt ? [alt] : [])].some(name => names.has(normalize(name))); }) || null; } function countsFor(entryId, votes, users) { const counts = { flame: 0, good: 0, neutral: 0, skip: 0 }; for (const name of Object.keys(users)) { const rating = votes[name]?.[entryId]; if (RATINGS.includes(rating)) counts[rating]++; } return counts; } function scoreFor(counts) { return RATINGS.reduce((sum, rating) => sum + counts[rating] * WEIGHT[rating], 0); } module.exports = { RATINGS, WEIGHT, normalize, loadCatalog, findOwned, countsFor, scoreFor };