import json
import networkx as nx
import unicodedata
import subprocess
import os

# Function to sanitize node names for Graphviz and HTML
def sanitize_name(name):
    # Normalize to ASCII, removing accents and special characters
    return ''.join(c for c in unicodedata.normalize('NFKD', name)
                    if unicodedata.category(c) != 'Mn')

# Load JSON data from file
try:
    with open("friends8.json", "r") as f:
        data = json.load(f)
    print("Successfully loaded friends8.json")
except FileNotFoundError:
    print("Error: friends8.json not found. Please ensure the file exists in the working directory.")
    exit(1)
except json.JSONDecodeError:
    print("Error: Invalid JSON format in friends8.json.")
    exit(1)

# Create an undirected graph
G = nx.Graph()

# Add edges from the JSON data
for person, friends in data.items():
    for friend in friends:
        G.add_edge(person, friend)

# Add new authentic connections
new_connections = [
    # ("Caitlin Clark", "Kelsey Mitchell"),  # WNBA teammates
    # ("Numberphile", "Veritasium")  # Science YouTube collaboration
]
G.add_edges_from(new_connections)
print("Added new connections:", new_connections)

# Find connected components
components = list(nx.connected_components(G))
print(f"Number of connected components after adding edges: {len(components)}")

# List nodes by degree (number of connections)
degree_list = sorted(G.degree(), key=lambda x: x[1], reverse=True)
print("\nNodes sorted by number of connections (top 10 or all if fewer):")
for i, (node, degree) in enumerate(degree_list[:10], 1):
    print(f"{i}. {node}: {degree} connections")

# Print component details
print("\nComponent details:")
for i, component in enumerate(components, 1):
    print(f"Component {i} (size {len(component)}): {component}")

# Check if the graph is fully connected
if len(components) == 1:
    print("Graph is now a single connected tree with no islanded trees.")
else:
    print(f"Graph still has {len(components)} components. Some islanders remain as expected.")

# Check Katteyes' status
if "Katteyes" in G.nodes():
    for component in components:
        if "Katteyes" in component:
            print(f"Katteyes is in a component of size {len(component)}, connected to: {list(G.neighbors('Katteyes'))}")
            break
else:
    print("Katteyes is not in the graph.")

# Prompt user for root node with component representatives
print("\nAvailable components (first node as representative):")
for i, component in enumerate(components, 1):
    first_node = next(iter(component))  # Get first node in component
    print(f"{i}: {first_node} (size {len(component)})")
print(f"\nEnter a component number (1-{len(components)}) or a node name (e.g., 'Kim Kardashian'), or press Enter for largest component:")
user_input = input().strip()

# Select component to visualize
selected_component = None
if user_input:
    try:
        # Try parsing input as a component number
        component_idx = int(user_input) - 1
        if 0 <= component_idx < len(components):
            selected_component = components[component_idx]
            first_node = next(iter(selected_component))
            print(f"Selected component {component_idx + 1} containing {first_node} (size {len(selected_component)}): {selected_component}")
        else:
            print(f"Error: Component number must be between 1 and {len(components)}. Defaulting to largest component.")
            selected_component = max(components, key=len)
    except ValueError:
        # Try parsing input as a node name
        for component in components:
            if user_input in component:
                selected_component = component
                print(f"Selected component containing {user_input} (size {len(component)}): {component}")
                break
        if not selected_component:
            print(f"Error: Node '{user_input}' not found in the graph. Defaulting to largest component.")
            selected_component = max(components, key=len)
else:
    selected_component = max(components, key=len)
    first_node = next(iter(selected_component))
    print(f"Selected largest component containing {first_node} (size {len(selected_component)}): {selected_component}")

# Prompt for output file type
print("\nEnter output file type for selected component (png, svg, pdf) [default: svg]:")
file_type = input().strip().lower()
if file_type not in ['png', 'svg', 'pdf']:
    print(f"Invalid file type '{file_type}'. Defaulting to svg.")
    file_type = 'svg'

