/**
 * Code Plugin (Built-in)
 *
 * Provides code nodes for Python code execution with Pyodide.
 * This is a self-contained plugin that combines:
 * - CodeNode protocol (custom node rendering)
 * - CodeFeature (self-healing and error recovery)
 */

import { BaseNode, Actions, HeaderButtons } from '../node-protocols.js';
import { NodeRegistry } from '../node-registry.js';
import { NodeType, DEFAULT_NODE_SIZES } from '../graph-types.js';
import { FeaturePlugin } from '../feature-plugin.js';
import { CancellableEvent } from '../plugin-events.js';
import { EdgeType, createNode, createEdge } from '../graph-types.js';
import { readSSEStream } from '../sse.js';
import { wrapNode } from '../node-protocols.js';

// =============================================================================
// Code Node Protocol
// =============================================================================

/**
 * Code Node Protocol Class
 * Defines how code nodes are rendered and what actions they support.
 */
class CodeNode extends BaseNode {
    getTypeLabel() {
        return 'Code';
    }

    getTypeIcon() {
        return '🐍';
    }

    getSummaryText(canvas) {
        if (this.node.title) return this.node.title;
        // Show first meaningful line of code
        const code = this.node.code || this.node.content || '';
        const firstLine = code.split('\n').find((line) => line.trim() && !line.trim().startsWith('#')) || 'Python code';
        return canvas.truncate(firstLine.trim(), 50);
    }

    renderContent(canvas) {
        const code = this.node.code || this.node.content || '';
        const executionState = this.node.executionState || 'idle';
        const csvNodeIds = this.node.csvNodeIds || [];

        // Build header comment showing available data
        let dataHint = '';
        if (csvNodeIds.length === 1) {
            dataHint = `<div class="code-data-hint"># Data available as: df</div>`;
        } else if (csvNodeIds.length > 1) {
            const vars = csvNodeIds.map((_, i) => `df${i + 1}`).join(', ');
            dataHint = `<div class="code-data-hint"># Data available as: ${vars}</div>`;
        }

        // Execution state indicator
        let stateClass = '';
        let stateIndicator = '';
        const selfHealingStatus = this.node.selfHealingStatus;
        const selfHealingAttempt = this.node.selfHealingAttempt;

        if (executionState === 'running') {
            stateClass = 'code-running';
            if (selfHealingAttempt) {
                stateIndicator = `<div class="code-state-indicator code-self-healing">${selfHealingStatus === 'verifying' ? '🔍 Verifying' : '🔧 Self-healing'} (attempt ${selfHealingAttempt}/3)...</div>`;
            } else {
                stateIndicator = '<div class="code-state-indicator">Running...</div>';
            }
        } else if (executionState === 'error') {
            stateClass = 'code-error';
        } else if (selfHealingStatus === 'fixed') {
            // Show success badge if code was self-healed
            stateIndicator = '<div class="code-state-indicator code-self-healed">✅ Self-healed</div>';
        } else if (selfHealingStatus === 'failed') {
            // Show failure badge if self-healing gave up
            stateIndicator = '<div class="code-state-indicator code-self-heal-failed">⚠️ Self-healing failed</div>';
        }

        // Syntax-highlighted read-only code display (click to edit opens modal)
        // Escape HTML to prevent XSS, highlight.js will handle the rest
        const escapedCode = canvas.escapeHtml(code) || '# Click Edit to add code...';

        let html = `<div class="code-node-content ${stateClass}">`;
        html += dataHint;
        html += `<div class="code-display" data-node-id="${this.node.id}">`;
        html += `<pre><code class="language-python">${escapedCode}</code></pre>`;
        html += `</div>`;
        html += stateIndicator;

        // Show inline error if present
        if (this.node.lastError) {
            html += `<div class="code-error-output">${canvas.escapeHtml(this.node.lastError)}</div>`;
        }

        html += `</div>`;
        return html;
    }

    /**
     * Check if this code node has output to display
     * @returns {boolean}
     */
    hasOutput() {
        return !!(
            this.node.outputHtml ||
            this.node.outputText ||
            this.node.outputStdout ||
            (this.node.installProgress && this.node.installProgress.length > 0)
        );
    }

