import pandas as pd
import ipywidgets as widgets
from IPython.display import display, clear_output, Markdown, HTML
import IPython

# 1. Import the helper function so we can pass it to the notebook
try:
    from anomaly_pipeline.helpers import help_anomaly
except ImportError:
    print("⚠️ Warning: Could not import 'help_anomaly'.")
    help_anomaly = None

# Assuming the detection function is imported from a sibling module
try:
    from .main import timeseries_anomaly_detection
except ImportError:
    print("⚠️ Warning: Could not import 'timeseries_anomaly_detection'. Make sure the module exists.")
    def timeseries_anomaly_detection(*args, **kwargs):
        return None

def anomaly_detection_guide():
    """
    Master workflow function that orchestrates:
    1. Selection of the Dataframe
    2. Configuration of Columns & Globals setting
    """
    
    ns = IPython.get_ipython().user_ns
    
    if help_anomaly:
        ns['help_anomaly'] = help_anomaly
    
    if timeseries_anomaly_detection:
        ns['timeseries_anomaly_detection'] = timeseries_anomaly_detection
    
    if run_anomaly_detection:
        ns['run_anomaly_detection'] = run_anomaly_detection
    else:
        print('run_anomaly_detection() not global')
    
    ns['eval_period'] = 1
    
    # --- OUTPUT CONTAINERS ---
    step1_output = widgets.Output()
    step2_output = widgets.Output()
    step3_output = widgets.Output()
    step4_output = widgets.Output()
    step5_output = widgets.Output()
    
    display(step1_output)
    display(step2_output)
    display(step3_output)
    display(step4_output)
    display(step5_output)

    # =========================================================================
    # STEP 1: SELECT DATAFRAME
    # =========================================================================
    with step1_output:
        active_dfs = [
            name for name, obj in ns.items() 
            if isinstance(obj, pd.DataFrame) 
            and not name.startswith('_') 
            and len(obj) >= 52
        ]
        
        if not active_dfs:
            print("❌ No DataFrames found with at least 52 rows.")
            return

        initial_df = active_dfs[0]
        
        dd_df = widgets.Dropdown(
            options=active_dfs, value=initial_df,
            description='DataFrame:', style={'description_width': 'initial'},
            layout={'width': '300px'}
        )
        
        btn_df = widgets.Button(
            description=f"Set '{initial_df}' as anomaly_detection_dataset", 
            button_style='primary', icon='check', layout={'width': '400px'}
        )
        
        out_df_preview = widgets.Output()
        
        def update_df_preview(change):
            if change['new']:
                btn_df.description = f"Set '{change['new']}' as anomaly_detection_dataset"
                with out_df_preview:
                    clear_output()
                    sel_df = ns[change['new']]
                    print(f"📄 Preview ({len(sel_df)} rows):")
                    display(sel_df.head(3))
        
        dd_df.observe(update_df_preview, names='value')
        
        with out_df_preview:
            print(f"📄 Preview ({len(ns[initial_df])} rows):")
            display(ns[initial_df].head(3))

        display(widgets.VBox([
            widgets.HTML("<h3>Step 1: Select Input DataFrame</h3>"),
            dd_df, btn_df, out_df_preview,
            widgets.HTML("<hr>")
        ]))

    # =========================================================================
    # LOGIC: TRANSITION TO STEP 2
    # =========================================================================
    def load_step_2(b):
        selected_name = dd_df.value
        # We temporarily store this to load columns, but final global set happens in Save
        temp_df = ns[selected_name].copy()
        
        step2_output.clear_output()
        step3_output.clear_output()
        
        with step2_output:
            print(f"✅ Selected '{selected_name}'. Loading Column Options...")
            
            # --- ANALYZE COLUMNS ---
            all_cols = temp_df.columns.tolist()
            
            def date_priority(col_name):
                if pd.api.types.is_datetime64_any_dtype(temp_df[col_name]): return 0 
                if any(x in col_name.lower() for x in ['date', 'time', 'ds', 'ts', 'week']): return 1
                if not pd.api.types.is_numeric_dtype(temp_df[col_name]): return 2
                return 3
            sorted_dates = sorted(all_cols, key=date_priority)
            
            numeric_cols = temp_df.select_dtypes(include=['number']).columns.tolist()
            numeric_cols.reverse()
            
            # Original candidates (non-numeric)
            group_candidates = temp_df.select_dtypes(exclude=['number']).columns.tolist()

            # --- MODIFICATION: EXTENDED OPTIONS ---
            # Add all other columns (numeric ones) to the bottom of the list
            other_cols = [c for c in all_cols if c not in group_candidates]
            extended_group_options = group_candidates + other_cols

            # --- STEP 2 WIDGETS ---
            w_group = widgets.SelectMultiple(
                options=extended_group_options, # Use the extended list
                rows=6,
                style={'description_width': 'initial'}, layout={'width': '350px'}
            )
            w_date = widgets.Dropdown(
                options=sorted_dates,
                style={'description_width': 'initial'}, layout={'width': '350px'}
            )
            w_target = widgets.Dropdown(
                options=numeric_cols,
                style={'description_width': 'initial'}, layout={'width': '350px'}
            )
            
            # --- WIDGET: FREQUENCY SELECTION ---
            # Define mapping for display aliases
            freq_mapping = {
                'D': 'Daily',
                'W': 'Weekly',
                'W-SUN': 'Weekly - Sundays',
                'W-MON': 'Weekly - Mondays',
                'MS': 'Monthly Start',
                'M': 'Monthly',
            }
            
            w_freq = widgets.Dropdown(
                options=[], # Populated dynamically
                style={'description_width': 'initial'}, layout={'width': '350px'}
            )

            btn_save_cfg = widgets.Button(
                description="Save Configuration", button_style='primary', 
                icon='save', layout={'width': '350px'}
            )
            out_cfg_status = widgets.Output()

            def on_date_change(change):
                new_date = change['new']
                # Update options based on extended list, excluding the selected date
                w_group.options = [c for c in extended_group_options if c != new_date]
                
                # --- INFER FREQUENCY ---
                # Attempt to infer the frequency from the data to populate the dropdown
                inferred = None
                try:
                    # Use unique values to speed up conversion and inference
                    uniques = temp_df[new_date].dropna().unique()
                    dt_idx = pd.Index(pd.to_datetime(uniques, errors='coerce')).dropna().sort_values()
                    
                    if len(dt_idx) > 2:
                        inferred = pd.infer_freq(dt_idx)
                except:
                    inferred = None
                
                # Construct options list of tuples: (Label, Value)
                # Label format: "'CODE' (Description)"
                dropdown_opts = []
                
                # 1. Add Inferred if exists
                if inferred:
                    inferred_str = str(inferred)
                    # Try to find a description, or default to 'Inferred'
                    desc = freq_mapping.get(inferred_str, 'Inferred')
                    label = f"'{inferred_str}' ({desc})"
                    dropdown_opts.append((label, inferred_str))
                
                # 2. Add Common Freqs
                for code, desc in freq_mapping.items():
                    # Skip if it matches the inferred one to avoid duplicates
                    if inferred and str(inferred) == code:
                        continue
                    label = f"'{code}' ({desc})"
                    dropdown_opts.append((label, code))
                
                w_freq.options = dropdown_opts
                # Default to first option (Inferred if valid, else 'D')
                if dropdown_opts:
                    w_freq.value = dropdown_opts[0][1]
            
            w_date.observe(on_date_change, names='value')
            # Trigger initial filter
            if w_date.value:
                on_date_change({'new': w_date.value})

            display(widgets.VBox([
                widgets.HTML("<h3>Step 2: Define Configuration Parameters for Anomaly Detection</h3>"),
                widgets.HTML("<b>A. Select Unique ID (primary key) columns excluding the date column (Hold Ctrl or Shift to select multiple columns):</b>"), w_group,
                widgets.HTML("<b>B. Select Date Column:</b>"), w_date,
                widgets.HTML("<b>C. Select Date Frequency:</b>"), w_freq,
                widgets.HTML("<b>D. Select Target Variable on which to perform anomaly detection:</b>"), w_target,
                widgets.HTML("<br>"), btn_save_cfg, out_cfg_status,
                widgets.HTML("<hr>")
            ]))

            # =================================================================
            # LOGIC: SAVE GLOBALS (Replaces Step 3 Transition)
            # =================================================================
            def save_globals(b):
                # REFRESH temp_df: Ensure we work with a fresh copy of the original dataframe
                # This prevents issues where previous conversions persist if validation fails
                temp_df = ns[selected_name].copy()

                date_col = w_date.value
                unique_ids = list(w_group.value)
                
                # 1. Convert Date Column to Datetime
                # This ensures the date column is proper datetime64 for downstream analysis
                try:
                    temp_df[date_col] = pd.to_datetime(temp_df[date_col])
                except Exception as e:
                    with out_cfg_status:
                        clear_output()
                        print(f"❌ Error converting '{date_col}' to datetime: {str(e)}")
                    return

                # 2. Convert Unique ID Columns to String
                # This ensures consistent grouping/merging by treating IDs as text
                try:
                    for uid in unique_ids:
                        temp_df[uid] = temp_df[uid].astype(str)
                except Exception as e:
                    with out_cfg_status:
                        clear_output()
                        print(f"❌ Error converting ID column '{uid}' to string: {str(e)}")
                    return

                # --- VALIDATION: Check for Duplicate Records ---
                # Check if unique_ids + date_col form a unique key
                check_cols = unique_ids + [date_col]
                try:
                    if temp_df.duplicated(subset=check_cols).any():
                        with out_cfg_status:
                            clear_output()
                            print("❌ Error: Your configuration has multiple records per unique_id - date.")
                            print(f"   The combination of {unique_ids} and '{date_col}' is not unique.")
                            print("   Please check your ID selection or input data.")
                        return
                except Exception as e:
                    with out_cfg_status:
                        clear_output()
                        print(f"❌ Error during validation: {str(e)}")
                    return

                # 3. Capture Frequency from Widget
                selected_freq = w_freq.value

                # 4. Set the specific global variables you requested
                ns['anomaly_detection_dataset'] = temp_df  # The dataframe (with converted dates and string IDs)
                ns['unique_ids'] = unique_ids # The grouping columns
                ns['date_column'] = date_col
                ns['variable'] = w_target.value
                ns['freq'] = selected_freq # Capture the selected frequency
                
                # Feedback in Step 2 container
                with out_cfg_status:
                    clear_output()
                    print("✅ Anomaly detection configuration saved!")
                    print(f"   anomaly_detection_dataset ({len(ns['anomaly_detection_dataset'])} rows, {len(ns['anomaly_detection_dataset'][ns['unique_ids']].drop_duplicates())} unique_id count)")
                    print(f"   unique_ids: {ns['unique_ids']}")
                    print(f"   date_column: {ns['date_column']}")
                    print(f"   freq: '{ns['freq']}'")
                    print(f"   variable: {ns['variable']}")
                
                step3_output.clear_output()
                with step3_output:
                    print("\nThank you for your selections. The package has been configured with the following parameters:\n")
                    
                    display(Markdown(f"""
```python
final_results, evaluation_report = timeseries_anomaly_detection(
    master_data=anomaly_detection_dataset,
    unique_ids={ns['unique_ids']},
    date_column='{ns['date_column']}',
    freq='{ns['freq']}',
    variable='{ns['variable']}'
    )
```
"""))
                    # Added horizontal line
                    display(widgets.HTML("<hr>"))

                step4_output.clear_output()
                with step4_output:
                    
                    print("\n▶️ Run the following function to detect anomalies on your data with the configuration above:")
                    
                    run_msg = """
<span style="background-color: ghostwhite; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-weight: bold;">
    run_anomaly_detection()
</span>
"""
                    
                    display(Markdown("""<div style="background-color: ghostwhite; padding: 10px; border: 1px solid #e0e0e0; border-radius: 5px; width: fit-content; font-weight: bold;">run_anomaly_detection()</div>"""))
                    # Added horizontal line
                    display(widgets.HTML("<hr>"))
                
                step5_output.clear_output()
                with step5_output:
                    print("\n❓For more help or information, use our custom-built help functions here:")
                    display(Markdown(f"""
<div style="background-color: ghostwhite; padding: 10px; border: 1px solid #e0e0e0; border-radius: 5px; width: fit-content;">
<pre style="background-color: ghostwhite; margin: 0; font-family: monospace; white-space: pre">help_anomaly()</pre></div>"""))
            
            btn_save_cfg.on_click(save_globals)

    btn_df.on_click(load_step_2)

    
def run_anomaly_detection():
    """
    Wrapper to run anomaly detection using variables from the global IPython namespace.
    Handles 'NoneType' returns gracefully and injects helpers into the notebook.
    """
    # 1. Access Global Namespace safely
    try:
        ip = IPython.get_ipython()
        if ip is None:
            raise RuntimeError("Not running in an IPython environment")
        ns = ip.user_ns
    except Exception as e:
        print(f"❌ Error accessing IPython namespace: {e}")
        return

    # 2. Check for required variables before calling
    required_keys = ['anomaly_detection_dataset', 'unique_ids', 'freq']
    missing = [k for k in required_keys if k not in ns]
    if missing:
        print(f"❌ Critical Error: Missing required global variables: {missing}")
        return

    # 3. Call the detection function
    timeseries_anomaly_detection(
        master_data=ns.get('anomaly_detection_dataset'),
        unique_ids=ns.get('unique_ids'),
        variable=ns.get('variable'),
        date_column=ns.get('date_column'),
        freq=ns.get('freq')
    )    
    # 7. Inject help_anomaly into the notebook namespace
    if help_anomaly:
        ns['help_anomaly'] = help_anomaly