import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
import base64
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Table, TableStyle, Paragraph, Image, Spacer
from reportlab.lib.units import inch

def prepare_phase_termination_alerts_table(filtered_df, signals_df, max_rows=10):
    """
    Prepare a sorted table of phase termination alerts with signal name, phase, and date
    
    Args:
        filtered_df: DataFrame containing phase termination alerts
        signals_df: DataFrame containing signal metadata (DeviceId, Name, Region)
        max_rows: Maximum number of rows to include in the table
        
    Returns:
        Tuple of (Sorted DataFrame with Signal Name, Phase, Date and Alert columns, total_alerts_count)
    """
    if filtered_df.empty:
        return pd.DataFrame(), 0
        
    # Join with signals data to get signal names
    alerts_df = filtered_df.copy()  # Use all data for sparklines, not just alerts
    
    # Ensure DeviceId is string type to avoid merge issues
    signals_df = signals_df.copy()
    signals_df['DeviceId'] = signals_df['DeviceId'].astype(str)
    
    # Filter alerts for table display
    alert_rows = filtered_df[filtered_df['Alert'] == 1].copy()
    if alert_rows.empty:
        return pd.DataFrame(), 0
        
    # Merge with signals dataframe to get names
    result = pd.merge(
        alert_rows[['DeviceId', 'Phase', 'Date', 'Alert', 'Percent MaxOut']],
        signals_df[['DeviceId', 'Name', 'Region']], # Added Region for consistency 
        on='DeviceId',
        how='left'
    )
    
    # Filter out rows where Signal is NaN
    result = result.dropna(subset=['Name'])
    
    if result.empty:
        return pd.DataFrame(), 0
    
    # Get the total count of alerts after filtering but before limiting rows
    total_alerts_count = len(result)
    
    # Rename and select columns
    result = result.rename(columns={'Name': 'Signal', 'Percent MaxOut': 'MaxOut %'})
    
    # Add sparkline column - Group by Signal and Phase and collect time series data
    sparkline_data = {}
    
    # Get all DeviceId and Phase pairs that have alerts
    device_phase_pairs = result[['DeviceId', 'Phase']].drop_duplicates().values.tolist()
    
    # For each device/phase pair with alerts, collect all data for sparklines
    for device_id, phase in device_phase_pairs:
        # Get all data for this device/phase pair, not just alerts
        device_data = alerts_df[(alerts_df['DeviceId'] == device_id) & 
                                (alerts_df['Phase'] == phase)]
        
        if not device_data.empty:
            # Sort by date to ensure correct time series
            device_data = device_data.sort_values('Date')
            # Store the full time series data
            sparkline_data[(device_id, phase)] = device_data['Percent MaxOut'].tolist()
    
    # Add the sparkline data to the result dataframe
    result['Sparkline_Data'] = result.apply(
        lambda row: sparkline_data.get((row['DeviceId'], row['Phase']), []), 
        axis=1
    )
    
    # Select and order columns
    result = result[['Signal', 'Phase', 'Date', 'MaxOut %', 'Sparkline_Data']]
    
    # Sort by MaxOut % in descending order, then by Signal, Phase
    result = result.sort_values(by=['MaxOut %', 'Signal', 'Phase'], ascending=[False, True, True])
    
    # Limit the number of rows
    if max_rows > 0 and len(result) > max_rows:
        result = result.head(max_rows)
    
    return result, total_alerts_count