    /**
     * Render the output panel content (called by canvas for the slide-out panel)
     * @param {Canvas} canvas - Canvas instance for helper methods
     * @returns {string} HTML string
     */
    renderOutputPanel(canvas) {
        const outputHtml = this.node.outputHtml || null;
        const outputText = this.node.outputText || null;
        const outputStdout = this.node.outputStdout || null;
        const installProgress = this.node.installProgress || null;

        let html = `<div class="code-output-panel-content">`;

        // Show installation progress if present (during running state)
        if (installProgress && installProgress.length > 0) {
            html += `<div class="code-install-progress">`;
            for (const msg of installProgress) {
                html += `<div class="install-progress-line">${canvas.escapeHtml(msg)}</div>`;
            }
            html += `</div>`;
        }

        // Show stdout first if present
        if (outputStdout) {
            html += `<pre class="code-output-stdout">${canvas.escapeHtml(outputStdout)}</pre>`;
        }

        // Show result (HTML or text)
        if (outputHtml) {
            html += `<div class="code-output-result code-output-html">${outputHtml}</div>`;
        } else if (outputText) {
            html += `<pre class="code-output-result code-output-text">${canvas.escapeHtml(outputText)}</pre>`;
        }

        html += `</div>`;
        return html;
    }

    /**
     * Update code content in-place (for streaming updates)
     * @param {string} nodeId - The node ID
     * @param {string} content - New code content
     * @param {boolean} isStreaming - Whether this is a streaming update
     * @param {Canvas} canvas - Canvas instance for DOM manipulation
     * @returns {boolean}
     */
    updateContent(nodeId, content, isStreaming, canvas) {
        // Update the code display in-place
        const wrapper = canvas.nodeElements.get(nodeId);
        if (!wrapper) return false;

        const codeEl = wrapper.querySelector('.code-display code');
        if (codeEl && window.hljs) {
            codeEl.textContent = content;
            codeEl.className = 'language-python';
            delete codeEl.dataset.highlighted;
            window.hljs.highlightElement(codeEl);
        }
        return true;
    }

    getHiddenActionIds() {
        return ['edit-content']; // Hide default edit, use edit-code instead
    }

    getAdditionalActions() {
        return [Actions.EDIT_CODE, Actions.GENERATE, Actions.RUN_CODE];
    }

    getKeyboardShortcuts() {
        const shortcuts = super.getKeyboardShortcuts();
        // Override 'e' to use edit-code instead of edit-content
        shortcuts['e'] = { action: 'edit-code', handler: 'nodeEditCode' };
        // Add Shift+A for AI generate
        shortcuts['A'] = { action: 'generate', handler: 'nodeGenerate', shift: true };
        return shortcuts;
    }

    supportsStopContinue() {
        return true;
    }

    getHeaderButtons() {
        return [
            HeaderButtons.NAV_PARENT,
            HeaderButtons.NAV_CHILD,
            HeaderButtons.STOP,
            HeaderButtons.CONTINUE,
            HeaderButtons.COLLAPSE,
            HeaderButtons.RESET_SIZE,
            HeaderButtons.FIT_VIEWPORT,
            HeaderButtons.DELETE,
        ];
    }

    /**
     * Check if this node supports code execution operations
     * @returns {boolean}
     */
    supportsCodeExecution() {
        return true;
    }

    /**
     * Get the code content from this node
     * @returns {string|null}
     */
    getCode() {
        return this.node.code || this.node.content || null;
    }