# Generate DOT file for the entire graph
dot_lines = ["graph G {"]
dot_lines.append("  rankdir=LR;")  # Horizontal layout
dot_lines.append("  node [shape=circle, style=filled, fillcolor=lightblue];")
dot_lines.append("  overlap=false;")  # Prevent node overlap
dot_lines.append("  splines=ortho;")  # Orthogonal edges for clarity
for node in G.nodes():
    sanitized_node = sanitize_name(node)
    dot_lines.append(f'  "{sanitized_node}" [label="{sanitized_node}"];')
for edge in G.edges():
    sanitized_node1 = sanitize_name(edge[0])
    sanitized_node2 = sanitize_name(edge[1])
    dot_lines.append(f'  "{sanitized_node1}" -- "{sanitized_node2}";')

# Add subgraphs for visual separation if multiple components
if len(components) > 1:
    for i, component in enumerate(components, 1):
        dot_lines.append(f"  subgraph cluster_{i} {{")
        dot_lines.append(f'    label="Component {i}";')
        dot_lines.append(f"    color=blue;")
        for node in component:
            sanitized_node = sanitize_name(node)
            dot_lines.append(f'    "{sanitized_node}";')
        dot_lines.append("  }")
dot_lines.append("}")

# Write to friends_network_full.dot
dot_content = "\n".join(dot_lines)
with open("friends_network_full.dot", "w", encoding="utf-8") as f:
    f.write(dot_content)
print("DOT file 'friends_network_full.dot' has been generated for the entire graph.")

# Generate DOT file for the selected component
selected_G = G.subgraph(selected_component)
dot_lines = ["graph G {"]
dot_lines.append("  rankdir=LR;")
dot_lines.append("  node [shape=circle, style=filled, fillcolor=lightblue];")
dot_lines.append("  overlap=false;")
dot_lines.append("  splines=ortho;")
for node in selected_G.nodes():
    sanitized_node = sanitize_name(node)
    dot_lines.append(f'  "{sanitized_node}" [label="{sanitized_node}"];')
for edge in selected_G.edges():
    sanitized_node1 = sanitize_name(edge[0])
    sanitized_node2 = sanitize_name(edge[1])
    dot_lines.append(f'  "{sanitized_node1}" -- "{sanitized_node2}";')
dot_lines.append("}")

# Write to friends_network_selected.dot
dot_content = "\n".join(dot_lines)
with open("friends_network_selected.dot", "w", encoding="utf-8") as f:
    f.write(dot_content)
print(f"DOT file 'friends_network_selected.dot' has been generated for the component containing {user_input or 'the largest component'}.")

# Generate visualization for selected component
try:
    subprocess.run([
        "dot", f"-T{file_type}", "-Gcharset=utf-8",
        "friends_network_selected.dot", "-o", f"friends_network_selected.{file_type}"
    ], check=True)
    print(f"Generated 'friends_network_selected.{file_type}' for the selected component.")
except subprocess.CalledProcessError:
    print(f"Error: Failed to generate 'friends_network_selected.{file_type}'. Ensure Graphviz is installed and 'dot' is in your PATH.")
except FileNotFoundError:
    print("Error: Graphviz 'dot' command not found. Please install Graphviz or check your PATH.")

# Prepare nodes and edges for vis.js using the FULL graph
nodes_data = []
node_id_map = {node: i for i, node in enumerate(G.nodes())}
for node, i in node_id_map.items():
    sanitized_node = sanitize_name(node)
    nodes_data.append(f'{{id: {i}, label: "{sanitized_node}"}}')
nodes_data_str = ",\n            ".join(nodes_data)

edges_data = []
# Give each edge a unique ID for highlighting
edge_id_counter = 0
for edge in G.edges():
    id1 = node_id_map[edge[0]]
    id2 = node_id_map[edge[1]]
    edges_data.append(f'{{id: {edge_id_counter}, from: {id1}, to: {id2}}}')
    edge_id_counter += 1
