package org.ds;

import java.util.*;

/**
 * Example usage patterns for Consistent Hashing in real-world scenarios.
 * This file demonstrates practical applications and best practices.
 */
public class ConsistentHashUsageExamples {

    // ============================================
    // Example 1: Cache System
    // ============================================
    
    static class CacheSystem {
        private ConsistentHash<String> hash;
        private Map<String, String> cacheServers = new HashMap<>();
        
        CacheSystem() {
            this.hash = new ConsistentHash<>(150);
        }
        
        // Add cache server
        void addCacheServer(String serverId) {
            hash.addNode(serverId);
            cacheServers.put(serverId, "cache://" + serverId);
            System.out.println("Added cache server: " + serverId);
        }
        
        // Remove cache server
        void removeCacheServer(String serverId) {
            hash.removeNode(serverId);
            cacheServers.remove(serverId);
            System.out.println("Removed cache server: " + serverId);
        }
        
        // Get value from cache
        String getValue(String key) {
            String cacheServer = hash.getNode(key);
            if (cacheServer == null) {
                return null;  // No cache servers available
            }
            return "Get from " + cacheServers.get(cacheServer);
        }
        
        // Put value to cache
        void putValue(String key, String value) {
            String cacheServer = hash.getNode(key);
            if (cacheServer != null) {
                System.out.println("Put '" + key + "' on " + cacheServer);
            }
        }
        
        void demonstrateCacheFailure() {
            System.out.println("\n=== Cache System Demonstration ===");
            addCacheServer("cache-01");
            addCacheServer("cache-02");
            addCacheServer("cache-03");
            
            putValue("user:100", "UserData");
            putValue("session:xyz", "SessionData");
            
            System.out.println("\n--- Cache Server Failure ---");
            removeCacheServer("cache-02");
            System.out.println("Cache-02 failed. Keys redistributed automatically.");
            
            System.out.println("\n--- Cache Recovery ---");
            addCacheServer("cache-02");
            System.out.println("Cache-02 recovered. Load rebalanced.");
        }
    }

    // ============================================
    // Example 2: Load Balancer
    // ============================================
    
    static class LoadBalancer {
        private ConsistentHash<String> hash;
        private int requestCount = 0;
        
        LoadBalancer() {
            this.hash = new ConsistentHash<>(150);
        }
        
        void addWebServer(String serverId) {
            hash.addNode(serverId);
        }
        
        void removeWebServer(String serverId) {
            hash.removeNode(serverId);
        }
        
        // Route request to server based on session ID
        String routeRequest(String sessionId) {
            String server = hash.getNode("session:" + sessionId);
            requestCount++;
            return server;
        }
        
        void demonstrateLoadBalancing() {
            System.out.println("\n=== Load Balancer Demonstration ===");
            addWebServer("web-01");
            addWebServer("web-02");
            addWebServer("web-03");
            
            // Simulate incoming requests
            System.out.println("\nRouting 15 requests:");
            for (int i = 1; i <= 15; i++) {
                String server = routeRequest("user-" + i);
                System.out.printf("Request from user-%d → %s\n", i, server);
            }
            
            // Load distribution
            Map<String, Integer> distribution = hash.getDistribution(1000);
            System.out.println("\nLoad distribution (1000 requests):");
            distribution.forEach((server, count) -> 
                System.out.printf("%s: %d requests (%.1f%%)\n", server, count, 
                    count * 100.0 / 1000));
        }
    }

    // ============================================
    // Example 3: Database Sharding
    // ============================================
    
    static class DatabaseShardRouter {
        private ConsistentHash<Integer> shardMap;
        private int shardCount;
        
        DatabaseShardRouter(int initialShards) {
            this.shardMap = new ConsistentHash<>(150);
            this.shardCount = initialShards;
            for (int i = 1; i <= initialShards; i++) {
                shardMap.addNode(i);
            }
        }
        
        // Get shard for user ID
        int getShardForUser(long userId) {
            return shardMap.getNode("user:" + userId);
        }
        
        // Add new shard
        void addShard() {
            shardCount++;
            shardMap.addNode(shardCount);
            System.out.println("Added shard-" + shardCount);
        }
        
        // Remove shard
        void removeShard(int shardId) {
            shardMap.removeNode(shardId);
            System.out.println("Removed shard-" + shardId);
        }
        
        void demonstrateSharding() {
            System.out.println("\n=== Database Sharding Demonstration ===");
            System.out.println("Initial setup: 3 shards");
            
            System.out.println("\nRouting users to shards:");
            for (long userId = 1001; userId <= 1010; userId++) {
                int shard = getShardForUser(userId);
                System.out.printf("User %d → Shard %d\n", userId, shard);
            }
            
            System.out.println("\nAdding shard-4 for scaling...");
            addShard();
            
            System.out.println("\nUser distribution after scaling:");
            Map<Integer, Integer> dist = shardMap.getDistribution(10000);
            dist.forEach((shard, count) -> 
                System.out.printf("Shard %d: %d users (%.1f%%)\n", shard, count, 
                    count * 100.0 / 10000));
        }
    }

