import { McpError, validationError } from '../../types-global/errors.js';
import { lazyImport } from '../../utils/internal/lazyImport.js';
import { logger } from '../../utils/internal/logger.js';
import { requestContextService } from '../../utils/internal/requestContext.js';
const getDefuddle = lazyImport(() => import('defuddle/node'), 'Install "defuddle" to use HTML article extraction: bun add defuddle linkedom');
const getLinkedom = lazyImport(() => import('linkedom'), 'Install "linkedom" to use HTML article extraction: bun add defuddle linkedom');
/** Flattens defuddle's `MetaTagItem[]` into a `Record<string, string>` keyed
 *  by `name ?? property`. Items without a usable key or content are skipped;
 *  returns `undefined` if nothing usable is left so callers can omit the
 *  field from the result entirely. */
function flattenMetaTags(tags) {
    if (!tags || tags.length === 0)
        return;
    const out = {};
    for (const tag of tags) {
        const key = tag.name ?? tag.property;
        if (!key || tag.content == null)
            continue;
        out[key] = tag.content;
    }
    return Object.keys(out).length > 0 ? out : undefined;
}
/**
 * Utility class for extracting main article content from raw HTML.
 *
 * Lazily loads `defuddle` and `linkedom` on first use — both are optional peer
 * dependencies (`bun add defuddle linkedom`). Returns cleaned main content
 * plus best-effort metadata: title, author, description, Open Graph fields,
 * schema.org data, word count.
 *
 * Does not guarantee structure beyond "main content of the page." For quirky
 * pages, malformed HTML, or SPA shells with minimal server-rendered content,
 * the result may be sparse — callers should degrade gracefully.
 */
export class HtmlExtractor {
    /**
     * Extracts the main article content from an HTML string.
     *
     * Async due to lazy loading of `defuddle` and `linkedom`, and because
     * Defuddle's node entry is itself async (supports async fallback extractors
     * gated by `useAsync`).
     *
     * @param html - Raw HTML string to extract from.
     * @param options - Optional extraction options (format, URL, content selector, etc.).
     * @param context - Optional `RequestContext` for correlated logging and error metadata.
     * @returns Extracted content and metadata. Only `content` is guaranteed to
     *   be present; all other fields are best-effort.
     * @throws {McpError} With `ConfigurationError` if `defuddle` or `linkedom` is not installed.
     * @throws {McpError} With `ValidationError` if the HTML string is empty after trimming,
     *   or if `defuddle` fails to parse the page.
     *
     * @example
     * ```typescript
     * import { htmlExtractor } from '../../utils/parsing/htmlExtractor.js';
     *
     * const html = await fetch('https://example.com/article').then((r) => r.text());
     * const result = await htmlExtractor.extract(html, {
     *   url: 'https://example.com/article',
     *   format: 'markdown',
     * });
     *
     * console.log(result.title);
     * console.log(result.content);
     * ```
     */
    async extract(html, options, context) {
        const logContext = context ??
            requestContextService.createRequestContext({
                operation: 'HtmlExtractor.extract',
            });
        const trimmed = html.trim();
        if (!trimmed) {
            throw validationError('HTML string is empty.', context);
        }
        const [{ Defuddle }, { parseHTML }] = await Promise.all([getDefuddle(), getLinkedom()]);
        const format = options?.format ?? 'markdown';
        const defuddleOptions = {
            markdown: format === 'markdown',
            useAsync: options?.useAsync ?? false,
            ...(options?.contentSelector !== undefined && {
                contentSelector: options.contentSelector,
            }),
            ...(options?.removeImages !== undefined && {
                removeImages: options.removeImages,
            }),
            ...(options?.debug !== undefined && { debug: options.debug }),
            ...(options?.language !== undefined && { language: options.language }),
        };
        logger.debug('Extracting article content from HTML.', {
            ...logContext,
            byteLength: trimmed.length,
            format,
            hasUrl: Boolean(options?.url),
            hasContentSelector: Boolean(options?.contentSelector),
        });
        try {
            const { document } = parseHTML(trimmed);
            const result = await Defuddle(document, options?.url, defuddleOptions);
            logger.debug('Successfully extracted article.', {
                ...logContext,
                wordCount: result.wordCount,
                titlePresent: Boolean(result.title),
                parseTimeMs: result.parseTime,
            });
            const out = { content: result.content ?? '' };
            if (result.title)
                out.title = result.title;
            if (result.author)
                out.author = result.author;
            if (result.description)
                out.description = result.description;
            if (result.domain)
                out.domain = result.domain;
            if (result.favicon)
                out.favicon = result.favicon;
            if (result.image)
                out.image = result.image;
            if (result.language)
                out.language = result.language;
            if (result.published)
                out.published = result.published;
            if (result.site)
                out.site = result.site;
            if (typeof result.parseTime === 'number')
                out.parseTime = result.parseTime;
            if (typeof result.wordCount === 'number')
                out.wordCount = result.wordCount;
            if (result.schemaOrgData)
                out.schemaOrgData = result.schemaOrgData;
            const metaTags = flattenMetaTags(result.metaTags);
            if (metaTags)
                out.metaTags = metaTags;
            return out;
        }
        catch (e) {
            if (e instanceof McpError)
                throw e;
            const error = e instanceof Error ? e : new Error(String(e));
            logger.error('Failed to extract article from HTML.', {
                ...logContext,
                errorDetails: error.message,
            });
            throw validationError(`Failed to extract article from HTML: ${error.message}`, {
                ...context,
                rawError: error.stack ?? String(error),
            });
        }
    }
}
/**
 * Singleton instance of {@link HtmlExtractor}.
 *
 * Prefer this over constructing a new `HtmlExtractor` directly. Lazily loads
 * `defuddle` and `linkedom` on first call, so there is no startup cost if
 * HTML extraction is never used.
 *
 * @example
 * ```typescript
 * import { htmlExtractor } from '../../utils/parsing/htmlExtractor.js';
 *
 * const article = await htmlExtractor.extract(rawHtml, {
 *   url: 'https://example.com/post',
 *   format: 'markdown',
 * });
 *
 * // Hand the content + metadata to the LLM
 * llm.prompt({ title: article.title, body: article.content });
 * ```
 */
export const htmlExtractor = new HtmlExtractor();
//# sourceMappingURL=htmlExtractor.js.map