// Recipe Modal Component
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, stripLoraTags, sendPromptToWorkflow, sendGenParamsToWorkflow } from '../utils/uiHelpers.js';
import { isModelWeightFile } from '../utils/modelFileTypes.js';
import { buildCivitaiUrl } from '../utils/civitaiUtils.js';
import { translate } from '../utils/i18nHelpers.js';
import { state } from '../state/index.js';
import { setSessionItem, removeSessionItem, getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow, extractRecipeId } from '../api/recipeApi.js';
import { downloadManager } from '../managers/DownloadManager.js';
import { MODEL_TYPES } from '../api/apiConfig.js';
import { openMediaViewer } from './shared/MediaViewer.js';
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
import { renderCompactTags, setupTagTooltip } from './shared/utils.js';
import { setupTagEditMode } from './shared/ModelTags.js';

const ALLOWED_GEN_PARAM_KEYS = new Set([
    'prompt',
    'negative_prompt',
    'steps',
    'sampler',
    'cfg_scale',
    'seed',
    'size',
    'clip_skip',
    'denoising_strength',
]);

const GEN_PARAM_NORMALIZATION = {
    cfg: 'cfg_scale',
    cfgScale: 'cfg_scale',
    clipSkip: 'clip_skip',
    negativePrompt: 'negative_prompt',
    Sampler: 'sampler',
    sampler_name: 'sampler',
    scheduler: 'sampler',
    Steps: 'steps',
    Seed: 'seed',
    Size: 'size',
    Prompt: 'prompt',
    'Negative prompt': 'negative_prompt',
    'Cfg scale': 'cfg_scale',
    'Clip skip': 'clip_skip',
    'Denoising strength': 'denoising_strength',
};

const PARAM_DISPLAY_NAMES = {
    steps: 'Steps',
    sampler: 'Sampler',
    cfg_scale: 'CFG',
    seed: 'Seed',
    size: 'Size',
    clip_skip: 'Clip Skip',
    denoising_strength: 'Denoising Strength',
};