    // ============================================
    // Example 4: Task Distribution (Job Queue)
    // ============================================
    
    static class TaskQueue {
        private ConsistentHash<String> hash;
        private Map<String, Integer> workerTaskCount;
        
        TaskQueue() {
            this.hash = new ConsistentHash<>(150);
            this.workerTaskCount = new HashMap<>();
        }
        
        void addWorker(String workerId) {
            hash.addNode(workerId);
            workerTaskCount.put(workerId, 0);
        }
        
        void removeWorker(String workerId) {
            hash.removeNode(workerId);
            workerTaskCount.remove(workerId);
        }
        
        // Assign task to worker
        void assignTask(String taskId) {
            String worker = hash.getNode(taskId);
            if (worker != null) {
                workerTaskCount.put(worker, workerTaskCount.get(worker) + 1);
                System.out.println("Task " + taskId + " → " + worker);
            }
        }
        
        void demonstrateTaskDistribution() {
            System.out.println("\n=== Task Queue Demonstration ===");
            addWorker("worker-1");
            addWorker("worker-2");
            addWorker("worker-3");
            
            System.out.println("Assigning 15 tasks:");
            for (int i = 1; i <= 15; i++) {
                assignTask("task-" + i);
            }
            
            System.out.println("\nTask distribution:");
            workerTaskCount.forEach((worker, count) -> 
                System.out.printf("%s: %d tasks\n", worker, count));
        }
    }

    // ============================================
    // Example 5: Session Affinity (Sticky Sessions)
    // ============================================
    
    static class SessionAffinityRouter {
        private ConsistentHash<String> hash;
        private Map<String, String> sessionToServer = new HashMap<>();
        
        SessionAffinityRouter() {
            this.hash = new ConsistentHash<>(150);
        }
        
        void addServer(String serverId) {
            hash.addNode(serverId);
        }
        
        // Route request to server ensuring sticky sessions
        String routeRequest(String sessionId) {
            // Check if session already has assigned server
            if (sessionToServer.containsKey(sessionId)) {
                return sessionToServer.get(sessionId);
            }
            
            // Assign server for first request
            String server = hash.getNode("session:" + sessionId);
            sessionToServer.put(sessionId, server);
            return server;
        }
        
        void demonstrateSessionAffinity() {
            System.out.println("\n=== Session Affinity Demonstration ===");
            addServer("app-server-1");
            addServer("app-server-2");
            addServer("app-server-3");
            
            System.out.println("Multiple requests from same session:");
            String sessionId = "session-abc-123";
            for (int i = 1; i <= 5; i++) {
                String server = routeRequest(sessionId);
                System.out.printf("Request %d: %s → %s\n", i, sessionId, server);
            }
            
            System.out.println("\nNotice: Same session always routes to same server");
        }
    }

    // ============================================
    // Example 6: Dynamic Configuration
    // ============================================
    
    static class ConfigurationManager {
        private ConsistentHash<String> hash;
        private Map<String, String> nodeConfigs;
        
        ConfigurationManager() {
            this.hash = new ConsistentHash<>(150);
            this.nodeConfigs = new HashMap<>();
        }
        
        void registerConfigServer(String serverId, String configLocation) {
            hash.addNode(serverId);
            nodeConfigs.put(serverId, configLocation);
        }
        
        String getConfigForService(String serviceName) {
            String server = hash.getNode(serviceName);
            return server != null ? nodeConfigs.get(server) : null;
        }
        
        void demonstrateConfiguration() {
            System.out.println("\n=== Configuration Manager Demonstration ===");
            registerConfigServer("config-1", "/etc/config/server1");
            registerConfigServer("config-2", "/etc/config/server2");
            registerConfigServer("config-3", "/etc/config/server3");
            
            System.out.println("Config server assignments:");
            String[] services = {"auth-service", "api-service", "cache-service", 
                               "database-service", "queue-service"};
            for (String service : services) {
                String configLocation = getConfigForService(service);
                System.out.printf("%s → %s\n", service, configLocation);
            }
        }
    }

    // ============================================
    // Example 7: Analytics and Monitoring
    // ============================================
    
    static class DistributedAnalytics {
        private ConsistentHash<String> hash;
        
        DistributedAnalytics() {
            this.hash = new ConsistentHash<>(150);
        }
        
        void addAnalyticsNode(String nodeId) {
            hash.addNode(nodeId);
        }
        
