// @ts-nocheck

import { eventSource, event_types } from "../../../../../script.js";
import { getContext } from "../../../../st-context.js";
import { xbLog } from "../core/debug-core.js";
import {
    loadWorldInfo,
    saveWorldInfo,
    reloadEditor,
    updateWorldInfoList,
    createNewWorldInfo,
    createWorldInfoEntry,
    deleteWorldInfoEntry,
    newWorldInfoEntryTemplate,
    setWIOriginalDataValue,
    originalWIDataKeyMap,
    METADATA_KEY,
    world_info,
    selected_world_info,
    world_names,
    onWorldInfoChange,
} from "../../../../world-info.js";
import { getCharaFilename, findChar } from "../../../../utils.js";

const SOURCE_TAG = "xiaobaix-host";
const resolveTargetOrigin = (origin) => {
    if (typeof origin === 'string' && origin) return origin;
    try { return window.location.origin; } catch { return '*'; }
};

function isString(value) {
    return typeof value === 'string';
}

function parseStringArray(input) {
    if (input === undefined || input === null) return [];
    const str = String(input).trim();
    try {
        if (str.startsWith('[')) {
            const arr = JSON.parse(str);
            return Array.isArray(arr) ? arr.map(x => String(x).trim()).filter(Boolean) : [];
        }
    } catch {}
    return str.split(',').map(x => x.trim()).filter(Boolean);
}

function isTrueBoolean(value) {
    const v = String(value).trim().toLowerCase();
    return v === 'true' || v === '1' || v === 'on' || v === 'yes';
}

function isFalseBoolean(value) {
    const v = String(value).trim().toLowerCase();
    return v === 'false' || v === '0' || v === 'off' || v === 'no';
}

function ensureTimedWorldInfo(ctx) {
    if (!ctx.chatMetadata.timedWorldInfo) ctx.chatMetadata.timedWorldInfo = {};
    return ctx.chatMetadata.timedWorldInfo;
}

class WorldbookBridgeService {
    constructor() {
        this._listener = null;
        this._forwardEvents = false;
        this._attached = false;
        this._allowedOrigins = ['*']; // Default: allow all origins
    }

    setAllowedOrigins(origins) {
        this._allowedOrigins = Array.isArray(origins) ? origins : [origins];
    }

    isOriginAllowed(origin) {
        if (this._allowedOrigins.includes('*')) return true;
        return this._allowedOrigins.some(allowed => {
            if (allowed === origin) return true;
            // Support wildcard subdomains like *.example.com
            if (allowed.startsWith('*.')) {
                const domain = allowed.slice(2);
                return origin.endsWith('.' + domain) || origin === domain;
            }
            return false;
        });
    }

    normalizeError(err, fallbackCode = 'API_ERROR', details = null) {
        try {
            if (!err) return { code: fallbackCode, message: 'Unknown error', details };
            if (typeof err === 'string') return { code: fallbackCode, message: err, details };
            const msg = err?.message || String(err);
            return { code: fallbackCode, message: msg, details };
        } catch {
            return { code: fallbackCode, message: 'Error serialization failed', details };
        }
    }

    sendResult(target, requestId, result, targetOrigin = null) {
        try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookResult', id: requestId, result }, resolveTargetOrigin(targetOrigin)); } catch {}
    }

    sendError(target, requestId, err, fallbackCode = 'API_ERROR', details = null, targetOrigin = null) {
        const e = this.normalizeError(err, fallbackCode, details);
        try { target?.postMessage({ source: SOURCE_TAG, type: 'worldbookError', id: requestId, error: e }, resolveTargetOrigin(targetOrigin)); } catch {}
    }

    postEvent(event, payload) {
        try { window?.postMessage({ source: SOURCE_TAG, type: 'worldbookEvent', event, payload }, resolveTargetOrigin()); } catch {}
    }

