/**
 * Story History Module
 * Stores received story output blocks in IndexedDB and tracks the render playhead.
 */
import { BaseModule } from './base-module.js';

class StoryHistoryModule extends BaseModule {
    constructor() {
        super('story-history', 'Story History');

        this.dependencies = ['persistence-manager'];
        this.dbName = 'ttsAudioCacheDB';
        this.dbVersion = 3;
        this.historyStore = 'storyHistoryStore';
        this.saveStore = 'storySaveStore';
        this.db = null;
        this.currentGameId = null;
        this.nextBlockId = 1;
        this.latestRenderedBlockId = 0;
        this.renderedLineCount = 0;
        this.visibleLimit = 20;

        this.bindMethods([
            'initialize',
            'openDB',
            'startNewGame',
            'setCurrentGame',
            'recordBlock',
            'recordBlocks',
            'markRendered',
            'updateBlockMetrics',
            'saveSlot',
            'loadSlot',
            'hasSaveSlot',
            'getSaveSlots',
            'getBlocks',
            'getBlock',
            'getBlocksRange',
            'getFirstBlockForTurn',
            'getRenderedLineCount',
            'findBlockForLine',
            'clearGame',
            'tx'
        ]);
    }

    async initialize() {
        await this.openDB();
        this.reportProgress(100, 'Story history ready');
        return true;
    }

