// Strict voting configuration - only exact alias matches count
const strictVotingConfig = {
    enabled: true
};

// One vote per user tracking (with memory limit)
const votedUsers = new Set();
const votedUsersQueue = []; // Track insertion order for eviction
const MAX_VOTED_USERS = 10000; // Prevent unbounded growth

// Track what each user voted for (for 'change' mode)
const userVotes = new Map(); // username -> { candidate, source }

// One vote per user config: 'off', 'lock', 'change'
const oneVotePerUserConfig = {
    mode: 'lock' // 'off' = unlimited, 'lock' = first vote only, 'change' = can change vote
};

// Source tracking configuration (standalone, no longer tied to doubleVote)
const sourceTrackingConfig = {
    enabled: false
};

// Source tracking API
window.sourceTracking = {
    isEnabled: function() {
        return sourceTrackingConfig.enabled;
    },
    setEnabled: function(enabled) {
        sourceTrackingConfig.enabled = enabled;
        console.log("Source tracking " + (enabled ? "enabled" : "disabled"));
    }
};

// Dynamic categories tracking (for combo voting)
const dynamicCategories = new Set();

// Ensure a dynamic category exists in stats
function ensureDynamicCategory(name) {
    if (!stats.has(name)) {
        stats.set(name, { total: 0, youtube: 0, twitch: 0, unknown: 0 });
        dynamicCategories.add(name);
        console.log(`Created dynamic category: ${name}`);
    }
}

// Clear all dynamic categories (called on poll reset)
function clearDynamicCategories() {
    dynamicCategories.forEach(name => {
        stats.delete(name);
    });
    dynamicCategories.clear();
    console.log('Dynamic categories cleared');
}

// Expose for other modules
window.clearDynamicCategories = clearDynamicCategories;

// Combo vote parsing - finds action+tile combinations
function parseComboVote(text, source, username) {
    // Strict mode: reject if any word is NOT in the registry
    if (strictVotingConfig.enabled) {
        const words = text.toLowerCase().trim().split(/\s+/);

        for (const word of words) {
            if (!findExactAlias(word)) {
                console.log(`Combo vote rejected (strict mode, "${word}" not in registry): "${text}"`);
                return false;
            }
        }

        // All words valid - find one action and one tile among them
        let actionMatch = null;
        let tileMatch = null;

        for (const word of words) {
            const value = findExactAlias(word);
            const isAction = typeof window.isActionCandidate === 'function'
                ? window.isActionCandidate(value)
                : false;

            if (isAction && !actionMatch) {
                actionMatch = value;
            } else if (!isAction && !tileMatch) {
                tileMatch = value;
            }
        }

        if (actionMatch && tileMatch) {
            const comboName = `${actionMatch} ${tileMatch}`;
            ensureDynamicCategory(comboName);

            if (username) {
                recordUserVote(username, comboName, source);
                recordVoteUsername(comboName, username);
            }
            updateMap(comboName, source);
            console.log(`Combo vote (strict): ${comboName} from ${username || 'anonymous'}`);
            return true;
        }

        // All words are valid but no action+tile pair found - treat as standalone vote
        const standaloneMatch = tileMatch || actionMatch;
        if (standaloneMatch) {
            if (username) {
                recordUserVote(username, standaloneMatch, source);
                recordVoteUsername(standaloneMatch, username);
            }
            updateMap(standaloneMatch, source);
            console.log(`Vote (strict, combo mode): ${standaloneMatch} from ${username || 'anonymous'}`);
            return true;
        }

        console.log(`Combo vote rejected (strict mode, no valid candidate found): "${text}"`);
        return false;
    }

    // Non-strict mode: find action+tile anywhere in message
    const sortedEntries = Array.from(translate.entries())
        .sort((a, b) => b[0].length - a[0].length);

    let actionMatch = null;
    let tileMatch = null;
    const textLower = text.toLowerCase();

    // Try to find both an action and a tile in the message
    for (const [key, value] of sortedEntries) {
        if (textLower.includes(key.toLowerCase())) {
            // Check if this candidate is an action
            const isAction = typeof window.isActionCandidate === 'function'
                ? window.isActionCandidate(value)
                : false;

            if (isAction && !actionMatch) {
                actionMatch = value;
            } else if (!isAction && !tileMatch) {
                tileMatch = value;
            }

            // If we found both, we can stop
            if (actionMatch && tileMatch) break;
        }
    }

    // Record the vote
    if (actionMatch && tileMatch) {
        // Create combo category "action tile" (normalized order)
        const comboName = `${actionMatch} ${tileMatch}`;
        ensureDynamicCategory(comboName);

        if (username) {
            recordUserVote(username, comboName, source);
            recordVoteUsername(comboName, username);
        }
        updateMap(comboName, source);
        console.log(`Combo vote: ${comboName} from ${username || 'anonymous'}`);
        return true;
    } else if (actionMatch || tileMatch) {
        // Standalone vote (only found one match)
        const match = actionMatch || tileMatch;
        if (username) {
            recordUserVote(username, match, source);
            recordVoteUsername(match, username);
        }
        updateMap(match, source);
        return true;
    }

    return false;
}