    /**
     * Show generate UI for AI code generation
     * @param {string} nodeId - The node ID
     * @param {Array<Object>} models - Available model options with {id, name}
     * @param {string} currentModel - Currently selected model ID
     * @param {Canvas} canvas - Canvas instance for DOM manipulation
     * @param {App} app - App instance for event emission
     * @returns {boolean}
     */
    showGenerateUI(nodeId, models, currentModel, canvas, app) {
        const wrapper = canvas.nodeElements.get(nodeId);
        if (!wrapper) return false;

        // Don't add if already present
        if (wrapper.querySelector('.code-generate-input')) return false;

        const div = wrapper.querySelector('.node');
        const codeContent = div.querySelector('.code-node-content');
        if (!codeContent) return false;

        // Create input container with model dropdown defaulting to current model
        const inputHtml = `
            <div class="code-generate-input">
                <input type="text" placeholder="Describe the code to generate..." class="generate-prompt-input" />
                <select class="generate-model-select">
                    ${models
                        .map((m) => {
                            const selected = m.id === currentModel ? 'selected' : '';
                            return `<option value="${canvas.escapeHtml(m.id)}" ${selected}>${canvas.escapeHtml(m.name)}</option>`;
                        })
                        .join('')}
                </select>
                <button class="generate-submit-btn" title="Generate code">→</button>
                <button class="generate-cancel-btn" title="Cancel">×</button>
            </div>
        `;

        // Insert at the beginning of code-node-content (before data hint or editor)
        codeContent.insertAdjacentHTML('afterbegin', inputHtml);

        // Setup event listeners
        const input = codeContent.querySelector('.generate-prompt-input');
        const modelSelect = codeContent.querySelector('.generate-model-select');
        const submitBtn = codeContent.querySelector('.generate-submit-btn');
        const cancelBtn = codeContent.querySelector('.generate-cancel-btn');

        const submit = () => {
            const prompt = input.value.trim();
            const model = modelSelect.value;
            if (prompt) {
                // Emit event through canvas (app listens to this event)
                canvas.emit('nodeGenerateSubmit', nodeId, prompt, model);
            }
        };

        submitBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            submit();
        });

        cancelBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            this.hideGenerateUI(nodeId, canvas);
        });

        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                submit();
            } else if (e.key === 'Escape') {
                e.preventDefault();
                this.hideGenerateUI(nodeId, canvas);
            }
        });

        // Focus the input
        input.focus();
        return true;
    }

    /**
     * Hide generate UI
     * @param {string} nodeId - The node ID
     * @param {Canvas} canvas - Canvas instance for DOM manipulation
     * @returns {boolean}
     */
    hideGenerateUI(nodeId, canvas) {
        const wrapper = canvas.nodeElements.get(nodeId);
        if (!wrapper) return false;

        const input = wrapper.querySelector('.code-generate-input');
        if (input) {
            input.remove();
            return true;
        }
        return false;
    }

    /**
     * Code-specific event bindings for syntax highlighting initialization
     */
    getEventBindings() {
        return [
            // Initialize syntax highlighting after render
            {
                selector: '.code-display',
                event: 'init', // Special event: called after render, not a DOM event
                handler: (nodeId, e, canvas) => {
                    if (window.hljs) {
                        const codeEl = e.currentTarget.querySelector('code');
                        if (codeEl) {
                            window.hljs.highlightElement(codeEl);
                        }
                    }
                },
            },
        ];
    }
}

// Register with NodeRegistry
NodeRegistry.register({
    type: NodeType.CODE,
    protocol: CodeNode,
    defaultSize: DEFAULT_NODE_SIZES[NodeType.CODE],
});

// Export CodeNode for testing
export { CodeNode };

// =============================================================================
// Code Feature Plugin
// =============================================================================

/**
 * CodeFeature - Manages code self-healing and error recovery
 *
 * Extension hooks:
 * - selfheal:before - Before self-healing starts (CancellableEvent)
 * - selfheal:error - When code execution encounters an error
 * - selfheal:failed - When max retries are exhausted
 * - selfheal:success - When code executes successfully after fixing
 * - selfheal:fix - Before generating fix prompt (CancellableEvent, can customize prompt)
 */
export class CodeFeature extends FeaturePlugin {
    constructor(context) {
        super(context);

        // All dependencies are now inherited from FeaturePlugin base class:
        // - this.pyodideRunner
        // - this.streamingManager (unified streaming state)
        // - this.apiUrl
        // - this.buildLLMRequest
        // - this.graph, this.canvas, this.saveSession, etc.
    }

    /**
     * Initialize the feature
     */
    async onLoad() {
        console.log('[CodeFeature] Loaded - self-healing enabled');
    }

