import { CliError } from '@jackwener/opencli/errors';
export const BLOOMBERG_FEEDS = {
    main: 'https://feeds.bloomberg.com/news.rss',
    markets: 'https://feeds.bloomberg.com/markets/news.rss',
    economics: 'https://feeds.bloomberg.com/economics/news.rss',
    industries: 'https://feeds.bloomberg.com/industries/news.rss',
    tech: 'https://feeds.bloomberg.com/technology/news.rss',
    politics: 'https://feeds.bloomberg.com/politics/news.rss',
    opinions: 'https://feeds.bloomberg.com/bview/news.rss',
    green: 'https://feeds.bloomberg.com/green/news.rss',
    crypto: 'https://feeds.bloomberg.com/crypto/news.rss',
    pursuits: 'https://feeds.bloomberg.com/pursuits/news.rss',
};
// Note: the Businessweek RSS feed (feeds.bloomberg.com/businessweek/news.rss) is now served
// empty by Bloomberg, so the `businessweek` command reads the section page instead (see
// businessweek.js). Other sections still publish working RSS feeds.
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; opencli)';
// Bloomberg's edge occasionally serves a transient empty/non-OK RSS response under load; a
// couple of quick retries turn those intermittent misses into a successful fetch instead of a
// hard NOT_FOUND. A feed that is genuinely empty still surfaces NOT_FOUND after the retries.
export async function fetchBloombergFeed(name, limit = 1) {
    const feedUrl = BLOOMBERG_FEEDS[name];
    if (!feedUrl) {
        throw new CliError('ARGUMENT', `Unknown Bloomberg feed: ${name}`);
    }
    let lastError;
    for (let attempt = 0; attempt < 3; attempt += 1) {
        if (attempt > 0) {
            await new Promise((resolve) => setTimeout(resolve, 400 * attempt));
        }
        const resp = await fetch(feedUrl, {
            headers: { 'User-Agent': DEFAULT_USER_AGENT },
        });
        if (!resp.ok) {
            lastError = new CliError('FETCH_ERROR', `Bloomberg RSS HTTP ${resp.status}`, 'Bloomberg may be temporarily unavailable; try again later.');
            continue;
        }
        const xml = await resp.text();
        const items = parseBloombergRss(xml);
        if (items.length) {
            const count = Math.max(1, Math.min(Number(limit) || 1, 20));
            return items.slice(0, count);
        }
        lastError = new CliError('NOT_FOUND', 'Bloomberg RSS feed returned no items', 'Bloomberg may have changed the feed format.');
    }
    throw lastError;
}
export function parseBloombergRss(xml) {
    const items = [];
    const itemRegex = /<item\b[^>]*>([\s\S]*?)<\/item>/gi;
    let match;
    while ((match = itemRegex.exec(xml))) {
        const block = match[1];
        const title = extractTagText(block, 'title');
        const summary = extractTagText(block, 'description');
        const link = extractTagText(block, 'link') || extractTagText(block, 'guid');
        const mediaLinks = extractMediaLinksFromRssItem(block);
        if (!title || !link)
            continue;
        items.push({
            title,
            summary,
            link,
            mediaLinks,
        });
    }
    return items;
}
export function normalizeBloombergLink(input) {
    const raw = String(input || '').trim();
    if (!raw) {
        throw new CliError('ARGUMENT', 'A Bloomberg link is required');
    }
    if (raw.startsWith('/'))
        return `https://www.bloomberg.com${raw}`;
    return raw;
}
export function validateBloombergLink(input) {
    const normalized = normalizeBloombergLink(input);
    let url;
    try {
        url = new URL(normalized);
    }
    catch {
        throw new CliError('ARGUMENT', `Invalid Bloomberg link: ${input}`, 'Pass a full https://www.bloomberg.com/... URL or a relative Bloomberg path.');
    }
    if (!/(?:\.|^)bloomberg\.com$/i.test(url.hostname)) {
        throw new CliError('ARGUMENT', `Expected a bloomberg.com link, got: ${url.hostname}`, 'Pass a Bloomberg article URL from bloomberg.com.');
    }
    return url.toString();
}
export function renderStoryBody(body) {
    const blocks = Array.isArray(body?.content) ? body.content : [];
    const parts = blocks
        .map((block) => renderBlock(block, 0))
        .map((part) => normalizeBlockText(part))
        .filter(Boolean);
    return parts.join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
}
export function extractStoryMediaLinks(story) {
    const urls = new Set();
    collectMediaUrls(story?.ledeImageUrl, urls);
    collectMediaUrls(story?.socialImageUrl, urls);
    collectMediaUrls(story?.lede, urls);
    collectMediaUrls(story?.imageAttachments, urls);
    collectMediaUrls(story?.videoAttachments, urls);
    const mediaBlocks = Array.isArray(story?.body?.content)
        ? story.body.content.filter((block) => block?.type === 'media')
        : [];
    collectMediaUrls(mediaBlocks, urls);
    return [...urls];
}
function renderBlock(block, depth) {
    if (!block || typeof block !== 'object')
        return '';
    switch (block.type) {
        case 'paragraph':
            return renderInlineNodes(block.content || []);
        case 'heading': {
            const text = renderInlineNodes(block.content || []);
            if (!text)
                return '';
            const level = Number(block.data?.level ?? block.data?.weight ?? 2);
            const prefix = level <= 1 ? '# ' : level === 2 ? '## ' : '### ';
            return `${prefix}${text}`;
        }
        case 'blockquote': {
            const text = renderInlineNodes(block.content || []);
            if (!text)
                return '';
            return text.split('\n').map((line) => line ? `> ${line}` : '>').join('\n');
        }
        case 'list':
            return renderListBlock(block, depth);
        case 'tabularData':
            return renderTabularDataBlock(block);
        case 'media':
            return renderMediaBlock(block);
        case 'inline-newsletter':
        case 'newsletter':
        case 'ad':
            return '';
        default: {
            if (Array.isArray(block.content) && block.content.length > 0) {
                const inlineText = renderInlineNodes(block.content);
                if (inlineText)
                    return inlineText;
                const nested = block.content.map((child) => renderBlock(child, depth + 1)).filter(Boolean);
                if (nested.length)
                    return nested.join('\n');
            }
            return extractGenericText(block);
        }
    }
}
function renderInlineNodes(nodes) {
    return nodes.map((node) => renderInlineNode(node)).join('');
}
function renderInlineNode(node) {
    if (node == null)
        return '';
    if (typeof node === 'string')
        return decodeXmlEntities(node);
    switch (node.type) {
        case 'text':
            return decodeXmlEntities(node.value || '');
        case 'linebreak':
            return '\n';
        case 'link':
        case 'entity':
        case 'strong':
        case 'emphasis':
        case 'italic':
        case 'underline':
        case 'span':
            if (Array.isArray(node.content) && node.content.length > 0) {
                return renderInlineNodes(node.content);
            }
            return decodeXmlEntities(node.value || '');
        default:
            if (Array.isArray(node.content) && node.content.length > 0) {
                return renderInlineNodes(node.content);
            }
            if (typeof node.value === 'string')
                return decodeXmlEntities(node.value);
            return '';
    }
}
function renderListBlock(block, depth) {
    const items = Array.isArray(block.content) ? block.content : [];
    if (!items.length)
        return '';
    const listStyle = String(block.subType || block.data?.style || block.data?.listType || '');
    const ordered = /\bordered\b|\bnumber(?:ed)?\b/i.test(listStyle);
    let index = 1;
    return items
        .map((item) => {
        const prefix = ordered ? `${index++}. ` : '- ';
        return renderListItem(item, prefix, depth);
    })
        .filter(Boolean)
        .join('\n');
}
function renderListItem(item, prefix, depth) {
    const indent = '  '.repeat(depth);
    const body = normalizeBlockText(renderListItemBody(item, depth + 1));
    if (!body)
        return '';
    const lines = body.split('\n');
    const head = `${indent}${prefix}${lines[0]}`;
    if (lines.length === 1)
        return head;
    const continuationIndent = `${indent}${' '.repeat(prefix.length)}`;
    const tail = lines.slice(1).map((line) => `${continuationIndent}${line}`).join('\n');
    return `${head}\n${tail}`;
}
function renderListItemBody(item, depth) {
    if (!item || typeof item !== 'object')
        return '';
    if (item.type === 'list-item' && Array.isArray(item.content)) {
        const parts = item.content
            .map((child) => child?.type === 'paragraph'
            ? renderInlineNodes(child.content || [])
            : renderBlock(child, depth))
            .map((part) => normalizeBlockText(part))
            .filter(Boolean);
        return parts.join('\n');
    }
    return renderBlock(item, depth);
}
function renderTabularDataBlock(block) {
    const rows = block?.data?.rows ?? block?.data?.table?.rows ?? block?.content;
    if (!Array.isArray(rows) || !rows.length) {
        return extractGenericText(block.data || block.content || block);
    }
    const lines = rows
        .map((row) => extractGenericText(row))
        .map((line) => normalizeBlockText(line))
        .filter(Boolean);
    return lines.join('\n');
}
function renderMediaBlock(block) {
    const candidates = [
        block?.data?.chart?.caption,
        block?.data?.attachment?.caption,
        block?.data?.attachment?.title,
        block?.data?.attachment?.subtitle,
        block?.data?.video?.caption,
    ];
    const caption = candidates
        .map((value) => normalizeBlockText(stripHtml(String(value || ''))))
        .find(Boolean);
    return caption || '';
}
function extractGenericText(value) {
    const parts = [];
    collectText(value, parts);
    return parts.join(' ').replace(/\s+/g, ' ').trim();
}
function collectText(value, out) {
    if (value == null)
        return;
    if (typeof value === 'string') {
        const text = normalizeBlockText(stripHtml(decodeXmlEntities(value)));
        if (text)
            out.push(text);
        return;
    }
    if (Array.isArray(value)) {
        for (const item of value)
            collectText(item, out);
        return;
    }
    if (typeof value === 'object') {
        if (typeof value.value === 'string') {
            const text = normalizeBlockText(stripHtml(decodeXmlEntities(value.value)));
            if (text)
                out.push(text);
            return;
        }
        if (Array.isArray(value.content)) {
            collectText(value.content, out);
            return;
        }
        for (const entry of Object.values(value))
            collectText(entry, out);
    }
}
function extractTagText(block, tag) {
    const safeTag = escapeRegExp(tag);
    const match = block.match(new RegExp(`<${safeTag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${safeTag}>`, 'i'));
    if (!match)
        return '';
    return normalizeBlockText(stripHtml(decodeXmlEntities(stripCdata(match[1]))));
}
function extractMediaLinksFromRssItem(block) {
    const links = new Set();
    const mediaRegex = /<(?:media:content|media:thumbnail|enclosure)\b[^>]*\burl="([^"]+)"[^>]*>/gi;
    let match;
    while ((match = mediaRegex.exec(block))) {
        const url = decodeXmlEntities(match[1] || '').trim();
        if (url)
            links.add(url);
    }
    return [...links];
}
function collectMediaUrls(value, out, seen = new WeakSet()) {
    if (value == null)
        return;
    if (typeof value === 'string') {
        const normalized = normalizeMediaUrl(value);
        if (normalized)
            out.add(normalized);
        return;
    }
    if (Array.isArray(value)) {
        for (const item of value)
            collectMediaUrls(item, out, seen);
        return;
    }
    if (typeof value === 'object') {
        if (seen.has(value))
            return;
        seen.add(value);
        for (const key of ['url', 'src', 'fallback', 'poster']) {
            const candidate = value[key];
            if (typeof candidate === 'string') {
                const normalized = normalizeMediaUrl(candidate);
                if (normalized)
                    out.add(normalized);
            }
        }
        for (const entry of Object.values(value)) {
            collectMediaUrls(entry, out, seen);
        }
    }
}
function normalizeMediaUrl(value) {
    const url = decodeXmlEntities(String(value || '')).trim();
    if (!/^https?:\/\//i.test(url))
        return null;
    if (!looksLikeMediaUrl(url))
        return null;
    return url;
}
function looksLikeMediaUrl(url) {
    return /(?:assets\.bwbx\.io|resource\.bloomberg\.com|media\.bloomberg\.com)/i.test(url)
        || /\.(?:jpg|jpeg|png|webp|gif|svg|mp4|m3u8)(?:[?#].*)?$/i.test(url);
}
function stripCdata(value) {
    const match = value.match(/^<!\[CDATA\[([\s\S]*?)\]\]>$/);
    return match ? match[1] : value;
}
function stripHtml(value) {
    return String(value || '').replace(/<[^>]+>/g, ' ');
}
function decodeXmlEntities(value) {
    return String(value || '')
        .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
        .replace(/&#(\d+);/g, (_m, code) => String.fromCodePoint(Number(code)))
        .replace(/&#x([0-9a-f]+);/gi, (_m, code) => String.fromCodePoint(parseInt(code, 16)))
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&apos;/g, "'")
        .replace(/&nbsp;/g, ' ');
}
function normalizeBlockText(value) {
    return String(value || '')
        .replace(/\r/g, '')
        .replace(/[ \t]+\n/g, '\n')
        .replace(/\n[ \t]+/g, '\n')
        .replace(/[ \t]{2,}/g, ' ')
        .trim();
}
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