def prepare_phase_skip_alerts_table(phase_skip_rows, signals_df, region=None, allowed_pairs=None, min_total_skips=0, max_rows=10):
    """
    Prepare the Phase Skip table showing one row per signal per date with aggregated phases.

    Args:
        phase_skip_rows: DataFrame with DeviceId, Phase, Date, TotalSkips
        signals_df: DataFrame containing signal metadata (DeviceId, Name, Region)
        region: Optional region filter. When provided (and not "All Regions") the table is limited to that region.
        allowed_pairs: Optional DataFrame with DeviceId and Phase pairs to filter
        min_total_skips: Minimum total skips threshold (applied after aggregation)
        max_rows: Maximum number of rows to include in the table

    Returns:
        Tuple of (Sorted DataFrame with Signal, Date, Phases, Total Skips columns, total_alerts_count)
    """
    if phase_skip_rows is None or phase_skip_rows.empty:
        return pd.DataFrame(), 0

    df = phase_skip_rows.copy()
    df['DeviceId'] = df['DeviceId'].astype(str)

    # Filter by allowed pairs if provided
    if allowed_pairs is not None and not allowed_pairs.empty:
        allowed = allowed_pairs[['DeviceId', 'Phase']].drop_duplicates().copy()
        allowed['DeviceId'] = allowed['DeviceId'].astype(str)
        allowed['Phase'] = allowed['Phase'].astype(int)
        df = df.merge(allowed, on=['DeviceId', 'Phase'], how='inner')

    if df.empty:
        return pd.DataFrame(), 0

    signals_df = signals_df.copy()
    signals_df['DeviceId'] = signals_df['DeviceId'].astype(str)

    # Merge with signals to get names and regions
    result = df.merge(
        signals_df[['DeviceId', 'Name', 'Region']],
        on='DeviceId',
        how='left'
    )

    result = result.dropna(subset=['Name'])
    if result.empty:
        return pd.DataFrame(), 0

    # Filter by region if specified
    if region and region != "All Regions":
        result = result[result['Region'] == region]
        if result.empty:
            return pd.DataFrame(), 0

    # Aggregate by Signal and Date to combine phases
    result['Date'] = pd.to_datetime(result['Date']).dt.date
    result = result.rename(columns={'Name': 'Signal'})
    
    # Group by Signal and Date, aggregating phases and summing skips
    aggregated = (
        result.groupby(['Signal', 'Date'], as_index=False)
        .agg(
            Phases=('Phase', lambda x: ', '.join(sorted(set(str(p) for p in x)))),
            TotalSkips=('TotalSkips', 'sum')
        )
    )
    
    # Apply min_total_skips filter after aggregation
    if min_total_skips and min_total_skips > 0:
        aggregated = aggregated[aggregated['TotalSkips'] >= min_total_skips]
    
    if aggregated.empty:
        return pd.DataFrame(), 0
    
    # Get the total count before limiting rows
    total_alerts_count = len(aggregated)
    
    # Rename and select final columns
    aggregated = aggregated.rename(columns={'TotalSkips': 'Total Skips'})
    aggregated = aggregated[['Signal', 'Date', 'Phases', 'Total Skips']]
    aggregated = aggregated.sort_values(by=['Total Skips', 'Signal', 'Date'], ascending=[False, True, True])
    
    # Limit the number of rows
    if max_rows > 0 and len(aggregated) > max_rows:
        aggregated = aggregated.head(max_rows)

    return aggregated, total_alerts_count


