/**
 * Socket Client Module
 * Handles WebSocket communication for receiving text fragments and game state
 */
import { BaseModule } from './base-module.js';

class SocketClientModule extends BaseModule {
    constructor() {
        super('socket-client', 'Socket Client');
        
        // Dependencies
        this.dependencies = ['text-buffer', 'markup-parser', 'story-history'];
        
        this.socket = null;
        this.textBuffer = null;
        this.storyHistory = null;
        this.isConnected = false;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 2000;
        this.url = null;
        this.eventListeners = {};
        this.defaultHost = 'localhost:3000';
        this.receivedBlockCounter = 0;
        this.receivedParagraphCounter = 0;
        
        // Bind methods using parent's bindMethods utility
        this.bindMethods([
            'connect',
            'disconnect',
            'send',
            'sendCommand',
            'callGameApi',
            'newGame',
            'loadGame',
            'saveGame',
            'chooseChoice',
            'hasSaveGame',
            'getSaveGames',
            'isGameRunning',
            'requestStartGame',
            'requestSaveGame',
            'requestLoadGame',
            'on',
            'off',
            'emitEvent',
            'setupGameEventHandlers',
            'processTurnResult',
            'processParagraphResult',
            'storeAndQueueBlocks',
            'normalizeHistoryBlock',
            'dispatchTurnTags',
            'isTimedCueTag',
            'cueMarkersFromTags',
            'dispatchChoices',
            'dispatchInputMode',
            'isStructuralTag',
            'blocksFromTags',
            'enqueueStructuredBlock',
            'parseImageTagOptions',
            'parseSfxTagOptions',
            'parseMusicTagOptions',
            'resolveAssetUrl',
            'looksLikeAssetPath',
            'attemptReconnect',
            'getConnectionStatus',
            'loadSocketIO'
        ]);
    }
    
    /**
     * Load Socket.IO client library
     * @returns {Promise<void>}
     */
    loadSocketIO() {
        // Use parent's loadScript method
        return this.loadScript('/socket.io/socket.io.js');
    }
    
    /**
     * Initialize the module
     * @returns {Promise<boolean>} - Resolves with success status
     */
    async initialize() {
        try {
            this.reportProgress(10, "Initializing Socket Client");
            
            // Dynamically load Socket.IO client if not already loaded
            if (!window.io) {
                this.reportProgress(20, "Loading Socket.IO client");
                await this.loadSocketIO();
                this.reportProgress(30, "Socket.IO client loaded");
            }
            
            // Get text buffer using parent's getModule method
            this.textBuffer = this.getModule('text-buffer');
            if (!this.textBuffer) {
                console.error("Socket Client: Failed to get text-buffer module");
                return false;
            }
            this.storyHistory = this.getModule('story-history');
            if (!this.storyHistory) {
                console.error("Socket Client: Failed to get story-history module");
                return false;
            }
            
            this.reportProgress(50, "Setting up connection parameters");
            
            // Use the current origin for the socket connection
            const currentUrl = window.location.origin;
            
            console.log(`Socket Client: Using origin for connection: ${currentUrl}`);
            this.url = currentUrl;
            
            this.reportProgress(100, "Socket client initialized");
            return true;
        } catch (error) {
            console.error("Error initializing Socket Client:", error);
            return false;
        }
    }
    
    /**
     * Connect to the Socket.IO server
     * @param {string} url - Optional custom WebSocket URL
     * @returns {Promise<boolean>} - Resolves with connection success
     */
    connect(url = null) {
        return new Promise((resolve) => {
            if (this.isConnected) {
                resolve(true);
                return;
            }
            
            // Use provided URL or default
            const socketUrl = url || this.url;
            
            try {
                console.log(`Socket Client: Connecting to ${socketUrl}`);
                
                // Create Socket.IO connection (will automatically use /socket.io endpoint)
                this.socket = window.io(socketUrl, {
                    reconnection: false, // We handle reconnection ourselves
                    transports: ['polling', 'websocket']
                });
                
                this.socket.on('connect', () => {
                    console.log('Socket Client: Connected to server with ID:', this.socket.id);
                    this.isConnected = true;
                    this.reconnectAttempts = 0;
                    this.emitEvent('connect');
                    resolve(true);
                });
                
                this.socket.on('disconnect', (reason) => {
                    console.log(`Socket Client: Connection closed: ${reason}`);
                    this.isConnected = false;
                    this.emitEvent('disconnect', reason);
                    this.attemptReconnect();
                    resolve(false);
                });
                
                this.socket.on('connect_error', (error) => {
                    console.error('Socket Client: Connection error:', error);
                    this.emitEvent('connect_error', error);
                    resolve(false);
                });
                
                // Set up game-specific event handlers
                this.setupGameEventHandlers();
                
            } catch (error) {
                console.error('Socket Client: Connection error:', error);
                this.emitEvent('connect_error', error);
                resolve(false);
            }
        });
    }
    
