import { sanitizeSsmlString } from "./SSMLExtractor.js";
/**
 * Accessibility Catalog Resolver Service
 *
 * Manages QTI 3.0 accessibility catalogs at both assessment and item levels.
 * Provides lookup and resolution services for alternative content representations.
 *
 * @example
 * ```typescript
 * // Initialize with assessment-level catalogs
 * const resolver = new AccessibilityCatalogResolver(assessment.accessibilityCatalogs);
 *
 * // Add item-level catalogs when rendering item
 * resolver.addItemCatalogs(item.accessibilityCatalogs);
 *
 * // Resolve catalog by identifier
 * const spokenContent = resolver.getAlternative('intro-passage', {
 *   type: 'spoken',
 *   language: 'en-US'
 * });
 *
 * // Check if catalog exists
 * if (resolver.hasCatalog('math-problem-1')) {
 *   const braille = resolver.getAlternative('math-problem-1', { type: 'braille' });
 * }
 * ```
 */
export class AccessibilityCatalogResolver {
    assessmentCatalogs = new Map();
    itemCatalogs = new Map();
    scopedCatalogs = new Map();
    // Matches the language the extractor tags cards with and the TTSService
    // passes on lookup ("en-US"), so the default-language fallback rung in
    // findMatchingCard is actually reachable for the common case.
    defaultLanguage = "en-US";
    // Egress sanitization cache. Catalogs are sanitized at registration, but that
    // is a no-op when indexing runs without a DOM (SSR). Sanitizing again as
    // spoken content leaves the resolver guarantees no raw author SSML reaches a
    // provider regardless of where indexing happened; the cache keeps it to one
    // pass per unique string (sanitizeSsmlString is idempotent).
    sanitizedSpokenCache = new Map();
    constructor(assessmentCatalogs, defaultLanguage = "en-US") {
        this.defaultLanguage = defaultLanguage;
        this.indexCatalogs(assessmentCatalogs ?? [], "assessment");
    }
    /**
     * Set the default language for fallback resolution
     */
    setDefaultLanguage(language) {
        this.defaultLanguage = language;
    }
    /**
     * Get the default language
     */
    getDefaultLanguage() {
        return this.defaultLanguage;
    }
    /**
     * Index catalogs into the appropriate map
     */
    indexCatalogs(catalogs, source) {
        const targetMap = source === "assessment" ? this.assessmentCatalogs : this.itemCatalogs;
        for (const catalog of this.sanitizeCatalogs(catalogs)) {
            if (targetMap.has(catalog.identifier)) {
                console.warn(`[AccessibilityCatalogResolver] Duplicate ${source} catalog "${catalog.identifier}" ignored`);
                continue;
            }
            targetMap.set(catalog.identifier, catalog);
        }
    }
    registerCatalogs(context, catalogs) {
        if (!catalogs || catalogs.length === 0)
            return () => { };
        const key = this.getOwnerKey(context);
        const existing = this.scopedCatalogs.get(key);
        const scoped = existing?.catalogs ?? new Map();
        const insertedIds = [];
        for (const catalog of this.sanitizeCatalogs(catalogs)) {
            if (scoped.has(catalog.identifier)) {
                console.warn(`[AccessibilityCatalogResolver] Duplicate scoped catalog "${catalog.identifier}" ignored for ${key}`);
                continue;
            }
            scoped.set(catalog.identifier, catalog);
            insertedIds.push(catalog.identifier);
        }
        if (!existing) {
            this.scopedCatalogs.set(key, {
                context: { ...context },
                catalogs: scoped,
            });
        }
        return () => {
            const current = this.scopedCatalogs.get(key);
            if (current?.catalogs !== scoped)
                return;
            for (const insertedId of insertedIds) {
                current.catalogs.delete(insertedId);
            }
            if (current.catalogs.size === 0) {
                this.scopedCatalogs.delete(key);
            }
        };
    }
    /**
     * Add item-level catalogs (called when rendering a new item)
     */
    addItemCatalogs(catalogs) {
        if (!catalogs || catalogs.length === 0)
            return;
        this.indexCatalogs(catalogs, "item");
    }
    /**
     * Clear item-level catalogs (called when leaving an item)
     */
    clearItemCatalogs() {
        this.itemCatalogs.clear();
    }
    /**
     * Check if a catalog exists (checks both assessment and item levels)
     */
    hasCatalog(catalogId) {
        return (this.itemCatalogs.has(catalogId) ||
            this.assessmentCatalogs.has(catalogId) ||
            Array.from(this.scopedCatalogs.values()).some((owner) => owner.catalogs.has(catalogId)));
    }
    /**
     * Get alternative content for a catalog identifier
     *
     * Priority: Item-level catalogs take precedence over assessment-level
     */
    getAlternative(catalogId, options) {
        const scopedCatalog = options.context
            ? this.scopedCatalogs
                .get(this.getOwnerKey(options.context))
                ?.catalogs.get(catalogId)
            : null;
        if (scopedCatalog) {
            const card = this.findMatchingCard(scopedCatalog, options);
            if (card)
                return this.resolveCard(catalogId, card, "item");
        }
        if (options.context) {
            const candidates = this.findScopedCandidates(catalogId, options.context);
            if (candidates.length === 1) {
                const card = this.findMatchingCard(candidates[0], options);
                if (card)
                    return this.resolveCard(catalogId, card, "item");
            }
            else if (candidates.length > 1) {
                console.warn(`[AccessibilityCatalogResolver] Ambiguous scoped catalog "${catalogId}" for owner context`, options.context);
                return null;
            }
        }
        // Check item-level first (higher precedence)
        const itemCatalog = this.itemCatalogs.get(catalogId);
        if (itemCatalog) {
            const card = this.findMatchingCard(itemCatalog, options);
            if (card)
                return this.resolveCard(catalogId, card, "item");
        }
        // Fallback to assessment-level
        const assessmentCatalog = this.assessmentCatalogs.get(catalogId);
        if (assessmentCatalog) {
            const card = this.findMatchingCard(assessmentCatalog, options);
            if (card)
                return this.resolveCard(catalogId, card, "assessment");
        }
        return null;
    }
    resolveCard(catalogId, card, source) {
        return {
            catalogId,
            type: card.catalog,
            language: card.language,
            content: card.catalog === "spoken"
                ? this.ensureSpokenSanitized(card.content)
                : card.content,
            source,
        };
    }
    ensureSpokenSanitized(content) {
        const cached = this.sanitizedSpokenCache.get(content);
        if (cached !== undefined)
            return cached;
        const sanitized = sanitizeSsmlString(content);
        this.sanitizedSpokenCache.set(content, sanitized);
        return sanitized;
    }
    getOwnerKey(context) {
        return [
            context.ownerKind,
            context.assessmentId || "",
            context.sectionId || "",
            context.canonicalItemId || "",
            context.itemId || "",
            context.passageId || "",
            context.modelId || "",
        ].join("|");
    }
    // Flattened [id, catalog] pairs for every context-scoped owner. The primary
    // runtime path registers catalogs as scoped, so enumeration APIs must include
    // these or they silently under-report what TTS content exists.
    scopedCatalogEntries() {
        const entries = [];
        for (const owner of this.scopedCatalogs.values()) {
            for (const entry of owner.catalogs.entries()) {
                entries.push(entry);
            }
        }
        return entries;
    }
    findScopedCandidates(catalogId, context) {
        return Array.from(this.scopedCatalogs.values())
            .filter((owner) => this.isCompatibleOwnerContext(owner.context, context))
            .map((owner) => owner.catalogs.get(catalogId))
            .filter((catalog) => Boolean(catalog));
    }
    isCompatibleOwnerContext(registered, lookup) {
        if (registered.ownerKind !== lookup.ownerKind)
            return false;
        if (lookup.assessmentId &&
            registered.assessmentId !== lookup.assessmentId) {
            return false;
        }
        if (lookup.sectionId && registered.sectionId !== lookup.sectionId) {
            return false;
        }
        if (lookup.canonicalItemId &&
            registered.canonicalItemId !== lookup.canonicalItemId) {
            return false;
        }
        if (lookup.itemId && registered.itemId !== lookup.itemId) {
            return false;
        }
        if (lookup.passageId && registered.passageId !== lookup.passageId) {
            return false;
        }
        if (lookup.modelId && registered.modelId !== lookup.modelId) {
            return false;
        }
        return true;
    }
    sanitizeCatalogs(catalogs) {
        return catalogs.map((catalog) => ({
            ...catalog,
            cards: catalog.cards.map((card) => ({
                ...card,
                content: card.catalog === "spoken"
                    ? sanitizeSsmlString(card.content)
                    : card.content,
            })),
        }));
    }
    /**
     * Find a matching catalog card based on lookup options
     */
    findMatchingCard(catalog, options) {
        const { type, language, useFallback = true } = options;
        // Try exact match (type + language)
        if (language) {
            const exactMatch = catalog.cards.find((card) => card.catalog === type && card.language === language);
            if (exactMatch)
                return exactMatch;
        }
        // Try type match with default language (if fallback enabled)
        if (useFallback) {
            const defaultMatch = catalog.cards.find((card) => card.catalog === type && card.language === this.defaultLanguage);
            if (defaultMatch)
                return defaultMatch;
            // Try type match without language constraint
            const typeMatch = catalog.cards.find((card) => card.catalog === type);
            if (typeMatch)
                return typeMatch;
        }
        return null;
    }
    /**
     * Get all available alternatives for a catalog identifier
     */
    getAllAlternatives(catalogId) {
        const results = [];
        // Add item-level alternatives
        const itemCatalog = this.itemCatalogs.get(catalogId);
        if (itemCatalog) {
            for (const card of itemCatalog.cards) {
                results.push({
                    catalogId,
                    type: card.catalog,
                    language: card.language,
                    content: card.content,
                    source: "item",
                });
            }
        }
        // Add assessment-level alternatives (if not already provided by item)
        const assessmentCatalog = this.assessmentCatalogs.get(catalogId);
        if (assessmentCatalog) {
            for (const card of assessmentCatalog.cards) {
                // Only add if not already provided by item catalog
                const exists = results.some((r) => r.type === card.catalog && r.language === card.language);
                if (!exists) {
                    results.push({
                        catalogId,
                        type: card.catalog,
                        language: card.language,
                        content: card.content,
                        source: "assessment",
                    });
                }
            }
        }
        // Add scoped (context-registered) alternatives. These resolve as "item"
        // in getAlternative, so report them the same way here.
        for (const [id, catalog] of this.scopedCatalogEntries()) {
            if (id !== catalogId)
                continue;
            for (const card of catalog.cards) {
                const exists = results.some((r) => r.type === card.catalog && r.language === card.language);
                if (!exists) {
                    results.push({
                        catalogId,
                        type: card.catalog,
                        language: card.language,
                        content: card.content,
                        source: "item",
                    });
                }
            }
        }
        return results;
    }
    /**
     * Get all catalog identifiers available (both assessment and item)
     */
    getAllCatalogIds() {
        const scopedIds = this.scopedCatalogEntries().map(([id]) => id);
        return Array.from(new Set([
            ...this.itemCatalogs.keys(),
            ...this.assessmentCatalogs.keys(),
            ...scopedIds,
        ]));
    }
    /**
     * Get statistics about available catalogs
     */
    getStatistics() {
        const allTypes = new Set();
        const allLanguages = new Set();
        // Collect from assessment catalogs
        for (const catalog of this.assessmentCatalogs.values()) {
            for (const card of catalog.cards) {
                allTypes.add(card.catalog);
                if (card.language)
                    allLanguages.add(card.language);
            }
        }
        // Collect from item catalogs
        for (const catalog of this.itemCatalogs.values()) {
            for (const card of catalog.cards) {
                allTypes.add(card.catalog);
                if (card.language)
                    allLanguages.add(card.language);
            }
        }
        return {
            totalCatalogs: this.assessmentCatalogs.size +
                this.itemCatalogs.size +
                Array.from(this.scopedCatalogs.values()).reduce((total, owner) => total + owner.catalogs.size, 0),
            assessmentCatalogs: this.assessmentCatalogs.size,
            itemCatalogs: this.itemCatalogs.size +
                Array.from(this.scopedCatalogs.values()).reduce((total, owner) => total + owner.catalogs.size, 0),
            availableTypes: allTypes,
            availableLanguages: allLanguages,
        };
    }
    /**
     * Check if a specific catalog type is available for a given catalog ID
     */
    hasAlternativeType(catalogId, type) {
        const alternatives = this.getAllAlternatives(catalogId);
        return alternatives.some((alt) => alt.type === type);
    }
    /**
     * Get all catalog IDs that have a specific type of alternative
     */
    getCatalogsByType(type) {
        const catalogIds = new Set();
        for (const [id, catalog] of this.assessmentCatalogs.entries()) {
            if (catalog.cards.some((card) => card.catalog === type)) {
                catalogIds.add(id);
            }
        }
        for (const [id, catalog] of this.itemCatalogs.entries()) {
            if (catalog.cards.some((card) => card.catalog === type)) {
                catalogIds.add(id);
            }
        }
        for (const [id, catalog] of this.scopedCatalogEntries()) {
            if (catalog.cards.some((card) => card.catalog === type)) {
                catalogIds.add(id);
            }
        }
        return Array.from(catalogIds);
    }
    /**
     * Reset all catalogs (both assessment and item)
     */
    reset() {
        this.assessmentCatalogs.clear();
        this.itemCatalogs.clear();
        this.scopedCatalogs.clear();
        this.sanitizedSpokenCache.clear();
    }
    /**
     * Destroy and cleanup
     */
    destroy() {
        this.reset();
    }
}
//# sourceMappingURL=AccessibilityCatalogResolver.js.map