edges_data_str = ",\n            ".join(edges_data)

# Generate interactive HTML visualization with full graph data and click-to-highlight/search functionality
# Fixed: All curly braces in JavaScript are properly escaped for Python format()
html_content = """<!DOCTYPE html>
<html>
<head>
    <title>Friends Network Interactive Visualization</title>
    <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
    <style>
        body {{ font-family: sans-serif; }}
        #network {{
            width: 100%;
            height: 600px;
            border: 1px solid lightgray;
        }}
        #controls {{
            margin-bottom: 10px;
            position: relative;
        }}
        #searchContainer {{
            position: relative;
            display: inline-block;
        }}
        #searchBox {{
            width: 200px;
            padding: 5px;
        }}
        #suggestions {{
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: white;
            border: 1px solid #ccc;
            border-top: none;
            max-height: 200px;
            overflow-y: auto;
            z-index: 1000;
            display: none;
        }}
        .suggestion-item {{
            padding: 8px;
            cursor: pointer;
            border-bottom: 1px solid #eee;
        }}
        .suggestion-item:hover {{
            background-color: #f0f0f0;
        }}
        .suggestion-item:last-child {{
            border-bottom: none;
        }}
    </style>
</head>
<body>
    <h2>Friends Network Interactive Visualization</h2>
    <div id="controls">
        <label for="searchBox">Find Person:</label>
        <div id="searchContainer">
            <input type="text" id="searchBox" placeholder="Enter a name" autocomplete="off">
            <div id="suggestions"></div>
        </div>
        <button onclick="findPerson()">Search</button>
        <button onclick="resetNetwork()">Reset View</button>
    </div>
    <p>Click a node to highlight its immediate connections. Double-click a node to remove it (with confirmation). Use your mouse to zoom/pan or drag nodes to rearrange them.</p>
    <div id="network"></div>
    <script>
        // Store the full graph data
        var all_nodes_data = new vis.DataSet([
            {nodes_data}
        ]);
        var all_edges_data = new vis.DataSet([
            {edges_data}
        ]);

        var container = document.getElementById('network');
        var network;

        function createNetwork(nodesData, edgesData) {{
            var data = {{
                nodes: nodesData,
                edges: edgesData
            }};
            var options = {{
                nodes: {{
                    shape: 'dot',
                    font: {{ size: 12 }},
                    color: {{ background: 'lightblue', border: 'blue' }}
                }},
                edges: {{ smooth: {{ type: 'continuous' }} }},
                physics: {{
                    enabled: true,
                    barnesHut: {{
                        gravitationalConstant: -2000,
                        centralGravity: 0.3,
                        springLength: 95,
                        springConstant: 0.04,
                        damping: 0.09,
                        avoidOverlap: 0.1
                    }},
                    stabilization: {{
                        enabled: true,
                        iterations: 1000
                    }}
                }},
                interaction: {{ hover: true, zoomView: true, dragView: true, dragNodes: true }}
            }};
            
            network = new vis.Network(container, data, options);

            // Adjust physics after stabilization to reduce snap-back
            network.on('stabilizationIterationsDone', function() {{
                network.setOptions({{ 
                    physics: {{ 
                        enabled: true, 
                        barnesHut: {{ 
                            springConstant: 0.001,  // Much lower for less snap-back
                            damping: 0.9,           // Higher damping
                            centralGravity: 0.1     // Lower central gravity
                        }}
                    }}
                }});
            }});

            // Add drag event listeners for fast, responsive dragging without snapback
            var isDragging = false;
            
            network.on('dragStart', function(params) {{
                if (params.nodes.length > 0) {{
                    isDragging = true;
                    // Enable very strong physics for fast, responsive dragging
                    network.setOptions({{
                        physics: {{ 
                            enabled: true,
                            barnesHut: {{ 
                                springConstant: 0.08,    // Higher for faster response
                                damping: 0.05,           // Lower damping for more immediate movement
                                centralGravity: 0.5,     // Higher for better control
                                springLength: 50         // Shorter for tighter response
                            }}
                        }}
                    }});
                }}
            }});

            network.on('dragEnd', function(params) {{
                if (params.nodes.length > 0) {{
                    isDragging = false;
                    // Completely disable physics to prevent any settling/normalization
                    network.setOptions({{
                        physics: {{ enabled: false }}
                    }});
                    // Don't re-enable physics at all - nodes stay exactly where placed
                }}
            }});

            // Add click event listener for highlighting
            network.on('click', function(params) {{
                if (params.nodes.length > 0) {{
                    var nodeId = params.nodes[0];
                    highlightConnections(nodeId);
                }}
            }});

            // Add double-click event listener for removing nodes
            network.on('doubleClick', function(params) {{
                if (params.nodes.length > 0) {{
                    var nodeId = params.nodes[0];
                    var node = all_nodes_data.get(nodeId);
                    if (node && confirm(`Remove ${{node.label}} from the network?`)) {{
                        removeNode(nodeId);
                        
                        // Restore focus to network canvas after dialog to fix scroll wheel
                        setTimeout(function() {{
                            var canvas = container.querySelector('canvas');
                            if (canvas) {{
                                canvas.focus();
                            }}
                            // Also trigger a dummy interaction to reset event handlers
                            network.redraw();
                        }}, 100);
                    }}
                }}
            }});
        }}

        function highlightConnections(centerNodeId) {{
            // Reset all colors
            var allNodes = all_nodes_data.get();
            var updatedNodes = allNodes.map(node => {{
                return {{ ...node, color: {{ background: 'lightblue', border: 'blue' }} }};
            }});
            all_nodes_data.update(updatedNodes);

            var allEdges = all_edges_data.get();
            var updatedEdges = allEdges.map(edge => {{
                return {{ ...edge, color: 'lightgray' }};
            }});
            all_edges_data.update(updatedEdges);
            
            // Highlight the center node and its connections
            var connectedEdges = all_edges_data.get({{ filter: function(item) {{
                return item.from === centerNodeId || item.to === centerNodeId;
            }} }});

            var connectedNodeIds = new Set();
            connectedNodeIds.add(centerNodeId);
            connectedEdges.forEach(function(edge) {{
                connectedNodeIds.add(edge.from);
                connectedNodeIds.add(edge.to);
            }});

            var nodesToHighlight = Array.from(connectedNodeIds).map(id => {{
                return {{ id: id, color: {{ background: 'lightgreen', border: 'darkgreen' }} }};
            }});
            all_nodes_data.update(nodesToHighlight);

            var edgesToHighlight = connectedEdges.map(edge => {{
                return {{ id: edge.id, color: 'darkgreen', width: 2 }};
            }});
            all_edges_data.update(edgesToHighlight);

            // Don't auto-focus to preserve manual zoom/pan control
            // Users can manually navigate to highlighted nodes
        }}

        function removeNode(nodeId) {{
            try {{
                // Store node info before removal
                var nodeToRemove = all_nodes_data.get(nodeId);
                if (!nodeToRemove) {{
                    console.warn('Node not found:', nodeId);
                    return;
                }}
                
                console.log(`Removing node "${{nodeToRemove.label}}"`);
                
                // Find and remove connected edges
                var edgesToRemove = all_edges_data.get({{ 
                    filter: function(item) {{
                        return item.from === nodeId || item.to === nodeId;
                    }}
                }});
                
                var edgeIdsToRemove = edgesToRemove.map(edge => edge.id);
                console.log(`Removing ${{edgeIdsToRemove.length}} connected edges`);
                
                // Remove edges first
                all_edges_data.remove(edgeIdsToRemove);
                
                // Remove the node
                all_nodes_data.remove(nodeId);
                
                console.log(`Successfully removed node and edges`);
                
                // Don't touch physics or interactions - let vis-network handle it naturally
                // This preserves all zoom and pan functionality
                
            }} catch (error) {{
                console.error('Error removing node:', error);
                alert(`Failed to remove node: ${{error.message}}`);
            }}
        }}

        function findPerson() {{
            var searchName = document.getElementById('searchBox').value.trim();
            if (searchName === '') return;

            var foundNode = all_nodes_data.get({{ filter: function(item) {{
                return item.label.toLowerCase() === searchName.toLowerCase();
            }} }})[0];
            
            if (foundNode) {{
                highlightConnections(foundNode.id);
                hideSuggestions();
            }} else {{
                alert('Person not found in the network.');
            }}
        }}
        
        function showSuggestions(query) {{
            if (!query || query.length < 1) {{
                hideSuggestions();
                return;
            }}
            
            var suggestions = all_nodes_data.get({{ filter: function(item) {{
                return item.label.toLowerCase().includes(query.toLowerCase());
            }} }});
            
            var suggestionsDiv = document.getElementById('suggestions');
            
            if (suggestions.length === 0) {{
                hideSuggestions();
                return;
            }}
            
            // Sort suggestions by relevance (starts with query first, then contains)
            suggestions.sort(function(a, b) {{
                var aLabel = a.label.toLowerCase();
                var bLabel = b.label.toLowerCase();
                var queryLower = query.toLowerCase();
                
                var aStartsWith = aLabel.startsWith(queryLower);
                var bStartsWith = bLabel.startsWith(queryLower);
                
                if (aStartsWith && !bStartsWith) return -1;
                if (!aStartsWith && bStartsWith) return 1;
                
                return aLabel.localeCompare(bLabel);
            }});
            
            // Limit to top 8 suggestions to avoid overwhelming UI
            suggestions = suggestions.slice(0, 8);
            
            suggestionsDiv.innerHTML = '';
            suggestions.forEach(function(node) {{
                var item = document.createElement('div');
                item.className = 'suggestion-item';
                item.textContent = node.label;
                item.onclick = function() {{
                    document.getElementById('searchBox').value = node.label;
                    highlightConnections(node.id);
                    hideSuggestions();
                }};
                suggestionsDiv.appendChild(item);
            }});
            
            suggestionsDiv.style.display = 'block';
        }}
        
        function hideSuggestions() {{
            document.getElementById('suggestions').style.display = 'none';
        }}

        // Set up search box event listeners
        document.getElementById('searchBox').addEventListener('input', function(e) {{
            showSuggestions(e.target.value);
        }});
        
        document.getElementById('searchBox').addEventListener('keydown', function(e) {{
            if (e.key === 'Enter') {{
                findPerson();
                e.preventDefault();
            }} else if (e.key === 'Escape') {{
                hideSuggestions();
            }}
        }});
        
        // Hide suggestions when clicking outside
        document.addEventListener('click', function(e) {{
            if (!document.getElementById('searchContainer').contains(e.target)) {{
                hideSuggestions();
            }}
        }});

        function resetNetwork() {{
            // Recreate the network with original data
            all_nodes_data.clear();
            all_nodes_data.add([
                {nodes_data}
            ]);
            all_edges_data.clear();
            all_edges_data.add([
                {edges_data}
            ]);
            createNetwork(all_nodes_data, all_edges_data);
        }}

        // Initialize with the full graph
        createNetwork(all_nodes_data, all_edges_data);
    </script>
</body>
</html>
"""

# Write interactive HTML file
html_output_filename = "friends_network_interactive.html"
html_output = html_content.format(nodes_data=nodes_data_str, edges_data=edges_data_str)
with open(html_output_filename, "w", encoding="utf-8") as f:
    f.write(html_output)
print(f"Interactive visualization '{html_output_filename}' has been generated.")