// Track usernames per candidate for tooltip display (with memory limit)
const voteUsernames = new Map();
const MAX_USERNAMES_PER_CANDIDATE = 100; // Keep last N voters for tooltips

// Track earliest adopter per candidate (with timestamp for comparison)
const earliestAdopters = new Map(); // candidate -> { username, timestamp }

// Track vote counts per user per candidate (for strongest soldier)
const voterCounts = new Map(); // candidate -> Map(username -> count)

// Record username for a candidate vote (for tooltip display)
function recordVoteUsername(candidate, username) {
    if (!username) return;

    // Track earliest adopter (first voter for this candidate) with timestamp
    if (!earliestAdopters.has(candidate)) {
        earliestAdopters.set(candidate, { username, timestamp: Date.now() });
    }

    // Track vote count per user for this candidate
    if (!voterCounts.has(candidate)) {
        voterCounts.set(candidate, new Map());
    }
    const candidateCounts = voterCounts.get(candidate);
    candidateCounts.set(username, (candidateCounts.get(username) || 0) + 1);

    // Track recent voters list
    if (!voteUsernames.has(candidate)) {
        voteUsernames.set(candidate, []);
    }

    const usernames = voteUsernames.get(candidate);
    usernames.push(username);

    // Trim to last N if over limit
    if (usernames.length > MAX_USERNAMES_PER_CANDIDATE) {
        usernames.shift(); // Remove oldest
    }
}

// Get usernames who voted for a candidate
function getVoteUsernames(candidate) {
    return voteUsernames.get(candidate) || [];
}

// Get earliest adopter for a candidate
function getEarliestAdopter(candidate) {
    const data = earliestAdopters.get(candidate);
    return data ? data.username : null;
}

// Get earliest adopter for a group of combined categories
function getEarliestAdopterForGroup(categories) {
    let earliest = null;
    let earliestTime = Infinity;

    for (const cat of categories) {
        const data = earliestAdopters.get(cat);
        if (data && data.timestamp < earliestTime) {
            earliestTime = data.timestamp;
            earliest = data.username;
        }
    }
    return earliest;
}

// Get strongest soldier (user with most votes) for a candidate
function getStrongestSoldier(candidate) {
    const counts = voterCounts.get(candidate);
    if (!counts || counts.size === 0) return null;

    let maxUser = null;
    let maxCount = 0;
    counts.forEach((count, username) => {
        if (count > maxCount) {
            maxCount = count;
            maxUser = username;
        }
    });

    // Only return if they voted more than once
    if (maxCount > 1) {
        return { username: maxUser, count: maxCount };
    }
    return null;
}

// Get strongest soldier for a group of combined categories (aggregates votes across all)
function getStrongestSoldierForGroup(categories) {
    const aggregatedCounts = new Map(); // username -> total count

    for (const cat of categories) {
        const counts = voterCounts.get(cat);
        if (counts) {
            counts.forEach((count, username) => {
                aggregatedCounts.set(username, (aggregatedCounts.get(username) || 0) + count);
            });
        }
    }

    if (aggregatedCounts.size === 0) return null;

    let maxUser = null;
    let maxCount = 0;
    aggregatedCounts.forEach((count, username) => {
        if (count > maxCount) {
            maxCount = count;
            maxUser = username;
        }
    });

    // Only return if they voted more than once
    if (maxCount > 1) {
        return { username: maxUser, count: maxCount };
    }
    return null;
}

// Decrement voter count for a candidate (used when changing votes)
function decrementVoterCount(candidate, username) {
    const counts = voterCounts.get(candidate);
    if (!counts) return;

    // Find the key with case-insensitive match (usernames stored with original case)
    const lowerUsername = username.toLowerCase();
    let actualKey = null;
    for (const key of counts.keys()) {
        if (key.toLowerCase() === lowerUsername) {
            actualKey = key;
            break;
        }
    }

    if (!actualKey) return;

    const currentCount = counts.get(actualKey) || 0;
    if (currentCount > 1) {
        counts.set(actualKey, currentCount - 1);
    } else {
        counts.delete(actualKey);
    }
}

// Remove username from voters list for a candidate (used when changing votes)
function removeFromVotersList(candidate, username) {
    const usernames = voteUsernames.get(candidate);
    if (!usernames) return;

    // Remove all occurrences of this username from the list
    const filtered = usernames.filter(u => u.toLowerCase() !== username.toLowerCase());
    if (filtered.length > 0) {
        voteUsernames.set(candidate, filtered);
    } else {
        voteUsernames.delete(candidate);
    }
}

