import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px


def initialize_fig(group, group_columns, variable, date_column, anomaly_detection_model):
    
    plot_title = "  --  ".join(list(group[group_columns].values[0])).upper() + "  --  " + anomaly_detection_model
    
    fig = go.Figure()
    
    # Actuals
    fig.add_trace(go.Scatter(
        x=group[date_column],
        y=group[variable],
        mode='lines',
        line=dict(color='seagreen', width=1.5),
        name=variable if variable == variable.upper() else variable.title(),
    ))

    fig.update_layout(
        title=dict(
                text=plot_title,
                y=0.96,
                x=0.5,
                xanchor='center',
                yanchor='top',
                font=dict(size=18, color='black', weight='bold'),
            ),
        height=350,
        width=1200,
        margin=dict(l=50, r=50, t=40, b=30),
        plot_bgcolor='snow',
        paper_bgcolor='whitesmoke',
        xaxis=dict(
            range=[group[date_column].min(), group[date_column].max()],
            showline=True,
            linewidth=0.5,
            linecolor='orange',
            zeroline=False,
            gridcolor='rgba(255, 165, 0, 0.1)',
            mirror=True
            ),
        yaxis=dict(
            range=[group[variable].min()*0.9, group[variable].max()*1.06],
            showline=True,
            linewidth=0.5,
            linecolor='orange',
            zeroline=False,
            gridcolor='rgba(255, 165, 0, 0.1)',
            mirror=True
            ),
        yaxis_title=dict(
            text=variable if variable == variable.upper() else variable.title(),
            font=dict(size=16, weight='bold', color='black')
            ),
        legend=dict(
            orientation="v",
            yanchor="top",
            y=1,
            xanchor="left",
            x=1.02,
            )
        )
    
    return fig


def add_anomalies(fig, group, date_column, variable):
    # Add anomalies markers
    fig.add_trace(go.Scatter(
        x=group[group['is_Anomaly'] == True][date_column],
        y=group[group['is_Anomaly'] == True][variable],
        mode='markers',
        marker=dict(color='pink', symbol='cross', line=dict(width=1), size=9),
        name='Anomalies',
        hoverinfo='skip',
        ))
    return fig


def add_model_anomalies(fig, group, date_column, variable, model):
    fig.add_trace(go.Scatter(
        x=group[group[f'is_{model}_anomaly'] == True][date_column],
        y=group[group[f'is_{model}_anomaly'] == True][variable],
        mode='markers',
        marker=dict(color='palevioletred', symbol='circle', line=dict(width=1), size=9),
        name=f'{model} Anomalies',
        customdata=group[group[f'is_{model}_anomaly'] == True][[f'{model}_anomaly']],
        hovertemplate=(
            f'Date: %{{x|%Y-%m-%d}}<br>' +
            f'{variable if variable == variable.upper() else variable.title()}: %{{y:~.2s}}<br>' +
            f'{model}' + ' Category: %{customdata[0]}<extra></extra>'
            )
        ))
    return fig


def add_anomaly_region(fig, region, group, variable, date_column, threshold_column, threshold_label):
    
    if region == 'upper':
        y0 = group[threshold_column].values[0]
        y1 = group[variable].max()*1.06
    elif region == 'lower':
        y0 = 0
        y1 = group[threshold_column].values[0]
    
    # Shading
    fig.add_shape(
        type="rect",
        x0=0, x1=1, xref="paper",
        y0=y0, y1=y1,
        yref="y",
        fillcolor="rgba(255, 0, 0, 0.055)",
        line=dict(width=0),
        layer="below"
    )

    # Upper Percentile line
    fig.add_trace(go.Scatter(
        x=group[date_column],
        y=group[threshold_column],
        mode='lines',
        line=dict(color='orangered', width=1, dash='dashdot'),
        name=threshold_label,
        showlegend=False
        ))
    
    return fig