        void analyzeDistribution(int sampleSize) {
            Map<String, Integer> distribution = hash.getDistribution(sampleSize);
            
            System.out.println("\n=== Analytics Distribution ===");
            System.out.println("Sample size: " + sampleSize + " events\n");
            
            distribution.entrySet().stream()
                .sorted((a, b) -> b.getValue().compareTo(a.getValue()))
                .forEach(entry -> {
                    String node = entry.getKey();
                    int count = entry.getValue();
                    double percentage = (count * 100.0) / sampleSize;
                    int barLength = (int)(percentage / 5);
                    System.out.printf("  %s: %5d [%s] %.1f%%\n",
                        node, count, "█".repeat(barLength), percentage);
                });
            
            List<Integer> counts = new ArrayList<>(distribution.values());
            double average = counts.stream().mapToDouble(Integer::doubleValue).average().orElse(0);
            int max = Collections.max(counts);
            int min = Collections.min(counts);
            
            System.out.printf("\nStatistics:\n");
            System.out.printf("  Average: %.0f\n", average);
            System.out.printf("  Max:     %d\n", max);
            System.out.printf("  Min:     %d\n", min);
            System.out.printf("  Imbalance: %.1f%%\n", 
                ((max - min) * 100.0) / average);
        }
        
        void demonstrateAnalytics() {
            addAnalyticsNode("collector-1");
            addAnalyticsNode("collector-2");
            addAnalyticsNode("collector-3");
            addAnalyticsNode("collector-4");
            
            analyzeDistribution(10000);
        }
    }

    // ============================================
    // Example 8: Failure Recovery Scenarios
    // ============================================
    
    static class ResilientCluster {
        private ConsistentHash<String> hash;
        private Set<String> failedNodes;
        
        ResilientCluster() {
            this.hash = new ConsistentHash<>(150);
            this.failedNodes = new HashSet<>();
        }
        
        void addNode(String nodeId) {
            hash.addNode(nodeId);
            failedNodes.remove(nodeId);
        }
        
        void simulateNodeFailure(String nodeId) {
            hash.removeNode(nodeId);
            failedNodes.add(nodeId);
            System.out.println("Node " + nodeId + " FAILED");
        }
        
        void recoverNode(String nodeId) {
            hash.addNode(nodeId);
            failedNodes.remove(nodeId);
            System.out.println("Node " + nodeId + " RECOVERED");
        }
        
        void demonstrateFailureRecovery() {
            System.out.println("\n=== Failure Recovery Demonstration ===");
            
            // Initial setup
            for (int i = 1; i <= 5; i++) {
                addNode("node-" + i);
            }
            
            Map<String, Integer> initialDist = hash.getDistribution(1000);
            System.out.println("\nInitial distribution:");
            initialDist.forEach((node, count) -> 
                System.out.printf("  %s: %d keys (%.1f%%)\n", node, count, 
                    count * 100.0 / 1000));
            
            // Simulate failures
            System.out.println("\n--- Cascading Failures ---");
            simulateNodeFailure("node-1");
            simulateNodeFailure("node-3");
            
            Map<String, Integer> afterFailureDist = hash.getDistribution(1000);
            System.out.println("\nAfter failures:");
            afterFailureDist.forEach((node, count) -> 
                System.out.printf("  %s: %d keys (%.1f%%)\n", node, count, 
                    count * 100.0 / 1000));
            
            // Recovery
            System.out.println("\n--- Recovery Phase ---");
            recoverNode("node-1");
            recoverNode("node-3");
            
            Map<String, Integer> afterRecoveryDist = hash.getDistribution(1000);
            System.out.println("\nAfter recovery:");
            afterRecoveryDist.forEach((node, count) -> 
                System.out.printf("  %s: %d keys (%.1f%%)\n", node, count, 
                    count * 100.0 / 1000));
        }
    }

    // ============================================
    // Main Demo
    // ============================================
    
    public static void main(String[] args) {
        System.out.println("╔════════════════════════════════════════════════════════════════╗");
        System.out.println("║   CONSISTENT HASHING - REAL-WORLD USAGE EXAMPLES               ║");
        System.out.println("╚════════════════════════════════════════════════════════════════╝");
        
        // Run all examples
        new CacheSystem().demonstrateCacheFailure();
        new LoadBalancer().demonstrateLoadBalancing();
        new DatabaseShardRouter(3).demonstrateSharding();
        new TaskQueue().demonstrateTaskDistribution();
        new SessionAffinityRouter().demonstrateSessionAffinity();
        new ConfigurationManager().demonstrateConfiguration();
        new DistributedAnalytics().demonstrateAnalytics();
        new ResilientCluster().demonstrateFailureRecovery();
        
        System.out.println("\n╔════════════════════════════════════════════════════════════════╗");
        System.out.println("║            All Examples Completed Successfully!                 ║");
        System.out.println("╚════════════════════════════════════════════════════════════════╝\n");
    }
}