def prepare_detector_health_alerts_table(filtered_df_actuations, signals_df, max_rows=10):
    """
    Prepare a sorted table of detector health alerts with signal name, detector, and date
    
    Args:
        filtered_df_actuations: DataFrame containing detector health alerts
        signals_df: DataFrame containing signal metadata (DeviceId, Name, Region)
        max_rows: Maximum number of rows to include in the table
        
    Returns:
        Tuple of (Sorted DataFrame with Signal Name, Detector, Date and Alert columns, total_alerts_count)
    """
    if filtered_df_actuations.empty:
        return pd.DataFrame(), 0
        
    # Use all data for sparklines, not just alerts
    detector_df = filtered_df_actuations.copy()
    
    # Ensure DeviceId is string type to avoid merge issues
    signals_df = signals_df.copy()
    signals_df['DeviceId'] = signals_df['DeviceId'].astype(str)
    
    # Filter alerts for table display
    alert_rows = filtered_df_actuations[filtered_df_actuations['Alert'] == 1].copy()
    if alert_rows.empty:
        return pd.DataFrame(), 0
        
    # Merge with signals dataframe to get names
    result = pd.merge(
        alert_rows[['DeviceId', 'Detector', 'Date', 'Alert', 'PercentAnomalous', 'Total']],
        signals_df[['DeviceId', 'Name', 'Region']],  # Updated column names to match signals_df
        on='DeviceId',
        how='left'
    )
    
    # Filter out rows where Signal is NaN
    result = result.dropna(subset=['Name'])
    
    if result.empty:
        return pd.DataFrame(), 0
    
    # Get the total count of alerts after filtering but before limiting rows
    total_alerts_count = len(result)
    
    # Rename and select columns
    result = result.rename(columns={'Name': 'Signal', 'PercentAnomalous': 'Anomalous %'})
    
    # Add sparkline column - Group by Signal and Detector and collect time series data
    sparkline_data = {}
    
    # Get all DeviceId and Detector pairs that have alerts
    device_detector_pairs = result[['DeviceId', 'Detector']].drop_duplicates().values.tolist()
    
    # For each device/detector pair with alerts, collect all data for sparklines
    for device_id, detector in device_detector_pairs:
        # Get all data for this device/detector pair, not just alerts
        device_data = detector_df[(detector_df['DeviceId'] == device_id) & 
                                  (detector_df['Detector'] == detector)]
        
        if not device_data.empty:
            # Sort by date to ensure correct time series
            device_data = device_data.sort_values('Date')
            # Store the Total values for sparklines instead of PercentAnomalous
            sparkline_data[(device_id, detector)] = device_data['Total'].tolist()
    
    # Add the sparkline data to the result dataframe
    result['Sparkline_Data'] = result.apply(
        lambda row: sparkline_data.get((row['DeviceId'], row['Detector']), []), 
        axis=1
    )
    
    # Select and order columns
    result = result[['Signal', 'Detector', 'Date', 'Anomalous %', 'Sparkline_Data']]
    
    # Sort by Anomalous % in descending order, then by Signal, Detector
    result = result.sort_values(by=['Anomalous %', 'Signal', 'Detector'], ascending=[False, True, True])
    
    # Limit the number of rows
    if max_rows > 0 and len(result) > max_rows:
        result = result.head(max_rows)
    
    return result, total_alerts_count

def create_sparkline(data, width=1.0, height=0.25, color='#1f77b4'):
    """
    Create a sparkline image from a list of values
    
    Args:
        data: List of values to plot
        width: Width of the image in inches
        height: Height of the image in inches
        color: Color of the sparkline
        
    Returns:
        ReportLab Image object
    """
    if not data or len(data) < 2:
        # Create an empty image if no data
        fig, ax = plt.subplots(figsize=(width, height), dpi=150)
        ax.set_xlim(0, 1)
        ax.set_ylim(0, 1)
        ax.axis('off')
        
        buf = BytesIO()
        fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0, transparent=True)
        plt.close(fig)
        buf.seek(0)
        
        return Image(buf, width=width*inch, height=height*inch)
    
    # Create the sparkline
    fig, ax = plt.subplots(figsize=(width, height), dpi=150)  # Increased DPI for better quality
    
    # Plot data points as a line only - no markers
    x = list(range(len(data)))
    ax.plot(x, data, color=color, linewidth=1.0)
    
    # No endpoint marker - removing this line
    # ax.scatter(x[-1], data[-1], color=color, s=15, zorder=3)
    
    # Set limits with a bit of padding
    y_min = min(data) * 0.9 if min(data) > 0 else min(data) * 1.1
    y_max = max(data) * 1.1
    ax.set_xlim(-0.5, len(data) - 0.5)
    ax.set_ylim(y_min, y_max)
    
    # Remove axes and borders
    ax.axis('off')
    fig.patch.set_alpha(0)
    
    # Tighter layout to remove excess whitespace
    plt.tight_layout(pad=0)
    
    # Convert to Image
    buf = BytesIO()
    fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0, transparent=True)
    plt.close(fig)
    buf.seek(0)
    
    return Image(buf, width=width*inch, height=height*inch)