def add_eval_period_highlight(fig, group, date_column, variable, eval_period):
    fig.add_trace(go.Scatter(
        x=group[date_column][-eval_period:],
        y=group[variable][-eval_period:],
        mode='lines',
        line=dict(
            color='rgba(0, 255, 0, 0.25)', # 'lime' with 0.25 alpha
            width=10
        ),
        name='Evalution Period',
        hoverinfo='skip',
    ))
    return fig


def anomaly_eval_plot(group, group_columns, variable, date_column, eval_period=12, show_anomaly_scores_on_main_plot=False):
    
    # IS ANOMALY Plot
    # This is the main plot
    """
    Generates an ensemble anomaly evaluation plot using Plotly.

    This function aggregates multiple anomaly detection models (columns starting with 'is_' 
    and ending with '_anomaly') to create a consensus 'Anomaly Score'. It visualizes 
    actual values, mean, median, and highlights points where the ensemble of models 
    agrees there is an anomaly.

    Args:
        group (pd.DataFrame): The processed dataframe containing original data and 
            boolean anomaly flags from various models (e.g., 'is_FB_anomaly').
        group_columns (list): List of column names used to identify the group 
            (e.g., ['Region', 'Product']).
        variable (str): The name of the numeric column being analyzed.
        date_column (str): The name of the datetime column.
        eval_period (int, optional): The number of recent periods evaluated. Defaults to 12.
        show_anomaly_scores_on_main_plot (bool, optional): If True, adds a secondary 
            Y-axis bar chart showing the normalized ensemble score (-100 to 100). 
            Defaults to False.

    Logic:
        - Voting: Counts all columns matching 'is_*_anomaly'.
        - is_Anomaly: True if >= 50% of the active models flag the point.
        - Anomaly Score: A normalized metric where 100 represents total consensus 
          among all models and negative values represent low-risk points.

    Returns:
        None: Displays an interactive Plotly figure.
    """
    try:
        group = group.copy()

        anomaly_cols = []
        for col in group.columns.to_list():
            if col.startswith('is_') and col.endswith('_anomaly') and col != 'is_anomaly':
                anomaly_cols.append(col)
        group['Anomaly Vote Models'] = group.apply(
            lambda row: sorted([col.removeprefix('is_').removesuffix('_anomaly')
                for col in anomaly_cols
                if pd.notna(row[col]) and row[col] == True]),
            axis=1)
        group['Anomaly Vote Models'] = group['Anomaly Vote Models'].apply(lambda x: ', '.join(x))
        group['Anomaly_Votes'] = group[anomaly_cols].sum(axis=1).astype(int)
        group['Vote_Cnt'] = group[anomaly_cols].replace(False, True).sum(axis=1).astype(int)
        group['Anomaly_Votes_Display'] = group['Anomaly_Votes'].astype(str) + ' out of ' + group['Vote_Cnt'].astype(str)
        group['is_Anomaly'] = np.where(group['Anomaly_Votes']/group['Vote_Cnt'] >= 0.5, True, False)
        group['Anomaly_Score'] = 2 * (group['Anomaly_Votes']/group['Vote_Cnt'] - 0.5).astype(float)
        group['Anomaly_Score_Display'] = np.where(group['Anomaly_Score'] < 0, np.floor(100*group['Anomaly_Score']),
                                                  np.where(group['Anomaly_Score'] > 0, np.ceil(100*group['Anomaly_Score']),
                                                  1)).astype(float)
        group['Mean'] = group[variable].mean()
        group['Median'] = group[variable].median()

        fig = initialize_fig(group, group_columns, variable, date_column, "Anomalies")

        # Mean
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['Mean'],
            mode='lines',
            line=dict(color='maroon', width=0.7, dash='dash'),
            name='Mean',
            showlegend=True,
            hoverinfo='skip',
        ))

        # Median
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['Median'],
            mode='lines',
            line=dict(color='darkblue', width=0.7, dash='dot'),
            name='Median',
            showlegend=True,
            hoverinfo='skip',
        ))

        # Anomalies
        fig.add_trace(go.Scatter(
            x=group[group['is_Anomaly'] == True][date_column],
            y=group[group['is_Anomaly'] == True][variable],
            mode='markers',
            marker=dict(color='red', symbol='circle', line=dict(width=1), size=5*(group[group['is_Anomaly'] == True]['Anomaly_Score'] + 2)),
            name='Anomalies',
            customdata=group[group['is_Anomaly'] == True][['Anomaly_Votes_Display', 'Anomaly Vote Models', 'Anomaly_Score_Display']],
            hovertemplate=(
                f'Date: %{{x|%Y-%m-%d}}<br>' +
                f'{variable if variable == variable.upper() else variable.title()}: %{{y:,d}}<br>' +
                'Anomaly Votes: %{customdata[0]}<br>' +
                'Anomaly Vote Models: %{customdata[1]}<br>' +
                'Anomaly Score: %{customdata[2]}<extra></extra><br>'
                )
            ))

        # Near Anomalies
        fig.add_trace(go.Scatter(
            x=group[(group['is_Anomaly'] == False) & (group['Anomaly_Votes'] >= 1)][date_column],
            y=group[(group['is_Anomaly'] == False) & (group['Anomaly_Votes'] >= 1)][variable],
            mode='markers',
            marker=dict(color='orange',
                        symbol='circle',
                        line=dict(width=1),
                        size=5*(group[(group['is_Anomaly'] == False) & (group['Anomaly_Votes'] >= 1)]['Anomaly_Score'] + 2)),
            name='Not Quite Anomalies',
            customdata=group[(group['is_Anomaly'] == False) & (group['Anomaly_Votes'] >= 1)][['Anomaly_Votes_Display', 'Anomaly Vote Models', 'Anomaly_Score_Display']],
            hovertemplate=(
                f'Date: %{{x|%Y-%m-%d}}<br>' +
                f'{variable if variable == variable.upper() else variable.title()}: %{{y:,d}}<br>' +
                'Anomaly Votes: %{customdata[0]}<br>' +
                'Anomaly Vote Models: %{customdata[1]}<br>' +
                'Anomaly Score: %{customdata[2]}<extra></extra><br>'
                )
            ))

        # Add Anomaly Scores to Secondary Axis
        if show_anomaly_scores_on_main_plot:
            fig.add_trace(go.Bar(
                x=group[date_column],
                y=group['Anomaly_Score_Display'],
                name='Anomaly Score',
                marker=dict(
                    color=np.where(group['Anomaly_Score_Display'] > 0, 'rgba(255, 0, 0, 0.35)', 'rgba(128, 128, 128, 0.15)'),
                    # line=dict(width=0.5, color='gray')
                ),
                yaxis='y2',
                # Ensure it doesn't clutter the main hover box
                hoverinfo='y+name',
                showlegend=True
                ))

            fig.update_layout(
                yaxis2=dict(
                        title='Anomaly Score',
                        overlaying='y',
                        side='right',
                        range=[-105, 105],
                        showgrid=False,
                    ),
                margin=dict(l=50, r=200, t=50, b=50),
                legend=dict(
                    orientation="v",
                    yanchor="top",
                    y=1,
                    xanchor="left",
                    x=1.07,
                ))

        fig.show()
        print("\n")
        
    except Exception as e:
        print(f"Anomaly Plot Failed: {e}")