    /**
     * Self-heal code by executing and auto-fixing errors
     * @param {string} nodeId - The Code node ID
     * @param {string} originalPrompt - The original user prompt
     * @param {string} model - Model to use for fixes
     * @param {Object} context - Code generation context
     * @param {number} attemptNum - Current attempt number
     * @param {number} maxAttempts - Maximum retry attempts
     */
    async selfHealCode(nodeId, originalPrompt, model, context, attemptNum = 1, maxAttempts = 3) {
        const node = this.graph.getNode(nodeId);
        if (!node) return;
        const wrapped = wrapNode(node);
        if (!wrapped.supportsCodeExecution || !wrapped.supportsCodeExecution()) return;

        // Get the current code via protocol
        const code = wrapped.getCode() || '';
        if (!code || code.includes('# Generating')) return;

        console.log(`🔧 Self-healing attempt ${attemptNum}/${maxAttempts}...`);

        // Emit before:selfheal hook
        const beforeEvent = new CancellableEvent('selfheal:before', {
            nodeId,
            code,
            attemptNum,
            maxAttempts,
            originalPrompt,
            model,
            context,
        });
        this.emit('selfheal:before', beforeEvent);

        // Check if plugin prevented self-healing
        if (beforeEvent.defaultPrevented) {
            console.log('[Self-healing] Prevented by plugin');
            return;
        }

        // Update node to show self-healing status
        this.graph.updateNode(nodeId, {
            selfHealingAttempt: attemptNum,
            selfHealingStatus: attemptNum === 1 ? 'verifying' : 'fixing',
        });
        this.canvas.renderNode(this.graph.getNode(nodeId));

        // Run the code
        const csvNodeIds = node.csvNodeIds || [];
        const csvDataMap = {};
        csvNodeIds.forEach((csvId, index) => {
            const csvNode = this.graph.getNode(csvId);
            if (csvNode && csvNode.csvData) {
                const varName = csvNodeIds.length === 1 ? 'df' : `df${index + 1}`;
                csvDataMap[varName] = csvNode.csvData;
            }
        });

        // Set execution state
        this.graph.updateNode(nodeId, {
            executionState: 'running',
            lastError: null,
            installProgress: [],
            outputExpanded: false,
        });
        this.canvas.renderNode(this.graph.getNode(nodeId));

        const installMessages = [];
        let drawerOpenedForInstall = false;

        const onInstallProgress = (msg) => {
            installMessages.push(msg);
            if (!drawerOpenedForInstall) {
                this.graph.updateNode(nodeId, {
                    installProgress: [...installMessages],
                    outputExpanded: true,
                });
                this.canvas.renderNode(this.graph.getNode(nodeId));
                drawerOpenedForInstall = true;
            } else {
                this.graph.updateNode(nodeId, {
                    installProgress: [...installMessages],
                });
                const updatedNode = this.graph.getNode(nodeId);
                this.canvas.updateOutputPanelContent(nodeId, updatedNode);
            }
        };

        try {
            const result = await this.pyodideRunner.run(code, csvDataMap, onInstallProgress);

            // Check for errors
            if (result.error) {
                console.log(`❌ Error on attempt ${attemptNum}:`, result.error);

                // Emit selfheal:error hook
                this.emit(
                    'selfheal:error',
                    new CancellableEvent('selfheal:error', {
                        nodeId,
                        code,
                        error: result.error,
                        attemptNum,
                        maxAttempts,
                        originalPrompt,
                        model,
                        context,
                    })
                );

                // If we've exhausted retries, show final error
                if (attemptNum >= maxAttempts) {
                    console.log(`🛑 Max retries (${maxAttempts}) exceeded. Giving up.`);

                    // Emit selfheal:failed hook
                    this.emit(
                        'selfheal:failed',
                        new CancellableEvent('selfheal:failed', {
                            nodeId,
                            code,
                            error: result.error,
                            attemptNum,
                            maxAttempts,
                            originalPrompt,
                            model,
                        })
                    );

                    this.graph.updateNode(nodeId, {
                        executionState: 'error',
                        lastError: result.error,
                        outputStdout: result.stdout?.trim() || null,
                        outputHtml: null,
                        outputText: null,
                        outputExpanded: true,
                        installProgress: null,
                        selfHealingAttempt: null,
                        selfHealingStatus: 'failed',
                    });
                    this.canvas.renderNode(this.graph.getNode(nodeId));
                    this.saveSession();
                    return;
                }

                // Otherwise, ask LLM to fix the error
                await this.fixCodeError(
                    nodeId,
                    originalPrompt,
                    model,
                    context,
                    code,
                    result.error,
                    attemptNum,
                    maxAttempts
                );
                return;
            }

            // Success! Store output and clear self-healing status
            console.log(`✅ Code executed successfully on attempt ${attemptNum}`);

            // Emit selfheal:success hook
            this.emit(
                'selfheal:success',
                new CancellableEvent('selfheal:success', {
                    nodeId,
                    code,
                    attemptNum,
                    originalPrompt,
                    model,
                    result,
                })
            );

            const stdout = result.stdout?.trim() || null;
            const resultHtml = result.resultHtml || null;
            const resultText = result.resultText || null;
            const hasOutput = !!(stdout || resultHtml || resultText);

            this.graph.updateNode(nodeId, {
                executionState: 'idle',
                lastError: null,
                outputStdout: stdout,
                outputHtml: resultHtml,
                outputText: resultText,
                outputExpanded: drawerOpenedForInstall || hasOutput,
                installProgress: drawerOpenedForInstall ? installMessages : null,
                installComplete: drawerOpenedForInstall,
                selfHealingAttempt: null,
                selfHealingStatus: attemptNum > 1 ? 'fixed' : null, // Show "fixed" badge if we recovered from error
            });

            // Create child nodes for figures
            if (result.figures && result.figures.length > 0) {
                for (let i = 0; i < result.figures.length; i++) {
                    const dataUrl = result.figures[i];
                    const base64Match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
                    if (base64Match) {
                        const position = this.graph.autoPosition([nodeId]);
                        const outputNode = createNode(NodeType.IMAGE, '', {
                            position,
                            title: result.figures.length === 1 ? 'Figure' : `Figure ${i + 1}`,
                            imageData: base64Match[2],
                            mimeType: base64Match[1],
                        });

                        this.graph.addNode(outputNode);
                        const edge = createEdge(nodeId, outputNode.id, EdgeType.GENERATES);
                        this.graph.addEdge(edge);
                        this.canvas.renderNode(outputNode);
                    }
                }
            }

            this.canvas.renderNode(this.graph.getNode(nodeId));
            this.canvas.updateAllEdges(this.graph);
            this.canvas.updateAllNavButtonStates(this.graph);
            this.saveSession();

            // Auto-clear success badge after 5 seconds
            if (attemptNum > 1) {
                setTimeout(() => {
                    const currentNode = this.graph.getNode(nodeId);
                    if (currentNode && currentNode.selfHealingStatus === 'fixed') {
                        this.graph.updateNode(nodeId, { selfHealingStatus: null });
                        this.canvas.renderNode(this.graph.getNode(nodeId));
                        this.saveSession();
                    }
                }, 5000);
            }
        } catch (error) {
            // Show error
            console.error('Self-healing execution error:', error);
            this.graph.updateNode(nodeId, {
                executionState: 'error',
                lastError: error.message,
                outputStdout: null,
                outputHtml: null,
                outputText: null,
                outputExpanded: true,
                installProgress: null,
                selfHealingAttempt: null,
                selfHealingStatus: 'failed',
            });
            this.canvas.renderNode(this.graph.getNode(nodeId));
            this.saveSession();
        }
    }