// Clear vote usernames (called when poll resets)
function clearVoteUsernames() {
    voteUsernames.clear();
    earliestAdopters.clear();
    voterCounts.clear();
}

// Check if user's vote should be blocked (returns false if vote should proceed)
function shouldBlockVote(username) {
    if (oneVotePerUserConfig.mode === 'off' || !username) return false;
    const lowerUsername = username.toLowerCase();

    if (oneVotePerUserConfig.mode === 'lock') {
        // Lock mode: block if already voted
        return votedUsers.has(lowerUsername);
    }

    // Change mode: never block, but will update existing vote
    return false;
}

// Check if user has already voted (for UI display purposes)
function hasUserVoted(username) {
    if (oneVotePerUserConfig.mode === 'off' || !username) return false;
    return votedUsers.has(username.toLowerCase());
}

// Get user's previous vote (for change mode)
function getUserPreviousVote(username) {
    if (!username) return null;
    return userVotes.get(username.toLowerCase()) || null;
}

// Record that a user has voted (with eviction when at capacity)
function recordUserVote(username, candidate, source) {
    if (!username) return;
    const lowerUsername = username.toLowerCase();

    // Handle 'change' mode - remove old vote first
    if (oneVotePerUserConfig.mode === 'change' && userVotes.has(lowerUsername)) {
        const oldVote = userVotes.get(lowerUsername);
        decrementVote(oldVote.candidate, oldVote.source);

        // Also decrement voter count for strongest soldier tracking
        decrementVoterCount(oldVote.candidate, lowerUsername);

        // Remove from the voters list for tooltips
        removeFromVotersList(oldVote.candidate, lowerUsername);

        console.log(`Vote changed: ${username} switched from ${oldVote.candidate}`);
    }

    // Don't add to votedUsers set if already exists (but still update userVotes)
    if (!votedUsers.has(lowerUsername)) {
        // Evict oldest if at capacity
        if (votedUsers.size >= MAX_VOTED_USERS) {
            const oldest = votedUsersQueue.shift();
            if (oldest) {
                votedUsers.delete(oldest);
                userVotes.delete(oldest);
            }
        }

        votedUsers.add(lowerUsername);
        votedUsersQueue.push(lowerUsername);
    }

    // Always update what they voted for
    userVotes.set(lowerUsername, { candidate, source });
}

// Decrement a vote (used when changing votes)
function decrementVote(candidate, source) {
    if (!stats.has(candidate)) return;

    const voteData = stats.get(candidate);
    voteData.total = Math.max(0, voteData.total - 1);

    if (source === 'youtube') {
        voteData.youtube = Math.max(0, voteData.youtube - 1);
    } else if (source === 'twitch') {
        voteData.twitch = Math.max(0, voteData.twitch - 1);
    } else {
        voteData.unknown = Math.max(0, voteData.unknown - 1);
    }
}

// Clear voted users (call when poll opens)
function clearVotedUsers() {
    votedUsers.clear();
    votedUsersQueue.length = 0; // Clear the queue too
    userVotes.clear(); // Clear vote tracking too
    clearVoteUsernames();
    console.log("Voted users list cleared");
}

// Helper function to find exact alias match
// Returns the candidate name if found, null otherwise
function findExactAlias(text) {
    const normalizedText = text.toLowerCase().trim();
    for (const [key, value] of translate.entries()) {
        if (normalizedText === key.toLowerCase()) {
            return value;
        }
    }
    return null;
}

// Find vote-matching alias positions in text (for highlighting).
// Returns array of { start, length } for matched substrings. Does NOT record votes.
function findVoteMatchesInText(text) {
    if (!text || translate.size === 0) return [];

    const matches = [];
    const textLower = text.toLowerCase();
    const comboMode = typeof window.comboModeEnabled !== 'undefined' && window.comboModeEnabled;

    if (strictVotingConfig.enabled) {
        // Strict mode: every word must be a valid alias, otherwise no match
        const words = text.trim().split(/\s+/);
        for (const word of words) {
            if (!findExactAlias(word)) return [];
        }
        // All words valid - highlight each word
        let searchFrom = 0;
        for (const word of words) {
            const idx = text.toLowerCase().indexOf(word.toLowerCase(), searchFrom);
            if (idx !== -1) {
                matches.push({ start: idx, length: word.length });
                searchFrom = idx + word.length;
            }
        }
        return matches;
    }

    // Non-strict: sort entries by key length (longest first)
    const sortedEntries = Array.from(translate.entries())
        .sort((a, b) => b[0].length - a[0].length);

    if (comboMode) {
        // Combo: find one action + one tile
        let actionMatch = null;
        let tileMatch = null;

        for (const [key, value] of sortedEntries) {
            const idx = textLower.indexOf(key.toLowerCase());
            if (idx !== -1) {
                const isAction = typeof window.isActionCandidate === 'function'
                    ? window.isActionCandidate(value) : false;

                if (isAction && !actionMatch) {
                    actionMatch = { start: idx, length: key.length };
                } else if (!isAction && !tileMatch) {
                    tileMatch = { start: idx, length: key.length };
                }
                if (actionMatch && tileMatch) break;
            }
        }
        if (actionMatch) matches.push(actionMatch);
        if (tileMatch) matches.push(tileMatch);
        return matches;
    }

    // Standard mode: first (longest) alias match only
    for (const [key] of sortedEntries) {
        const idx = textLower.indexOf(key.toLowerCase());
        if (idx !== -1) {
            matches.push({ start: idx, length: key.length });
            break;
        }
    }
    return matches;
}