def anomaly_percentile_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # Percentile Model Plot
    """
    Visualizes anomaly detection based on Percentile-derived thresholds.

    This function plots the time-series data alongside shaded regions representing 
    the upper and lower percentile boundaries. It highlights specific 'Percentile' 
    model anomalies and can optionally overlay the final consensus anomalies.

    Args:
        group (pd.DataFrame): Dataframe containing the time-series data and 
            calculated percentile columns ('Percentile_low', 'Percentile_high', 
            and 'is_Percentile_anomaly').
        group_columns (list): Column names used for grouping/title identification.
        variable (str): The numeric column name being plotted on the Y-axis.
        date_column (str): The datetime column name for the X-axis.
        final_anomalies (bool, optional): If True, overlays the final ensemble 
            consensus markers (red circles) on top of the model-specific markers. 
            Defaults to True.
        eval_period (int, optional): The look-back period used for the evaluation 
            context. Defaults to 12.

    Logic:
        - Shading: Uses `add_anomaly_region` to fill the area beyond 'Percentile_low' 
          and 'Percentile_high'.
        - Model Markers: Highlights points where 'is_Percentile_anomaly' is True.
        - Integration: Uses helper functions `initialize_fig`, `add_anomaly_region`, 
          and `add_model_anomalies` to maintain a consistent UI/UX.

    Returns:
        None: Displays an interactive Plotly figure.
    """
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "Percentile Anomaly Detection")
        # Lower anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'lower', group, variable, date_column, 'Percentile_low', 'Percentile Low')
        # Upper anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'upper', group, variable, date_column, 'Percentile_high', 'Percentile High')
        # Percentile Anomalies
        fig = add_model_anomalies(fig, group, date_column, variable, 'Percentile')
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"Percentile Anomaly Plot Failed: {e}") 


