'use strict'; const PUBLIC_MODES = new Set(['missing', 'latest', 'last', 'first', 'chapter', 'after', 'volume']); function chapterNumber(value, field = 'chapter') { const raw = String(value ?? '').trim(); if (!/^\d+(?:\.\d+)?$/.test(raw)) throw new Error(`${field} must be a numeric chapter`); const [whole, decimal = ''] = raw.split('.'); const normalizedWhole = whole.replace(/^0+(?=\d)/, '') || '0'; const normalizedDecimal = decimal.replace(/0+$/, ''); return normalizedDecimal ? `${normalizedWhole}.${normalizedDecimal}` : normalizedWhole; } function positiveCount(value) { const count = Number(value); if (!Number.isInteger(count) || count < 1 || count > 10000) throw new Error('count must be an integer from 1 to 10000'); return count; } function normalizeScope(input = {}) { const raw = input && typeof input === 'object' ? input : {}; const mode = String(raw.mode || 'missing').toLowerCase(); if (mode === 'ids') { if (!Array.isArray(raw.ids) || !raw.ids.length || raw.ids.length > 10000) throw new Error('ids scope requires 1 to 10000 source chapter ids'); return { mode, ids: [...new Set(raw.ids.map(value => String(value).trim()).filter(Boolean))] }; } if (!PUBLIC_MODES.has(mode)) throw new Error(`Unsupported queue scope: ${mode}`); if (mode === 'last' || mode === 'first') return { mode, count: positiveCount(raw.count) }; if (mode === 'chapter' || mode === 'after') return { mode, chapter: chapterNumber(raw.chapter) }; if (mode === 'volume') { const volume = String(raw.volume ?? '').trim(); if (!/^\d+(?:\.\d+)?$/.test(volume)) throw new Error('volume must be numeric'); return { mode, volume: chapterNumber(volume, 'volume') }; } return { mode }; } function volumeOf(ref) { const value = ref.volume ?? ref.book?.number; if (value === undefined || value === null || value === '') return null; try { return chapterNumber(value, 'volume'); } catch { return null; } } function selectByScope(chapters, input) { const scope = normalizeScope(input); const list = [...chapters]; if (scope.mode === 'missing') return list; if (scope.mode === 'latest') return list.length ? [list[list.length - 1]] : []; if (scope.mode === 'last') return list.slice(-scope.count); if (scope.mode === 'first') return list.slice(0, scope.count); const numbered = list.filter(ref => /^\d+(?:\.\d+)?$/.test(String(ref.number))); if (scope.mode === 'chapter') return numbered.filter(ref => chapterNumber(ref.number) === scope.chapter); if (scope.mode === 'after') return numbered.filter(ref => Number(chapterNumber(ref.number)) > Number(scope.chapter)); if (scope.mode === 'volume') return list.filter(ref => volumeOf(ref) === scope.volume); if (scope.mode === 'ids') { const ids = new Set(scope.ids); return list.filter(ref => ids.has(String(ref.id))); } return []; } function scopeLabel(input) { const scope = normalizeScope(input); if (scope.mode === 'missing') return 'all missing chapters'; if (scope.mode === 'latest') return 'newest chapter'; if (scope.mode === 'last') return `last ${scope.count} chapters`; if (scope.mode === 'first') return `first ${scope.count} chapters`; if (scope.mode === 'chapter') return `chapter ${scope.chapter}`; if (scope.mode === 'after') return `chapters after ${scope.chapter}`; if (scope.mode === 'volume') return `volume ${scope.volume}`; return `${scope.ids.length} discovered chapters`; } module.exports = { normalizeScope, selectByScope, scopeLabel, chapterNumber, volumeOf };