def create_reportlab_table(df, title, styles, total_count=None, max_rows=10, include_trend=True, trend_header='Trend'):
    """Create a ReportLab table from a pandas DataFrame
    
    Args:
        trend_header: Custom header name for the trend column (default: 'Trend')
    """
    if df.empty:
        return [Paragraph("No alerts found", styles['Normal'])]
    
    # Create a copy to avoid modifying the original
    df_display = df.copy()
    
    # Show message about total alerts vs. displayed alerts
    if total_count is not None:
        table_notice = f"Showing top {len(df)} of {total_count} total alerts"
    else:
        table_notice = f"Showing {len(df)} alerts"
    
    # Get sparkline data and remove it from display DataFrame
    sparkline_data = None
    sparkline_columns = ['Sparkline_Data', 'Hourly Services Trend', 'Services (7d)']
    for col in sparkline_columns:
        if col in df_display.columns:
            sparkline_data = df_display[col].tolist()
            df_display = df_display.drop(columns=[col])
            break
    
    # Format percentage columns if they exist
    if 'MaxOut %' in df_display.columns:
        df_display['MaxOut %'] = df_display['MaxOut %'].apply(lambda x: f"{x:.1%}")
    if 'Anomalous %' in df_display.columns:
        df_display['Anomalous %'] = df_display['Anomalous %'].apply(lambda x: f"{x:.1%}")
    if 'Missing Data %' in df_display.columns:
        df_display['Missing Data %'] = df_display['Missing Data %'].apply(lambda x: f"{x:.1%}")
      # Convert non-sparkline columns to strings
    for col in df_display.columns:
        df_display[col] = df_display[col].astype(str)
    
    # Add Trend column header only if requested
    if include_trend:
        df_display[trend_header] = ""
    
    # Create header and data for the table
    header = df_display.columns.tolist()
    data = [header]
    
    # Add rows
    for _, row in df_display.iterrows():
        data.append(row.tolist())
      # Create the table
    if include_trend:
        colWidths = [None] * (len(header) - 1) + [1.2*inch]  # Make the Trend column wider
    else:
        colWidths = [None] * len(header)  # Equal width for all columns
    table = Table(data, colWidths=colWidths)
    
    # Style the table
    table_style = TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
        ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, 0), 10),
        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
        ('BACKGROUND', (0, 1), (-1, -1), colors.white),
        ('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
        ('ALIGN', (0, 1), (-1, -1), 'LEFT'),
        ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE', (0, 1), (-1, -1), 9),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
    ])
    
    # Add alternating row colors
    for i in range(1, len(data), 2):
        table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
    
    # Apply the table style
    table.setStyle(table_style)
      # If we have sparklines and trend column is included, add them to the last column after the table is created
    if sparkline_data is not None and include_trend:
        for i, data_points in enumerate(sparkline_data):
            row_index = i + 1  # +1 because row 0 is the header
            
            # Only create sparkline if we have data points
            if data_points and len(data_points) >= 2:
                # Use consistent color for all sparklines
                sparkline = create_sparkline(data_points, width=1.0, height=0.25, color='#1f77b4')
                
                # Replace the content of the last column with the sparkline image
                table._cellvalues[row_index][-1] = sparkline
    
    result_elements = [
        Paragraph(table_notice, styles['Normal']),
        Spacer(1, 0.05*inch),
        table
    ]
    
    return result_elements