def anomaly_sd_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # SD Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "SD Anomaly Detection")
        # Lower anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'lower', group, variable, date_column, 'SD2_low', 'SD Low')
        # Upper anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'upper', group, variable, date_column, 'SD2_high', 'SD High')
        # SD Anomalies
        fig = add_model_anomalies(fig, group, date_column, variable, 'SD')
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"SD Anomaly Plot Failed: {e}")
        
        
def anomaly_mad_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # MAD Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "MAD Anomaly Detection")
        # Lower anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'lower', group, variable, date_column, 'MAD_low', 'MAD Low')
        # Upper anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'upper', group, variable, date_column, 'MAD_high', 'MAD High')
        # MAD Anomalies
        fig = add_model_anomalies(fig, group, date_column, variable, 'MAD')
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"MAD Anomaly Plot Failed: {e}")


def anomaly_iqr_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # IQR Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "IQR Anomaly Detection")
        # Lower anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'lower', group, variable, date_column, 'IQR_low', 'IQR Low')
        # Upper anomaly region shading and threshold line
        fig = add_anomaly_region(fig, 'upper', group, variable, date_column, 'IQR_high', 'IQR High')
        # IQR Anomalies
        fig = add_model_anomalies(fig, group, date_column, variable, 'IQR')
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"IQR Anomaly Plot Failed: {e}")


def anomaly_ewma_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # EWMA Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "EWMA Anomaly Detection")
        # EWMA Forecast
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['EWMA_forecast'],
            mode='lines',
            line=dict(color='slateblue', width=0.5, dash='dashdot'),
            name='EWMA Forecast',
            showlegend=True
            ))
        # EWMA low line
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['EWMA_low'],
            mode='lines',
            line=dict(color='orangered', width=1, dash='dashdot'),
            name='EWMA Low',
            showlegend=False
            ))
        # Lower Shading
        fig.add_trace(go.Scatter(
            x=group[group['EWMA_low'].isna()==False][date_column],
            y=[0] * len(group[group['EWMA_low'].isna()==False]),
            mode="lines",
            line=dict(width=0),
            hoverinfo="skip",
            showlegend=False,
            fill="tonexty",
            fillcolor="rgba(255, 0, 0, 0.07)"
        ))
        # EWMA high line
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['EWMA_high'],
            mode='lines',
            line=dict(color='orangered', width=1, dash='dashdot'),
            name='EWMA High',
            showlegend=False,
            ))
        # Upper Shading
        fig.add_trace(go.Scatter(
            x=group[group['EWMA_high'].isna()==False][date_column],
            y=[group[variable].max() * 1.06] * len(group[group['EWMA_high'].isna()==False]),
            mode="lines",
            line=dict(width=0),
            hoverinfo="skip",
            showlegend=False,
            fill="tonexty",
            fillcolor="rgba(255, 0, 0, 0.07)"
            ))
        # EWMA Anomalies
        fig.add_trace(go.Scatter(
            x=group[group['is_EWMA_anomaly'] == True][date_column],
            y=group[group['is_EWMA_anomaly'] == True][variable],
            mode='markers',
            marker=dict(color='palevioletred', symbol='circle', line=dict(width=1), size=9),
            name='EWMA Anomalies',
            ))
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"EWMA Anomaly Plot Failed: {e}")