window.findVoteMatchesInText = findVoteMatchesInText;

// Enhanced parseMessageForVote with source tracking and user tracking
function parseMessageForVote(text, source = 'unknown', username = null) {
    // One vote per user check (lock mode blocks, change mode allows)
    if (username && shouldBlockVote(username)) {
        console.log(`Vote rejected (already voted): ${username}`);
        return;
    }

    // Check if combo mode is enabled (from toggle-button.js)
    if (typeof window.comboModeEnabled !== 'undefined' && window.comboModeEnabled) {
        // Use combo vote parsing
        parseComboVote(text, source, username);
        return;
    }

    // Strict mode: reject if any word in the message is NOT in the registry
    if (strictVotingConfig.enabled) {
        const words = text.trim().split(/\s+/);
        for (const word of words) {
            if (!findExactAlias(word)) {
                console.log(`Vote rejected (strict mode, "${word}" not in registry): "${text.substring(0, 50)}"`);
                return;
            }
        }

        // All words are valid - find the candidate to vote for using longest alias match
        const sortedEntries = Array.from(translate.entries())
            .sort((a, b) => b[0].length - a[0].length);

        for (const [key, value] of sortedEntries) {
            if (text.toLowerCase().includes(key.toLowerCase())) {
                if (username) {
                    recordUserVote(username, value, source);
                    recordVoteUsername(value, username);
                }
                updateMap(value, source);
                console.log(`Vote (strict): ${value} from ${username || 'anonymous'}`);
                return;
            }
        }
        return;
    }

    // Non-strict mode: substring matching (longest match first)
    const sortedEntries = Array.from(translate.entries())
        .sort((a, b) => b[0].length - a[0].length);

    for (const [key, value] of sortedEntries) {
        if (text.toLowerCase().includes(key.toLowerCase())) {
            // Record that this user has voted (before recording the vote)
            if (username) {
                recordUserVote(username, value, source);
                recordVoteUsername(value, username);
            }
            // Always pass the actual source - we track sources regardless of display toggle
            updateMap(value, source);
            break; // Stop after first match
        }
    }
}

// creates the object that holds vote data
const stats = new Map();

// Enhanced updateMap function - ALWAYS tracks source data
// The source tracking toggle only affects display, not data collection
function updateMap(value, source = 'unknown') {
    // Always use the object format so source data is preserved
    // This allows toggling source split on/off without losing data
    if (!stats.has(value)) {
        stats.set(value, {
            total: 0,
            youtube: 0,
            twitch: 0,
            unknown: 0
        });
    }

    const voteData = stats.get(value);
    voteData.total += 1;

    // Track by source
    if (source === 'youtube') {
        voteData.youtube += 1;
    } else if (source === 'twitch') {
        voteData.twitch += 1;
    } else {
        voteData.unknown += 1;
    }

    stats.set(value, voteData);
}

// creates the object that holds the candidate registry 
const translate = new Map();

function addToTranslate(to, from) {
    translate.set(to, from); 
};

// Initialize stats - ALWAYS uses object format to track sources
function initializeStats() {
    // First clear all existing stats
    if (typeof stats !== 'undefined') {
        stats.clear();
    } else {
        console.error("stats map is not defined!");
        return;
    }

    // Then add all candidates from the registry with zero votes
    // Always use object format so source data is preserved when toggling display
    if (typeof candidateRegistry !== 'undefined') {
        candidateRegistry.forEach(entry => {
            if (entry.candidate) { // Skip empty entries
                stats.set(entry.candidate, {
                    total: 0,
                    youtube: 0,
                    twitch: 0,
                    unknown: 0
                });
            }
        });
    } else {
        console.error("candidateRegistry is not defined!");
    }

    console.log("Stats map initialized with", stats.size, "candidates");
}