def prepare_missing_data_alerts_table(filtered_df_missing_data, signals_df, max_rows=10):
    """
    Prepare a sorted table of missing data alerts with signal name and date
    
    Args:
        filtered_df_missing_data: DataFrame containing missing data alerts
        signals_df: DataFrame containing signal metadata (DeviceId, Name, Region)
        max_rows: Maximum number of rows to include in the table
        
    Returns:
        Tuple of (Sorted DataFrame with Signal Name, Date and Alert columns, total_alerts_count)
    """
    if filtered_df_missing_data.empty:
        return pd.DataFrame(), 0
        
    # Use all data for sparklines, not just alerts
    missing_data_df = filtered_df_missing_data.copy()
    
    # Ensure DeviceId is string type to avoid merge issues
    signals_df = signals_df.copy()
    signals_df['DeviceId'] = signals_df['DeviceId'].astype(str)
    
    # Filter alerts for table display
    alert_rows = filtered_df_missing_data[filtered_df_missing_data['Alert'] == 1].copy()
    if alert_rows.empty:
        return pd.DataFrame(), 0
      # Merge with signals dataframe to get names
    result = pd.merge(
        alert_rows[['DeviceId', 'Date', 'Alert', 'MissingData']],
        signals_df[['DeviceId', 'Name', 'Region']],  # Updated to include Region for consistency
        on='DeviceId',
        how='left'
    )
    
    # Filter out rows where Signal is NaN
    result = result.dropna(subset=['Name'])
    
    if result.empty:
        return pd.DataFrame(), 0
    
    # Get the total count of unique devices with alerts after region filtering
    unique_devices_with_alerts = result['DeviceId'].nunique()
    
    # Rename and select columns
    result = result.rename(columns={'Name': 'Signal', 'MissingData': 'Missing Data %'})
    
    # For missing data, only show one row per device (the worst one)
    # First, find the index of the maximum MissingData value for each device
    idx = result.groupby('DeviceId')['Missing Data %'].idxmax()
    
    # Use these indices to filter the dataframe to get just one row per device
    result = result.loc[idx]
    
    # Add sparkline column - Group by Signal and collect time series data
    sparkline_data = {}
    
    # Get all DeviceIds that have alerts
    device_ids = result['DeviceId'].unique()
    
    # For each device with alerts, collect all data for sparklines
    for device_id in device_ids:
        # Get all data for this device, not just alerts
        device_data = missing_data_df[missing_data_df['DeviceId'] == device_id]
        
        if not device_data.empty:
            # Sort by date to ensure correct time series
            device_data = device_data.sort_values('Date')
            # Store the MissingData values for sparklines
            sparkline_data[device_id] = device_data['MissingData'].tolist()
    
    # Add the sparkline data to the result dataframe
    result['Sparkline_Data'] = result.apply(
        lambda row: sparkline_data.get(row['DeviceId'], []), 
        axis=1
    )
    
    # Select and order columns
    result = result[['Signal', 'Date', 'Missing Data %', 'Sparkline_Data']]
    
    # Sort by Missing Data % in descending order, then by Signal
    result = result.sort_values(by=['Missing Data %', 'Signal'], ascending=[False, True])
    
    # Limit the number of rows
    if max_rows > 0 and len(result) > max_rows:
        result = result.head(max_rows)
    
    return result, unique_devices_with_alerts