def anomaly_fb_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # FB Prophet Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "FB Prophet Anomaly Detection")    
        # FB Forecast
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['FB_forecast'],
            mode='lines',
            line=dict(color='slateblue', width=0.5, dash='dashdot'),
            name='FB Forecast',
            showlegend=True
            ))
        # Lower FB line
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['FB_low'],
            mode='lines',
            line=dict(color='orangered', width=1, dash='dashdot'),
            name='FB Low',
            showlegend=False
            ))
        # Lower Shading
        fig.add_trace(go.Scatter(
            x=group[group['FB_low'].isna()==False][date_column],
            y=[0] * len(group[group['FB_low'].isna()==False]),
            mode="lines",
            line=dict(width=0),
            hoverinfo="skip",
            showlegend=False,
            fill="tonexty",
            fillcolor="rgba(255, 0, 0, 0.07)"
        ))
        # Upper FB line
        fig.add_trace(go.Scatter(
            x=group[date_column],
            y=group['FB_high'],
            mode='lines',
            line=dict(color='orangered', width=1, dash='dashdot'),
            name='FB High',
            showlegend=False,
            ))
        # Upper Shading
        fig.add_trace(go.Scatter(
            x=group[group['FB_high'].isna()==False][date_column],
            y=[group[variable].max() * 1.06] * len(group[group['FB_high'].isna()==False]),
            mode="lines",
            line=dict(width=0),
            hoverinfo="skip",
            showlegend=False,
            fill="tonexty",
            fillcolor="rgba(255, 0, 0, 0.07)"
            ))
        # FB Anomalies
        fig.add_trace(go.Scatter(
            x=group[group['is_FB_anomaly'] == True][date_column],
            y=group[group['is_FB_anomaly'] == True][variable],
            mode='markers',
            marker=dict(color='palevioletred', symbol='circle', line=dict(width=1), size=9),
            name='FB Anomalies',
            ))
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"EWMA Anomaly Plot Failed: {e}")


def anomaly_dbscan_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # DBSCAN Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "DBSCAN Anomaly Detection")
        # Evaluation Period
        if eval_period >= 1:
            fig = add_eval_period_highlight(fig, group, date_column, variable, eval_period)
        # DBSCAN Anomalies
        fig.add_trace(go.Scatter(
            x=group[group['is_DBSCAN_anomaly'] == True][date_column],
            y=group[group['is_DBSCAN_anomaly'] == True][variable],
            mode='markers',
            marker=dict(color='mediumorchid', symbol='circle', line=dict(width=1), size=7),
            name='DBSCAN Anomalies',
            ))
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"DBSCAN Anomaly Plot Failed: {e}")


def anomaly_isolation_forest_timeseries_plot(group, group_columns, variable, date_column, final_anomalies=True, eval_period=12):
    # Isolation Forest Model Plot
    try:
        group = group.copy()
        fig = initialize_fig(group, group_columns, variable, date_column, "Isolation Forest Anomaly Detection")
        # Evaluation Period
        if eval_period >= 1:
            fig = add_eval_period_highlight(fig, group, date_column, variable, eval_period)
        # Isolation Forest Anomalies
        fig.add_trace(go.Scatter(
            x=group[group['is_IsolationForest_anomaly_timeseries'] == True][date_column],
            y=group[group['is_IsolationForest_anomaly_timeseries'] == True][variable],
            mode='markers',
            marker=dict(color='mediumorchid', symbol='circle', line=dict(width=1), size=7),
            name='Isolation Forest Anomalies',
            ))
        # Add anomalies markers
        if final_anomalies:
            fig = add_anomalies(fig, group, date_column, variable)
        fig.show()
        print("\n")
    except Exception as e:
        print(f"Isolation Forest Time Series Anomaly Plot Failed: {e}")