function escapeHtml(value) {
    if (value == null) {
        return '';
    }
    return String(value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

class RecipeModal {
    constructor() {
        this.promptEditorState = {};
        this.recipeHydrationRequestId = 0;
        this.navigationKeyHandler = null;
        this.navigationInProgress = false;
        this.resetLocalEditState();
        this.init();
    }

    createLocalEditState() {
        return {
            title: { commitVersion: 0, isDirty: false },
            tags: { commitVersion: 0, isDirty: false },
            prompt: { commitVersion: 0, isDirty: false },
            negative_prompt: { commitVersion: 0, isDirty: false },
            source_path: { commitVersion: 0, isDirty: false },
        };
    }

    resetLocalEditState() {
        this.localEditState = this.createLocalEditState();
        this.sourceUrlEditState = this.localEditState.source_path;
    }

    getLocalEditState(field) {
        if (!this.localEditState[field]) {
            this.localEditState[field] = { commitVersion: 0, isDirty: false };
        }
        return this.localEditState[field];
    }

    markFieldDirty(field) {
        this.getLocalEditState(field).isDirty = true;
    }

    clearFieldDirty(field) {
        this.getLocalEditState(field).isDirty = false;
    }

    commitField(field) {
        const fieldState = this.getLocalEditState(field);
        fieldState.isDirty = false;
        fieldState.commitVersion += 1;
    }

    captureLocalEditVersions() {
        return Object.fromEntries(
            Object.entries(this.localEditState).map(([field, state]) => [
                field,
                state.commitVersion,
            ])
        );
    }

    shouldPreserveField(field, requestVersions) {
        const fieldState = this.getLocalEditState(field);
        const requestVersion = requestVersions?.[field] ?? fieldState.commitVersion;
        return fieldState.isDirty || fieldState.commitVersion !== requestVersion;
    }

    hasFieldCommittedSinceRequest(field, requestVersions) {
        const fieldState = this.getLocalEditState(field);
        const requestVersion = requestVersions?.[field] ?? fieldState.commitVersion;
        return fieldState.commitVersion !== requestVersion;
    }

    init() {
        this.setupCopyButtons();
        this.setupStripLoraToggle();
        this.setupPromptEditors();
        this.setupNavigationControls();
        this.setupDeleteControl();

        // Set up document click handler to close edit fields
        document.addEventListener('click', (event) => {
            const recipeModal = document.getElementById('recipeModal');
            if (recipeModal && recipeModal.style.display !== 'none') {
                const mediaEl = event.target.closest('.recipe-preview-media');
                if (mediaEl && mediaEl.tagName) {
                    event.stopPropagation();
                    const isVideo = mediaEl.tagName === 'VIDEO';
                    const url = mediaEl.src || mediaEl.currentSrc;
                    if (url) {
                        openMediaViewer(url, {
                            type: isVideo ? 'video' : 'image',
                            title: document.getElementById('recipeModalTitle')?.textContent || ''
                        });
                    }
                    return;
                }
            }

            // Handle title edit
            const titleEditor = document.getElementById('recipeTitleEditor');
            if (titleEditor && titleEditor.classList.contains('active') &&
                !titleEditor.contains(event.target) &&
                !event.target.closest('.edit-icon')) {
                this.saveTitleEdit();
            }

            // Handle reconnect input
            const reconnectContainers = document.querySelectorAll('.lora-reconnect-container');
            reconnectContainers.forEach(container => {
                if (container.classList.contains('active') &&
                    !container.contains(event.target) &&
                    !event.target.closest('.lora-reconnect')) {
                    this.hideReconnectInput(container);
                }
            });
        });
    }

    setupNavigationControls() {
        const prevBtn = document.getElementById('recipeNavPrevBtn');
        const nextBtn = document.getElementById('recipeNavNextBtn');

        if (prevBtn) {
            prevBtn.addEventListener('click', () => this.handleDirectionalNavigation('prev'));
        }
        if (nextBtn) {
            nextBtn.addEventListener('click', () => this.handleDirectionalNavigation('next'));
        }
        this.updateNavigationControls();
    }

    setupDeleteControl() {
        const deleteBtn = document.getElementById('deleteRecipeBtn');
        if (deleteBtn) {
            deleteBtn.addEventListener('click', () => this.handleDeleteRecipe());
        }
    }

    handleDeleteRecipe() {
        if (!this.currentRecipe) return;
        showRecipeDeleteConfirmation(this.currentRecipe);
    }

    shouldIgnoreNavigationKey(event) {
        const target = event.target;
        if (!target) return false;
        const tagName = target.tagName ? target.tagName.toLowerCase() : '';
        return target.isContentEditable || ['input', 'textarea', 'select', 'button'].includes(tagName);
    }

    updateNavigationControls() {
        const modalElement = document.getElementById('recipeModal');
        if (!modalElement) return;

        const prevBtn = modalElement.querySelector('#recipeNavPrevBtn');
        const nextBtn = modalElement.querySelector('#recipeNavNextBtn');
        if (!prevBtn || !nextBtn) return;

        const scroller = state.virtualScroller;
        if (!scroller || typeof scroller.getNavigationState !== 'function') {
            prevBtn.disabled = true;
            nextBtn.disabled = true;
            return;
        }

        const { hasPrev, hasNext } = scroller.getNavigationState(this.listFilePath || this.filePath || '');
        prevBtn.disabled = this.navigationInProgress || !hasPrev;
        nextBtn.disabled = this.navigationInProgress || !hasNext;
    }

    cleanupNavigationShortcuts() {
        if (this.navigationKeyHandler) {
            document.removeEventListener('keydown', this.navigationKeyHandler);
            this.navigationKeyHandler = null;
        }
        this.navigationInProgress = false;
    }

    setupNavigationShortcuts() {
        const modalElement = document.getElementById('recipeModal');
        if (!modalElement) return;

        this.cleanupNavigationShortcuts();

        this.navigationKeyHandler = (event) => {
            if (this.shouldIgnoreNavigationKey(event)) return;

            if (event.key === 'ArrowLeft') {
                event.preventDefault();
                this.handleDirectionalNavigation('prev');
            } else if (event.key === 'ArrowRight') {
                event.preventDefault();
                this.handleDirectionalNavigation('next');
            } else if (event.key === 'Delete') {
                event.preventDefault();
                this.handleDeleteRecipe();
            }
        };

        document.addEventListener('keydown', this.navigationKeyHandler);
    }

    async handleDirectionalNavigation(direction) {
        if (this.navigationInProgress) return;

        const scroller = state.virtualScroller;
        const filePath = this.listFilePath || this.filePath || '';

        if (!filePath || !scroller || typeof scroller.getAdjacentItemByFilePath !== 'function') {
            return;
        }

        this.navigationInProgress = true;
        this.updateNavigationControls();

        try {
            const adjacent = await scroller.getAdjacentItemByFilePath(filePath, direction);
            if (!adjacent || !adjacent.item) {
                const toastKey = direction === 'prev' ? 'toast.recipes.noPreviousRecipe' : 'toast.recipes.noNextRecipe';
                const toastFallback = direction === 'prev' ? 'No previous recipe available' : 'No next recipe available';
                showToast(toastKey, {}, 'info', toastFallback);
                return;
            }

            this.showRecipeDetails(adjacent.item);
        } finally {
            this.navigationInProgress = false;
            this.updateNavigationControls();
        }
    }

    showRecipeDetails(recipe) {
        const hydratedRecipe = recipe || {};
        this.resetLocalEditState();
        // Store the full recipe for editing
        this.currentRecipe = hydratedRecipe;
        this.resetPromptEditors();

        // Set modal title with edit icon
        const modalTitle = document.getElementById('recipeModalTitle');
        if (modalTitle) {
            modalTitle.innerHTML = `
                <div class="editable-content">
                    <span class="content-text">${hydratedRecipe.title || 'Recipe Details'}</span>
                    <button class="edit-icon" title="Edit recipe name"><i class="fas fa-pencil-alt"></i></button>
                </div>
                <div id="recipeTitleEditor" class="content-editor">
                    <input type="text" class="title-input" value="${hydratedRecipe.title || ''}">
                </div>
            `;

            // Add event listener for title editing
            const editIcon = modalTitle.querySelector('.edit-icon');
            editIcon.addEventListener('click', () => this.showTitleEditor());

            // Add key event listener for Enter key
            const titleInput = modalTitle.querySelector('.title-input');
            titleInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    this.saveTitleEdit();
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    this.cancelTitleEdit();
                }
            });
        }

        // Store the recipe ID for copy syntax API call
        this.recipeId = hydratedRecipe.id;
        this.filePath = hydratedRecipe.file_path;
        this.listFilePath = hydratedRecipe.file_path;

        // Render tags using shared utility
        const tagsContainer = document.getElementById('recipeTagsContainer');
        if (tagsContainer) {
            this.updateTagsDisplay(tagsContainer, hydratedRecipe.tags || []);
        }

        // Set recipe image
        const mediaContainer = document.getElementById('recipePreviewContainer');
        if (mediaContainer) {
            this.syncPreviewMedia(hydratedRecipe);
            mediaContainer.querySelector('.source-url-container')?.remove();
            mediaContainer.querySelector('.source-url-editor')?.remove();

            // Add source URL container if the recipe has a source_path
            const sourceUrlContainer = document.createElement('div');
            sourceUrlContainer.className = 'source-url-container';
            const hasSourceUrl = hydratedRecipe.source_path && hydratedRecipe.source_path.trim().length > 0;
            const sourceUrl = hasSourceUrl ? hydratedRecipe.source_path : '';
            const isValidUrl = hasSourceUrl && (sourceUrl.startsWith('http://') || sourceUrl.startsWith('https://'));

            sourceUrlContainer.innerHTML = `
                <div class="source-url-content">
                    <span class="source-url-icon"><i class="fas fa-link"></i></span>
                    <span class="source-url-text" title="${isValidUrl ? 'Click to open source URL' : 'No valid URL'}">${hasSourceUrl ? sourceUrl : 'No source URL'
                }</span>
                </div>
                <button class="source-url-edit-btn" title="Edit source URL">
                    <i class="fas fa-pencil-alt"></i>
                </button>
            `;

            // Add source URL editor
            const sourceUrlEditor = document.createElement('div');
            sourceUrlEditor.className = 'source-url-editor';
            sourceUrlEditor.innerHTML = `
                <input type="text" class="source-url-input" placeholder="Enter source URL (e.g., https://civitai.com/...)" value="${sourceUrl}">
                <div class="source-url-actions">
                    <button class="source-url-cancel-btn">Cancel</button>
                    <button class="source-url-save-btn">Save</button>
                </div>
            `;

            // Append both containers to the media container
            mediaContainer.appendChild(sourceUrlContainer);
            mediaContainer.appendChild(sourceUrlEditor);

            // Delay binding slightly so modal layout is stable, but skip if this render was torn down.
            const sourceUrlContainerRef = sourceUrlContainer;
            const sourceUrlEditorRef = sourceUrlEditor;
            setTimeout(() => {
                if (!document.body.contains(sourceUrlContainerRef) || !document.body.contains(sourceUrlEditorRef)) {
                    return;
                }
                this.setupSourceUrlHandlers();
            }, 50);
        }

        this.syncGenerationParams(hydratedRecipe.gen_params);
        this.syncResourcesSection(hydratedRecipe);
        this.syncHeaderActions();

        // Show the modal
        modalManager.showModal('recipeModal', null, null, () => this.cleanupNavigationShortcuts());
        this.updateNavigationControls();
        this.setupNavigationShortcuts();

        if (this.recipeId) {
            // Fire-and-forget: record this open for the "Recently Opened"
            // sort. Tracking must never disturb the modal, so failures are
            // swallowed.
            fetch(`/api/lm/recipe/${encodeURIComponent(this.recipeId)}/opened`, {
                method: 'POST',
                keepalive: true,
            }).catch(() => {});

            const hydrationRequestId = ++this.recipeHydrationRequestId;
            const requestEditVersions = this.captureLocalEditVersions();
            this.hydrateRecipeDetails(
                this.recipeId,
                hydrationRequestId,
                requestEditVersions
            );
        }
    }

    async hydrateRecipeDetails(recipeId, requestId, requestEditVersions = {}) {
        try {
            const fullRecipe = await fetchRecipeDetails(recipeId);
            if (requestId !== this.recipeHydrationRequestId || !fullRecipe) {
                return;
            }

            const nextRecipe = { ...this.currentRecipe };

            if (!this.hasFieldCommittedSinceRequest('title', requestEditVersions) && fullRecipe.title !== undefined) {
                nextRecipe.title = fullRecipe.title;
            }

            if (!this.hasFieldCommittedSinceRequest('tags', requestEditVersions) && fullRecipe.tags !== undefined) {
                nextRecipe.tags = Array.isArray(fullRecipe.tags) ? [...fullRecipe.tags] : fullRecipe.tags;
            }

            if (!this.hasFieldCommittedSinceRequest('source_path', requestEditVersions)) {
                nextRecipe.source_path = fullRecipe.source_path || '';
            }

            const previousFilePath = nextRecipe.file_path;
            if (fullRecipe.file_path !== undefined) {
                nextRecipe.file_path = fullRecipe.file_path;
            }
            if (fullRecipe.file_url !== undefined) {
                nextRecipe.file_url = fullRecipe.file_url;
            }
            if (fullRecipe.preview_url !== undefined) {
                nextRecipe.preview_url = fullRecipe.preview_url;
            }
            if (
                fullRecipe.file_path !== undefined &&
                fullRecipe.file_path !== previousFilePath &&
                fullRecipe.file_url === undefined &&
                fullRecipe.preview_url === undefined
            ) {
                delete nextRecipe.file_url;
                delete nextRecipe.preview_url;
            }

            if (fullRecipe.gen_params !== undefined) {
                const previousGenParams = nextRecipe.gen_params || {};
                const incomingGenParams = { ...(fullRecipe.gen_params || {}) };
                for (const [key, value] of Object.entries(previousGenParams)) {
                    if (this.hasFieldCommittedSinceRequest(key, requestEditVersions)) {
                        incomingGenParams[key] = value;
                    }
                }
                nextRecipe.gen_params = incomingGenParams;
            } else {
                const previousGenParams = nextRecipe.gen_params || {};
                const preservedGenParams = {};
                for (const [key, value] of Object.entries(previousGenParams)) {
                    if (this.hasFieldCommittedSinceRequest(key, requestEditVersions)) {
                        preservedGenParams[key] = value;
                    }
                }
                nextRecipe.gen_params = preservedGenParams;
            }

            if (fullRecipe.has_workflow !== undefined) {
                nextRecipe.has_workflow = fullRecipe.has_workflow;
            }

            if (fullRecipe.checkpoint !== undefined) {
                nextRecipe.checkpoint = fullRecipe.checkpoint;
            } else {
                delete nextRecipe.checkpoint;
            }
            if (fullRecipe.loras !== undefined) {
                nextRecipe.loras = Array.isArray(fullRecipe.loras) ? [...fullRecipe.loras] : fullRecipe.loras;
            } else {
                delete nextRecipe.loras;
            }

            this.currentRecipe = nextRecipe;
            this.filePath = this.currentRecipe.file_path || this.filePath;

            this.syncHydratedRecipeFields(requestEditVersions);
        } catch (error) {
            // Keep the cached recipe visible if hydration fails.
            console.warn('Failed to hydrate recipe details:', error);
        }
    }

    syncHydratedRecipeFields(requestEditVersions = {}) {
        this.syncPreviewMedia(this.currentRecipe);

        if (!this.shouldPreserveField('title', requestEditVersions)) {
            this.syncTitleDisplay(this.currentRecipe?.title || '');
        }

        if (!this.shouldPreserveField('tags', requestEditVersions)) {
            this.syncTagsDisplay(this.currentRecipe?.tags || []);
        }

        if (!this.shouldPreserveField('prompt', requestEditVersions)) {
            this.syncPromptField(
                'prompt',
                this.currentRecipe?.gen_params?.prompt || '',
                'No prompt information available'
            );
        }

        if (!this.shouldPreserveField('negative_prompt', requestEditVersions)) {
            this.syncPromptField(
                'negative_prompt',
                this.currentRecipe?.gen_params?.negative_prompt || '',
                'No negative prompt information available'
            );
        }

        this.syncGenerationParams(this.currentRecipe?.gen_params, { promptFieldsOnly: true });
        this.syncResourcesSection(this.currentRecipe);

        if (!this.shouldPreserveField('source_path', requestEditVersions)) {
            this.updateSourceUrlDisplay(this.currentRecipe.source_path || '', { forceInputSync: true });
        } else {
            this.updateSourceUrlDisplay(this.currentRecipe.source_path || '');
        }
        this.syncHeaderActions();
    }

    getPreviewMediaUrl(recipe = {}) {
        return recipe.file_url ||
            recipe.preview_url ||
            (recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` :
                '/loras_static/images/no-preview.png');
    }

    syncPreviewMedia(recipe = {}) {
        const mediaContainer = document.getElementById('recipePreviewContainer');
        if (!mediaContainer) {
            return;
        }

        const previewUrl = this.getPreviewMediaUrl(recipe);
        const isVideo = previewUrl.toLowerCase().endsWith('.mp4');
        const expectedElementId = isVideo ? 'recipeModalVideo' : 'recipeModalImage';
        let previewElement = mediaContainer.querySelector(`#${expectedElementId}`);
        const existingPreviewElement = mediaContainer.querySelector('.recipe-preview-media');

        if (!previewElement || (existingPreviewElement && existingPreviewElement !== previewElement)) {
            if (existingPreviewElement?.tagName === 'VIDEO') {
                const existingVideo = existingPreviewElement;
                existingVideo.pause();
                existingVideo.currentTime = 0;
            }

            existingPreviewElement?.remove();
            previewElement = document.createElement(isVideo ? 'video' : 'img');
            previewElement.id = expectedElementId;
            previewElement.className = 'recipe-preview-media';
            mediaContainer.prepend(previewElement);
        }

        previewElement.src = previewUrl;
        previewElement.alt = recipe.title || 'Recipe Preview';

        if (isVideo) {
            previewElement.controls = true;
            previewElement.autoplay = false;
            previewElement.loop = true;
            previewElement.muted = true;
        }
    }

    getMetadataUpdateOptions() {
        return this.listFilePath ? { listFilePath: this.listFilePath } : {};
    }

    syncTitleDisplay(title) {
        const titleContainer = document.getElementById('recipeModalTitle');
        if (!titleContainer) {
            return;
        }

        const contentText = titleContainer.querySelector('.content-text');
        if (contentText) {
            contentText.textContent = title || 'Recipe Details';
        }

        const titleInput = titleContainer.querySelector('.title-input');
        if (titleInput) {
            titleInput.value = title || '';
        }
    }

    syncHeaderActions() {
        const actionsContainer = document.getElementById('recipeHeaderActions');
        if (!actionsContainer) {
            return;
        }

        actionsContainer.querySelectorAll('.recipe-source-url-btn').forEach(btn => btn.remove());

        // Keep the delete button as the last (rightmost) header action;
        // insertBefore with null falls back to appendChild if it is missing.
        const deleteBtn = document.getElementById('deleteRecipeBtn');

        if (this.currentRecipe?.has_workflow === true) {
            const workflowBtn = document.createElement('button');
            workflowBtn.className = 'recipe-source-url-btn';
            workflowBtn.id = 'sendWorkflowBtn';
            workflowBtn.title = 'Send Workflow to ComfyUI';
            workflowBtn.innerHTML = '<i class="fas fa-project-diagram"></i> Send Workflow to ComfyUI';
            workflowBtn.addEventListener('click', () => {
                this.sendWorkflowToComfyUI();
            });
            actionsContainer.insertBefore(workflowBtn, deleteBtn);
        }

        const sourcePath = this.currentRecipe?.source_path || '';
        const isValidUrl = sourcePath.startsWith('http://') || sourcePath.startsWith('https://');
        if (isValidUrl) {
            const btn = document.createElement('button');
            btn.className = 'recipe-source-url-btn';
            btn.title = sourcePath;
            btn.innerHTML = '<i class="fas fa-globe"></i> Open Source URL';
            btn.addEventListener('click', () => {
                window.open(sourcePath, '_blank');
            });
            actionsContainer.insertBefore(btn, deleteBtn);
        }
    }

    async sendWorkflowToComfyUI() {
        if (!this.recipeId) {
            return;
        }

        try {
            const result = await sendRecipeWorkflow(this.recipeId);
            if (result?.success) {
                showToast('toast.recipes.workflowSent', {}, 'success', 'Workflow sent to ComfyUI');
                return;
            }

            const error = result?.error || '';
            if (error === 'Standalone Mode Active') {
                showToast('toast.general.cannotInteractStandalone', {}, 'warning', 'Cannot interact with ComfyUI in standalone mode');
            } else if (error === 'no_workflow') {
                showToast('toast.recipes.workflowNoWorkflow', {}, 'warning', 'No embedded workflow found in this recipe');
            } else {
                showToast('toast.recipes.workflowSendFailed', { error }, 'error', `Failed to send workflow to ComfyUI: ${error}`);
            }
        } catch (error) {
            console.error('Failed to send workflow to ComfyUI:', error);
            showToast('toast.recipes.workflowSendFailed', { error: error.message }, 'error', `Failed to send workflow to ComfyUI: ${error.message}`);
        }
    }

    syncTagsDisplay(tags) {
        const container = document.getElementById('recipeTagsContainer');
        if (!container) return;
        this.updateTagsDisplay(container, tags || []);
    }

    // Re-render tags display using shared utility, wire edit mode with ModelTags
    updateTagsDisplay(container, tags) {
        const filePath = this.filePath || '';

        container.innerHTML = renderCompactTags(tags, filePath);

        // Setup tooltip for all tags
        setupTagTooltip(container);

        // Wire edit button using shared tag editing (no suggestions for recipes)
        setupTagEditMode(null, {
            container: container,
            showSuggestions: false,
            normalizeTag: false,
            saveHandler: async (filePath, tags) => {
                await updateRecipeMetadata(filePath, { tags }, this.getMetadataUpdateOptions());
            },
            onSaved: (tags) => {
                this.currentRecipe.tags = tags;
                this.commitField('tags');
                const c = document.getElementById('recipeTagsContainer');
                if (c) this.updateTagsDisplay(c, tags);
            },
        });
    }

    syncPromptField(field, value, placeholder) {
        const contentId = field === 'prompt' ? 'recipePrompt' : 'recipeNegativePrompt';
        const editorId = field === 'prompt' ? 'recipePromptEditor' : 'recipeNegativePromptEditor';
        const inputId = field === 'prompt' ? 'recipePromptInput' : 'recipeNegativePromptInput';

        this.renderPromptContent(document.getElementById(contentId), value, placeholder);

        const input = document.getElementById(inputId);
        if (input) {
            input.value = value || '';
        }
    }

    syncGenerationParams(genParams, options = {}) {
        const promptElement = document.getElementById('recipePrompt');
        const negativePromptElement = document.getElementById('recipeNegativePrompt');
        const otherParamsElement = document.getElementById('recipeOtherParams');
        const promptInput = document.getElementById('recipePromptInput');
        const negativePromptInput = document.getElementById('recipeNegativePromptInput');
        const promptFieldsOnly = options.promptFieldsOnly === true;
        const sanitizedGenParams = this.sanitizeGenParams(genParams);

        if (sanitizedGenParams) {
            if (!promptFieldsOnly) {
                this.renderPromptContent(promptElement, sanitizedGenParams.prompt, 'No prompt information available');
                this.renderPromptContent(negativePromptElement, sanitizedGenParams.negative_prompt, 'No negative prompt information available');

                if (promptInput) {
                    promptInput.value = sanitizedGenParams.prompt || '';
                }

                if (negativePromptInput) {
                    negativePromptInput.value = sanitizedGenParams.negative_prompt || '';
                }
            }

            if (otherParamsElement) {
                otherParamsElement.innerHTML = '';
                const excludedParams = ['prompt', 'negative_prompt'];

                for (const [key, value] of Object.entries(sanitizedGenParams)) {
                    if (!excludedParams.includes(key) && value !== undefined && value !== null) {
                        const displayName = PARAM_DISPLAY_NAMES[key] || key;
                        const paramTag = document.createElement('div');
                        paramTag.className = 'param-tag';
                        paramTag.innerHTML = `
                            <span class="param-name">${displayName}:</span>
                            <span class="param-value">${value}</span>
                        `;
                        otherParamsElement.appendChild(paramTag);
                    }
                }

                if (otherParamsElement.children.length === 0) {
                    otherParamsElement.innerHTML = '<div class="no-params">No additional parameters available</div>';
                }
            }
            return;
        }

        if (!promptFieldsOnly) {
            this.renderPromptContent(promptElement, '', 'No prompt information available');
            this.renderPromptContent(negativePromptElement, '', 'No negative prompt information available');
            if (promptInput) promptInput.value = '';
            if (negativePromptInput) negativePromptInput.value = '';
        }

        if (otherParamsElement) {
            otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
        }
    }

    sanitizeGenParams(genParams) {
        if (!genParams || typeof genParams !== 'object') {
            return null;
        }

        const sanitized = {};

        for (const [key, value] of Object.entries(genParams)) {
            if (value === undefined || value === null || value === '') {
                continue;
            }

            if (!ALLOWED_GEN_PARAM_KEYS.has(key)) {
                continue;
            }

            sanitized[key] = value;
        }

        for (const [key, value] of Object.entries(genParams)) {
            if (value === undefined || value === null || value === '') {
                continue;
            }

            const normalizedKey = GEN_PARAM_NORMALIZATION[key] || key;
            if (!ALLOWED_GEN_PARAM_KEYS.has(normalizedKey)) {
                continue;
            }

            if (sanitized[normalizedKey] === undefined || sanitized[normalizedKey] === null || sanitized[normalizedKey] === '') {
                sanitized[normalizedKey] = value;
            }
        }

        return sanitized;
    }

    syncResourcesSection(recipe = {}) {
        const checkpointContainer = document.getElementById('recipeCheckpoint');
        const resourceDivider = document.getElementById('recipeResourceDivider');
        const lorasListElement = document.getElementById('recipeLorasList');
        const lorasCountElement = document.getElementById('recipeLorasCount');
        const loras = Array.isArray(recipe.loras) ? recipe.loras : [];

        if (checkpointContainer) {
            checkpointContainer.innerHTML = '';
            if (recipe.checkpoint && typeof recipe.checkpoint === 'object') {
                checkpointContainer.innerHTML = this.renderCheckpoint(recipe.checkpoint);
                this.setupCheckpointActions(checkpointContainer, recipe.checkpoint);
                this.setupCheckpointNavigation(checkpointContainer, recipe.checkpoint);
            }
        }

        let allLorasAvailable = true;
        let missingLorasCount = 0;
        let deletedLorasCount = 0;

        loras.forEach(lora => {
            if (lora.isDeleted) {
                deletedLorasCount++;
            } else if (!lora.inLibrary) {
                allLorasAvailable = false;
                missingLorasCount++;
            }
        });

        if (lorasCountElement) {
            const totalCount = loras.length;
            let statusHTML = '';
            if (totalCount > 0) {
                if (allLorasAvailable && deletedLorasCount === 0) {
                    statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle" aria-hidden="true"></i> ${translate('recipes.status.ready', {}, 'Ready to use')}</div>`;
                } else if (missingLorasCount > 0) {
                    // Rendered as a real button so the affordance is visible without
                    // hover and the control is keyboard/screen-reader accessible.
                    // Leading download icon: the red tint + "missing" text already
                    // encode the state, so the icon's job is to hint the action.
                    statusHTML = `<button type="button" class="recipe-status missing clickable"
                        title="${translate('recipes.status.downloadMissingTooltip', {}, 'Click to download missing LoRAs')}"
                        aria-label="${translate('recipes.status.downloadMissing', { count: missingLorasCount }, `Download ${missingLorasCount} missing LoRAs`)}">
                        <i class="fas fa-download" aria-hidden="true"></i> ${translate('recipes.status.missingCount', { count: missingLorasCount }, `${missingLorasCount} missing`)}
                    </button>`;
                } else if (deletedLorasCount > 0 && missingLorasCount === 0) {
                    statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle" aria-hidden="true"></i> ${translate('recipes.status.deletedCount', { count: deletedLorasCount }, `${deletedLorasCount} deleted`)}</div>`;
                }
            }

            lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} ${totalCount === 1 ? 'LoRA' : 'LoRAs'} ${statusHTML}`;

            const missingStatus = lorasCountElement.querySelector('.recipe-status.missing');
            if (missingStatus && missingLorasCount > 0) {
                missingStatus.addEventListener('click', () => this.showDownloadMissingLorasModal());
            }

            setTimeout(() => {
                const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
                if (viewRecipeLorasBtn) {
                    viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
                }
            }, 100);
        }

        if (lorasListElement && loras.length > 0) {
            lorasListElement.innerHTML = loras.map(lora => {
                const existsLocally = lora.inLibrary;
                const isDeleted = lora.isDeleted;
                const loraIndex = loras.indexOf(lora);

                // Status badges are pure indicators (consistent with the
                // versions-tab pattern): they never carry click behavior,
                // only a tooltip. Remediation lives in the action row below.
                let statusBadge;
                if (existsLocally) {
                    statusBadge = `
                        <div class="local-badge" title="${escapeHtml(translate('recipes.resources.inLibraryTooltip', {}, 'This model exists in your local library'))}">
                            <i class="fas fa-check" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.inLibrary', {}, 'In Library'))}
                        </div>`;
                } else if (isDeleted) {
                    statusBadge = `
                        <div class="deleted-badge" title="${escapeHtml(translate('recipes.resources.deletedTooltip', {}, 'This LoRA was deleted from the source and is no longer available for download'))}">
                            <i class="fas fa-trash-alt" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.deleted', {}, 'Deleted'))}
                        </div>`;
                } else if (lora.hashInvalid) {
                    statusBadge = `
                        <div class="invalid-hash-badge" title="${escapeHtml(translate('recipes.resources.hashInvalidTooltip', {}, 'This LoRA hash cannot be resolved on CivitAI - the model may have been updated'))}">
                            <i class="fas fa-question-circle" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.hashInvalid', {}, 'Unresolvable Hash'))}
                        </div>`;
                } else {
                    statusBadge = `
                        <div class="missing-badge" title="${escapeHtml(translate('recipes.resources.notInLibraryTooltip', {}, 'This model is not in your library'))}">
                            <i class="fas fa-exclamation-triangle" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.notInLibrary', {}, 'Not in Library'))}
                        </div>`;
                }

                const actionsRow = this.renderLoraItemActions(lora, loraIndex, { existsLocally, isDeleted });

                // The Civitai link belongs to the model name (it answers
                // "what is this"), so it sits inline in the title — the same
                // pattern the versions tab uses — never in the action row.
                // Skipped for deleted models: their source page is gone.
                const titleLink = isDeleted
                    ? ''
                    : this.renderCivitaiLink(this.getResourceCivitaiUrl(lora));

                const isPreviewVideo = lora.preview_url && lora.preview_url.toLowerCase().endsWith('.mp4');
                const previewMedia = isPreviewVideo ?
                    `<video class="thumbnail-video" autoplay loop muted playsinline>
                        <source src="${lora.preview_url}" type="video/mp4">
                     </video>` :
                    `<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`;

                let loraItemClass = 'recipe-lora-item';
                if (existsLocally) {
                    loraItemClass += ' exists-locally';
                } else if (isDeleted) {
                    loraItemClass += ' is-deleted';
                } else {
                    loraItemClass += ' missing-locally';
                }

                // Only in-library items are row-navigable (they open the local
                // LoRA detail); make that affordance keyboard-accessible.
                const rowA11yAttributes = existsLocally
                    ? ` role="button" tabindex="0" aria-label="${escapeHtml(translate('recipes.resources.openLoraDetails', { name: lora.modelName }, `View ${lora.modelName} in the LoRA library`))}"`
                    : '';

                return `
                    <div class="${loraItemClass}" data-lora-index="${loraIndex}"${rowA11yAttributes}>
                        <div class="recipe-lora-thumbnail">
                            ${previewMedia}
                        </div>
                        <div class="recipe-lora-content">
                            <div class="recipe-lora-header">
                                <div class="recipe-lora-title">
                                    <h4>${lora.modelName}</h4>
                                    ${titleLink}
                                </div>
                                <div class="badge-container">${statusBadge}</div>
                            </div>
                            <div class="recipe-lora-info">
                                ${lora.modelVersionName ? `<div class="recipe-lora-version">${lora.modelVersionName}</div>` : ''}
                                <div class="recipe-lora-weight">Weight: ${lora.strength || 1.0}</div>
                                ${lora.baseModel ? `<div class="base-model">${lora.baseModel}</div>` : ''}
                            </div>
                            ${actionsRow}
                            ${isDeleted ? `
                            <div class="lora-reconnect-container" data-lora-index="${loraIndex}">
                                <div class="reconnect-instructions">
                                    <p>Enter LoRA Syntax or Name to Reconnect:</p>
                                    <small>Example: <code>&lt;lora:Boris_Vallejo_BV_flux_D:1&gt;</code> or just <code>Boris_Vallejo_BV_flux_D</code></small>
                                </div>
                                <div class="reconnect-form">
                                    <input type="text" class="reconnect-input" placeholder="Enter LoRA name or syntax">
                                    <div class="reconnect-actions">
                                        <button class="reconnect-cancel-btn">Cancel</button>
                                        <button class="reconnect-confirm-btn">Reconnect</button>
                                    </div>
                                </div>
                            </div>` : ''}
                        </div>
                    </div>
                `;
            }).join('');

            setTimeout(() => {
                this.setupReconnectButtons();
                this.setupLoraItemActions();
                this.setupLoraItemsClickable();
            }, 100);

            this.recipeLorasSyntax = '';
        } else if (lorasListElement) {
            lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
            this.recipeLorasSyntax = '';
        }

        if (resourceDivider) {
            const hasCheckpoint = checkpointContainer && checkpointContainer.querySelector('.recipe-lora-item');
            const hasLoraItems = lorasListElement && lorasListElement.querySelector('.recipe-lora-item');
            resourceDivider.style.display = hasCheckpoint && hasLoraItems ? 'block' : 'none';
        }
    }

    updateSourceUrlDisplay(sourcePath, options = {}) {
        const sourceUrlContainer = document.querySelector('.source-url-container');
        const sourceUrlEditor = document.querySelector('.source-url-editor');
        if (!sourceUrlContainer || !sourceUrlEditor) {
            return;
        }

        const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
        const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
        if (!sourceUrlText || !sourceUrlInput) {
            return;
        }

        const normalizedSourcePath = typeof sourcePath === 'string' ? sourcePath.trim() : '';
        const isValidUrl = normalizedSourcePath.startsWith('http://') || normalizedSourcePath.startsWith('https://');

        sourceUrlText.textContent = normalizedSourcePath || 'No source URL';
        sourceUrlText.title = normalizedSourcePath
            ? (isValidUrl ? 'Click to open source URL' : 'No valid URL')
            : 'No valid URL';
        if (options.forceInputSync || !sourceUrlEditor.classList.contains('active') || !this.sourceUrlEditState.isDirty) {
            sourceUrlInput.value = normalizedSourcePath;
        }
    }

    // Title editing methods
    showTitleEditor() {
        const titleContainer = document.getElementById('recipeModalTitle');
        if (titleContainer) {
            titleContainer.querySelector('.editable-content').classList.add('hide');
            const editor = titleContainer.querySelector('#recipeTitleEditor');
            editor.classList.add('active');
            const input = editor.querySelector('input');
            input.oninput = () => this.markFieldDirty('title');
            input.focus();
            input.select();
        }
    }

    saveTitleEdit() {
        const titleContainer = document.getElementById('recipeModalTitle');
        if (titleContainer) {
            const editor = titleContainer.querySelector('#recipeTitleEditor');
            const input = editor.querySelector('input');
            const newTitle = input.value.trim();

            // Check if title changed
            if (newTitle && newTitle !== this.currentRecipe.title) {
                // Update title in the UI
                titleContainer.querySelector('.content-text').textContent = newTitle;

                // Update the recipe on the server
                updateRecipeMetadata(this.filePath, { title: newTitle }, this.getMetadataUpdateOptions())
                    .then(data => {
                        // Show success toast
                        showToast('toast.recipes.nameUpdated', {}, 'success');

                        // Update the current recipe object
                        this.currentRecipe.title = newTitle;
                        this.commitField('title');
                    })
                    .catch(error => {
                        // Error is handled in the API function
                        // Reset the UI if needed
                        titleContainer.querySelector('.content-text').textContent = this.currentRecipe.title || '';
                        this.clearFieldDirty('title');
                    });
            } else {
                this.clearFieldDirty('title');
            }

            // Hide editor
            editor.classList.remove('active');
            titleContainer.querySelector('.editable-content').classList.remove('hide');
        }
    }

    cancelTitleEdit() {
        const titleContainer = document.getElementById('recipeModalTitle');
        if (titleContainer) {
            // Reset input value
            const editor = titleContainer.querySelector('#recipeTitleEditor');
            const input = editor.querySelector('input');
            input.value = this.currentRecipe.title || '';
            this.clearFieldDirty('title');

            // Hide editor
            editor.classList.remove('active');
            titleContainer.querySelector('.editable-content').classList.remove('hide');
        }
    }

    setupPromptEditors() {
        const promptConfigs = [
            {
                editButtonId: 'editPromptBtn',
                contentId: 'recipePrompt',
                editorId: 'recipePromptEditor',
                inputId: 'recipePromptInput',
                field: 'prompt',
                placeholder: 'No prompt information available',
                successKey: 'toast.recipes.promptUpdated',
                successFallback: 'Prompt updated successfully',
            },
            {
                editButtonId: 'editNegativePromptBtn',
                contentId: 'recipeNegativePrompt',
                editorId: 'recipeNegativePromptEditor',
                inputId: 'recipeNegativePromptInput',
                field: 'negative_prompt',
                placeholder: 'No negative prompt information available',
                successKey: 'toast.recipes.negativePromptUpdated',
                successFallback: 'Negative prompt updated successfully',
            }
        ];

        promptConfigs.forEach((config) => {
            const editButton = document.getElementById(config.editButtonId);
            const input = document.getElementById(config.inputId);

            if (editButton) {
                editButton.addEventListener('click', () => this.showPromptEditor(config));
            }

            if (input) {
                input.addEventListener('input', () => this.markFieldDirty(config.field));
                input.addEventListener('keydown', (event) => {
                    if (event.key === 'Escape') {
                        event.preventDefault();
                        event.stopPropagation();
                        this.cancelPromptEdit(config);
                        return;
                    }

                    if (event.key === 'Enter' && !event.shiftKey) {
                        event.preventDefault();
                        event.stopPropagation();
                        this.promptEditorState[config.field] = {
                            ...(this.promptEditorState[config.field] || {}),
                            skipBlurSave: true,
                        };
                        this.savePromptEdit(config);
                    }
                });
                input.addEventListener('blur', () => {
                    const promptState = this.promptEditorState[config.field] || {};
                    if (promptState.skipBlurSave) {
                        this.promptEditorState[config.field] = {
                            ...promptState,
                            skipBlurSave: false,
                        };
                        return;
                    }

                    this.savePromptEdit(config);
                });
            }
        });
    }

    renderPromptContent(element, value, placeholder) {
        if (!element) {
            return;
        }

        const text = value || '';
        if (text) {
            element.textContent = text;
            element.classList.remove('is-placeholder');
        } else {
            element.textContent = placeholder;
            element.classList.add('is-placeholder');
        }
    }

    resetPromptEditors() {
        this.hidePromptEditor({ contentId: 'recipePrompt', editorId: 'recipePromptEditor' });
        this.hidePromptEditor({ contentId: 'recipeNegativePrompt', editorId: 'recipeNegativePromptEditor' });
    }

    showPromptEditor(config) {
        const content = document.getElementById(config.contentId);
        const editor = document.getElementById(config.editorId);
        const input = document.getElementById(config.inputId);

        if (!content || !editor || !input) {
            return;
        }

        const currentValue = this.currentRecipe?.gen_params?.[config.field] || '';
        input.value = currentValue;
        this.promptEditorState[config.field] = {
            initialValue: currentValue,
            skipBlurSave: false,
            isSaving: false,
        };
        content.classList.add('hide');
        editor.classList.add('active');
        input.focus();
        input.setSelectionRange(input.value.length, input.value.length);
    }

    async savePromptEdit(config) {
        const content = document.getElementById(config.contentId);
        const editor = document.getElementById(config.editorId);
        const input = document.getElementById(config.inputId);

        if (!content || !editor || !input || !this.currentRecipe) {
            return;
        }

        const promptState = this.promptEditorState[config.field] || {};
        if (promptState.isSaving) {
            return;
        }

        const currentGenParams = this.currentRecipe.gen_params || {};
        const nextValue = input.value.trim() === '' ? '' : input.value;
        const currentValue = this.sanitizeGenParams(currentGenParams)?.[config.field] || '';

        if (nextValue === currentValue) {
            this.clearFieldDirty(config.field);
            this.hidePromptEditor(config);
            return;
        }

        const nextGenParams = {
            ...currentGenParams,
            [config.field]: nextValue,
        };

        try {
            this.promptEditorState[config.field] = {
                ...promptState,
                isSaving: true,
            };
            await updateRecipeMetadata(this.filePath, { gen_params: nextGenParams }, this.getMetadataUpdateOptions());
            this.currentRecipe.gen_params = nextGenParams;
            this.renderPromptContent(content, nextValue, config.placeholder);
            showToast(config.successKey, {}, 'success', config.successFallback);
            this.commitField(config.field);
        } catch (error) {
            this.renderPromptContent(content, currentValue, config.placeholder);
            input.value = currentValue;
            this.clearFieldDirty(config.field);
        } finally {
            this.clearFieldDirty(config.field);
            this.hidePromptEditor(config);
        }
    }

    cancelPromptEdit(config) {
        const input = document.getElementById(config.inputId);
        if (input) {
            input.value = this.currentRecipe?.gen_params?.[config.field] || '';
        }

        this.clearFieldDirty(config.field);
        this.hidePromptEditor(config);
    }

    hidePromptEditor(config) {
        const content = document.getElementById(config.contentId);
        const editor = document.getElementById(config.editorId);

        if (content) {
            content.classList.remove('hide');
        }

        if (editor) {
            editor.classList.remove('active');
        }

        delete this.promptEditorState[config.field];
    }

    // Setup source URL handlers
    setupSourceUrlHandlers() {
        const sourceUrlContainer = document.querySelector('.source-url-container');
        const sourceUrlEditor = document.querySelector('.source-url-editor');
        if (!sourceUrlContainer || !sourceUrlEditor) {
            return;
        }
        const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
        const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn');
        const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn');
        const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn');
        const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');

        if (!sourceUrlText || !sourceUrlEditBtn || !sourceUrlCancelBtn || !sourceUrlSaveBtn || !sourceUrlInput) {
            return;
        }

        // Show editor on edit button click
        sourceUrlEditBtn.addEventListener('click', () => {
            sourceUrlContainer.classList.add('hide');
            sourceUrlEditor.classList.add('active');
            sourceUrlInput.focus();
        });

        sourceUrlInput.addEventListener('input', () => {
            this.sourceUrlEditState.isDirty = true;
        });

        // Cancel editing
        sourceUrlCancelBtn.addEventListener('click', () => {
            sourceUrlEditor.classList.remove('active');
            sourceUrlContainer.classList.remove('hide');
            this.updateSourceUrlDisplay(this.currentRecipe.source_path || '', { forceInputSync: true });
            this.clearFieldDirty('source_path');
        });

        // Save new source URL
        sourceUrlSaveBtn.addEventListener('click', () => {
            const newSourceUrl = sourceUrlInput.value.trim();
            if (newSourceUrl !== this.currentRecipe.source_path) {
                // Update the recipe on the server
                updateRecipeMetadata(this.filePath, { source_path: newSourceUrl }, this.getMetadataUpdateOptions())
                    .then(data => {
                        // Show success toast
                        showToast('toast.recipes.sourceUrlUpdated', {}, 'success');

                        // Update source URL in the UI
                        this.commitField('source_path');
                        this.updateSourceUrlDisplay(newSourceUrl, { forceInputSync: true });
                        this.syncHeaderActions();

                        // Update the current recipe object
                        this.currentRecipe.source_path = newSourceUrl;
                    })
                    .catch(error => {
                        // Error is handled in the API function
                        this.clearFieldDirty('source_path');
                    });
            } else {
                this.clearFieldDirty('source_path');
            }

            // Hide editor
            sourceUrlEditor.classList.remove('active');
            sourceUrlContainer.classList.remove('hide');
        });

        // Open source URL in a new tab if it's valid
        sourceUrlText.addEventListener('click', () => {
            const url = sourceUrlText.textContent.trim();
            if (url.startsWith('http://') || url.startsWith('https://')) {
                window.open(url, '_blank');
            }
        });
    }

    // Setup copy buttons for prompts and send recipe button
    setupCopyButtons() {
        const copyPromptBtn = document.getElementById('copyPromptBtn');
        const copyNegativePromptBtn = document.getElementById('copyNegativePromptBtn');
        const sendRecipeBtn = document.getElementById('sendRecipeBtn');

        if (copyPromptBtn) {
            copyPromptBtn.addEventListener('click', () => {
                let promptText = this.currentRecipe?.gen_params?.prompt || '';
                if (this.shouldStripLoraOnCopy()) {
                    promptText = RecipeModal.stripLoraTags(promptText);
                }
                this.copyToClipboard(promptText, 'Prompt copied to clipboard');
            });
        }

        if (copyNegativePromptBtn) {
            copyNegativePromptBtn.addEventListener('click', () => {
                let negativePromptText = this.currentRecipe?.gen_params?.negative_prompt || '';
                if (this.shouldStripLoraOnCopy()) {
                    negativePromptText = RecipeModal.stripLoraTags(negativePromptText);
                }
                this.copyToClipboard(negativePromptText, 'Negative prompt copied to clipboard');
            });
        }

        if (sendRecipeBtn) {
            sendRecipeBtn.addEventListener('click', () => {
                // Send recipe to ComfyUI workflow
                this.sendRecipeToWorkflow();
            });
        }

        // Send prompt to workflow buttons
        const sendPromptBtn = document.getElementById('sendPromptBtn');
        const sendNegativePromptBtn = document.getElementById('sendNegativePromptBtn');

        if (sendPromptBtn) {
            sendPromptBtn.addEventListener('click', () => {
                let promptText = this.currentRecipe?.gen_params?.prompt || '';
                if (this.shouldStripLoraOnCopy()) {
                    promptText = RecipeModal.stripLoraTags(promptText);
                }
                if (!promptText.trim()) {
                    showToast('toast.recipes.noPromptToSend', {}, 'warning');
                    return;
                }
                sendPromptToWorkflow(promptText);
            });
        }

        if (sendNegativePromptBtn) {
            sendNegativePromptBtn.addEventListener('click', () => {
                let negativePromptText = this.currentRecipe?.gen_params?.negative_prompt || '';
                if (this.shouldStripLoraOnCopy()) {
                    negativePromptText = RecipeModal.stripLoraTags(negativePromptText);
                }
                if (!negativePromptText.trim()) {
                    showToast('toast.recipes.noPromptToSend', {}, 'warning');
                    return;
                }
                sendPromptToWorkflow(negativePromptText, {
                    actionTypeText: 'Negative Prompt',
                });
            });
        }

        // Send params to workflow button
        const sendParamsBtn = document.getElementById('sendParamsBtn');
        if (sendParamsBtn) {
            sendParamsBtn.addEventListener('click', () => {
                const genParams = this.currentRecipe?.gen_params || {};
                if (!genParams || Object.keys(genParams).length === 0) {
                    showToast('No generation parameters available', {}, 'warning');
                    return;
                }
                sendGenParamsToWorkflow(genParams);
            });
        }
    }

    /**
     * Strip <lora:...> tags from prompt text and clean up residual punctuation/whitespace.
     * Handles both unescaped (<lora:...>) and HTML-escaped (&lt;lora:...&gt;) variants.
     * Cleans up artifacts like leading ", ", double commas, and extra whitespace.
     */
    static stripLoraTags(text) {
        return stripLoraTags(text);
    }

    shouldStripLoraOnCopy() {
        const toggle = document.getElementById('stripLoraOnCopyToggle');
        return toggle ? toggle.checked : false;
    }

    setupStripLoraToggle() {
        const toggle = document.getElementById('stripLoraOnCopyToggle');
        if (!toggle) return;

        const stored = getStorageItem('strip_lora_on_copy');
        if (stored !== null) {
            toggle.checked = stored === true;
        }

        toggle.addEventListener('change', () => {
            const checked = toggle.checked;
            setStorageItem('strip_lora_on_copy', checked);
            state.global.settings.strip_lora_on_copy = checked;
        });
    }

    // Helper method to copy text to clipboard
    copyToClipboard(text, successMessage) {
        copyToClipboard(text, successMessage);
    }

    // Send recipe to ComfyUI workflow
    async sendRecipeToWorkflow() {
        if (!this.recipeId) {
            showToast('toast.recipes.noRecipeId', {}, 'error');
            return;
        }

        try {
            // Fetch recipe syntax from backend
            const response = await fetch(`/api/lm/recipe/${this.recipeId}/syntax`);

            if (!response.ok) {
                throw new Error(`Failed to get recipe syntax: ${response.statusText}`);
            }

            const data = await response.json();

            if (data.success && data.syntax) {
                // Send the recipe syntax to ComfyUI workflow
                await sendLoraToWorkflow(data.syntax, false, 'recipe');
            } else {
                throw new Error(data.error || 'No syntax returned from server');
            }
        } catch (error) {
            console.error('Error sending recipe to workflow:', error);
            showToast('toast.recipes.sendToWorkflowFailed', { message: error.message }, 'error');
        }
    }

    // Add new method to handle downloading missing LoRAs
    async showDownloadMissingLorasModal() {
        console.log("currentRecipe", this.currentRecipe);
        // Get missing LoRAs from the current recipe
        const missingLoras = this.currentRecipe.loras.filter(lora => !lora.inLibrary);
        console.log("missingLoras", missingLoras);

        if (missingLoras.length === 0) {
            showToast('toast.recipes.noMissingLoras', {}, 'info');
            return;
        }

        try {
            state.loadingManager.showSimpleLoading('Getting version info for missing LoRAs...');

            // Get version info for each missing LoRA by calling the appropriate API endpoint
            const missingLorasWithVersionInfoPromises = missingLoras.map(async lora => {
                let endpoint;

                // Determine which endpoint to use based on available data
                if (lora.modelVersionId) {
                    endpoint = `/api/lm/loras/civitai/model/version/${lora.modelVersionId}`;
                } else if (lora.hash) {
                    endpoint = `/api/lm/loras/civitai/model/hash/${lora.hash}`;
                } else {
                    console.error("Missing both hash and modelVersionId for lora:", lora);
                    return null;
                }

                const response = await fetch(endpoint);
                const versionInfo = await response.json();

                // Return original lora data combined with version info
                return {
                    ...lora,
                    civitaiInfo: versionInfo
                };
            });

            // Wait for all API calls to complete
            const lorasWithVersionInfo = await Promise.all(missingLorasWithVersionInfoPromises);
            console.log("Loras with version info:", lorasWithVersionInfo);

            // Filter out null values (failed requests)
            const validLoras = lorasWithVersionInfo.filter(lora => lora !== null);

            if (validLoras.length === 0) {
                showToast('toast.recipes.missingLorasInfoFailed', {}, 'error');
                return;
            }

            // Close the recipe modal first
            modalManager.closeModal('recipeModal');

            // Prepare data for import manager using the retrieved information
            const recipeData = {
                loras: validLoras.map(lora => {
                    const civitaiInfo = lora.civitaiInfo;
                    const modelFile = civitaiInfo.files ?
                        civitaiInfo.files.find(file => isModelWeightFile(file.type)) : null;

                    return {
                        // Basic lora info
                        name: civitaiInfo.model?.name || lora.name,
                        version: civitaiInfo.name || '',
                        strength: lora.strength || 1.0,

                        // Model identifiers
                        modelId: lora.modelId || lora.model_id || civitaiInfo.modelId,
                        hash: modelFile?.hashes?.SHA256?.toLowerCase() || lora.hash,
                        id: civitaiInfo.id || lora.modelVersionId,

                        // Metadata
                        thumbnailUrl: civitaiInfo.images?.[0]?.url || '',
                        baseModel: civitaiInfo.baseModel || '',
                        downloadUrl: civitaiInfo.downloadUrl || '',
                        size: modelFile ? (modelFile.sizeKB * 1024) : 0,
                        file_name: modelFile ? modelFile.name.split('.')[0] : '',

                        // Status flags
                        existsLocally: false,
                        isDeleted: civitaiInfo.error === "Model not found",
                        isEarlyAccess: !!civitaiInfo.earlyAccessEndsAt,
                        earlyAccessEndsAt: civitaiInfo.earlyAccessEndsAt || ''
                    };
                })
            };

            console.log("recipeData for import:", recipeData);

            // Call ImportManager's download missing LoRAs method
            window.importManager.downloadMissingLoras(recipeData, this.currentRecipe.id);
        } catch (error) {
            console.error("Error downloading missing LoRAs:", error);
            showToast('toast.recipes.preparingForDownloadFailed', {}, 'error');
        } finally {
            state.loadingManager.hide();
        }
    }

    // Wire the reconnect form controls. The entry point is the explicit
    // "Reconnect" ghost button rendered for deleted LoRAs (see
    // setupLoraItemActions), keeping badges as pure status indicators.
    setupReconnectButtons() {
        // Add event listeners to reconnect cancel buttons
        const cancelButtons = document.querySelectorAll('.reconnect-cancel-btn');
        cancelButtons.forEach(button => {
            button.addEventListener('click', (e) => {
                const container = button.closest('.lora-reconnect-container');
                this.hideReconnectInput(container);
            });
        });

        // Add event listeners to reconnect confirm buttons
        const confirmButtons = document.querySelectorAll('.reconnect-confirm-btn');
        confirmButtons.forEach(button => {
            button.addEventListener('click', (e) => {
                const container = button.closest('.lora-reconnect-container');
                const input = container.querySelector('.reconnect-input');
                const loraIndex = container.getAttribute('data-lora-index');
                this.reconnectLora(loraIndex, input.value);
            });
        });

        // Add keydown handlers to reconnect inputs
        const reconnectInputs = document.querySelectorAll('.reconnect-input');
        reconnectInputs.forEach(input => {
            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    const container = input.closest('.lora-reconnect-container');
                    const loraIndex = container.getAttribute('data-lora-index');
                    this.reconnectLora(loraIndex, input.value);
                } else if (e.key === 'Escape') {
                    const container = input.closest('.lora-reconnect-container');
                    this.hideReconnectInput(container);
                }
            });
        });
    }

    showReconnectInput(loraIndex) {
        // Hide any currently active reconnect containers
        document.querySelectorAll('.lora-reconnect-container.active').forEach(active => {
            active.classList.remove('active');
        });

        // Show the reconnect container for this lora
        const container = document.querySelector(`.lora-reconnect-container[data-lora-index="${loraIndex}"]`);
        if (container) {
            container.classList.add('active');
            const input = container.querySelector('.reconnect-input');
            input.focus();
        }
    }

    hideReconnectInput(container) {
        if (container && container.classList.contains('active')) {
            container.classList.remove('active');
            const input = container.querySelector('.reconnect-input');
            if (input) input.value = '';
        }
    }

    async reconnectLora(loraIndex, inputValue) {
        if (!inputValue || !inputValue.trim()) {
            showToast('toast.recipes.enterLoraName', {}, 'error');
            return;
        }

        try {
            // Parse input value to extract file_name
            let loraSyntaxMatch = inputValue.match(/<lora:([^:>]+)(?::[^>]+)?>/);
            let fileName = loraSyntaxMatch ? loraSyntaxMatch[1] : inputValue.trim();

            // Remove .safetensors extension if present
            fileName = fileName.replace(/\.safetensors$/, '');

            state.loadingManager.showSimpleLoading('Reconnecting LoRA...');

            // Call API to reconnect the LoRA
            const response = await fetch('/api/lm/recipe/lora/reconnect', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    recipe_id: this.recipeId,
                    lora_index: loraIndex,
                    target_name: fileName
                })
            });

            const result = await response.json();

            if (result.success) {
                // Hide the reconnect input
                const container = document.querySelector(`.lora-reconnect-container[data-lora-index="${loraIndex}"]`);
                this.hideReconnectInput(container);

                // Update the current recipe with the updated lora data
                this.currentRecipe.loras[loraIndex] = result.updated_lora;

                // Show success message
                showToast('toast.recipes.reconnectedSuccessfully', {}, 'success');

                // Refresh modal to show updated content
                setTimeout(() => {
                    this.showRecipeDetails(this.currentRecipe);
                }, 500);

                state.virtualScroller.updateSingleItem(this.listFilePath || this.currentRecipe.file_path, {
                    loras: this.currentRecipe.loras
                });
            } else {
                showToast('toast.recipes.reconnectFailed', { message: result.error }, 'error');
            }
        } catch (error) {
            console.error('Error reconnecting LoRA:', error);
            showToast('toast.recipes.reconnectFailed', { message: error.message }, 'error');
        } finally {
            state.loadingManager.hide();
        }
    }

    renderCheckpoint(checkpoint) {
        const existsLocally = !!checkpoint.inLibrary;
        const localPath = checkpoint.localPath || '';
        const previewUrl = checkpoint.preview_url || checkpoint.thumbnailUrl || '/loras_static/images/no-preview.png';
        const isPreviewVideo = typeof previewUrl === 'string' && previewUrl.toLowerCase().endsWith('.mp4');
        const checkpointName = checkpoint.name || checkpoint.modelName || checkpoint.file_name || 'Checkpoint';
        const versionLabel = checkpoint.version || checkpoint.modelVersionName || '';
        const baseModel = checkpoint.baseModel || checkpoint.base_model || '';
        const modelTypeRaw = (checkpoint.sub_type || checkpoint.type || 'checkpoint').toLowerCase();
        const modelTypeLabel = modelTypeRaw === 'diffusion_model' ? 'Diffusion Model' : 'Checkpoint';

        const previewMedia = isPreviewVideo ? `
            <video class="thumbnail-video" autoplay loop muted playsinline>
                <source src="${previewUrl}" type="video/mp4">
            </video>
        ` : `<img src="${previewUrl}" alt="Checkpoint preview" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`;

        // Status badge: pure indicator with a tooltip, mirroring the LoRA
        // items and the versions-tab badge pattern. The header carries only
        // the badge; every action lives in the bottom action row.
        const badge = existsLocally ? `
            <div class="local-badge" title="${escapeHtml(translate('recipes.resources.inLibraryTooltip', {}, 'This model exists in your local library'))}">
                <i class="fas fa-check" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.inLibrary', {}, 'In Library'))}
            </div>
        ` : `
            <div class="missing-badge" title="${escapeHtml(translate('recipes.resources.notInLibraryTooltip', {}, 'This model is not in your library'))}">
                <i class="fas fa-exclamation-triangle" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.notInLibrary', {}, 'Not in Library'))}
            </div>
        `;

        const actions = [];
        if (existsLocally && localPath) {
            actions.push(`
                <button class="resource-action primary compact checkpoint-send">
                    <i class="fas fa-paper-plane"></i>
                    <span>${translate('recipes.actions.sendCheckpoint', {}, 'Send to ComfyUI')}</span>
                </button>
            `);
        } else if (!existsLocally && this.canDownloadCheckpoint(checkpoint)) {
            actions.push(`
                <button class="resource-action primary compact checkpoint-download">
                    <i class="fas fa-download"></i>
                    <span>${translate('modals.model.versions.actions.download', {}, 'Download')}</span>
                </button>
            `);
        }
        const actionsMarkup = actions.filter(Boolean).join('');
        const actionsRow = actionsMarkup
            ? `<div class="recipe-lora-actions">${actionsMarkup}</div>`
            : '';

        // Civitai link lives inline with the title, same as LoRA items.
        const titleLink = this.renderCivitaiLink(this.getResourceCivitaiUrl(checkpoint));

        // Only in-library checkpoints are row-navigable; make it keyboard-accessible.
        const rowA11yAttributes = existsLocally
            ? ` role="button" tabindex="0" aria-label="${escapeHtml(translate('recipes.resources.openCheckpointDetails', { name: checkpointName }, `View ${checkpointName} in the model library`))}"`
            : '';

        return `
            <div class="recipe-lora-item checkpoint-item ${existsLocally ? 'exists-locally' : 'missing-locally'}"${rowA11yAttributes}>
                <div class="recipe-lora-thumbnail">
                    ${previewMedia}
                </div>
                <div class="recipe-lora-content">
                    <div class="recipe-lora-header">
                        <div class="recipe-lora-title">
                            <h4>${checkpointName}</h4>
                            ${titleLink}
                        </div>
                        <div class="badge-container">${badge}</div>
                    </div>
                    <div class="recipe-lora-info recipe-checkpoint-meta">
                        ${versionLabel ? `<div class="recipe-lora-version">${versionLabel}</div>` : ''}
                        ${baseModel ? `<div class="base-model">${baseModel}</div>` : ''}
                        ${modelTypeLabel ? `<span class="checkpoint-type-text">${modelTypeLabel}</span>` : ''}
                    </div>
                    ${actionsRow}
                </div>
            </div>
        `;
    }

    setupCheckpointActions(container, checkpoint) {
        const sendBtn = container.querySelector('.checkpoint-send');
        if (sendBtn) {
            sendBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.sendCheckpointToWorkflow(checkpoint);
            });
        }

        const downloadBtn = container.querySelector('.checkpoint-download');
        if (downloadBtn) {
            downloadBtn.addEventListener('click', async (e) => {
                e.stopPropagation();
                await this.downloadCheckpoint(checkpoint, downloadBtn);
            });
        }
    }

    setupCheckpointNavigation(container, checkpoint) {
        // Only in-library checkpoints navigate (to the local detail view).
        // Missing checkpoints expose explicit Download / Civitai-link
        // controls instead, so a row click never has two different outcomes.
        if (!checkpoint.inLibrary) {
            return;
        }
        const checkpointItem = container.querySelector('.checkpoint-item');
        if (!checkpointItem) return;

        checkpointItem.addEventListener('click', (e) => {
            if (e.target.closest('.resource-action') || e.target.closest('.recipe-civitai-link')) {
                return;
            }
            this.navigateToCheckpointPage(checkpoint);
        });
        checkpointItem.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                this.navigateToCheckpointPage(checkpoint);
            }
        });
    }

    canDownloadCheckpoint(checkpoint) {
        if (!checkpoint) return false;
        const modelId = checkpoint.modelId || checkpoint.modelID || checkpoint.model_id;
        const versionId = checkpoint.id || checkpoint.modelVersionId;
        return !!(modelId && versionId);
    }

    async sendCheckpointToWorkflow(checkpoint) {
        if (!checkpoint || !checkpoint.localPath) {
            showToast('toast.recipes.missingCheckpointPath', {}, 'error');
            return;
        }

        const modelType = (checkpoint.sub_type || checkpoint.type || 'checkpoint').toLowerCase();
        const isDiffusionModel = modelType === 'diffusion_model' || modelType === 'unet';
        const widgetName = isDiffusionModel ? 'unet_name' : 'ckpt_name';

        const actionTypeText = translate(
            isDiffusionModel ? 'uiHelpers.nodeSelector.diffusionModel' : 'uiHelpers.nodeSelector.checkpoint',
            {},
            isDiffusionModel ? 'Diffusion Model' : 'Checkpoint'
        );
        const successMessage = translate(
            'uiHelpers.workflow.modelUpdated',
            {},
            'Model updated in workflow'
        );
        const failureMessage = translate(
            'uiHelpers.workflow.modelFailed',
            {},
            'Failed to update model node'
        );
        const missingNodesMessage = translate(
            'uiHelpers.workflow.noMatchingNodes',
            {},
            'No compatible nodes available in the current workflow'
        );
        const missingTargetMessage = translate(
            'uiHelpers.workflow.noTargetNodeSelected',
            {},
            'No target node selected'
        );

        await sendModelPathToWorkflow(checkpoint.localPath, {
            widgetName,
            collectionType: MODEL_TYPES.CHECKPOINT,
            actionTypeText,
            successMessage,
            failureMessage,
            missingNodesMessage,
            missingTargetMessage,
        });
    }

    async downloadCheckpoint(checkpoint, button) {
        if (!this.canDownloadCheckpoint(checkpoint)) {
            showToast('toast.recipes.missingCheckpointInfo', {}, 'error');
            return;
        }

        const modelId = checkpoint.modelId || checkpoint.modelID || checkpoint.model_id;
        const versionId = checkpoint.id || checkpoint.modelVersionId;
        const versionName = checkpoint.version || checkpoint.modelVersionName || checkpoint.name || 'Checkpoint';

        if (button) {
            button.disabled = true;
        }

        try {
            const success = await downloadManager.downloadVersionWithDefaults(
                MODEL_TYPES.CHECKPOINT,
                modelId,
                versionId,
                {
                    versionName,
                    source: 'recipe-modal',
                }
            );
            if (success) {
                await this.refreshResourcesAfterDownload();
            }
        } catch (error) {
            console.error('Error downloading checkpoint:', error);
            showToast('toast.recipes.downloadCheckpointFailed', { message: error.message }, 'error');
        } finally {
            if (button) {
                button.disabled = false;
            }
        }
    }

    getResourceCivitaiUrl(resource) {
        if (!resource) {
            return null;
        }
        const modelId = resource.modelId || resource.modelID || resource.model_id || null;
        const versionId = resource.id || resource.modelVersionId || null;
        const modelName = resource.modelName || resource.name || resource.file_name || null;
        return buildCivitaiUrl({
            modelId,
            versionId,
            modelName,
            host: state?.global?.settings?.civitai_host,
        });
    }

    canDownloadLora(lora) {
        if (!lora) return false;
        const modelId = lora.modelId || lora.modelID || lora.model_id;
        const versionId = lora.id || lora.modelVersionId;
        // Direct download needs both identifiers; a hash alone is enough
        // because downloadRecipeLora resolves it to a version on demand —
        // the same fallback the bulk "download missing" flow uses.
        return !!((modelId && versionId) || lora.hash);
    }

    renderCivitaiLink(url) {
        if (!url) {
            return '';
        }
        const tooltip = translate('recipes.resources.viewOnCivitai', {}, 'View on Civitai');
        return `
            <a
                class="recipe-civitai-link"
                href="${escapeHtml(url)}"
                target="_blank"
                rel="noopener noreferrer"
                title="${escapeHtml(tooltip)}"
                aria-label="${escapeHtml(tooltip)}"
            >
                <i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>
            </a>
        `;
    }

    renderLoraItemActions(lora, loraIndex, { existsLocally, isDeleted }) {
        // In-library LoRAs need no remediation: the badge and the local path
        // already tell the full story.
        if (existsLocally) {
            return '';
        }

        const controls = [];
        if (isDeleted || lora.hashInvalid) {
            const reconnectLabel = translate('recipes.resources.reconnect', {}, 'Reconnect');
            const reconnectTooltip = translate('recipes.resources.reconnectTooltip', {}, 'Reconnect with a local LoRA');
            controls.push(`
                <button type="button" class="resource-action ghost compact lora-reconnect" data-lora-index="${loraIndex}"
                    title="${escapeHtml(reconnectTooltip)}" aria-label="${escapeHtml(reconnectTooltip)}">
                    <i class="fas fa-link" aria-hidden="true"></i>
                    <span>${escapeHtml(reconnectLabel)}</span>
                </button>
            `);
        } else {
            if (this.canDownloadLora(lora)) {
                const downloadLabel = translate('recipes.resources.download', {}, 'Download');
                const downloadTooltip = translate('recipes.resources.downloadLoraTooltip', {}, 'Download this LoRA');
                controls.push(`
                    <button type="button" class="resource-action primary compact lora-download" data-lora-index="${loraIndex}"
                        title="${escapeHtml(downloadTooltip)}" aria-label="${escapeHtml(downloadTooltip)}">
                        <i class="fas fa-download" aria-hidden="true"></i>
                        <span>${escapeHtml(downloadLabel)}</span>
                    </button>
                `);
            }
        }

        const markup = controls.filter(Boolean).join('');
        if (!markup) {
            return '';
        }
        return `<div class="recipe-lora-actions">${markup}</div>`;
    }

    setupLoraItemActions() {
        const lorasListElement = document.getElementById('recipeLorasList');
        if (!lorasListElement) {
            return;
        }

        // Deferred wiring can run again after a hydration re-render while the
        // latest DOM is already in place; a data flag prevents stacking
        // duplicate handlers (which would fire the download twice).
        lorasListElement.querySelectorAll('.lora-download').forEach(button => {
            if (button.dataset.wired === 'true') {
                return;
            }
            button.dataset.wired = 'true';
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                const loraIndex = parseInt(button.dataset.loraIndex, 10);
                const lora = this.currentRecipe?.loras?.[loraIndex];
                if (lora) {
                    this.downloadRecipeLora(lora, button, loraIndex);
                }
            });
        });

        lorasListElement.querySelectorAll('.lora-reconnect').forEach(button => {
            if (button.dataset.wired === 'true') {
                return;
            }
            button.dataset.wired = 'true';
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                this.showReconnectInput(button.dataset.loraIndex);
            });
        });
    }

    /**
     * Resolve the Civitai model/version identifiers needed for download.
     * Recipe LoRAs parsed from PNG metadata often carry only a hash; resolve
     * it through the same endpoint the bulk "download missing" flow uses.
     */
    async resolveLoraDownloadIdentifiers(lora) {
        let modelId = lora.modelId || lora.modelID || lora.model_id;
        let versionId = lora.id || lora.modelVersionId;
        let versionName = lora.modelVersionName || lora.modelName || lora.name || 'LoRA';

        if (modelId && versionId) {
            return { modelId, versionId, versionName };
        }

        if (!lora.hash) {
            return null;
        }

        const response = await fetch(`/api/lm/loras/civitai/model/hash/${lora.hash}`);
        const versionInfo = await response.json();
        if (versionInfo?.error) {
            return null;
        }

        modelId = versionInfo.modelId || versionInfo.model?.id;
        versionId = versionInfo.id;
        versionName = versionInfo.name || versionName;

        return modelId && versionId ? { modelId, versionId, versionName } : null;
    }

    /**
     * A completed download flips inLibrary flags server-side; re-fetch the
     * recipe and reconcile both this modal's resources section and the
     * recipe card on the listing page (mirrors the bulk download flow in
     * BulkMissingLoraDownloadManager).
     */
    async refreshResourcesAfterDownload() {
        try {
            const recipeId =
                this.recipeId ||
                extractRecipeId(this.listFilePath || this.currentRecipe?.file_path);
            if (!recipeId) {
                return;
            }
            const updated = await fetchRecipeDetails(recipeId);
            if (!updated) {
                return;
            }
            this.currentRecipe.loras = updated.loras ?? this.currentRecipe.loras;
            this.currentRecipe.checkpoint = updated.checkpoint ?? this.currentRecipe.checkpoint;
            this.syncResourcesSection(this.currentRecipe);
            if (state.virtualScroller) {
                state.virtualScroller.updateSingleItem(
                    this.listFilePath || this.currentRecipe.file_path,
                    updated
                );
            }
        } catch (error) {
            console.warn('Failed to refresh recipe resources after download:', error);
        }
    }

    async downloadRecipeLora(lora, button, loraIndex) {
        if (!this.canDownloadLora(lora)) {
            showToast('toast.recipes.missingLoraDownloadInfo', {}, 'error');
            return;
        }

        if (button) {
            button.disabled = true;
        }

        // Hash-only LoRAs need a network round trip to resolve identifiers
        // before the progress UI can appear; show immediate feedback so the
        // click never feels dead.
        const hasDirectIds = !!(
            (lora.modelId || lora.modelID || lora.model_id) &&
            (lora.id || lora.modelVersionId)
        );
        if (!hasDirectIds) {
            state.loadingManager.showSimpleLoading(
                translate('recipes.resources.preparingDownload', {}, 'Preparing download...')
            );
        }

        try {
            const identifiers = await this.resolveLoraDownloadIdentifiers(lora);
            if (!hasDirectIds) {
                state.loadingManager.hide();
            }
            if (!identifiers) {
                if (!hasDirectIds && lora.hash) {
                    await this.markLoraHashInvalid(loraIndex);
                    showToast('toast.recipes.hashNotFoundOnCivitai', {}, 'error');
                } else {
                    showToast('toast.recipes.missingLoraDownloadInfo', {}, 'error');
                }
                return;
            }

            const success = await downloadManager.downloadVersionWithDefaults(
                MODEL_TYPES.LORA,
                identifiers.modelId,
                identifiers.versionId,
                {
                    versionName: identifiers.versionName,
                    source: 'recipe-modal',
                }
            );
            if (success) {
                await this.refreshResourcesAfterDownload();
            }
        } catch (error) {
            if (!hasDirectIds) {
                state.loadingManager.hide();
            }
            console.error('Error downloading LoRA:', error);
            showToast('toast.recipes.downloadLoraFailed', { message: error.message }, 'error');
        } finally {
            if (button) {
                button.disabled = false;
            }
        }
    }

    async markLoraHashInvalid(loraIndex) {
        const recipeId =
            this.recipeId ||
            extractRecipeId(this.listFilePath || this.currentRecipe?.file_path);
        if (!recipeId) {
            return;
        }
        try {
            await fetch('/api/lm/recipe/lora/mark-hash-invalid', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    recipe_id: recipeId,
                    lora_index: loraIndex,
                }),
            });
            if (this.currentRecipe?.loras?.[loraIndex]) {
                this.currentRecipe.loras[loraIndex].hashInvalid = true;
                this.syncResourcesSection(this.currentRecipe);
            }
        } catch (error) {
            console.warn('Failed to mark LoRA hash invalid:', error);
        }
    }

    navigateToCheckpointPage(checkpoint) {
        const checkpointHash = this._getCheckpointHash(checkpoint);

        if (!checkpointHash) {
            showToast('toast.recipes.missingCheckpointInfo', {}, 'error');
            return;
        }

        modalManager.closeModal('recipeModal');

        removeSessionItem('recipe_to_checkpoint_filterHash');
        removeSessionItem('recipe_to_checkpoint_filterHashes');
        removeSessionItem('filterCheckpointRecipeName');

        setSessionItem('recipe_to_checkpoint_filterHash', checkpointHash.toLowerCase());
        if (this.currentRecipe?.title) {
            setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title);
        }

        window.location.href = '/checkpoints';
    }

    _getCheckpointHash(checkpoint) {
        if (!checkpoint) return '';
        const hash =
            checkpoint.hash ||
            checkpoint.sha256 ||
            checkpoint.sha256_hash ||
            checkpoint.sha256Hash ||
            checkpoint.SHA256;
        return hash ? hash.toString() : '';
    }

    // New method to navigate to the LoRAs page
    navigateToLorasPage(specificLoraIndex = null) {
        // Close the current modal
        modalManager.closeModal('recipeModal');

        // Clear any previous filters first
        removeSessionItem('recipe_to_lora_filterLoraHash');
        removeSessionItem('recipe_to_lora_filterLoraHashes');
        removeSessionItem('filterRecipeName');
        removeSessionItem('viewLoraDetail');

        if (specificLoraIndex !== null) {
            // If a specific LoRA index is provided, navigate to view just that one LoRA
            const lora = this.currentRecipe.loras[specificLoraIndex];

            if (lora && lora.hash) {
                // Set session storage to open the LoRA modal directly
                setSessionItem('recipe_to_lora_filterLoraHash', lora.hash.toLowerCase());
                setSessionItem('viewLoraDetail', 'true');
                setSessionItem('filterRecipeName', this.currentRecipe.title);
            }
        } else {
            // If no specific LoRA index is provided, show all LoRAs from this recipe
            // Collect all hashes from the recipe's LoRAs
            const loraHashes = this.currentRecipe.loras
                .filter(lora => lora.hash)
                .map(lora => lora.hash.toLowerCase());

            if (loraHashes.length > 0) {
                // Store the LoRA hashes and recipe name in sessionStorage
                setSessionItem('recipe_to_lora_filterLoraHashes', JSON.stringify(loraHashes));
                setSessionItem('filterRecipeName', this.currentRecipe.title);
            }
        }

        // Navigate to the LoRAs page
        window.location.href = '/loras';
    }

    // Only in-library LoRA items are row-navigable: the row opens the local
    // LoRA detail. Missing/deleted rows expose explicit action buttons
    // instead (download / reconnect / Civitai link), so a single gesture
    // never produces two different outcomes.
    setupLoraItemsClickable() {
        const loraItems = document.querySelectorAll('.recipe-lora-item.exists-locally:not(.checkpoint-item)');
        loraItems.forEach(item => {
            // Guard against duplicate wiring from deferred re-runs (see
            // setupLoraItemActions).
            if (item.dataset.navigationWired === 'true') {
                return;
            }
            item.dataset.navigationWired = 'true';

            // Get the lora index from the data attribute
            const loraIndex = parseInt(item.dataset.loraIndex);

            item.addEventListener('click', (e) => {
                // The inline Civitai link inside the title keeps its own
                // navigation; don't let it trigger the row navigation.
                if (e.target.closest('.recipe-civitai-link')) {
                    return;
                }
                this.navigateToLorasPage(loraIndex);
            });
            item.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    this.navigateToLorasPage(loraIndex);
                }
            });
        });
    }
}

export { RecipeModal };