    async ensureWorldExists(name, autoCreate) {
        if (!isString(name) || !name.trim()) throw new Error('MISSING_PARAMS');
        if (world_names?.includes(name)) return name;
        if (!autoCreate) throw new Error(`Worldbook not found: ${name}`);
        await createNewWorldInfo(name, { interactive: false });
        await updateWorldInfoList();
        return name;
    }

    // ===== Basic actions =====
    async getChatBook(params) {
        const ctx = getContext();
        const name = ctx.chatMetadata?.[METADATA_KEY];
        if (name && world_names?.includes(name)) return name;
        const desired = isString(params?.name) ? String(params.name) : null;
        const newName = desired && !world_names.includes(desired)
            ? desired
            : `Chat Book ${ctx.getCurrentChatId?.() || ''}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64);
        await createNewWorldInfo(newName, { interactive: false });
        ctx.chatMetadata[METADATA_KEY] = newName;
        await ctx.saveMetadata();
        return newName;
    }

    async getGlobalBooks() {
        if (!selected_world_info?.length) return JSON.stringify([]);
        return JSON.stringify(selected_world_info.slice());
    }

    async listWorldbooks() {
        return Array.isArray(world_names) ? world_names.slice() : [];
    }

    async getPersonaBook() {
        const ctx = getContext();
        return ctx.powerUserSettings?.persona_description_lorebook || '';
    }

    async getCharBook(params) {
        const ctx = getContext();
        const type = String(params?.type ?? 'primary').toLowerCase();
        let characterName = params?.name ?? null;
        if (!characterName) {
            const active = ctx.characters?.[ctx.characterId];
            characterName = active?.avatar || active?.name || '';
        }
        const character = findChar({ name: characterName, allowAvatar: true, preferCurrentChar: false, quiet: true });
        if (!character) return type === 'primary' ? '' : JSON.stringify([]);

        const books = [];
        if (type === 'all' || type === 'primary') {
            books.push(character.data?.extensions?.world);
        }
        if (type === 'all' || type === 'additional') {
            const fileName = getCharaFilename(null, { manualAvatarKey: character.avatar });
            const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
            if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) books.push(...extraCharLore.extraBooks);
        }
        if (type === 'primary') return books[0] ?? '';
        return JSON.stringify(books.filter(Boolean));
    }

    async world(params) {
        const state = params?.state ?? undefined; // 'on'|'off'|'toggle'|undefined
        const silent = !!params?.silent;
        const name = isString(params?.name) ? params.name : '';
        // Use internal callback to ensure parity with STscript behavior
        await onWorldInfoChange({ state, silent }, name);
        return '';
    }

    // ===== Entries =====
    async findEntry(params) {
        const file = params?.file;
        const field = params?.field || 'key';
        const text = String(params?.text ?? '').trim();
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) return '';
        const entries = Object.values(data.entries);
        if (!entries.length) return '';

        let needle = text;
        if (typeof newWorldInfoEntryTemplate[field] === 'boolean') {
            if (isTrueBoolean(text)) needle = 'true';
            else if (isFalseBoolean(text)) needle = 'false';
        }

        let FuseRef = null;
        try { FuseRef = window?.Fuse || Fuse; } catch {}
        if (FuseRef) {
            const fuse = new FuseRef(entries, { keys: [{ name: field, weight: 1 }], includeScore: true, threshold: 0.3 });
            const results = fuse.search(needle);
            const uid = results?.[0]?.item?.uid;
            return uid === undefined ? '' : String(uid);
        } else {
            // Fallback: simple includes on stringified field
            const f = entries.find(e => String((Array.isArray(e[field]) ? e[field].join(' ') : e[field]) ?? '').toLowerCase().includes(needle.toLowerCase()));
            return f?.uid !== undefined ? String(f.uid) : '';
        }
    }

    async getEntryField(params) {
        const file = params?.file;
        const field = params?.field || 'content';
        const uid = String(params?.uid ?? '').trim();
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) return '';
        const entry = data.entries[uid];
        if (!entry) return '';
        if (newWorldInfoEntryTemplate[field] === undefined) return '';

        const ctx = getContext();
        const tags = ctx.tags || [];

        let fieldValue;
        switch (field) {
            case 'characterFilterNames':
                fieldValue = entry.characterFilter ? entry.characterFilter.names : undefined;
                if (Array.isArray(fieldValue)) {
                    // Map avatar keys back to friendly names if possible (best-effort)
                    return JSON.stringify(fieldValue.slice());
                }
                break;
            case 'characterFilterTags':
                fieldValue = entry.characterFilter ? entry.characterFilter.tags : undefined;
                if (!Array.isArray(fieldValue)) return '';
                return JSON.stringify(tags.filter(tag => fieldValue.includes(tag.id)).map(tag => tag.name));
            case 'characterFilterExclude':
                fieldValue = entry.characterFilter ? entry.characterFilter.isExclude : undefined;
                break;
            default:
                fieldValue = entry[field];
        }

        if (fieldValue === undefined) return '';
        if (Array.isArray(fieldValue)) return JSON.stringify(fieldValue.map(x => String(x)));
        return String(fieldValue);
    }

    async setEntryField(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const field = params?.field || 'content';
        let value = params?.value;
        if (value === undefined) throw new Error('MISSING_PARAMS');
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');

        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = data.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');
        if (newWorldInfoEntryTemplate[field] === undefined) throw new Error('VALIDATION_FAILED: field');

        const ctx = getContext();
        const tags = ctx.tags || [];

        const ensureCharacterFilterObject = () => {
            if (!entry.characterFilter) {
                Object.assign(entry, { characterFilter: { isExclude: false, names: [], tags: [] } });
            }
        };

        // Unescape escaped special chars (compat with STscript input style)
        value = String(value).replace(/\\([{}|])/g, '$1');

        switch (field) {
            case 'characterFilterNames': {
                ensureCharacterFilterObject();
                const names = parseStringArray(value);
                const avatars = names
                    .map((name) => findChar({ name, allowAvatar: true, preferCurrentChar: false, quiet: true })?.avatar)
                    .filter(Boolean);
                // Convert to canonical filenames
                entry.characterFilter.names = avatars
                    .map((avatarKey) => getCharaFilename(null, { manualAvatarKey: avatarKey }))
                    .filter(Boolean);
                setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
                break;
            }
            case 'characterFilterTags': {
                ensureCharacterFilterObject();
                const tagNames = parseStringArray(value);
                entry.characterFilter.tags = tags.filter((t) => tagNames.includes(t.name)).map((t) => t.id);
                setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
                break;
            }
            case 'characterFilterExclude': {
                ensureCharacterFilterObject();
                entry.characterFilter.isExclude = isTrueBoolean(value);
                setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
                break;
            }
            default: {
                if (Array.isArray(entry[field])) {
                    entry[field] = parseStringArray(value);
                } else if (typeof entry[field] === 'boolean') {
                    entry[field] = isTrueBoolean(value);
                } else if (typeof entry[field] === 'number') {
                    entry[field] = Number(value);
                } else {
                    entry[field] = String(value);
                }
                if (originalWIDataKeyMap[field]) {
                    setWIOriginalDataValue(data, uid, originalWIDataKeyMap[field], entry[field]);
                }
                break;
            }
        }

        await saveWorldInfo(file, data, true);
        reloadEditor(file);
        this.postEvent('ENTRY_UPDATED', { file, uid, fields: [field] });
        return '';
    }

    async createEntry(params) {
        const file = params?.file;
        const key = params?.key;
        const content = params?.content;
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = createWorldInfoEntry(file, data);
        if (key) { entry.key.push(String(key)); entry.addMemo = true; entry.comment = String(key); }
        if (content) entry.content = String(content);
        await saveWorldInfo(file, data, true);
        reloadEditor(file);
        this.postEvent('ENTRY_CREATED', { file, uid: entry.uid });
        return String(entry.uid);
    }

    async listEntries(params) {
        const file = params?.file;
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) return [];
        return Object.values(data.entries).map(e => ({
            uid: e.uid,
            comment: e.comment || '',
            key: Array.isArray(e.key) ? e.key.slice() : [],
            keysecondary: Array.isArray(e.keysecondary) ? e.keysecondary.slice() : [],
            position: e.position,
            depth: e.depth,
            order: e.order,
            probability: e.probability,
            useProbability: !!e.useProbability,
            disable: !!e.disable,
        }));
    }

    async deleteEntry(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const ok = await deleteWorldInfoEntry(data, uid, { silent: true });
        if (ok) {
            await saveWorldInfo(file, data, true);
            reloadEditor(file);
            this.postEvent('ENTRY_DELETED', { file, uid });
        }
        return ok ? 'ok' : '';
    }

    // ===== Enhanced Entry Operations =====
    async getEntryAll(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = data.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');

        const result = {};

        // Get all template fields
        for (const field of Object.keys(newWorldInfoEntryTemplate)) {
            try {
                result[field] = await this.getEntryField({ file, uid, field });
            } catch {
                result[field] = '';
            }
        }

        return result;
    }

    async batchSetEntryFields(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const fields = params?.fields || {};
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        if (typeof fields !== 'object' || !fields) throw new Error('VALIDATION_FAILED: fields must be object');

        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = data.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');

        // Apply all field changes
        for (const [field, value] of Object.entries(fields)) {
            try {
                await this.setEntryField({ file, uid, field, value });
            } catch (err) {
                // Continue with other fields, but collect errors
                console.warn(`Failed to set field ${field}:`, err);
            }
        }

        this.postEvent('ENTRY_UPDATED', { file, uid, fields: Object.keys(fields) });
        return 'ok';
    }

    async cloneEntry(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const newKey = params?.newKey;
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');

        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const sourceEntry = data.entries[uid];
        if (!sourceEntry) throw new Error('NOT_FOUND');

        // Create new entry with same data
        const newEntry = createWorldInfoEntry(file, data);
        
        // Copy all fields from source (except uid which is auto-generated)
        for (const [key, value] of Object.entries(sourceEntry)) {
            if (key !== 'uid') {
                if (Array.isArray(value)) {
                    newEntry[key] = value.slice();
                } else if (typeof value === 'object' && value !== null) {
                    newEntry[key] = JSON.parse(JSON.stringify(value));
                } else {
                    newEntry[key] = value;
                }
            }
        }

        // Update key if provided
        if (newKey) {
            newEntry.key = [String(newKey)];
            newEntry.comment = `Copy of: ${String(newKey)}`;
        } else if (sourceEntry.comment) {
            newEntry.comment = `Copy of: ${sourceEntry.comment}`;
        }

        await saveWorldInfo(file, data, true);
        reloadEditor(file);
        this.postEvent('ENTRY_CREATED', { file, uid: newEntry.uid, clonedFrom: uid });
        return String(newEntry.uid);
    }

    async moveEntry(params) {
        const sourceFile = params?.sourceFile;
        const targetFile = params?.targetFile;
        const uid = String(params?.uid ?? '').trim();
        if (!sourceFile || !world_names.includes(sourceFile)) throw new Error('VALIDATION_FAILED: sourceFile');
        if (!targetFile || !world_names.includes(targetFile)) throw new Error('VALIDATION_FAILED: targetFile');

        const sourceData = await loadWorldInfo(sourceFile);
        const targetData = await loadWorldInfo(targetFile);
        if (!sourceData?.entries || !targetData?.entries) throw new Error('NOT_FOUND');
        
        const entry = sourceData.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');

        // Create new entry in target with same data
        const newEntry = createWorldInfoEntry(targetFile, targetData);
        for (const [key, value] of Object.entries(entry)) {
            if (key !== 'uid') {
                if (Array.isArray(value)) {
                    newEntry[key] = value.slice();
                } else if (typeof value === 'object' && value !== null) {
                    newEntry[key] = JSON.parse(JSON.stringify(value));
                } else {
                    newEntry[key] = value;
                }
            }
        }

        // Remove from source
        delete sourceData.entries[uid];

        // Save both files
        await saveWorldInfo(sourceFile, sourceData, true);
        await saveWorldInfo(targetFile, targetData, true);
        reloadEditor(sourceFile);
        reloadEditor(targetFile);

        this.postEvent('ENTRY_MOVED', { 
            sourceFile, 
            targetFile, 
            oldUid: uid, 
            newUid: newEntry.uid 
        });
        return String(newEntry.uid);
    }

    async reorderEntry(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const newOrder = Number(params?.newOrder ?? 0);
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');

        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = data.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');

        entry.order = newOrder;
        setWIOriginalDataValue(data, uid, 'order', newOrder);

        await saveWorldInfo(file, data, true);
        reloadEditor(file);
        this.postEvent('ENTRY_UPDATED', { file, uid, fields: ['order'] });
        return 'ok';
    }

    // ===== File-level Operations =====
    async renameWorldbook(params) {
        const oldName = params?.oldName;
        const newName = params?.newName;
        if (!oldName || !world_names.includes(oldName)) throw new Error('VALIDATION_FAILED: oldName');
        if (!newName || world_names.includes(newName)) throw new Error('VALIDATION_FAILED: newName already exists');

        // This is a complex operation that would require ST core support
        // For now, we'll throw an error indicating it's not implemented
        throw new Error('NOT_IMPLEMENTED: renameWorldbook requires ST core support');
    }

    async deleteWorldbook(params) {
        const name = params?.name;
        if (!name || !world_names.includes(name)) throw new Error('VALIDATION_FAILED: name');

        // This is a complex operation that would require ST core support
        // For now, we'll throw an error indicating it's not implemented
        throw new Error('NOT_IMPLEMENTED: deleteWorldbook requires ST core support');
    }

    async exportWorldbook(params) {
        const file = params?.file;
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        
        const data = await loadWorldInfo(file);
        if (!data) throw new Error('NOT_FOUND');
        
        return JSON.stringify(data, null, 2);
    }

    async importWorldbook(params) {
        const name = params?.name;
        const jsonData = params?.data;
        const overwrite = !!params?.overwrite;
        
        if (!name) throw new Error('VALIDATION_FAILED: name');
        if (!jsonData) throw new Error('VALIDATION_FAILED: data');
        
        if (world_names.includes(name) && !overwrite) {
            throw new Error('VALIDATION_FAILED: worldbook exists and overwrite=false');
        }
        
        let data;
        try {
            data = JSON.parse(jsonData);
        } catch {
            throw new Error('VALIDATION_FAILED: invalid JSON data');
        }
        
        if (!world_names.includes(name)) {
            await createNewWorldInfo(name, { interactive: false });
            await updateWorldInfoList();
        }
        
        await saveWorldInfo(name, data, true);
        reloadEditor(name);
        this.postEvent('WORLDBOOK_IMPORTED', { name });
        return 'ok';
    }

    // ===== Timed effects (minimal parity) =====
    async wiGetTimedEffect(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const effect = String(params?.effect ?? '').trim().toLowerCase(); // 'sticky'|'cooldown'
        const format = String(params?.format ?? 'bool').trim().toLowerCase(); // 'bool'|'number'
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        if (!uid) throw new Error('MISSING_PARAMS');
        if (!['sticky', 'cooldown'].includes(effect)) throw new Error('VALIDATION_FAILED: effect');
        const ctx = getContext();
        const key = `${file}.${uid}`;
        const t = ensureTimedWorldInfo(ctx);
        const store = t[effect] || {};
        const meta = store[key];
        if (format === 'number') {
            const remaining = meta ? Math.max(0, Number(meta.end || 0) - (ctx.chat?.length || 0)) : 0;
            return String(remaining);
        }
        return String(!!meta);
    }

    async wiSetTimedEffect(params) {
        const file = params?.file;
        const uid = String(params?.uid ?? '').trim();
        const effect = String(params?.effect ?? '').trim().toLowerCase(); // 'sticky'|'cooldown'
        let value = params?.value; // 'toggle'|'true'|'false'|boolean
        if (!file || !world_names.includes(file)) throw new Error('VALIDATION_FAILED: file');
        if (!uid) throw new Error('MISSING_PARAMS');
        if (!['sticky', 'cooldown'].includes(effect)) throw new Error('VALIDATION_FAILED: effect');
        const data = await loadWorldInfo(file);
        if (!data || !data.entries) throw new Error('NOT_FOUND');
        const entry = data.entries[uid];
        if (!entry) throw new Error('NOT_FOUND');
        if (!entry[effect]) throw new Error('VALIDATION_FAILED: entry has no effect configured');

        const ctx = getContext();
        const key = `${file}.${uid}`;
        const t = ensureTimedWorldInfo(ctx);
        if (!t[effect] || typeof t[effect] !== 'object') t[effect] = {};
        const store = t[effect];
        const current = !!store[key];

        let newState;
        const vs = String(value ?? '').trim().toLowerCase();
        if (vs === 'toggle' || vs === '') newState = !current;
        else if (isTrueBoolean(vs)) newState = true;
        else if (isFalseBoolean(vs)) newState = false;
        else newState = current;

        if (newState) {
            const duration = Number(entry[effect]) || 0;
            store[key] = { end: (ctx.chat?.length || 0) + duration, world: file, uid };
        } else {
            delete store[key];
        }
        await ctx.saveMetadata();
        return '';
    }

    // ===== Bind / Unbind =====
    async bindWorldbookToChat(params) {
        const name = await this.ensureWorldExists(params?.worldbookName, !!params?.autoCreate);
        const ctx = getContext();
        ctx.chatMetadata[METADATA_KEY] = name;
        await ctx.saveMetadata();
        return { name };
    }

    async unbindWorldbookFromChat() {
        const ctx = getContext();
        delete ctx.chatMetadata[METADATA_KEY];
        await ctx.saveMetadata();
        return { name: '' };
    }

    async bindWorldbookToCharacter(params) {
        const ctx = getContext();
        const target = String(params?.target ?? 'primary').toLowerCase();
        const name = await this.ensureWorldExists(params?.worldbookName, !!params?.autoCreate);

        const charName = params?.character?.name || ctx.characters?.[ctx.characterId]?.avatar || ctx.characters?.[ctx.characterId]?.name;
        const character = findChar({ name: charName, allowAvatar: true, preferCurrentChar: true, quiet: true });
        if (!character) throw new Error('NOT_FOUND: character');

        if (target === 'primary') {
            if (typeof ctx.writeExtensionField === 'function') {
                await ctx.writeExtensionField('world', name);
            } else {
                // Fallback: set on active character only
                const active = ctx.characters?.[ctx.characterId];
                if (active) {
                    active.data = active.data || {};
                    active.data.extensions = active.data.extensions || {};
                    active.data.extensions.world = name;
                }
            }
            return { primary: name };
        }

        // additional => world_info.charLore
        const fileName = getCharaFilename(null, { manualAvatarKey: character.avatar });
        let list = world_info.charLore || [];
        const idx = list.findIndex(e => e.name === fileName);
        if (idx === -1) {
            list.push({ name: fileName, extraBooks: [name] });
        } else {
            const eb = new Set(list[idx].extraBooks || []);
            eb.add(name);
            list[idx].extraBooks = Array.from(eb);
        }
        world_info.charLore = list;
        getContext().saveSettingsDebounced?.();
        return { additional: (world_info.charLore.find(e => e.name === fileName)?.extraBooks) || [name] };
    }

    async unbindWorldbookFromCharacter(params) {
        const ctx = getContext();
        const target = String(params?.target ?? 'primary').toLowerCase();
        const name = isString(params?.worldbookName) ? params.worldbookName : null;
        const charName = params?.character?.name || ctx.characters?.[ctx.characterId]?.avatar || ctx.characters?.[ctx.characterId]?.name;
        const character = findChar({ name: charName, allowAvatar: true, preferCurrentChar: true, quiet: true });
        if (!character) throw new Error('NOT_FOUND: character');

        const result = {};
        if (target === 'primary' || target === 'all') {
            if (typeof ctx.writeExtensionField === 'function') {
                await ctx.writeExtensionField('world', '');
            } else {
                const active = ctx.characters?.[ctx.characterId];
                if (active?.data?.extensions) active.data.extensions.world = '';
            }
            result.primary = '';
        }

        if (target === 'additional' || target === 'all') {
            const fileName = getCharaFilename(null, { manualAvatarKey: character.avatar });
            let list = world_info.charLore || [];
            const idx = list.findIndex(e => e.name === fileName);
            if (idx !== -1) {
                if (name) {
                    list[idx].extraBooks = (list[idx].extraBooks || []).filter(e => e !== name);
                    if (list[idx].extraBooks.length === 0) list.splice(idx, 1);
                } else {
                    // remove all
                    list.splice(idx, 1);
                }
                world_info.charLore = list;
                getContext().saveSettingsDebounced?.();
                result.additional = world_info.charLore.find(e => e.name === fileName)?.extraBooks || [];
            } else {
                result.additional = [];
            }
        }
        return result;
    }

    // ===== Dispatcher =====
    async handleRequest(action, params) {
        switch (action) {
            // Basic operations
            case 'getChatBook': return await this.getChatBook(params);
            case 'getGlobalBooks': return await this.getGlobalBooks(params);
            case 'listWorldbooks': return await this.listWorldbooks(params);
            case 'getPersonaBook': return await this.getPersonaBook(params);
            case 'getCharBook': return await this.getCharBook(params);
            case 'world': return await this.world(params);
            
            // Entry operations
            case 'findEntry': return await this.findEntry(params);
            case 'getEntryField': return await this.getEntryField(params);
            case 'setEntryField': return await this.setEntryField(params);
            case 'createEntry': return await this.createEntry(params);
            case 'listEntries': return await this.listEntries(params);
            case 'deleteEntry': return await this.deleteEntry(params);
            
            // Enhanced entry operations
            case 'getEntryAll': return await this.getEntryAll(params);
            case 'batchSetEntryFields': return await this.batchSetEntryFields(params);
            case 'cloneEntry': return await this.cloneEntry(params);
            case 'moveEntry': return await this.moveEntry(params);
            case 'reorderEntry': return await this.reorderEntry(params);
            
            // File-level operations
            case 'renameWorldbook': return await this.renameWorldbook(params);
            case 'deleteWorldbook': return await this.deleteWorldbook(params);
            case 'exportWorldbook': return await this.exportWorldbook(params);
            case 'importWorldbook': return await this.importWorldbook(params);
            
            // Timed effects
            case 'wiGetTimedEffect': return await this.wiGetTimedEffect(params);
            case 'wiSetTimedEffect': return await this.wiSetTimedEffect(params);
            
            // Binding operations
            case 'bindWorldbookToChat': return await this.bindWorldbookToChat(params);
            case 'unbindWorldbookFromChat': return await this.unbindWorldbookFromChat(params);
            case 'bindWorldbookToCharacter': return await this.bindWorldbookToCharacter(params);
            case 'unbindWorldbookFromCharacter': return await this.unbindWorldbookFromCharacter(params);
            
            default: throw new Error('INVALID_ACTION');
        }
    }

    attachEventsForwarding() {
        if (this._forwardEvents) return;
        this._onWIUpdated = (name, data) => this.postEvent('WORLDBOOK_UPDATED', { name });
        this._onWISettings = () => this.postEvent('WORLDBOOK_SETTINGS_UPDATED', {});
        this._onWIActivated = (entries) => this.postEvent('WORLDBOOK_ACTIVATED', { entries });
        eventSource.on(event_types.WORLDINFO_UPDATED, this._onWIUpdated);
        eventSource.on(event_types.WORLDINFO_SETTINGS_UPDATED, this._onWISettings);
        eventSource.on(event_types.WORLD_INFO_ACTIVATED, this._onWIActivated);
        this._forwardEvents = true;
    }

    detachEventsForwarding() {
        if (!this._forwardEvents) return;
        try { eventSource.removeListener(event_types.WORLDINFO_UPDATED, this._onWIUpdated); } catch {}
        try { eventSource.removeListener(event_types.WORLDINFO_SETTINGS_UPDATED, this._onWISettings); } catch {}
        try { eventSource.removeListener(event_types.WORLD_INFO_ACTIVATED, this._onWIActivated); } catch {}
        this._forwardEvents = false;
    }

    init({ forwardEvents = false, allowedOrigins = null } = {}) {
        if (this._attached) return;
        if (allowedOrigins) this.setAllowedOrigins(allowedOrigins);
        
        const self = this;
        this._listener = async function (event) {
            try {
                // Security check: validate origin
                if (!self.isOriginAllowed(event.origin)) {
                    console.warn('Worldbook bridge: Rejected request from unauthorized origin:', event.origin);
                    return;
                }
                
                const data = event && event.data || {};
                if (!data || data.type !== 'worldbookRequest') return;
                const id = data.id;
                const action = data.action;
                const params = data.params || {};
                try {
                    try {
                        if (xbLog.isEnabled?.()) {
                            xbLog.info('worldbookBridge', `worldbookRequest id=${id} action=${String(action || '')}`);
                        }
                    } catch {}
                    const result = await self.handleRequest(action, params);
                    self.sendResult(event.source || window, id, result, event.origin);
                } catch (err) {
                    try { xbLog.error('worldbookBridge', `worldbookRequest failed id=${id} action=${String(action || '')}`, err); } catch {}
                    self.sendError(event.source || window, id, err, 'API_ERROR', null, event.origin);
                }
            } catch {}
        };
        // eslint-disable-next-line no-restricted-syntax -- validated by isOriginAllowed before handling.
        try { window.addEventListener('message', this._listener); } catch {}
        this._attached = true;
        if (forwardEvents) this.attachEventsForwarding();
    }

    cleanup() {
        if (!this._attached) return;
        try { xbLog.info('worldbookBridge', 'cleanup'); } catch {}
        try { window.removeEventListener('message', this._listener); } catch {}
        this._attached = false;
        this._listener = null;
        this.detachEventsForwarding();
    }
}

const worldbookBridge = new WorldbookBridgeService();

export function initWorldbookHostBridge(options) {
    try { xbLog.info('worldbookBridge', 'initWorldbookHostBridge'); } catch {}
    try { worldbookBridge.init(options || {}); } catch {}
}

export function cleanupWorldbookHostBridge() {
    try { xbLog.info('worldbookBridge', 'cleanupWorldbookHostBridge'); } catch {}
    try { worldbookBridge.cleanup(); } catch {}
}

if (typeof window !== 'undefined') {
    Object.assign(window, { 
        xiaobaixWorldbookService: worldbookBridge, 
        initWorldbookHostBridge, 
        cleanupWorldbookHostBridge,
        setWorldbookBridgeOrigins: (origins) => worldbookBridge.setAllowedOrigins(origins)
    });
    try { initWorldbookHostBridge({ forwardEvents: true }); } catch {}
    try {
        window.addEventListener('xiaobaixEnabledChanged', (e) => {
            try {
                const enabled = e && e.detail && e.detail.enabled === true;
                if (enabled) initWorldbookHostBridge({ forwardEvents: true }); else cleanupWorldbookHostBridge();
            } catch (_) {}
        });
        document.addEventListener('xiaobaixEnabledChanged', (e) => {
            try {
                const enabled = e && e.detail && e.detail.enabled === true;
                if (enabled) initWorldbookHostBridge({ forwardEvents: true }); else cleanupWorldbookHostBridge();
            } catch (_) {}
        });
        window.addEventListener('beforeunload', () => { try { cleanupWorldbookHostBridge(); } catch (_) {} });
    } catch (_) {}
}