    openDB() {
        if (this.db) return Promise.resolve(this.db);
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(this.dbName, this.dbVersion);
            request.onerror = () => reject(request.error);
            request.onsuccess = () => {
                this.db = request.result;
                resolve(this.db);
            };
            request.onupgradeneeded = () => {
                const db = request.result;
                if (!db.objectStoreNames.contains('audioCacheStore')) {
                    const audioStore = db.createObjectStore('audioCacheStore', { keyPath: 'hash' });
                    audioStore.createIndex('lastAccessed', 'lastAccessed', { unique: false });
                    audioStore.createIndex('size', 'size', { unique: false });
                }
                if (!db.objectStoreNames.contains(this.historyStore)) {
                    const historyStore = db.createObjectStore(this.historyStore, { keyPath: 'key' });
                    historyStore.createIndex('gameId', 'gameId', { unique: false });
                    historyStore.createIndex('gameOrder', ['gameId', 'blockId'], { unique: true });
                }
                if (!db.objectStoreNames.contains(this.saveStore)) {
                    db.createObjectStore(this.saveStore, { keyPath: 'slot' });
                }
            };
        });
    }

    tx(storeName, mode = 'readonly') {
        return this.db.transaction(storeName, mode).objectStore(storeName);
    }

    async startNewGame() {
        const gameId = `game-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        this.currentGameId = gameId;
        this.nextBlockId = 1;
        this.latestRenderedBlockId = 0;
        this.renderedLineCount = 0;
        const persistenceManager = this.getModule('persistence-manager');
        persistenceManager?.updatePreference?.('app', 'currentGameId', gameId);
        return gameId;
    }

    setCurrentGame(gameId, latestBlockId = 0, latestRenderedBlockId = 0, renderedLineCount = 0) {
        this.currentGameId = gameId || this.currentGameId;
        this.nextBlockId = Math.max(1, Number(latestBlockId || 0) + 1);
        this.latestRenderedBlockId = Math.max(0, Number(latestRenderedBlockId || 0));
        this.renderedLineCount = Math.max(0, Number(renderedLineCount || 0));
    }

    recordBlock(block) {
        if (!this.db || !this.currentGameId || !block) return Promise.resolve(null);
        const blockId = this.nextBlockId++;
        const record = {
            ...block,
            key: `${this.currentGameId}:${blockId}`,
            gameId: this.currentGameId,
            blockId,
            createdAt: Date.now()
        };
        return new Promise((resolve, reject) => {
            const request = this.tx(this.historyStore, 'readwrite').put(record);
            request.onsuccess = () => resolve(record);
            request.onerror = () => reject(request.error);
        });
    }

    async recordBlocks(blocks = []) {
        if (!Array.isArray(blocks) || blocks.length === 0) return [];
        const records = [];
        for (const block of blocks) {
            const record = await this.recordBlock(block);
            if (record) records.push(record);
        }
        return records;
    }

    markRendered(blockId) {
        const rendered = Math.max(0, Number(blockId || 0));
        if (rendered > this.latestRenderedBlockId) {
            this.latestRenderedBlockId = rendered;
        }
        return this.latestRenderedBlockId;
    }

    async updateBlockMetrics(blockId, metrics = {}) {
        if (!this.db || !this.currentGameId || blockId == null) return null;
        const id = Math.max(1, Number(blockId || 1));
        const key = `${this.currentGameId}:${id}`;
        const record = await new Promise((resolve, reject) => {
            const request = this.tx(this.historyStore).get(key);
            request.onsuccess = () => resolve(request.result || null);
            request.onerror = () => reject(request.error);
        });
        if (!record) return null;

        const lineCount = Math.max(0, Number(metrics.lineCount ?? record.lineCount ?? 1));
        const lineStart = Number.isFinite(Number(metrics.lineStart))
            ? Math.max(0, Number(metrics.lineStart))
            : Number.isFinite(Number(record.lineStart))
                ? Math.max(0, Number(record.lineStart))
                : this.renderedLineCount;

        const updated = {
            ...record,
            lineStart,
            lineCount,
            metricsUpdatedAt: Date.now()
        };

        this.renderedLineCount = Math.max(this.renderedLineCount, lineStart + lineCount);

        await new Promise((resolve, reject) => {
            const request = this.tx(this.historyStore, 'readwrite').put(updated);
            request.onsuccess = () => resolve(true);
            request.onerror = () => reject(request.error);
        });

        return updated;
    }

    saveSlot(slot = 1, saveData = {}) {
        if (!this.db) return Promise.resolve(false);
        const record = {
            slot,
            ...saveData,
            gameId: saveData.gameId || this.currentGameId,
            latestBlockId: Math.max(0, this.nextBlockId - 1),
            latestRenderedBlockId: Math.max(0, Number(saveData.latestRenderedBlockId ?? this.latestRenderedBlockId ?? 0)),
            renderedLineCount: Math.max(0, Number(saveData.renderedLineCount ?? this.renderedLineCount ?? 0)),
            savedAt: Date.now()
        };
        return new Promise((resolve, reject) => {
            const request = this.tx(this.saveStore, 'readwrite').put(record);
            request.onsuccess = () => resolve(record);
            request.onerror = () => reject(request.error);
        });
    }

    loadSlot(slot = 1) {
        if (!this.db) return Promise.resolve(null);
        return new Promise((resolve, reject) => {
            const request = this.tx(this.saveStore).get(slot);
            request.onsuccess = () => resolve(request.result || null);
            request.onerror = () => reject(request.error);
        });
    }

    async hasSaveSlot(slot = 1) {
        return Boolean(await this.loadSlot(slot));
    }

    getSaveSlots() {
        if (!this.db) return Promise.resolve([]);
        return new Promise((resolve, reject) => {
            const request = this.tx(this.saveStore).getAllKeys();
            request.onsuccess = () => resolve(request.result || []);
            request.onerror = () => reject(request.error);
        });
    }

    getBlocks(gameId = this.currentGameId, limit = this.visibleLimit, beforeBlockId = Infinity) {
        if (!this.db || !gameId) return Promise.resolve([]);
        return new Promise((resolve, reject) => {
            const blocks = [];
            const index = this.tx(this.historyStore).index('gameOrder');
            const upper = Number.isFinite(beforeBlockId) ? beforeBlockId : Number.MAX_SAFE_INTEGER;
            const range = IDBKeyRange.bound([gameId, 0], [gameId, upper], false, true);
            const request = index.openCursor(range, 'prev');
            request.onsuccess = () => {
                const cursor = request.result;
                if (!cursor || blocks.length >= limit) {
                    resolve(blocks.reverse());
                    return;
                }
                blocks.push(cursor.value);
                cursor.continue();
            };
            request.onerror = () => reject(request.error);
        });
    }

    getBlock(gameId = this.currentGameId, blockId = null) {
        if (!this.db || !gameId || blockId == null) return Promise.resolve(null);
        const id = Math.max(1, Number(blockId || 1));
        return new Promise((resolve, reject) => {
            const request = this.tx(this.historyStore).get(`${gameId}:${id}`);
            request.onsuccess = () => resolve(request.result || null);
            request.onerror = () => reject(request.error);
        });
    }

    getBlocksRange(gameId = this.currentGameId, startBlockId = 1, endBlockId = Infinity) {
        if (!this.db || !gameId) return Promise.resolve([]);
        const start = Math.max(1, Number(startBlockId || 1));
        const end = Number.isFinite(endBlockId) ? Number(endBlockId) : Number.MAX_SAFE_INTEGER;
        if (end < start) return Promise.resolve([]);

        return new Promise((resolve, reject) => {
            const blocks = [];
            const index = this.tx(this.historyStore).index('gameOrder');
            const range = IDBKeyRange.bound([gameId, start], [gameId, end]);
            const request = index.openCursor(range, 'next');
            request.onsuccess = () => {
                const cursor = request.result;
                if (!cursor) {
                    resolve(blocks);
                    return;
                }
                blocks.push(cursor.value);
                cursor.continue();
            };
            request.onerror = () => reject(request.error);
        });
    }

    async getFirstBlockForTurn(gameId = this.currentGameId, turnId) {
        if (!this.db || !gameId || turnId == null) return null;
        return new Promise((resolve, reject) => {
            const index = this.tx(this.historyStore).index('gameId');
            const request = index.openCursor(IDBKeyRange.only(gameId), 'next');
            request.onsuccess = () => {
                const cursor = request.result;
                if (!cursor) {
                    resolve(null);
                    return;
                }
                if (String(cursor.value?.turnId) === String(turnId)) {
                    resolve(cursor.value);
                    return;
                }
                cursor.continue();
            };
            request.onerror = () => reject(request.error);
        });
    }

    async getRenderedLineCount(gameId = this.currentGameId, latestRenderedBlockId = this.latestRenderedBlockId) {
        const latest = Math.max(0, Number(latestRenderedBlockId || 0));
        if (!this.db || !gameId || latest <= 0) return 0;
        const blocks = await this.getBlocksRange(gameId, 1, latest);
        const measured = blocks.reduce((max, block) => {
            const start = Number(block.lineStart);
            const count = Number(block.lineCount);
            if (!Number.isFinite(start) || !Number.isFinite(count)) return max;
            return Math.max(max, start + Math.max(1, count));
        }, 0);
        this.renderedLineCount = Math.max(this.renderedLineCount, measured);
        return this.renderedLineCount;
    }

    async findBlockForLine(gameId = this.currentGameId, line = 0, latestRenderedBlockId = this.latestRenderedBlockId) {
        const latest = Math.max(0, Number(latestRenderedBlockId || 0));
        if (!this.db || !gameId || latest <= 0) return null;
        const targetLine = Math.max(0, Number(line || 0));
        const blocks = await this.getBlocksRange(gameId, 1, latest);
        return blocks.find((block) => {
            const start = Number(block.lineStart);
            const count = Math.max(1, Number(block.lineCount || 1));
            return Number.isFinite(start) && targetLine >= start && targetLine < start + count;
        }) || blocks.at(-1) || null;
    }

    clearGame(gameId = this.currentGameId) {
        if (!this.db || !gameId) return Promise.resolve();
        return new Promise((resolve, reject) => {
            const store = this.tx(this.historyStore, 'readwrite');
            const index = store.index('gameId');
            const request = index.openCursor(IDBKeyRange.only(gameId));
            request.onsuccess = () => {
                const cursor = request.result;
                if (!cursor) {
                    resolve();
                    return;
                }
                cursor.delete();
                cursor.continue();
            };
            request.onerror = () => reject(request.error);
        });
    }
}

const storyHistory = new StoryHistoryModule();

export { storyHistory as StoryHistory };

if (window.moduleRegistry) {
    window.moduleRegistry.register(storyHistory);
}

window.StoryHistory = storyHistory;