    /**
     * Ask LLM to fix code errors and regenerate
     * @param {string} nodeId - The Code node ID
     * @param {string} originalPrompt - The original user prompt
     * @param {string} model - Model to use for fixes
     * @param {Object} context - Code generation context
     * @param {string} failedCode - The code that failed
     * @param {string} errorMessage - The error message from execution
     * @param {number} attemptNum - Current attempt number
     * @param {number} maxAttempts - Maximum retry attempts
     */
    async fixCodeError(nodeId, originalPrompt, model, context, failedCode, errorMessage, attemptNum, maxAttempts) {
        const node = this.graph.getNode(nodeId);
        if (!node) return;
        const wrapped = wrapNode(node);
        if (!wrapped.supportsCodeExecution || !wrapped.supportsCodeExecution()) return;

        console.log(`🩹 Asking LLM to fix error...`);

        // Build fix prompt
        let fixPrompt = `The previous code failed with this error:

\`\`\`
${errorMessage}
\`\`\`

Failed code:
\`\`\`python
${failedCode}
\`\`\`

Please fix the error and provide corrected Python code that accomplishes the original task: "${originalPrompt}"

Output ONLY the corrected Python code, no explanations.`;

        // Emit selfheal:fix hook to allow plugins to customize fix strategy
        const fixEvent = new CancellableEvent('selfheal:fix', {
            nodeId,
            failedCode,
            errorMessage,
            originalPrompt,
            model,
            context,
            attemptNum,
            maxAttempts,
            fixPrompt,
            customFixPrompt: null, // Plugins can set this
        });
        this.emit('selfheal:fix', fixEvent);

        // Use custom fix prompt if plugin provided one
        if (fixEvent.defaultPrevented && fixEvent.data.customFixPrompt) {
            console.log('[Self-healing] Using custom fix strategy from plugin');
            fixPrompt = fixEvent.data.customFixPrompt;
        }

        try {
            // Show placeholder
            const placeholderCode = `# Fixing error (attempt ${attemptNum + 1}/${maxAttempts})...\n`;
            const codeNode = this.graph.getNode(nodeId);
            if (codeNode) {
                const codeWrapped = wrapNode(codeNode);
                codeWrapped.updateContent(nodeId, placeholderCode, true, this.canvas);
            }
            this.graph.updateNode(nodeId, {
                content: placeholderCode,
                executionState: 'idle', // Clear running state
                selfHealingStatus: 'fixing',
            });
            this.canvas.renderNode(this.graph.getNode(nodeId));

            // Create AbortController and register with StreamingManager (auto-shows stop button)
            const abortController = new AbortController();
            this.streamingManager.register(nodeId, {
                abortController,
                featureId: 'code',
                context: { originalPrompt, model, nodeContext: context },
                // Code self-healing doesn't support continue (would need to restart)
            });

            // Build request body
            const requestBody = this.buildLLMRequest({
                prompt: fixPrompt,
                existing_code: '', // Don't send failed code again in existing_code field
                dataframe_info: context.dataframeInfo,
                context: context.ancestorContext,
            });

            // Stream fixed code
            const response = await fetch(this.apiUrl('/api/generate-code'), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(requestBody),
                signal: abortController.signal,
            });

            if (!response.ok) {
                throw new Error(`API error: ${response.statusText}`);
            }

            let fixedCode = '';
            await readSSEStream(response, {
                onEvent: (eventType, data) => {
                    if (eventType === 'message' && data) {
                        fixedCode += data;
                        const codeNode = this.graph.getNode(nodeId);
                        if (codeNode) {
                            const codeWrapped = wrapNode(codeNode);
                            codeWrapped.updateContent(nodeId, fixedCode, true, this.canvas);
                        }
                    }
                },
                onDone: async () => {
                    // Clean up streaming state (auto-hides stop button)
                    this.streamingManager.unregister(nodeId);

                    // Final update
                    const codeNode = this.graph.getNode(nodeId);
                    if (codeNode) {
                        const codeWrapped = wrapNode(codeNode);
                        codeWrapped.updateContent(nodeId, fixedCode, false, this.canvas);
                    }
                    this.graph.updateNode(nodeId, { content: fixedCode, code: fixedCode });
                    this.saveSession();

                    // Retry with fixed code
                    await this.selfHealCode(nodeId, originalPrompt, model, context, attemptNum + 1, maxAttempts);
                },
                onError: (err) => {
                    throw err;
                },
            });
        } catch (error) {
            // Clean up on error (auto-hides stop button)
            this.streamingManager.unregister(nodeId);

            // Check if it was aborted
            if (error.name === 'AbortError') {
                return;
            }

            // Show error
            console.error('Code fix generation failed:', error);
            const errorCode = `# Code fix generation failed: ${error.message}\n`;
            const codeNode = this.graph.getNode(nodeId);
            if (codeNode) {
                const codeWrapped = wrapNode(codeNode);
                codeWrapped.updateContent(nodeId, errorCode, false, this.canvas);
            }
            this.graph.updateNode(nodeId, {
                content: errorCode,
                selfHealingStatus: 'failed',
            });
            this.saveSession();
        }
    }
}
