# target_simulator/gui/performance_analysis_window.py
"""
Performance Analysis Window for detailed packet processing diagnostics.

This window provides in-depth visualization of packet processing performance
including timing breakdowns, spike detection, and statistical analysis.
"""

import tkinter as tk
from tkinter import ttk, messagebox
import logging
from typing import Optional, Dict, Any, List
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import statistics

logger = logging.getLogger(__name__)


class PerformanceAnalysisWindow(tk.Toplevel):
    """
    Dedicated window for analyzing packet processing performance data.
    
    Displays:
    - Time-series plot of component processing times
    - Statistical summary table
    - Distribution histogram
    """

    def __init__(self, parent, performance_samples: List[Dict[str, Any]], scenario_name: str = "Unknown"):
        """
        Initialize the performance analysis window.
        
        Args:
            parent: Parent Tkinter window
            performance_samples: List of performance sample dictionaries
            scenario_name: Name of the scenario for display
        """
        super().__init__(parent)
        self.title(f"Performance Analysis - {scenario_name}")
        self.geometry("1200x900")
        
        self.performance_samples = performance_samples
        self.scenario_name = scenario_name
        
        # Extract data arrays
        self._extract_data()
        
        # Show loading dialog while creating widgets
        self._show_loading_and_create_widgets()

    def _extract_data(self):
        """Extract data arrays from performance samples."""
        if not self.performance_samples:
            self.timestamps = []
            self.total_ms = []
            self.parse_ms = []
            self.hub_ms = []
            self.archive_ms = []
            self.listener_ms = []
            self.clock_ms = []
            return
        
        self.timestamps = [s['timestamp'] for s in self.performance_samples]
        self.total_ms = [s['total_ms'] for s in self.performance_samples]
        self.parse_ms = [s['parse_ms'] for s in self.performance_samples]
        self.hub_ms = [s['hub_ms'] for s in self.performance_samples]
        self.archive_ms = [s['archive_ms'] for s in self.performance_samples]
        self.listener_ms = [s['listener_ms'] for s in self.performance_samples]
        self.clock_ms = [s['clock_ms'] for s in self.performance_samples]

    def _show_loading_and_create_widgets(self):
        """Show loading dialog and create widgets asynchronously."""
        # Create loading dialog
        loading_dialog = tk.Toplevel(self)
        loading_dialog.title("Loading Performance Data")
        loading_dialog.geometry("350x120")
        loading_dialog.transient(self)
        loading_dialog.grab_set()
        
        # Center the dialog
        loading_dialog.update_idletasks()
        x = self.winfo_x() + (self.winfo_width() // 2) - (loading_dialog.winfo_width() // 2)
        y = self.winfo_y() + (self.winfo_height() // 2) - (loading_dialog.winfo_height() // 2)
        loading_dialog.geometry(f"+{x}+{y}")
        
        ttk.Label(
            loading_dialog,
            text=f"Processing {len(self.performance_samples)} samples...",
            font=("Segoe UI", 10)
        ).pack(pady=20)
        
        progress_label = ttk.Label(loading_dialog, text="Calculating statistics...")
        progress_label.pack(pady=5)
        
        progress_bar = ttk.Progressbar(loading_dialog, mode='indeterminate', length=300)
        progress_bar.pack(pady=10)
        progress_bar.start(10)
        
        def load_and_display():
            try:
                progress_label.config(text="Computing statistics...")
                self.update()
                
                # Compute statistics
                self._compute_statistics()
                
                progress_label.config(text="Creating widgets...")
                self.update()
                
                # Create UI
                self._create_widgets()
                
                progress_label.config(text="Rendering plots...")
                self.update()
                
                # Populate data
                self._populate_plots()
                
                # Close loading dialog
                loading_dialog.destroy()
                
            except Exception as e:
                loading_dialog.destroy()
                messagebox.showerror(
                    "Performance Analysis Error",
                    f"Failed to load performance data:\n{e}",
                    parent=self
                )
                self.destroy()
        
        # Schedule loading after dialog is visible
        self.after(100, load_and_display)

    def _compute_statistics(self):
        """Compute statistical metrics from performance data."""
        if not self.total_ms:
            self.stats = {}
            return
        
        # Overall statistics
        self.stats = {
            'total_samples': len(self.total_ms),
            'total': {
                'mean': statistics.mean(self.total_ms),
                'median': statistics.median(self.total_ms),
                'stdev': statistics.stdev(self.total_ms) if len(self.total_ms) > 1 else 0.0,
                'min': min(self.total_ms),
                'max': max(self.total_ms),
                'p95': self._percentile(self.total_ms, 95),
                'p99': self._percentile(self.total_ms, 99),
            },
            'parse': {
                'mean': statistics.mean(self.parse_ms),
                'max': max(self.parse_ms),
            },
            'hub': {
                'mean': statistics.mean(self.hub_ms),
                'max': max(self.hub_ms),
            },
            'archive': {
                'mean': statistics.mean(self.archive_ms),
                'max': max(self.archive_ms),
            },
            'listener': {
                'mean': statistics.mean(self.listener_ms),
                'max': max(self.listener_ms),
            },
            'clock': {
                'mean': statistics.mean(self.clock_ms),
                'max': max(self.clock_ms),
            },
        }
        
        # Count spikes (> 100ms)
        self.stats['spike_count'] = sum(1 for t in self.total_ms if t > 100)
        self.stats['spike_percentage'] = (self.stats['spike_count'] / len(self.total_ms)) * 100
        
        # Find dominant component for max spike
        max_idx = self.total_ms.index(self.stats['total']['max'])
        components = {
            'parse': self.parse_ms[max_idx],
            'hub': self.hub_ms[max_idx],
            'archive': self.archive_ms[max_idx],
            'listener': self.listener_ms[max_idx],
            'clock': self.clock_ms[max_idx],
        }
        self.stats['max_component'] = max(components, key=components.get)
        self.stats['max_component_value'] = components[self.stats['max_component']]

    def _percentile(self, data: List[float], p: float) -> float:
        """Calculate percentile of data."""
        sorted_data = sorted(data)
        k = (len(sorted_data) - 1) * (p / 100)
        f = int(k)
        c = f + 1
        if c >= len(sorted_data):
            return sorted_data[-1]
        d0 = sorted_data[f]
        d1 = sorted_data[c]
        return d0 + (d1 - d0) * (k - f)

    def _create_widgets(self):
        """Create the UI widgets."""
        # Main container with paned window
        main_pane = ttk.PanedWindow(self, orient=tk.VERTICAL)
        main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # Top section: Statistics table (left) + Info panel (right)
        top_container = ttk.Frame(main_pane)
        main_pane.add(top_container, weight=1)
        
        # Left side: Statistics table
        stats_frame = ttk.LabelFrame(top_container, text="Performance Statistics", padding=10)
        stats_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
        self._create_stats_table(stats_frame)
        
        # Right side: Info/Legend panel
        self._create_info_panel(top_container)
        
        # Bottom section: Plots
        plots_frame = ttk.Frame(main_pane)
        main_pane.add(plots_frame, weight=4)
        
        self._create_plots(plots_frame)
    
    def _create_info_panel(self, parent):
        """Create an informational panel explaining the metrics."""
        info_frame = ttk.LabelFrame(parent, text="ℹ About Performance Analysis", padding=10)
        info_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False)
        
        info_text = (
            "This window analyzes radar packet\n"
            "processing times during simulation.\n\n"
            "📊 Measured Components:\n"
            "• Parse: Decode raw SFP payload\n"
            "  (ctypes binary deserialization)\n"
            "• Hub: Update SimulationStateHub\n"
            "  (thread-safe data buffer)\n"
            "• Archive: Persist data to JSON file\n"
            "• Listener: Broadcast events to GUI\n"
            "• Clock: Synchronize timestamps\n\n"
            "⚠ Spikes (>100ms):\n"
            "Critical slowdowns - likely Garbage\n"
            "Collection, disk I/O, or lock contention.\n\n"
            "🎯 Bottleneck:\n"
            "Component responsible for the\n"
            "maximum recorded delay."
        )
        
        info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, 
                               font=("Segoe UI", 9), wraplength=320)
        info_label.pack(anchor=tk.W)

    def _create_stats_table(self, parent):
        """Create the statistics table."""
        # Create treeview with scrollbar
        tree_frame = ttk.Frame(parent)
        tree_frame.pack(fill=tk.BOTH, expand=True)
        
        scrollbar = ttk.Scrollbar(tree_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        columns = ("Metric", "Value", "Details")
        self.stats_tree = ttk.Treeview(
            tree_frame,
            columns=columns,
            show="headings",
            height=12,
            yscrollcommand=scrollbar.set
        )
        scrollbar.config(command=self.stats_tree.yview)
        
        # Configure columns
        self.stats_tree.heading("Metric", text="Metric")
        self.stats_tree.heading("Value", text="Value")
        self.stats_tree.heading("Details", text="Details")
        
        self.stats_tree.column("Metric", width=200)
        self.stats_tree.column("Value", width=150)
        self.stats_tree.column("Details", width=300)
        
        self.stats_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # Populate statistics
        self._populate_stats_table()

    def _populate_stats_table(self):
        """Populate the statistics table with computed metrics."""
        if not self.stats:
            self.stats_tree.insert("", "end", values=("No Data", "N/A", ""))
            return
        
        # General info
        self.stats_tree.insert("", "end", values=(
            "Total Samples",
            f"{self.stats['total_samples']:,}",
            ""
        ))
        
        self.stats_tree.insert("", "end", values=(
            "Spikes (>100ms)",
            f"{self.stats['spike_count']:,}",
            f"{self.stats['spike_percentage']:.2f}% of packets"
        ))
        
        # Separator
        self.stats_tree.insert("", "end", values=("", "", ""))
        
        # Overall timing
        self.stats_tree.insert("", "end", values=(
            "Total Processing Time",
            "",
            ""
        ), tags=("header",))
        
        total = self.stats['total']
        self.stats_tree.insert("", "end", values=(
            "  Mean",
            f"{total['mean']:.3f} ms",
            ""
        ))
        self.stats_tree.insert("", "end", values=(
            "  Median",
            f"{total['median']:.3f} ms",
            ""
        ))
        self.stats_tree.insert("", "end", values=(
            "  Std Dev",
            f"{total['stdev']:.3f} ms",
            ""
        ))
        self.stats_tree.insert("", "end", values=(
            "  Min / Max",
            f"{total['min']:.3f} / {total['max']:.1f} ms",
            ""
        ))
        self.stats_tree.insert("", "end", values=(
            "  95th Percentile",
            f"{total['p95']:.3f} ms",
            ""
        ))
        self.stats_tree.insert("", "end", values=(
            "  99th Percentile",
            f"{total['p99']:.3f} ms",
            ""
        ))
        
        # Separator
        self.stats_tree.insert("", "end", values=("", "", ""))
        
        # Component breakdown
        self.stats_tree.insert("", "end", values=(
            "Component Breakdown",
            "",
            ""
        ), tags=("header",))
        
        for comp_name in ['parse', 'hub', 'archive', 'listener', 'clock']:
            comp = self.stats[comp_name]
            bottleneck = " ⚠ BOTTLENECK" if comp_name == self.stats['max_component'] else ""
            self.stats_tree.insert("", "end", values=(
                f"  {comp_name.capitalize()}",
                f"{comp['mean']:.3f} ms",
                f"Max: {comp['max']:.1f} ms{bottleneck}"
            ))
        
        # Configure tag styling
        self.stats_tree.tag_configure("header", font=("Segoe UI", 9, "bold"))

    def _create_plots(self, parent):
        """Create matplotlib plots."""
        # Create figure with two subplots
        self.fig = Figure(figsize=(10, 8), dpi=100)
        
        # Use GridSpec for layout
        gs = self.fig.add_gridspec(2, 1, height_ratios=[2, 1], hspace=0.3, top=0.95)
        
        # Time series plot
        self.ax_timeseries = self.fig.add_subplot(gs[0, 0])
        self.ax_timeseries.set_title("Packet Processing Time Over Simulation")
        self.ax_timeseries.set_xlabel("Time (s)")
        self.ax_timeseries.set_ylabel("Processing Time (ms)")
        self.ax_timeseries.grid(True, alpha=0.3)
        
        # Histogram plot
        self.ax_histogram = self.fig.add_subplot(gs[1, 0])
        self.ax_histogram.set_title("Processing Time Distribution")
        self.ax_histogram.set_xlabel("Processing Time (ms)")
        self.ax_histogram.set_ylabel("Frequency")
        self.ax_histogram.grid(True, alpha=0.3)
        
        # Create canvas and toolbar
        canvas_frame = ttk.Frame(parent)
        canvas_frame.pack(fill=tk.BOTH, expand=True)
        
        # Toolbar at top
        toolbar_frame = ttk.Frame(canvas_frame)
        toolbar_frame.pack(side=tk.TOP, fill=tk.X)
        
        self.canvas = FigureCanvasTkAgg(self.fig, master=canvas_frame)
        
        toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)
        toolbar.update()
        
        # Canvas below toolbar
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

    def _populate_plots(self):
        """Populate the plots with performance data."""
        if not self.timestamps:
            self.canvas.draw()
            return
        
        # Time series plot - stacked area chart
        self.ax_timeseries.clear()
        self.ax_timeseries.set_title("Packet Processing Time Over Simulation")
        self.ax_timeseries.set_xlabel("Time (s)")
        self.ax_timeseries.set_ylabel("Processing Time (ms)")
        
        # Plot individual components as lines
        self.ax_timeseries.plot(self.timestamps, self.hub_ms, 
                                label='Hub', color='#2E86AB', linewidth=1.5, alpha=0.8)
        self.ax_timeseries.plot(self.timestamps, self.archive_ms, 
                                label='Archive', color='#06A77D', linewidth=1.5, alpha=0.8)
        self.ax_timeseries.plot(self.timestamps, self.listener_ms, 
                                label='Listener', color='#D62246', linewidth=1.5, alpha=0.8)
        self.ax_timeseries.plot(self.timestamps, self.parse_ms, 
                                label='Parse', color='#F77F00', linewidth=1, alpha=0.7)
        self.ax_timeseries.plot(self.timestamps, self.clock_ms, 
                                label='Clock', color='#8B5A99', linewidth=1, alpha=0.7)
        
        # Add horizontal line for 100ms threshold
        self.ax_timeseries.axhline(y=100, color='red', linestyle='--', 
                                   linewidth=1, alpha=0.5, label='100ms threshold')
        
        self.ax_timeseries.legend(loc='upper right', fontsize=9)
        self.ax_timeseries.grid(True, alpha=0.3)
        
        # Use log scale if there are large spikes
        if max(self.total_ms) > 100:
            self.ax_timeseries.set_yscale('log')
            self.ax_timeseries.set_ylabel("Processing Time (ms, log scale)")
        
        # Histogram - distribution of total processing times
        self.ax_histogram.clear()
        self.ax_histogram.set_title("Processing Time Distribution")
        self.ax_histogram.set_xlabel("Processing Time (ms)")
        self.ax_histogram.set_ylabel("Frequency")
        
        # Calculate appropriate bins
        # Separate normal from spikes for better visualization
        normal_times = [t for t in self.total_ms if t <= 100]
        spike_times = [t for t in self.total_ms if t > 100]
        
        if normal_times:
            self.ax_histogram.hist(normal_times, bins=50, color='#2E86AB', 
                                  alpha=0.7, label=f'Normal ({len(normal_times)} samples)')
        
        if spike_times:
            # Create separate bins for spikes
            spike_bins = 20
            self.ax_histogram.hist(spike_times, bins=spike_bins, color='#D62246', 
                                  alpha=0.7, label=f'Spikes ({len(spike_times)} samples)')
        
        self.ax_histogram.legend(loc='upper right', fontsize=9)
        self.ax_histogram.grid(True, alpha=0.3)
        
        # Draw canvas
        self.fig.tight_layout()
        self.canvas.draw()
