User:Polygnotus/Scripts/DuplicateParameters.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
// Enhanced Duplicate Parameter Finder for MediaWiki
// Combines robust template parsing, automatic fixes, and comprehensive error handling
// Version: 2.3 - Added nested template support
// https://en.wikipedia.org/wiki/Category:Articles_using_duplicate_arguments_in_template_calls

(function() {
    'use strict';
    
    // ============================================================================
    // CONFIGURATION
    // ============================================================================
    
    const CONFIG = {
        buttonText: 'Check Duplicate Parameters',
        summaryText: 'Clean up [[:Category:Articles using duplicate arguments in template calls|duplicate template arguments]]',
        moreFoundMessage: 'More duplicates found, fix some and run again!',
        noneFoundMessage: 'No duplicate parameters found.',
        showResultsBox: true,
        showAlertBox: false,
        maxAlertsBeforeMessage: 10,
        maxTemplatesPerRun: 1000,
        debugMode: true  // Always on for debugging
    };
    
    // Allow user overrides
    if (typeof window.findargdupseditsummary === 'string') CONFIG.summaryText = window.findargdupseditsummary;
    if (typeof window.findargdupsmorefound === 'string') CONFIG.moreFoundMessage = window.findargdupsmorefound;
    if (typeof window.findargdupslinktext === 'string') CONFIG.buttonText = window.findargdupslinktext;
    if (typeof window.findargdupsnonefound === 'string') CONFIG.noneFoundMessage = window.findargdupsnonefound;
    if (typeof window.findargdupsresultsbox === 'string') CONFIG.showResultsBox = true;
    if (typeof window.findargdupsnoalertbox === 'string') { CONFIG.showAlertBox = false; CONFIG.showResultsBox = true; }
    
    // ============================================================================
    // UTILITY FUNCTIONS
    // ============================================================================
    
    function debugLog(message, data) {
        if (CONFIG.debugMode) {
            console.log('[DuplicateParameters]', message, data !== undefined ? data : '');
        }
    }
    
    function escapeHtml(str) {
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }
    
    function escapeRegex(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    
    // ============================================================================
    // TEMPLATE PARSER
    // ============================================================================
    
    class TemplateParser {
        constructor(text) {
            this.text = text;
            this.position = 0;
        }
        
        /**
         * Extract all templates from text with proper brace matching
         * Now includes nested templates
         */
        static extractTemplates(text) {
            console.log('=== EXTRACTING TEMPLATES (INCLUDING NESTED) ===');
            const templates = [];
            let pos = 0;
            
            try {
                while (pos < text.length) {
                    const start = text.indexOf('{{', pos);
                    if (start === -1) break;
                    
                    const end = TemplateParser.findMatchingCloseBraces(text, start);
                    
                    if (end !== -1) {
                        const templateText = text.substring(start, end);
                        console.log(`Template ${templates.length}: start=${start}, end=${end}, length=${templateText.length}`);
                        console.log(`  First 100 chars: ${templateText.substring(0, 100)}`);
                        
                        templates.push({
                            text: templateText,
                            start: start,
                            end: end,
                            level: 0  // Top-level template
                        });
                        
                        // Now search for nested templates within this template
                        const nestedTemplates = TemplateParser.extractNestedTemplates(templateText, start, 1);
                        templates.push(...nestedTemplates);
                        
                        pos = end;
                    } else {
                        console.log(`  No matching close braces found for opening at ${start}`);
                        pos = start + 2;
                    }
                }
            } catch (error) {
                console.error('Error extracting templates:', error);
            }
            
            console.log(`Total templates extracted (including nested): ${templates.length}`);
            return templates;
        }
        
        /**
         * Extract nested templates from within a template
         * @param {string} templateText - The template text to search within
         * @param {number} baseOffset - The offset of this template in the original document
         * @param {number} level - The nesting level
         */
        static extractNestedTemplates(templateText, baseOffset, level) {
            console.log(`  Searching for nested templates at level ${level}`);
            const nestedTemplates = [];
            
            // Skip the outer {{ and }}
            const innerText = templateText.substring(2, templateText.length - 2);
            let pos = 0;
            
            try {
                while (pos < innerText.length) {
                    const start = innerText.indexOf('{{', pos);
                    if (start === -1) break;
                    
                    // Find matching close for this nested template
                    const end = TemplateParser.findMatchingCloseBraces(innerText, start);
                    
                    if (end !== -1) {
                        const nestedText = innerText.substring(start, end);
                        const absoluteStart = baseOffset + 2 + start;  // +2 for the outer {{
                        const absoluteEnd = absoluteStart + nestedText.length;
                        
                        console.log(`    Nested template at level ${level}: start=${absoluteStart}, end=${absoluteEnd}`);
                        
                        nestedTemplates.push({
                            text: nestedText,
                            start: absoluteStart,
                            end: absoluteEnd,
                            level: level
                        });
                        
                        // Recursively search for templates nested even deeper
                        const deeperNested = TemplateParser.extractNestedTemplates(nestedText, absoluteStart, level + 1);
                        nestedTemplates.push(...deeperNested);
                        
                        pos = end;
                    } else {
                        pos = start + 2;
                    }
                }
            } catch (error) {
                console.error(`Error extracting nested templates at level ${level}:`, error);
            }
            
            console.log(`  Found ${nestedTemplates.length} nested template(s) at level ${level}`);
            return nestedTemplates;
        }
        
        /**
         * Find matching closing braces for opening {{
         */
        static findMatchingCloseBraces(text, startPos) {
            let depth = 0;
            let i = startPos;
            let inComment = false;
            let inNowiki = false;
            let inPre = false;
            
            try {
                while (i < text.length) {
                    // Handle HTML comments
                    if (!inNowiki && !inPre && i < text.length - 3 && text.substring(i, i + 4) === '<!--') {
                        inComment = true;
                        i += 4;
                        continue;
                    }
                    
                    if (inComment && i < text.length - 2 && text.substring(i, i + 3) === '-->') {
                        inComment = false;
                        i += 3;
                        continue;
                    }
                    
                    // Handle nowiki tags
                    if (!inComment && !inPre && i < text.length - 7) {
                        const lower = text.substring(i, i + 8).toLowerCase();
                        if (lower === '<nowiki>') {
                            inNowiki = true;
                            i += 8;
                            continue;
                        }
                    }
                    
                    if (inNowiki && i < text.length - 8) {
                        const lower = text.substring(i, i + 9).toLowerCase();
                        if (lower === '</nowiki>') {
                            inNowiki = false;
                            i += 9;
                            continue;
                        }
                    }
                    
                    // Handle pre tags
                    if (!inComment && !inNowiki && i < text.length - 4) {
                        const lower = text.substring(i, i + 5).toLowerCase();
                        if (lower === '<pre>') {
                            inPre = true;
                            i += 5;
                            continue;
                        }
                    }
                    
                    if (inPre && i < text.length - 5) {
                        const lower = text.substring(i, i + 6).toLowerCase();
                        if (lower === '</pre>') {
                            inPre = false;
                            i += 6;
                            continue;
                        }
                    }
                    
                    if (inComment || inNowiki || inPre) {
                        i++;
                        continue;
                    }
                    
                    // Count braces
                    if (i < text.length - 1) {
                        if (text[i] === '{' && text[i + 1] === '{') {
                            depth++;
                            i += 2;
                            continue;
                        } else if (text[i] === '}' && text[i + 1] === '}') {
                            depth--;
                            if (depth === 0) {
                                return i + 2;
                            }
                            i += 2;
                            continue;
                        }
                    }
                    
                    i++;
                }
            } catch (error) {
                console.error('Error finding matching braces:', error);
            }
            
            return -1;
        }
        
        /**
         * Parse template parameters with proper nesting handling
         */
        static parseParameters(templateText) {
            console.log('=== PARSING PARAMETERS ===');
            console.log('Template text:', templateText.substring(0, 200));
            
            const params = [];
            
            try {
                if (!templateText.startsWith('{{') || !templateText.endsWith('}}')) {
                    console.log('Invalid template format - missing {{ or }}');
                    return params;
                }
                
                const inner = templateText.substring(2, templateText.length - 2);
                console.log('Inner text length:', inner.length);
                
                // Skip template name
                const firstPipe = TemplateParser.findNextPipe(inner, 0);
                console.log('First pipe position (after template name):', firstPipe);
                
                if (firstPipe === -1) {
                    console.log('No parameters found (no pipe after template name)');
                    return params;
                }
                
                const templateName = inner.substring(0, firstPipe).trim();
                console.log('Template name:', templateName);
                
                let pos = firstPipe + 1;
                let unnamedIndex = 1;
                let paramIndex = 0;
                
                while (pos < inner.length) {
                    const nextPipe = TemplateParser.findNextPipe(inner, pos);
                    const paramText = nextPipe === -1 
                        ? inner.substring(pos).trim() 
                        : inner.substring(pos, nextPipe).trim();
                    
                    console.log(`\nParameter ${paramIndex}:`);
                    console.log(`  Position: ${pos} to ${nextPipe === -1 ? 'end' : nextPipe}`);
                    console.log(`  Raw text: "${paramText.substring(0, 100)}${paramText.length > 100 ? '...' : ''}"`);
                    
                    if (paramText.length > 0) {
                        const equalsPos = TemplateParser.findFirstEquals(paramText);
                        console.log(`  Equals position: ${equalsPos}`);
                        
                        if (equalsPos !== -1) {
                            const name = paramText.substring(0, equalsPos).trim();
                            const value = paramText.substring(equalsPos + 1).trim();
                            
                            console.log(`  → Named parameter: "${name}" = "${value.substring(0, 50)}${value.length > 50 ? '...' : ''}"`);
                            
                            if (name.length > 0) {
                                params.push({
                                    name: name,
                                    value: value,
                                    originalText: paramText,
                                    isNamed: true
                                });
                            } else {
                                console.log('  → Skipped: empty parameter name');
                            }
                        } else {
                            // Unnamed parameter
                            console.log(`  → Unnamed parameter [${unnamedIndex}]: "${paramText.substring(0, 50)}${paramText.length > 50 ? '...' : ''}"`);
                            params.push({
                                name: String(unnamedIndex),
                                value: paramText,
                                originalText: paramText,
                                isNamed: false
                            });
                            unnamedIndex++;
                        }
                    } else {
                        console.log('  → Skipped: empty parameter');
                    }
                    
                    pos = nextPipe === -1 ? inner.length : nextPipe + 1;
                    paramIndex++;
                }
            } catch (error) {
                console.error('Error parsing parameters:', error);
            }
            
            console.log(`\nTotal parameters parsed: ${params.length}`);
            params.forEach((p, i) => {
                console.log(`  [${i}] "${p.name}" = "${p.value.substring(0, 30)}${p.value.length > 30 ? '...' : ''}"`);
            });
            
            return params;
        }
        
        /**
         * Find next pipe not inside nested structures
         */
        static findNextPipe(text, startPos) {
            let braceDepth = 0;
            let bracketDepth = 0;
            let i = startPos;
            
            try {
                while (i < text.length) {
                    if (i < text.length - 1) {
                        const twoChar = text.substring(i, i + 2);
                        
                        if (twoChar === '{{') {
                            braceDepth++;
                            i += 2;
                            continue;
                        } else if (twoChar === '}}') {
                            braceDepth--;
                            i += 2;
                            continue;
                        } else if (twoChar === '[[') {
                            bracketDepth++;
                            i += 2;
                            continue;
                        } else if (twoChar === ']]') {
                            bracketDepth--;
                            i += 2;
                            continue;
                        }
                    }
                    
                    if (text[i] === '|' && braceDepth === 0 && bracketDepth === 0) {
                        return i;
                    }
                    
                    i++;
                }
            } catch (error) {
                console.error('Error finding next pipe:', error);
            }
            
            return -1;
        }
        
        /**
         * Find first equals sign not inside nested structures
         */
        static findFirstEquals(text) {
            let braceDepth = 0;
            let bracketDepth = 0;
            let i = 0;
            
            try {
                while (i < text.length) {
                    if (i < text.length - 1) {
                        const twoChar = text.substring(i, i + 2);
                        
                        if (twoChar === '{{') {
                            braceDepth++;
                            i += 2;
                            continue;
                        } else if (twoChar === '}}') {
                            braceDepth--;
                            i += 2;
                            continue;
                        } else if (twoChar === '[[') {
                            bracketDepth++;
                            i += 2;
                            continue;
                        } else if (twoChar === ']]') {
                            bracketDepth--;
                            i += 2;
                            continue;
                        }
                    }
                    
                    if (text[i] === '=' && braceDepth === 0 && bracketDepth === 0) {
                        return i;
                    }
                    
                    i++;
                }
            } catch (error) {
                console.error('Error finding equals sign:', error);
            }
            
            return -1;
        }
    }
    
    // ============================================================================
    // DUPLICATE DETECTION
    // ============================================================================
    
    class DuplicateDetector {
        static findDuplicates(params) {
            console.log('\n=== FINDING DUPLICATES ===');
            const paramMap = new Map();
            const duplicates = [];
            
            try {
                // Group parameters by name (case-insensitive)
                params.forEach((param, index) => {
                    const normalizedName = param.name.toLowerCase().trim();
                    
                    console.log(`Processing param ${index}: "${param.name}" (normalized: "${normalizedName}")`);
                    
                    if (!paramMap.has(normalizedName)) {
                        paramMap.set(normalizedName, []);
                        console.log(`  → New parameter name, created group`);
                    } else {
                        console.log(`  → DUPLICATE DETECTED! Adding to existing group`);
                    }
                    
                    paramMap.get(normalizedName).push({
                        ...param,
                        originalIndex: index
                    });
                });
                
                console.log('\n=== PARAMETER GROUPS ===');
                paramMap.forEach((instances, name) => {
                    console.log(`Parameter "${name}": ${instances.length} instance(s)`);
                    instances.forEach((inst, i) => {
                        console.log(`  [${i}] value: "${inst.value.substring(0, 50)}${inst.value.length > 50 ? '...' : ''}"`);
                    });
                });
                
                // Find duplicates
                paramMap.forEach((instances, name) => {
                    if (instances.length > 1) {
                        const values = instances.map(p => p.value.trim());
                        const uniqueValues = [...new Set(values)];
                        
                        console.log(`\nDUPLICATE FOUND: "${name}"`);
                        console.log(`  Instances: ${instances.length}`);
                        console.log(`  Unique values: ${uniqueValues.length}`);
                        console.log(`  All same value: ${uniqueValues.length === 1}`);
                        
                        duplicates.push({
                            name: instances[0].name, // Use original casing
                            instances: instances,
                            allSameValue: uniqueValues.length === 1,
                            uniqueValues: uniqueValues
                        });
                    }
                });
            } catch (error) {
                console.error('Error finding duplicates:', error);
            }
            
            console.log(`\n=== DUPLICATE SUMMARY ===`);
            console.log(`Total duplicate parameter names found: ${duplicates.length}`);
            
            return duplicates;
        }
    }
    
    // ============================================================================
    // TEMPLATE FIXER
    // ============================================================================
    
    class TemplateFixer {
        /**
         * Remove specific parameter instances from template
         * @param {string} templateText - The full template text
         * @param {Array} instancesToRemove - Array of {name, value} objects to remove
         */
        static removeParameterInstances(templateText, instancesToRemove) {
            if (!instancesToRemove || instancesToRemove.length === 0) {
                return templateText;
            }
            
            console.log('\n=== REMOVING PARAMETER INSTANCES ===');
            console.log(`Instances to remove: ${instancesToRemove.length}`);
            
            try {
                let result = templateText;
                
                // Sort by position (if available) to remove from end to start
                // This prevents position shifts
                const sorted = [...instancesToRemove].reverse();
                
                sorted.forEach((inst, idx) => {
                    const paramName = inst.name.trim();
                    const paramValue = inst.value.trim();
                    
                    console.log(`\nRemoving instance ${idx}:`);
                    console.log(`  Name: "${paramName}"`);
                    console.log(`  Value: "${paramValue.substring(0, 50)}${paramValue.length > 50 ? '...' : ''}"`);
                    
                    // Build a pattern that matches this specific parameter with this specific value
                    // We need to be very precise here
                    const escapedName = escapeRegex(paramName);
                    const escapedValue = escapeRegex(paramValue);
                    
                    // Match |paramname = value (with flexible whitespace)
                    // Use a more specific pattern that includes the value
                    const pattern = new RegExp(
                        '\\|\\s*' + escapedName + '\\s*=\\s*' + escapedValue + '(?=\\s*\\||\\s*\\}\\})',
                        'i'
                    );
                    
                    const beforeLength = result.length;
                    // Only remove the first occurrence (since we're processing each instance)
                    result = result.replace(pattern, '');
                    const afterLength = result.length;
                    
                    if (beforeLength === afterLength) {
                        console.log(`  → WARNING: Pattern did not match anything!`);
                        console.log(`  → Pattern: ${pattern}`);
                    } else {
                        console.log(`  → Removed ${beforeLength - afterLength} characters`);
                    }
                });
                
                return result;
            } catch (error) {
                console.error('Error removing parameter instances:', error);
                return templateText;
            }
        }
        
        /**
         * Rename specific parameter instance in template
         * @param {string} templateText - The full template text
         * @param {Object} instance - {name, value} object to rename
         * @param {string} newName - New parameter name
         */
        static renameParameterInstance(templateText, instance, newName) {
            console.log('\n=== RENAMING PARAMETER INSTANCE ===');
            console.log(`Old name: "${instance.name}"`);
            console.log(`Value: "${instance.value.substring(0, 50)}${instance.value.length > 50 ? '...' : ''}"`);
            console.log(`New name: "${newName}"`);
            
            try {
                const paramName = instance.name.trim();
                const paramValue = instance.value.trim();
                
                // Find the exact string |paramname = value
                // We'll search for it directly to ensure we match the right instance
                const escapedName = escapeRegex(paramName);
                const escapedValue = escapeRegex(paramValue);
                
                // Build pattern - match parameter name and exact value
                // No case-insensitive flag, so values must match exactly
                const pattern = new RegExp(
                    '(\\|\\s*)(' + escapedName + ')(\\s*=\\s*)(' + escapedValue + ')(?=\\s*\\||\\s*\\}\\})'
                );
                
                console.log(`Pattern: ${pattern}`);
                
                // Replace only the first occurrence
                const result = templateText.replace(pattern, function(match, prefix, oldName, equals, value) {
                    console.log(`  → Match found: "${match.substring(0, 100)}"`);
                    return `${prefix}${newName}${equals}${value}`;
                });
                
                if (result === templateText) {
                    console.log(`  → WARNING: No match found for rename!`);
                } else {
                    console.log(`  → Successfully renamed`);
                }
                
                return result;
            } catch (error) {
                console.error('Error renaming parameter instance:', error);
                return templateText;
            }
        }
        
        /**
         * Legacy method for backward compatibility
         */
        static removeParameters(templateText, paramNamesToRemove) {
            if (!paramNamesToRemove || paramNamesToRemove.length === 0) {
                return templateText;
            }
            
            try {
                const removeSet = new Set(paramNamesToRemove.map(n => n.toLowerCase().trim()));
                let result = templateText;
                
                // Remove each parameter
                removeSet.forEach(paramName => {
                    const escaped = escapeRegex(paramName);
                    // Match |paramname=value where value can span multiple lines
                    const pattern = new RegExp(
                        '\\|\\s*' + escaped + '\\s*=(?:[^|{}]|\\{[^{}]*\\})*?(?=\\s*\\||\\s*\\}\\})',
                        'gi'
                    );
                    result = result.replace(pattern, '');
                });
                
                return result;
            } catch (error) {
                console.error('Error removing parameters:', error);
                return templateText;
            }
        }
    }
    
    // ============================================================================
    // UI COMPONENTS
    // ============================================================================
    
    class UI {
        static init() {
            try {
                UI.addToolbarLink();
                UI.createMessageArea();
            } catch (error) {
                console.error('Error initializing UI:', error);
            }
        }
        
        static addToolbarLink() {
            if (typeof mw === 'undefined' || !mw.loader) return;
            
            mw.loader.using(['mediawiki.util']).done(() => {
                try {
                    const portletLink = mw.util.addPortletLink(
                        'p-tb',
                        '#',
                        CONFIG.buttonText,
                        't-findargdups'
                    );
                    
                    if (portletLink) {
                        $(portletLink).click((e) => {
                            e.preventDefault();
                            DuplicateFinder.run();
                        });
                    }
                } catch (error) {
                    console.error('Error adding toolbar link:', error);
                }
            });
        }
        
        static createMessageArea() {
            if ($('#duplicate-params-message').length) return;
            
            try {
                $('body').append(
                    $('<div>')
                        .attr('id', 'duplicate-params-message')
                        .css({
                            'position': 'fixed',
                            'bottom': '20px',
                            'right': '20px',
                            'padding': '12px 16px',
                            'background-color': '#f8f9fa',
                            'border': '1px solid #a2a9b1',
                            'border-radius': '4px',
                            'box-shadow': '0 2px 8px rgba(0, 0, 0, 0.15)',
                            'z-index': '10000',
                            'display': 'none',
                            'max-width': '400px',
                            'font-family': 'sans-serif',
                            'font-size': '14px'
                        })
                );
            } catch (error) {
                console.error('Error creating message area:', error);
            }
        }
        
        static showMessage(message, type = 'info') {
            console.log(`[UI Message] ${type}: ${message}`);
            
            try {
                const colors = {
                    success: '#d4edda',
                    error: '#f8d7da',
                    warning: '#fff3cd',
                    info: '#d1ecf1'
                };
                
                const $message = $('#duplicate-params-message');
                if ($message.length) {
                    $message
                        .text(message)
                        .css('background-color', colors[type] || colors.info)
                        .fadeIn()
                        .delay(5000)
                        .fadeOut();
                }
                
                if (CONFIG.showResultsBox) {
                    UI.addResultsBox(message);
                }
            } catch (error) {
                console.error('Error showing message:', error);
            }
        }
        
        static addResultsBox(text) {
            try {
                const summaryLabel = document.getElementById('wpSummaryLabel');
                if (!summaryLabel) return;
                
                const parentDiv = summaryLabel.parentNode;
                if (!parentDiv) return;
                
                let resultsBox = document.getElementById('FindArgDupsResultsBox');
                
                if (!resultsBox) {
                    resultsBox = document.createElement('div');
                    resultsBox.id = 'FindArgDupsResultsBox';
                    parentDiv.insertBefore(resultsBox, parentDiv.firstChild);
                }
                
                resultsBox.innerHTML = '';
                
                const messageDiv = document.createElement('div');
                messageDiv.className = 'FindArgDupsResultsBox';
                messageDiv.style.cssText = 'max-height: 6em; overflow: auto; padding: 8px; ' +
                    'border: 1px solid #aaa; background-color: #fffacd; margin-bottom: 10px; ' +
                    'border-radius: 3px; font-family: monospace; font-size: 0.9em;';
                messageDiv.textContent = text;
                
                resultsBox.appendChild(messageDiv);
            } catch (error) {
                console.error('Error adding results box:', error);
            }
        }
        
        static clearResultsBox() {
            try {
                const resultsBox = document.getElementById('FindArgDupsResultsBox');
                if (resultsBox) {
                    resultsBox.innerHTML = '';
                }
            } catch (error) {
                console.error('Error clearing results box:', error);
            }
        }
        
        static showConflictDialog(duplicates, templateText, callback) {
            try {
                // Use MediaWiki OOUI which is always available
                mw.loader.using(['oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).done(() => {
                    UI.showOOUIDialog(duplicates, templateText, callback);
                }).fail((error) => {
                    console.error('Failed to load OOUI:', error);
                    // Fallback to custom modal
                    UI.showCustomModal(duplicates, templateText, callback);
                });
            } catch (error) {
                console.error('Error showing conflict dialog:', error);
                // Ultimate fallback to simple confirmation
                if (confirm(`Parameter "${duplicates[0].name}" has multiple different values. Keep the first one?`)) {
                    callback({action: 'keep', selectedIndex: 0});
                } else {
                    callback({action: 'skip'});
                }
            }
        }
        
        static showOOUIDialog(duplicates, templateText, callback) {
            try {
                const instances = duplicates[0].instances;
                const textInputs = [];
                const checkboxes = [];
                
                // Create message dialog
                function ConflictDialog(config) {
                    ConflictDialog.super.call(this, config);
                }
                OO.inheritClass(ConflictDialog, OO.ui.ProcessDialog);
                
                ConflictDialog.static.name = 'conflictDialog';
                ConflictDialog.static.title = 'Duplicate Parameter Found';
                ConflictDialog.static.actions = [
                    {
                        action: 'skip',
                        label: 'Skip',
                        flags: 'safe'
                    },
                    {
                        action: 'save',
                        label: 'Save',
                        flags: ['primary', 'progressive']
                    }
                ];
                
                ConflictDialog.prototype.initialize = function() {
                    ConflictDialog.super.prototype.initialize.call(this);
                    
                    const messageLabel = new OO.ui.LabelWidget({
                        label: new OO.ui.HtmlSnippet(
                            `<div style="margin-bottom: 15px; line-height: 1.6;">` +
                            `Parameter <strong>"${escapeHtml(duplicates[0].name)}"</strong> has ` +
                            `${instances.length} instances with different values.<br>` +
                            `Edit parameter names to keep multiple values, or uncheck to remove.</div>`
                        )
                    });
                    
                    const fieldset = new OO.ui.FieldsetLayout({
                        label: 'Parameter Instances'
                    });
                    
                    instances.forEach((inst, i) => {
                        const checkbox = new OO.ui.CheckboxInputWidget({
                            selected: true
                        });
                        checkboxes.push(checkbox);
                        
                        const input = new OO.ui.TextInputWidget({
                            value: inst.name,  // Just use the original name, user will edit as needed
                            disabled: false
                        });
                        textInputs.push(input);
                        
                        // Disable input when checkbox is unchecked
                        checkbox.on('change', function(selected) {
                            input.setDisabled(!selected);
                            if (!selected) {
                                input.$element.css('opacity', '0.5');
                            } else {
                                input.$element.css('opacity', '1');
                            }
                        });
                        
                        const valuePreview = inst.value.substring(0, 100) + (inst.value.length > 100 ? '...' : '');
                        
                        // Create a horizontal layout with checkbox and text input
                        const $container = $('<div>').css({
                            'margin-bottom': '12px',
                            'padding': '12px',
                            'background': '#f8f9fa',
                            'border': '1px solid #c8ccd1',
                            'border-radius': '4px'
                        });
                        
                        const $topRow = $('<div>').css({
                            'display': 'flex',
                            'align-items': 'center',
                            'gap': '10px',
                            'margin-bottom': '8px'
                        });
                        
                        const $checkboxContainer = $('<div>').css({
                            'flex-shrink': '0'
                        });
                        $checkboxContainer.append(checkbox.$element);
                        
                        const $inputContainer = $('<div>').css({
                            'flex': '1',
                            'min-width': '0'
                        });
                        $inputContainer.append(input.$element);
                        
                        $topRow.append($checkboxContainer, $inputContainer);
                        
                        const $valueDiv = $('<div>').css({
                            'padding': '8px',
                            'background': 'white',
                            'border': '1px solid #ddd',
                            'border-radius': '3px',
                            'font-family': 'monospace',
                            'font-size': '12px',
                            'color': '#555',
                            'overflow-x': 'auto',
                            'word-break': 'break-all'
                        }).text('= ' + valuePreview);
                        
                        $container.append($topRow, $valueDiv);
                        
                        const field = new OO.ui.FieldLayout($container, {
                            align: 'top'
                        });
                        
                        fieldset.addItems([field]);
                    });
                    
                    // Create template preview
                    const templatePreview = $('<div>')
                        .css({
                            'padding': '10px',
                            'background': '#f8f9fa',
                            'border': '1px solid #c8ccd1',
                            'border-radius': '3px',
                            'max-height': '100px',
                            'overflow': 'auto',
                            'font-family': 'monospace',
                            'font-size': '11px',
                            'white-space': 'pre-wrap',
                            'word-break': 'break-all',
                            'margin-top': '15px'
                        })
                        .text(templateText.substring(0, 300) + (templateText.length > 300 ? '...' : ''));
                    
                    const templateLabel = new OO.ui.LabelWidget({
                        label: new OO.ui.HtmlSnippet('<strong style="font-size: 13px;">Template context:</strong>')
                    });
                    
                    const content = new OO.ui.PanelLayout({
                        padded: true,
                        expanded: false,
                        scrollable: true
                    });
                    
                    content.$element.append(
                        messageLabel.$element,
                        fieldset.$element,
                        templateLabel.$element,
                        templatePreview
                    );
                    
                    this.$body.append(content.$element);
                };
                
                ConflictDialog.prototype.getBodyHeight = function() {
                    return Math.min(600, 250 + instances.length * 90);
                };
                
                ConflictDialog.prototype.getActionProcess = function(action) {
                    const dialog = this;
                    
                    if (action === 'save') {
                        return new OO.ui.Process(function() {
                            const renames = [];
                            
                            textInputs.forEach((input, i) => {
                                const isChecked = checkboxes[i].isSelected();
                                renames.push({
                                    originalName: instances[i].name,
                                    originalValue: instances[i].value,
                                    newName: isChecked ? input.getValue().trim() : '',
                                    instance: instances[i]
                                });
                            });
                            
                            console.log('User dialog result:', renames);
                            
                            // Validate: check for duplicate new names (only among checked items)
                            const newNames = renames
                                .filter(r => r.newName)
                                .map(r => r.newName.toLowerCase());
                            const duplicateNewNames = newNames.filter((name, i) => newNames.indexOf(name) !== i);
                            
                            if (duplicateNewNames.length > 0) {
                                OO.ui.alert('Error: You have duplicate parameter names. Each parameter must have a unique name.');
                                return;
                            }
                            
                            callback({action: 'rename', renames: renames});
                            dialog.close({ action: action });
                        });
                    } else if (action === 'skip') {
                        return new OO.ui.Process(function() {
                            callback({action: 'skip'});
                            dialog.close({ action: action });
                        });
                    }
                    
                    return ConflictDialog.super.prototype.getActionProcess.call(this, action);
                };
                
                // Create window manager and open dialog
                const windowManager = new OO.ui.WindowManager();
                $('body').append(windowManager.$element);
                
                const dialog = new ConflictDialog({
                    size: 'large'
                });
                
                windowManager.addWindows([dialog]);
                windowManager.openWindow(dialog);
                
            } catch (error) {
                console.error('Error showing OOUI dialog:', error);
                UI.showCustomModal(duplicates, templateText, callback);
            }
        }
        
        
        static showCustomModal(duplicates, templateText, callback) {
            try {
                const instances = duplicates[0].instances;
                
                // Create custom modal overlay
                const overlay = document.createElement('div');
                overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 100000; display: flex; align-items: center; justify-content: center;';
                
                const modal = document.createElement('div');
                modal.style.cssText = 'background: white; border-radius: 8px; padding: 24px; max-width: 700px; max-height: 85vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-family: sans-serif;';
                
                // Build modal content
                let html = `
                    <h3 style="margin: 0 0 16px 0; font-size: 18px; color: #202122;">Duplicate Parameter Found</h3>
                    <p style="margin: 0 0 16px 0; line-height: 1.5;">Parameter <strong>"${escapeHtml(duplicates[0].name)}"</strong> has ${instances.length} instances with different values.<br>Edit parameter names to keep multiple values, or uncheck to remove.</p>
                    <div style="max-height: 450px; overflow-y: auto; margin-bottom: 16px;">
                `;
                
                instances.forEach((inst, i) => {
                    const valuePreview = inst.value.substring(0, 120);
                    
                    html += `
                        <div style="margin-bottom: 12px; padding: 12px; background: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px;">
                            <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
                                <input 
                                    type="checkbox" 
                                    class="param-checkbox" 
                                    data-index="${i}"
                                    checked
                                    style="width: 18px; height: 18px; cursor: pointer; flex-shrink: 0;">
                                <input 
                                    type="text" 
                                    class="param-input" 
                                    data-index="${i}"
                                    value="${escapeHtml(inst.name)}"
                                    style="flex: 1; padding: 8px; border: 1px solid #a2a9b1; border-radius: 3px; font-size: 14px; font-family: monospace;">
                            </div>
                            <div style="padding: 8px; background: white; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 12px; color: #555; overflow-x: auto; word-break: break-all;">
                                = ${escapeHtml(valuePreview)}${inst.value.length > 120 ? '...' : ''}
                            </div>
                        </div>
                    `;
                });
                
                html += `
                    </div>
                    <div style="padding: 10px; background: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px; margin-bottom: 16px; max-height: 100px; overflow: auto;">
                        <strong style="font-size: 13px;">Template context:</strong><br>
                        <code style="font-size: 11px; word-break: break-all; color: #202122;">${escapeHtml(templateText.substring(0, 300))}${templateText.length > 300 ? '...' : ''}</code>
                    </div>
                    <div style="display: flex; gap: 8px; justify-content: flex-end;">
                        <button id="skip-btn" style="padding: 8px 16px; background: white; border: 1px solid #a2a9b1; border-radius: 2px; cursor: pointer; font-size: 14px;">Skip</button>
                        <button id="save-btn" style="padding: 8px 16px; background: #36c; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 14px; font-weight: 500;">Save</button>
                    </div>
                `;
                
                modal.innerHTML = html;
                overlay.appendChild(modal);
                document.body.appendChild(overlay);
                
                // Add checkbox change handlers to enable/disable inputs
                const checkboxes = modal.querySelectorAll('.param-checkbox');
                const inputs = modal.querySelectorAll('.param-input');
                
                checkboxes.forEach((checkbox, i) => {
                    checkbox.addEventListener('change', function() {
                        inputs[i].disabled = !this.checked;
                        inputs[i].style.opacity = this.checked ? '1' : '0.5';
                        inputs[i].style.backgroundColor = this.checked ? 'white' : '#f0f0f0';
                    });
                });
                
                // Save button handler
                modal.querySelector('#save-btn').addEventListener('click', () => {
                    const renames = [];
                    
                    inputs.forEach((input, i) => {
                        const isChecked = checkboxes[i].checked;
                        renames.push({
                            originalName: instances[i].name,
                            originalValue: instances[i].value,
                            newName: isChecked ? input.value.trim() : '',
                            instance: instances[i]
                        });
                    });
                    
                    // Validate: check for duplicate new names (only among checked items)
                    const newNames = renames
                        .filter(r => r.newName)
                        .map(r => r.newName.toLowerCase());
                    const duplicateNewNames = newNames.filter((name, i) => newNames.indexOf(name) !== i);
                    
                    if (duplicateNewNames.length > 0) {
                        alert('Error: You have duplicate parameter names. Each parameter must have a unique name.');
                        return;
                    }
                    
                    document.body.removeChild(overlay);
                    callback({action: 'rename', renames: renames});
                });
                
                // Skip button handler
                modal.querySelector('#skip-btn').addEventListener('click', () => {
                    document.body.removeChild(overlay);
                    callback({action: 'skip'});
                });
                
                // Close on overlay click
                overlay.addEventListener('click', (e) => {
                    if (e.target === overlay) {
                        document.body.removeChild(overlay);
                        callback({action: 'skip'});
                    }
                });
                
                // Close on Escape key
                const escapeHandler = (e) => {
                    if (e.key === 'Escape') {
                        if (document.body.contains(overlay)) {
                            document.body.removeChild(overlay);
                            callback({action: 'skip'});
                        }
                        document.removeEventListener('keydown', escapeHandler);
                    }
                };
                document.addEventListener('keydown', escapeHandler);
                
                // Focus first input
                setTimeout(() => {
                    const firstInput = modal.querySelector('.param-input');
                    if (firstInput) firstInput.focus();
                }, 100);
                
            } catch (error) {
                console.error('Error showing custom modal:', error);
                // Ultimate fallback
                if (confirm(`Parameter "${duplicates[0].name}" has multiple different values. Keep the first one?`)) {
                    callback({action: 'keep', selectedIndex: 0});
                } else {
                    callback({action: 'skip'});
                }
            }
        }
    }
    
    // ============================================================================
    // MAIN DUPLICATE FINDER
    // ============================================================================
    
    class DuplicateFinder {
        static run() {
            console.log('\n========================================');
            console.log('STARTING DUPLICATE PARAMETER CHECK');
            console.log('========================================\n');
            
            try {
                const textbox = document.getElementById('wpTextbox1');
                if (!textbox) {
                    UI.showMessage('Edit textbox not found.', 'error');
                    return;
                }
                
                const originalContent = textbox.value;
                console.log(`Content length: ${originalContent.length} characters`);
                
                const templates = TemplateParser.extractTemplates(originalContent);
                
                if (templates.length === 0) {
                    UI.showMessage(CONFIG.noneFoundMessage, 'info');
                    return;
                }
                
                // Limit templates to prevent browser hang
                const templatesToCheck = templates.slice(0, CONFIG.maxTemplatesPerRun);
                
                if (templates.length > CONFIG.maxTemplatesPerRun) {
                    console.log(`\nLimited to first ${CONFIG.maxTemplatesPerRun} templates out of ${templates.length} total`);
                }
                
                const problemTemplates = [];
                let totalDuplicates = 0;
                
                // Check each template
                templatesToCheck.forEach((template, index) => {
                    console.log(`\n--- Checking template ${index + 1} of ${templatesToCheck.length} (level ${template.level}) ---`);
                    
                    const params = TemplateParser.parseParameters(template.text);
                    
                    if (params.length === 0) {
                        console.log('No parameters found, skipping');
                        return;
                    }
                    
                    const duplicates = DuplicateDetector.findDuplicates(params);
                    
                    if (duplicates.length > 0) {
                        console.log(`✓ Template has ${duplicates.length} duplicate parameter(s)`);
                        
                        problemTemplates.push({
                            template: template,
                            params: params,
                            duplicates: duplicates
                        });
                        
                        totalDuplicates += duplicates.length;
                    } else {
                        console.log('✓ No duplicates found in this template');
                    }
                });
                
                console.log('\n========================================');
                console.log(`SCAN COMPLETE`);
                console.log(`Templates checked: ${templatesToCheck.length}`);
                console.log(`Templates with duplicates: ${problemTemplates.length}`);
                console.log(`Total duplicate parameters: ${totalDuplicates}`);
                console.log('========================================\n');
                
                if (problemTemplates.length === 0) {
                    UI.showMessage(CONFIG.noneFoundMessage, 'success');
                    UI.clearResultsBox();
                    return;
                }
                
                // Highlight first problematic template
                if (problemTemplates.length > 0) {
                    const firstTemplate = problemTemplates[0].template;
                    textbox.setSelectionRange(firstTemplate.start, firstTemplate.end);
                    textbox.focus();
                    console.log(`Highlighted first problematic template at position ${firstTemplate.start}-${firstTemplate.end}`);
                }
                
                // Process templates
                DuplicateFinder.processTemplates(problemTemplates, originalContent, textbox);
                
            } catch (error) {
                console.error('CRITICAL ERROR in run():', error);
                UI.showMessage('An error occurred: ' + error.message, 'error');
            }
        }
        
        static processTemplates(problemTemplates, currentContent, textbox) {
            console.log('\n=== PROCESSING TEMPLATES ===');
            
            try {
                let autoRemovedCount = 0;
                let needUserInputCount = 0;
                let newContent = currentContent;
                let offset = 0;
                
                // First pass: auto-fix identical duplicates
                problemTemplates.forEach((item, idx) => {
                    console.log(`\nProcessing template ${idx + 1}:`);
                    
                    const instancesToRemove = [];
                    let hasConflicts = false;
                    
                    item.duplicates.forEach(dup => {
                        if (dup.allSameValue) {
                            console.log(`  Parameter "${dup.name}": ${dup.instances.length} identical values - auto-removing duplicates`);
                            // Remove all but the first instance
                            for (let i = 1; i < dup.instances.length; i++) {
                                instancesToRemove.push({
                                    name: dup.instances[i].name,
                                    value: dup.instances[i].value
                                });
                            }
                            autoRemovedCount += dup.instances.length - 1;
                        } else {
                            console.log(`  Parameter "${dup.name}": ${dup.instances.length} different values - needs user input`);
                            hasConflicts = true;
                            needUserInputCount++;
                        }
                    });
                    
                    if (instancesToRemove.length > 0) {
                        const fixedTemplate = TemplateFixer.removeParameterInstances(item.template.text, instancesToRemove);
                        const start = item.template.start + offset;
                        const end = item.template.end + offset;
                        
                        newContent = newContent.substring(0, start) + fixedTemplate + newContent.substring(end);
                        offset += fixedTemplate.length - item.template.text.length;
                        
                        console.log(`  Applied auto-fixes, offset is now ${offset}`);
                    }
                    
                    item.hasConflicts = hasConflicts;
                });
                
                // Apply auto-fixes
                if (autoRemovedCount > 0) {
                    console.log(`\nApplying ${autoRemovedCount} auto-fixes to textbox`);
                    textbox.value = newContent;
                    DuplicateFinder.updateEditSummary();
                    UI.showMessage(
                        `Auto-removed ${autoRemovedCount} duplicate parameter(s) with identical values.`,
                        'success'
                    );
                }
                
                // Handle conflicts that need user input
                if (needUserInputCount > 0) {
                    console.log(`\n${needUserInputCount} conflict(s) need user input`);
                    const conflictTemplates = problemTemplates.filter(item => item.hasConflicts);
                    
                    // Process conflicts sequentially
                    DuplicateFinder.processConflictsSequentially(conflictTemplates, 0, textbox);
                }
                
            } catch (error) {
                console.error('Error processing templates:', error);
                UI.showMessage('Error processing templates: ' + error.message, 'error');
            }
        }
        
        static processConflictsSequentially(conflictTemplates, index, textbox) {
            console.log(`\n=== PROCESSING CONFLICT ${index + 1} of ${conflictTemplates.length} ===`);
            
            if (index >= conflictTemplates.length) {
                console.log('\n✓ All conflicts resolved!');
                UI.showMessage('All conflicts resolved!', 'success');
                return;
            }
            
            try {
                const item = conflictTemplates[index];
                const conflictDuplicates = item.duplicates.filter(dup => !dup.allSameValue);
                
                if (conflictDuplicates.length === 0) {
                    console.log('No conflicts in this template, moving to next');
                    // No conflicts in this template, move to next
                    DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
                    return;
                }
                
                // Show dialog for first conflict in this template
                const currentConflict = conflictDuplicates[0];
                console.log(`Showing dialog for parameter "${currentConflict.name}"`);
                
                UI.showConflictDialog(
                    [currentConflict],
                    item.template.text,
                    (result) => {
                        console.log(`Dialog result:`, result);
                        
                        if (result.action === 'rename') {
                            // User chose to rename/remove parameters
                            let currentContent = textbox.value;
                            let templateStart = currentContent.indexOf(item.template.text);
                            
                            if (templateStart !== -1) {
                                let fixedTemplate = item.template.text;
                                let renameCount = 0;
                                
                                console.log('Applying user changes:');
                                
                                // Apply renames and removals
                                // Process in order - the value match ensures we get the right instance
                                result.renames.forEach((rename, i) => {
                                    console.log(`  [${i}] "${rename.originalName}" → "${rename.newName || '(remove)'}"`);
                                    
                                    if (rename.newName) {
                                        // Rename this instance if the name actually changed
                                        if (rename.newName.toLowerCase() !== rename.originalName.toLowerCase()) {
                                            const beforeRename = fixedTemplate;
                                            fixedTemplate = TemplateFixer.renameParameterInstance(
                                                fixedTemplate,
                                                rename.instance,
                                                rename.newName
                                            );
                                            if (fixedTemplate !== beforeRename) {
                                                renameCount++;
                                            }
                                        }
                                    } else {
                                        // Remove this instance (empty newName means unchecked)
                                        fixedTemplate = TemplateFixer.removeParameterInstances(
                                            fixedTemplate,
                                            [{name: rename.instance.name, value: rename.instance.value}]
                                        );
                                    }
                                });
                                
                                const templateEnd = templateStart + item.template.text.length;
                                textbox.value = currentContent.substring(0, templateStart) +
                                               fixedTemplate +
                                               currentContent.substring(templateEnd);
                                
                                // Update template text for next conflict
                                item.template.text = fixedTemplate;
                                
                                DuplicateFinder.updateEditSummary();
                                
                                console.log(`Applied ${renameCount} rename(s) and ${result.renames.filter(r => !r.newName).length} removal(s)`);
                            }
                        } else {
                            console.log('User skipped this conflict');
                        }
                        
                        // Move to next template
                        setTimeout(() => {
                            DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
                        }, 100);
                    }
                );
                
            } catch (error) {
                console.error('Error processing conflict:', error);
                // Continue to next template
                DuplicateFinder.processConflictsSequentially(conflictTemplates, index + 1, textbox);
            }
        }
        
        static updateEditSummary() {
            try {
                const summaryField = document.getElementsByName('wpSummary')[0];
                if (!summaryField) return;
                
                if (summaryField.value.indexOf(CONFIG.summaryText) === -1) {
                    if (summaryField.value.match(/[^\*\/\s][^\/\s]?\s*$/)) {
                        summaryField.value += '; ' + CONFIG.summaryText;
                    } else {
                        summaryField.value += CONFIG.summaryText;
                    }
                    console.log('Updated edit summary');
                }
            } catch (error) {
                console.error('Error updating edit summary:', error);
            }
        }
    }
    
    // ============================================================================
    // INITIALIZATION
    // ============================================================================
    
    function initialize() {
        try {
            // Only run on edit pages
            if (typeof mw === 'undefined') return;
            
            const action = mw.config.get('wgAction');
            const namespace = mw.config.get('wgNamespaceNumber');
            
            if (action !== 'edit' && namespace !== -1) return;
            
            // Wait for jQuery
            if (typeof $ === 'undefined' || typeof jQuery === 'undefined') {
                setTimeout(initialize, 100);
                return;
            }
            
            $(document).ready(() => {
                UI.init();
                console.log('[DuplicateParameters] Initialized successfully');
            });
            
        } catch (error) {
            console.error('[DuplicateParameters] Initialization error:', error);
        }
    }
    
    // Start initialization
    initialize();
    
})();
// </nowiki>