def prepare_ped_alerts_table(filtered_df_ped, ped_hourly_df, signals_df, max_rows=10):
    """
    Prepare a sorted table of pedestrian alerts with signal name, phase, and hourly ped services
    
    Args:
        filtered_df_ped: DataFrame containing pedestrian alerts (with DeviceId, Phase, Date)
        ped_hourly_df: DataFrame containing hourly pedestrian data (with DeviceId, Phase, TimeStamp, PedServices)
        signals_df: DataFrame containing signal metadata (DeviceId, Name, Region)
        max_rows: Maximum number of rows to include in the table
        
    Returns:
        Tuple of (Sorted DataFrame with Signal Name, Phase, Alert Dates and Sparkline columns, total_alerts_count)
    """
    if filtered_df_ped.empty:
        return pd.DataFrame(), 0
    
    # Ensure DeviceId is string type to avoid merge issues
    signals_df = signals_df.copy()
    signals_df['DeviceId'] = signals_df['DeviceId'].astype(str)
    filtered_df_ped = filtered_df_ped.copy()
    filtered_df_ped['DeviceId'] = filtered_df_ped['DeviceId'].astype(str)
    ped_hourly_df = ped_hourly_df.copy()
    ped_hourly_df['DeviceId'] = ped_hourly_df['DeviceId'].astype(str)
      # Group all dates for each DeviceId/Phase combination
    dates_grouped = filtered_df_ped.groupby(['DeviceId', 'Phase'])['Date'].agg(list).reset_index()
    
    # Join with signals data to get signal names
    result = pd.merge(
        dates_grouped,
        signals_df[['DeviceId', 'Name', 'Region']], # Added Region for consistency
        on='DeviceId',
        how='left'
    )
    
    # Filter out rows where Signal is NaN
    result = result.dropna(subset=['Name'])
    
    if result.empty:
        return pd.DataFrame(), 0
    
    # Get the total count of unique ped detectors with alerts after region filtering (distinct DeviceId/Phase combinations)
    total_alerts_count = result[['DeviceId', 'Phase']].drop_duplicates().shape[0]
    
    # Format dates as strings and join with newlines for stacked appearance
    result['Date'] = result['Date'].apply(
        lambda dates: '\n'.join(d.strftime('%Y-%m-%d') for d in sorted(dates))
    )
    
    # Rename columns
    result = result.rename(columns={
        'Name': 'Signal',
        'Date': 'Alert Dates'
    })
    
    # Add sparkline column using ped_hourly_df data
    sparkline_data = {}
    
    # Get all DeviceId and Phase pairs
    device_phase_pairs = result[['DeviceId', 'Phase']].drop_duplicates().values.tolist()
    
    # For each device/phase pair, collect hourly ped services data for sparklines
    for device_id, phase in device_phase_pairs:
        # Get hourly data for this device/phase pair
        hourly_data = ped_hourly_df[
            (ped_hourly_df['DeviceId'] == device_id) & 
            (ped_hourly_df['Phase'] == phase)
        ]
        
        if not hourly_data.empty:
            # Sort by timestamp to ensure correct time series
            hourly_data = hourly_data.sort_values('TimeStamp')
            # Store the PedServices data for sparklines
            sparkline_data[(device_id, phase)] = hourly_data['PedServices'].tolist()
    
    # Add the sparkline data to the result dataframe
    result['Services (7d)'] = result.apply(
        lambda row: sparkline_data.get((row['DeviceId'], row['Phase']), []), 
        axis=1
    )
    
    # Select and order columns
    result = result[['Signal', 'Phase', 'Alert Dates', 'Services (7d)']]
    
    # Sort by Signal and Phase
    result = result.sort_values(by=['Signal', 'Phase'])
    
    # Limit the number of rows if needed
    if len(result) > max_rows:
        result = result.head(max_rows)
    
    return result, total_alerts_count

def prepare_system_outages_table(system_outages_df, max_rows=10):
    """
    Prepare a sorted table of system outages showing dates and regions with >30% missing data
    
    Args:
        system_outages_df: DataFrame containing system outages (Date, Region, MissingData)
        max_rows: Maximum number of rows to include in the table
        
    Returns:
        Tuple of (Sorted DataFrame with Date, Region, Missing Data % columns, total_outages_count)
    """
    if system_outages_df.empty:
        return pd.DataFrame(), 0
    
    # Make a copy to avoid modifying the original
    result = system_outages_df.copy()
    
    # Get the total count before limiting rows
    total_outages_count = len(result)
    
    # Convert MissingData to percentage and rename columns
    result['Missing Data %'] = result['MissingData']
    result = result[['Date', 'Region', 'Missing Data %']]
    
    # Sort by Date descending (most recent first), then by Region
    result = result.sort_values(by=['Date', 'Region'], ascending=[False, True])
    
    # Limit the number of rows if needed
    if max_rows > 0 and len(result) > max_rows:
        result = result.head(max_rows)
    
    return result, total_outages_count