    /**
     * Set up event handlers for game-specific Socket.IO events
     */
    setupGameEventHandlers() {
        if (!this.socket) return;
        
        // Map all incoming Socket.IO events to our internal event system
        this.socket.onAny((event, ...args) => {
            console.log(`Socket Client: Received ${event} event from server`, args);
            this.emitEvent(event, args[0]);
        });
        
        // Special handling for narrative text
        this.socket.on('narrativeResponse', (data) => {
            this.processTurnResult(data);
        });
        
        this.socket.on('gameConfig', (data) => {
            document.dispatchEvent(new CustomEvent('game:config', {
                detail: data
            }));
        });
    }

    async processTurnResult(data) {
        if (!data) return;

        const turnId = Number(data.turnId);
        if (!Number.isInteger(turnId) || turnId < 1 || !Array.isArray(data.paragraphs)) {
            console.error('Socket Client: Invalid TurnResult received', data);
            return;
        }
        if (turnId === 1) {
            this.receivedBlockCounter = 0;
            this.receivedParagraphCounter = 0;
        }

        const globalTags = Array.isArray(data.globalTags) ? data.globalTags : [];
        const endState = data.gameState?.endState || null;
        if (endState && !globalTags.some((tag) => tag?.key === 'score' || tag?.key === 'error')) {
            globalTags.push({
                key: endState.type === 'error' ? 'error' : 'score',
                value: endState.message || ''
            });
        }

        if (globalTags.length > 0) {
            document.dispatchEvent(new CustomEvent('story:global-tags', {
                detail: globalTags
            }));
            this.dispatchTurnTags(globalTags, null);
        }

        document.dispatchEvent(new CustomEvent('story:turn-start', {
            detail: { turnId, turn: data }
        }));

        let pendingParagraph = {
            role: null,
            cueTags: []
        };
        const turnBlocks = [];
        data.paragraphs.forEach((paragraph) => {
            const result = this.processParagraphResult(paragraph, turnId, pendingParagraph);
            pendingParagraph = result.pendingParagraph;
            turnBlocks.push(...result.blocks);
        });

        await this.storeAndQueueBlocks(turnBlocks);

        const choices = Array.isArray(data.choices) ? data.choices : [];
        const inputMode = data.inputMode || (choices.length > 0 ? 'choice' : 'text');
        this.dispatchChoices(choices);
        this.dispatchInputMode(inputMode);
        if (turnBlocks.length === 0 && choices.length > 0) {
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'ready', reason: 'choice-only-turn', turnId }
            }));
        } else if (turnBlocks.length === 0 && inputMode === 'end') {
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'ready', reason: 'empty-end-turn', turnId }
            }));
        }
    }

    dispatchTurnTags(tags, paragraph = null) {
        if (!Array.isArray(tags)) return;
        tags.forEach((tag) => {
            if (!tag || !tag.key) return;
            document.dispatchEvent(new CustomEvent('story:tag', {
                detail: {
                    ...tag,
                    paragraph
                }
            }));
        });
    }

    dispatchChoices(choices) {
        document.dispatchEvent(new CustomEvent('story:choices', {
            detail: choices
        }));
    }

    dispatchInputMode(inputMode) {
        const mode = ['text', 'choice', 'end'].includes(inputMode) ? inputMode : 'text';
        document.dispatchEvent(new CustomEvent('story:input-mode', {
            detail: mode
        }));
    }

    processParagraphResult(paragraph, turnId, pendingParagraph = null) {
        const pending = pendingParagraph && typeof pendingParagraph === 'object'
            ? pendingParagraph
            : { role: pendingParagraph || null, cueTags: [] };
        const tags = Array.isArray(paragraph?.tags) ? paragraph.tags : [];
        const { blocks, paragraphRole } = this.blocksFromTags(tags, turnId);
        const text = String(paragraph?.text || '').trim();
        const cueTags = tags.filter(tag => this.isTimedCueTag(tag));
        const immediateTags = tags.filter(tag => !this.isStructuralTag(tag) && !this.isTimedCueTag(tag));

        this.dispatchTurnTags(immediateTags, paragraph);
        if (!text) {
            return {
                blocks,
                pendingParagraph: {
                    role: paragraphRole || pending.role || null,
                    cueTags: [
                        ...(Array.isArray(pending.cueTags) ? pending.cueTags : []),
                        ...cueTags
                    ]
                }
            };
        }

        const role = pending.role || paragraphRole || 'body';
        const cueMarkers = [
            ...(Array.isArray(paragraph.cueMarkers) ? paragraph.cueMarkers : []),
            ...this.cueMarkersFromTags([
                ...(Array.isArray(pending.cueTags) ? pending.cueTags : []),
                ...cueTags
            ])
        ];
        blocks.push({
            type: 'paragraph',
            text,
            layoutText: paragraph.layoutText || text,
            cueMarkers,
            role,
            isFirstParagraphInChapter: role === 'chapter-first' || role === 'textblock-first',
            dropCap: role === 'chapter-first',
            addTopSpace: role === 'textblock-first',
            turnId
        });

        return { blocks, pendingParagraph: { role: null, cueTags: [] } };
    }

    async storeAndQueueBlocks(blocks = []) {
        if (!Array.isArray(blocks) || blocks.length === 0) return;

        if (!this.storyHistory) {
            this.storyHistory = this.getModule('story-history');
        }

        const normalizedBlocks = blocks.map(block => this.normalizeHistoryBlock(block));
        let records = normalizedBlocks;
        if (this.storyHistory && typeof this.storyHistory.recordBlocks === 'function') {
            records = await this.storyHistory.recordBlocks(normalizedBlocks);
            document.dispatchEvent(new CustomEvent('story:history-updated', {
                detail: {
                    gameId: this.storyHistory.currentGameId || null,
                    latestBlockId: this.storyHistory.nextBlockId - 1
                }
            }));
        } else {
            console.warn('Socket Client: Story history unavailable; queueing unstored blocks');
        }

        records.forEach(block => this.enqueueStructuredBlock(block));
    }

    normalizeHistoryBlock(block) {
        const type = String(block?.type || 'paragraph');
        this.receivedBlockCounter += 1;
        const normalized = {
            ...block,
            type,
            id: block.id || `${type}-${block.turnId || 'turn'}-${this.receivedBlockCounter}`
        };

        if (type === 'paragraph') {
            normalized.paragraphIndex = Number.isInteger(block.paragraphIndex)
                ? block.paragraphIndex
                : this.receivedParagraphCounter;
            this.receivedParagraphCounter += 1;
        }

        return normalized;
    }

    isStructuralTag(tag) {
        const key = String(tag?.key || '').toLowerCase();
        return ['chapter', 'heading', 'section', 'textblock', 'image', 'music'].includes(key);
    }

    isTimedCueTag(tag) {
        const key = String(tag?.key || '').toLowerCase();
        return ['sfx', 'sound', 'audio'].includes(key);
    }

    cueMarkersFromTags(tags) {
        if (!Array.isArray(tags)) return [];

        return tags
            .filter(tag => this.isTimedCueTag(tag))
            .map(tag => {
                const filename = String(tag?.value || tag?.filename || '').trim();
                if (!filename) return null;
                const options = this.parseSfxTagOptions(tag?.param || tag?.options || '');
                return {
                    type: 'sfx',
                    ...options,
                    filename,
                    url: this.resolveAssetUrl('sounds', filename),
                    wordIndex: 0,
                    charIndex: 0
                };
            })
            .filter(Boolean);
    }

    blocksFromTags(tags, turnId = null) {
        const result = {
            blocks: [],
            paragraphRole: null
        };

        if (!Array.isArray(tags)) return result;

        tags.forEach((tag) => {
            const key = String(tag?.key || '').toLowerCase();
            const value = String(tag?.value || '').trim();
            const param = String(tag?.param || tag?.options || '').trim();

            if ((key === 'chapter' || key === 'heading') && value) {
                result.blocks.push({
                    type: 'heading',
                    text: value,
                    layoutText: value,
                    role: 'chapter-heading',
                    turnId
                });
                result.paragraphRole = 'chapter-first';
            } else if (key === 'section' || key === 'textblock') {
                result.blocks.push({
                    type: 'heading',
                    text: value || '* * *',
                    layoutText: value || '* * *',
                    role: 'section-heading',
                    turnId
                });
                result.paragraphRole = 'textblock-first';
            } else if (key === 'image') {
                let filename = value;
                let optionText = param;
                if (this.looksLikeAssetPath(param) && value && !this.looksLikeAssetPath(value)) {
                    filename = param;
                    optionText = value;
                }
                if (!filename) return;
                const options = this.parseImageTagOptions(optionText);
                const chapterOpening = result.paragraphRole === 'chapter-first';
                result.blocks.push({
                    type: 'image',
                    ...options,
                    floatSide: chapterOpening && String(options.size || '').toLowerCase() === 'portrait' ? 'right' : 'left',
                    chapterOpening,
                    filename,
                    url: this.resolveAssetUrl('images', filename),
                    turnId
                });
            } else if (key === 'music') {
                let filename = value;
                let optionText = param;
                if (this.looksLikeAssetPath(param) && value && !this.looksLikeAssetPath(value)) {
                    filename = param;
                    optionText = value;
                }
                if (!filename) return;
                const options = this.parseMusicTagOptions(optionText);
                const leadInSeconds = Number(options.leadInSeconds);
                result.blocks.push({
                    type: 'music',
                    ...options,
                    leadInSeconds: Number.isFinite(leadInSeconds) ? leadInSeconds : 0,
                    leadIn: Number.isFinite(leadInSeconds) ? leadInSeconds : 0,
                    pause: Number.isFinite(leadInSeconds) ? leadInSeconds : 0,
                    filename,
                    url: this.resolveAssetUrl('music', filename),
                    turnId
                });
            }
        });

        return result;
    }

    enqueueStructuredBlock(block) {
        if (!block) return;

        if (!this.textBuffer) {
            this.textBuffer = this.getModule('text-buffer');
        }

        if (this.textBuffer && typeof this.textBuffer.addBlock === 'function') {
            console.log(`Socket Client: Queueing ${block.type} block`);
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'waiting-generating', reason: 'server-response-received' }
            }));
            this.textBuffer.addBlock(block);
            return;
        }

        console.error('Socket Client: Text buffer not available for structured block', block);
    }

    parseImageTagOptions(optionText) {
        const parser = this.getModule('markup-parser');
        if (parser && typeof parser.parseImageOptions === 'function') {
            return parser.parseImageOptions(optionText);
        }
        return { size: 'landscape', leadInSeconds: 0 };
    }

    parseSfxTagOptions(optionText) {
        const parser = this.getModule('markup-parser');
        if (parser && typeof parser.parseSfxOptions === 'function') {
            return parser.parseSfxOptions(optionText);
        }
        return { maxDurationSeconds: 0, endMode: 'stop', fadeDurationSeconds: 2 };
    }

    parseMusicTagOptions(optionText) {
        const parser = this.getModule('markup-parser');
        if (parser && typeof parser.parseMusicOptions === 'function') {
            return parser.parseMusicOptions(optionText);
        }
        return { mode: 'crossfade', loop: true, leadInSeconds: 0 };
    }

    resolveAssetUrl(kind, filename) {
        const parser = this.getModule('markup-parser');
        if (parser && typeof parser.resolveAssetUrl === 'function') {
            return parser.resolveAssetUrl(kind, filename);
        }

        const root = kind === 'images' ? '/images/' : kind === 'music' ? '/music/' : '/sounds/';
        const safeName = String(filename || '').replace(/\\/g, '/').replace(/^\/+/, '');
        if (!safeName || safeName.includes('..') || /^[a-z]+:/i.test(safeName)) {
            return '';
        }
        return root + safeName.split('/').map(encodeURIComponent).join('/');
    }

    looksLikeAssetPath(value) {
        return /[./\\]/.test(String(value || '')) || /\.(png|jpe?g|gif|webp|svg|ogg|mp3|wav|m4a|flac)$/i.test(String(value || ''));
    }
    
    /**
     * Attempt to reconnect to the server
     */
    attemptReconnect() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            console.error('Socket Client: Max reconnect attempts reached');
            return;
        }
        
        this.reconnectAttempts++;
        const delay = this.reconnectDelay * this.reconnectAttempts;
        
        console.log(`Socket Client: Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
        
        setTimeout(() => {
            if (!this.isConnected) {
                this.connect();
            }
        }, delay);
    }
    
    /**
     * Disconnect from the server
     */
    disconnect() {
        if (this.socket && this.isConnected) {
            this.socket.disconnect();
            this.isConnected = false;
        }
    }
    
    /**
     * Send a message to the server
     * @param {Object|string} data - Data to send
     * @returns {boolean} - Success status
     */
    send(data) {
        if (!this.isConnected || !this.socket) {
            console.error('Socket Client: Not connected');
            return false;
        }
        
        try {
            // For Socket.IO we send structured events
            if (typeof data === 'object') {
                const { type, ...restData } = data;
                if (type) {
                    // Use the type as the event name
                    this.socket.emit(type, restData);
                } else {
                    // Default to 'message' event
                    this.socket.emit('message', data);
                }
            } else {
                // Plain strings go to 'message' event
                this.socket.emit('message', { text: data });
            }
            return true;
        } catch (error) {
            console.error('Socket Client: Error sending message:', error);
            return false;
        }
    }

    /**
     * Send a command to the server
     * @param {string} command - The player's command
     * @returns {boolean} - Success status
     */
    sendCommand(command) {
        if (!this.isConnected || !this.socket) {
            console.error('Socket Client: Not connected, cannot send command');
            return false;
        }
        
        try {
            this.socket.emit('playerCommand', { command });
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'command-waiting', reason: 'command-sent', command }
            }));
            return true;
        } catch (error) {
            console.error('Socket Client: Error sending command:', error);
            return false;
        }
    }

    async callGameApi(method, args = []) {
        if (!this.isConnected || !this.socket) {
            const connected = await this.connect();
            if (!connected || !this.socket) {
                return { success: false, error: 'not_connected' };
            }
        }

        return new Promise((resolve) => {
            if (!this.socket) {
                resolve({ success: false, error: 'not_connected' });
                return;
            }

            this.socket.emit('gameApi', { method, args }, (response) => {
                resolve(response || { success: false, error: 'empty_response' });
            });
        });
    }

    newGame() {
        return this.callGameApi('newGame', []);
    }

    loadGame(slot = 1, savedState = null) {
        return this.callGameApi('loadGame', savedState ? [slot, savedState] : [slot]);
    }

    saveGame(slot = 1) {
        return this.callGameApi('saveGame', [slot]);
    }

    chooseChoice(choiceIndex) {
        return this.callGameApi('chooseChoice', [choiceIndex]);
    }

    hasSaveGame(slot = 1) {
        return this.callGameApi('hasSaveGame', [slot]);
    }

    getSaveGames() {
        return this.callGameApi('getSaveGames', []);
    }

    isGameRunning() {
        return this.callGameApi('isGameRunning', []);
    }

    /**
     * Request to start a new game
     * @returns {boolean} - Success status
     */
    requestStartGame() {
        this.newGame();
        return true;
    }

    /**
     * Request to save the current game state
     * @returns {boolean} - Success status
     */
    requestSaveGame() {
        this.saveGame(1);
        return true;
    }

    /**
     * Request to load a saved game state
     * @returns {boolean} - Success status
     */
    requestLoadGame() {
        this.loadGame(1);
        return true;
    }

    /**
     * Register an event handler
     * @param {string} event - Event name
     * @param {Function} callback - Event callback
     */
    on(event, callback) {
        if (!this.eventListeners[event]) {
            this.eventListeners[event] = [];
        }
        this.eventListeners[event].push(callback);
    }

    /**
     * Remove an event handler
     * @param {string} event - Event name
     * @param {Function} callback - Event callback to remove
     */
    off(event, callback) {
        if (!this.eventListeners[event]) return;
        
        if (callback) {
            // Remove specific callback
            this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
        } else {
            // Remove all callbacks for this event
            delete this.eventListeners[event];
        }
    }

    /**
     * Emit an event to all registered listeners
     * @param {string} event - Event name
     * @param {*} data - Event data
     */
    emitEvent(event, data) {
        if (!this.eventListeners[event]) return;
        
        for (const callback of this.eventListeners[event]) {
            try {
                callback(data);
            } catch (error) {
                console.error(`Socket Client: Error in '${event}' event handler:`, error);
            }
        }
    }

    /**
     * Check if the socket is connected
     * @returns {boolean} - Connection status
     */
    getConnectionStatus() {
        return this.isConnected;
    }
}

// Create the singleton instance
const SocketClient = new SocketClientModule();

// Export the module
export { SocketClient };
