package org.ds;

import java.util.*;

/**
 * Demo of Consistent Hashing covering various use cases and properties.
 * 
 * This demo shows:
 * 1. Basic consistent hashing functionality
 * 2. Load distribution across nodes
 * 3. Key redistribution on node addition/removal
 * 4. Comparison of different virtual node counts
 * 5. Cache cluster simulation
 * 6. Load balancer simulation
 */
public class ConsistentHashDemo {
    
    public static void main(String[] args) {
        System.out.println("╔════════════════════════════════════════════════════════════════╗");
        System.out.println("║          CONSISTENT HASHING COMPREHENSIVE DEMO                 ║");
        System.out.println("╚════════════════════════════════════════════════════════════════╝\n");
        
        demo1_BasicFunctionality();
        demo2_LoadDistribution();
        demo3_VirtualNodesImpact();
        demo4_KeyRedistribution();
        demo5_CacheClusterFailure();
        demo6_LoadBalancer();
        demo7_EdgeCases();
        demo8_PerformanceAnalysis();
    }

    /**
     * 
     * 
     * easy demo
     * Demo 1: Basic Consistent Hashing Functionality
     */
    static void demo1_BasicFunctionality() {
        System.out.println("\n┌─ DEMO 1: BASIC FUNCTIONALITY ─────────────────────────────────────┐");
        System.out.println("│ Adding nodes and routing keys                                   │\n");
        
        ConsistentHash<String> hash = new ConsistentHash<>(150);
        
        System.out.println("Adding nodes: node1, node2, node3");
        hash.addNode("node1");
        hash.addNode("node2");
        hash.addNode("node3");
        
        System.out.println("Physical nodes: " + hash.getNodeCount());
        System.out.println("Virtual nodes: " + hash.getRingSize());
        System.out.println("Virtual nodes per physical node: 150\n");
        
        System.out.println("Routing consistency test:");
        String[] testKeys = {"user:123", "session:456", "cache:789", "data:001", "object:999"};
        for (String key : testKeys) {
            String node = hash.getNode(key);
            System.out.printf("  %s → %s\n", key, node);
        }
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * Demo 2: Load Distribution Analysis
     */
    static void demo2_LoadDistribution() {
        System.out.println("\n┌─ DEMO 2: LOAD DISTRIBUTION ───────────────────────────────────────┐");
        System.out.println("│ Key distribution across multiple nodes                          │\n");
        
        ConsistentHash<String> hash = new ConsistentHash<>(150);
        hash.addNode("server-A");
        hash.addNode("server-B");
        hash.addNode("server-C");
        hash.addNode("server-D");
        
        int sampleSize = 10000;
        Map<String, Integer> distribution = hash.getDistribution(sampleSize);
        
        System.out.printf("Distribution of %d keys across 4 servers:\n\n", sampleSize);
        
        distribution.entrySet().stream()
            .sorted((a, b) -> b.getValue().compareTo(a.getValue()))
            .forEach(entry -> {
                String server = entry.getKey();
                int count = entry.getValue();
                double percentage = (count * 100.0) / sampleSize;
                int barLength = (int)(percentage / 2);
                System.out.printf("  %-12s: %5d [%s] %.2f%%\n", 
                    server, count, "█".repeat(barLength), percentage);
            });
        
        // Calculate distribution metrics
        List<Integer> counts = new ArrayList<>(distribution.values());
        int maxCount = Collections.max(counts);
        int minCount = Collections.min(counts);
        double avgCount = counts.stream().mapToInt(Integer::intValue).average().orElse(0);
        
        System.out.printf("\nDistribution Metrics:\n");
        System.out.printf("  Average:   %.0f\n", avgCount);
        System.out.printf("  Max:       %d\n", maxCount);
        System.out.printf("  Min:       %d\n", minCount);
        System.out.printf("  Imbalance: %.2f%%\n", 
            ((maxCount - minCount) * 100.0) / avgCount);
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * Demo 3: Impact of Virtual Nodes
     */
    static void demo3_VirtualNodesImpact() {
        System.out.println("\n┌─ DEMO 3: VIRTUAL NODES IMPACT ────────────────────────────────────┐");
        System.out.println("│ How virtual nodes improve load distribution                     │\n");
        
        int[] virtualNodeCounts = {1, 10, 50, 150, 500};
        String[] servers = {"server1", "server2", "server3"};
        
        System.out.println("Distributing 10000 keys with different virtual node counts:\n");
        System.out.printf("%-5s | %-20s | %-20s | %-20s | Imbalance\n", 
            "VN", "Server1", "Server2", "Server3");
        System.out.println(String.join("", Collections.nCopies(85, "─")));
        
        for (int vnCount : virtualNodeCounts) {
            ConsistentHash<String> hash = new ConsistentHash<>(vnCount);
            for (String server : servers) {
                hash.addNode(server);
            }
            
            Map<String, Integer> dist = hash.getDistribution(10000);
            List<Integer> counts = new ArrayList<>(dist.values());
            
            double imbalance = (Collections.max(counts) - Collections.min(counts)) * 100.0 / 
                              (10000.0 / servers.length);
            
            System.out.printf("%-5d | %5d (%5.1f%%) | %5d (%5.1f%%) | %5d (%5.1f%%) | %6.2f%%\n",
                vnCount,
                dist.get("server1"), dist.get("server1") * 100.0 / 10000,
                dist.get("server2"), dist.get("server2") * 100.0 / 10000,
                dist.get("server3"), dist.get("server3") * 100.0 / 10000,
                imbalance);
        }
        
        System.out.println("\n✓ More virtual nodes = better distribution uniformity");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * Demo 4: Key Redistribution on Node Changes
     */
    static void demo4_KeyRedistribution() {
        System.out.println("\n┌─ DEMO 4: KEY REDISTRIBUTION ───────────────────────────────────────┐");
        System.out.println("│ Minimal key movement on node addition/removal                   │\n");
        
        ConsistentHash<String> hash = new ConsistentHash<>(150);
        hash.addNode("node1");
        hash.addNode("node2");
        
        int sampleSize = 5000;
        Map<String, String> beforeState = captureAssignments(hash, sampleSize);
        
        // Add a new node
        System.out.println("Initial setup: 2 nodes (node1, node2)");
        System.out.println("Sample size: " + sampleSize + " keys\n");
        
        System.out.println("→ Adding node3...\n");
        hash.addNode("node3");
        
        Map<String, String> afterAddition = captureAssignments(hash, sampleSize);
        int movedOnAddition = countMovedKeys(beforeState, afterAddition);
        double percentMoved = (movedOnAddition * 100.0) / sampleSize;
        
        System.out.printf("Keys moved to new node:  %d (%.2f%%)\n", movedOnAddition, percentMoved);
        System.out.printf("Keys remained stable:    %d (%.2f%%)\n\n", 
            sampleSize - movedOnAddition, 100 - percentMoved);
        
        // Now remove a node
        beforeState = captureAssignments(hash, sampleSize);
        System.out.println("→ Removing node2...\n");
        hash.removeNode("node2");
        
        Map<String, String> afterRemoval = captureAssignments(hash, sampleSize);
        int movedOnRemoval = countMovedKeys(beforeState, afterRemoval);
        double percentMovedRemoval = (movedOnRemoval * 100.0) / sampleSize;
        
        System.out.printf("Keys affected by removal: %d (%.2f%%)\n", movedOnRemoval, percentMovedRemoval);
        System.out.printf("Keys remained stable:     %d (%.2f%%)\n", 
            sampleSize - movedOnRemoval, 100 - percentMovedRemoval);
        
        System.out.println("\n✓ Only ~1/3 of keys affected on node addition (1 out of 3 nodes)");
        System.out.println("✓ Minimal disruption compared to naive approaches");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * Demo 5: Cache Cluster with Node Failure
     */
    static void demo5_CacheClusterFailure() {
        System.out.println("\n┌─ DEMO 5: CACHE CLUSTER FAILURE RECOVERY ─────────────────────────────┐");
        System.out.println("│ Simulating cache server failure and recovery                    │\n");
        
        ConsistentHash<String> cache = new ConsistentHash<>(150);
        cache.addNode("cache-01");
        cache.addNode("cache-02");
        cache.addNode("cache-03");
        cache.addNode("cache-04");
        
        // Initial distribution
        Map<String, Integer> initialDist = cache.getDistribution(4000);
        System.out.println("Initial cache distribution (4000 items):");
        initialDist.forEach((node, count) -> 
            System.out.printf("  %s: %d items (%.1f%%)\n", node, count, 
                count * 100.0 / 4000));
        
        // Simulate failure
        System.out.println("\n⚠ cache-02 FAILS!\n");
        cache.removeNode("cache-02");
        
        Map<String, Integer> afterFailure = cache.getDistribution(4000);
        System.out.println("After failure (redistribution):");
        afterFailure.forEach((node, count) -> 
            System.out.printf("  %s: %d items (%.1f%%)\n", node, count, 
                count * 100.0 / 4000));
        
        // Show what was lost
        int itemsFromFailedNode = initialDist.get("cache-02");
        System.out.printf("\nItems lost in cache-02: %d\n", itemsFromFailedNode);
        System.out.println("Redistribution:");
        System.out.printf("  → cache-01: +%d items\n", 
            afterFailure.get("cache-01") - initialDist.get("cache-01"));
        System.out.printf("  → cache-03: +%d items\n", 
            afterFailure.get("cache-03") - initialDist.get("cache-03"));
        System.out.printf("  → cache-04: +%d items\n", 
            afterFailure.get("cache-04") - initialDist.get("cache-04"));
        
        // Recovery
        System.out.println("\n✓ cache-02 recovered!\n");
        cache.addNode("cache-02");
        
        Map<String, Integer> afterRecovery = cache.getDistribution(4000);
        System.out.println("After recovery:");
        afterRecovery.forEach((node, count) -> 
            System.out.printf("  %s: %d items (%.1f%%)\n", node, count, 
                count * 100.0 / 4000));
        
        System.out.println("\n✓ System automatically redistributed load to recovered node");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * Demo 6: Load Balancer
     */
    static void demo6_LoadBalancer() {
        System.out.println("\n┌─ DEMO 6: LOAD BALANCER ───────────────────────────────────────────┐");
        System.out.println("│ Route user sessions to web servers based on user ID             │\n");
        
        ConsistentHash<String> lb = new ConsistentHash<>(150);
        lb.addNode("web-server-1");
        lb.addNode("web-server-2");
        lb.addNode("web-server-3");
        
        System.out.println("Web servers: web-server-1, web-server-2, web-server-3\n");
        
        // Route some users
        System.out.println("Routing example user sessions:\n");
        System.out.println("User ID          → Server");
        System.out.println(String.join("", Collections.nCopies(40, "─")));
        
        for (int userId = 1001; userId <= 1015; userId++) {
            String key = "user:" + userId;
            String server = lb.getNode(key);
            System.out.printf("user:%d           → %s\n", userId, server);
        }
        
        // Load distribution
        System.out.println("\nLoad distribution across 10,000 users:");
        Map<String, Integer> load = lb.getDistribution(10000);
        load.forEach((server, count) -> {
            double percentage = count * 100.0 / 10000;
            int barLength = (int)(percentage / 5);
            System.out.printf("  %s: %5d [%s] %.1f%%\n", 
                server, count, "█".repeat(barLength), percentage);
        });
        
        System.out.println("\n✓ Each user consistently routes to same server (sticky sessions)");
        System.out.println("✓ Load balanced across all servers");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * 
     * easy demo
     * Demo 7: Edge Cases
     */
    static void demo7_EdgeCases() {
        System.out.println("\n┌─ DEMO 7: EDGE CASES & ROBUSTNESS ─────────────────────────────────┐");
        System.out.println("│ Handling various edge cases                                     │\n");
        
        ConsistentHash<String> hash = new ConsistentHash<>(150);
        
        System.out.println("Test 1: Empty ring");
        System.out.printf("  Get node for 'test': %s (null expected)\n\n", hash.getNode("test"));
        
        System.out.println("Test 2: Single node");
        hash.addNode("only-node");
        System.out.printf("  Get node for 'test': %s\n", hash.getNode("test"));
        System.out.printf("  Get node for 'abc':  %s\n\n", hash.getNode("abc"));
        
        System.out.println("Test 3: Keys with special characters");
        String[] specialKeys = {
            "key-with-dash",
            "key_with_underscore",
            "192.168.1.1",
            "user@domain.com",
            "🔑emoji🔑"
        };
        
        for (String key : specialKeys) {
            System.out.printf("  '%s' → %s\n", key, hash.getNode(key));
        }
        
        System.out.println("\nTest 4: Large number of nodes");
        ConsistentHash<Integer> largeHash = new ConsistentHash<>(150);
        for (int i = 1; i <= 100; i++) {
            largeHash.addNode(i);
        }
        System.out.printf("  Added 100 nodes: %d physical nodes, %d virtual nodes\n",
            largeHash.getNodeCount(), largeHash.getRingSize());
        System.out.printf("  Sample key routed to node: %s\n", largeHash.getNode("test"));
        
        System.out.println("\n✓ Handles edge cases gracefully");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    /**
     * 
     * easy demo
     * Demo 8: Performance Analysis
     */
    static void demo8_PerformanceAnalysis() {
        System.out.println("\n┌─ DEMO 8: PERFORMANCE ANALYSIS ────────────────────────────────────┐");
        System.out.println("│ Measurement of key lookup performance                           │\n");
        
        ConsistentHash<String> hash = new ConsistentHash<>(150);
        for (int i = 1; i <= 10; i++) {
            hash.addNode("server-" + i);
        }
        
        // Warm up
        for (int i = 0; i < 1000; i++) {
            hash.getNode("warmup:" + i);
        }
        
        // Benchmark
        System.out.println("Benchmarking key lookup performance:\n");
        
        long startTime = System.nanoTime();
        int iterations = 1_000_000;
        for (int i = 0; i < iterations; i++) {
            hash.getNode("benchmark:" + i);
        }
        long endTime = System.nanoTime();
        
        double duration = (endTime - startTime) / 1_000_000.0; // Convert to ms
        double avgTime = duration / iterations; // Time per operation in ms
        
        System.out.printf("Operations:      %,d lookups\n", iterations);
        System.out.printf("Total time:      %.2f ms\n", duration);
        System.out.printf("Avg per lookup:  %.3f µs\n", avgTime * 1000);
        System.out.printf("Throughput:      %,.0f ops/sec\n", iterations / (duration / 1000.0));
        
        System.out.println("\n✓ Consistent hashing is very fast - O(log n) complexity");
        
        System.out.println("\n└───────────────────────────────────────────────────────────────────┘");
    }

    // ============================================
    // Helper Methods
    // ============================================

    static Map<String, String> captureAssignments(ConsistentHash<String> hash, int count) {
        Map<String, String> assignments = new HashMap<>();
        for (int i = 0; i < count; i++) {
            String key = "key:" + i;
            assignments.put(key, hash.getNode(key));
        }
        return assignments;
    }

    static int countMovedKeys(Map<String, String> before, Map<String, String> after) {
        int count = 0;
        for (String key : before.keySet()) {
            if (!before.get(key).equals(after.get(key))) {
                count++;
            }
        }
        return count;
    }
}

