from .pipeline import run_pipeline
import pandas as pd
import re

"""
class Colors:
    BLUE = '\033[94m'
    RED = '\033[91m'
    END = '\033[0m'
    BOLD = '\033[1m'
    BOLD_ITALIC = "\033[1;3m"
    RESET = "\033[0m"

def timeseries_anomaly_detection(
    master_data=None, 
    group_columns = None, 
    variable= None,
    date_column = None,
    freq="W-MON",
    min_records=None,  
    max_records =None,
    contamination=0.03, 
    random_state=42,
    alpha=0.3, 
    sigma=1.5, 
    eval_period=1,
    prophet_CI=0.90, 
    mad_threshold=2, 
    mad_scale_factor=0.6745,
    interpolation_method="linear"
):
   
    
Performs anomaly detection on grouped time-series data. Currently, we support daily, weekly, and monthly data. Data for missing time units is interpolated. Maximum interpolation is 25% of the series. Combines 8 models (Statistical + ML) to provide a robust Anomaly_Score and a final is_Anomaly consensus. To improve user experaince we specifically designed a help function for this package. Please access the customized help function using the below code. 


```python

from anomaly_pipeline import help_anomaly
help_anomaly()

```

# Mandatory Columns:
- master_data: Input DataFrame containing variables, dates, and group identifiers.
- group_columns: List of columns used to segment the data (e.g., ['Region', 'Product']).
- variable (numeric): The numerical target column to analyze for outliers.
- date_column: The datetime column representing the time axis.
    
# Default arguments:
    - freq (str): Frequency of the time series (Pandas offset alias). Defaults to 'W-MON', if it is daily replace it with 'D', Monthly replace it with 'M' or 'MS'
    - min_records: Minimum history required per group. Default is None; If None, extracts based on freq                             (1 Year +  eval_period). Ex: if freq is weekly and eval_period is 1: min_records = 52+1.
    - max_records: Maximum history to retain per group. Default is None; if provided, filters for the most recent N records.
    - contamination (float): Expected proportion of outliers in the data (0 to 0.5). Defaults to 0.03.
    - random_state (int): Seed for reproducibility in stochastic models. Defaults to 42.
    - alpha (float): Smoothing factor for trend calculations. Defaults to 0.3.
    - sigma (float): Standard deviation multiplier for thresholding. Defaults to 1.5.
    - eval_period: The number of trailing records in each group to evaluate for anomalies.
    - prophet_CI (float): The confidence level for the prediction interval (0 to 1). Defaults to 0.9.
    - Mad_threshold (int): Default 2.
    - mad_scale_factor (float): Default 0.6745. 
    - interpolation_method (str): Interpolation technique adopted to fill the missing values. Default 'linear'.

Returns:
    tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
        - final_results: The main dataframe containing original data, interpolated values, 
          forecasts, residuals, and anomaly flags (e.g., is_FB_anomaly, is_IQR_anomaly).
        - exclusion_report: A summary table for successful groups showing 'initial_record_count', 
          'interpolated_record_count', 'interpolation_pct', 'anomalies' and 'anomaly_rate'
        - exclusion_report: A diagnostic table listing groups dropped from the analysis 
          and the specific reason (e.g., "Insufficient records" or "High Interpolation").
       
    # making robust with input parameters
    if isinstance(group_columns, str):
        group_columns = [group_columns]
    
    
        
    # --- 1. MANDATORY PARAMETER VALIDATION ---
    required_params = {
        "master_data": master_data,
        "group_columns": group_columns,
        "variable": variable,
        "date_column": date_column
    }

    missing_params = [name for name, val in required_params.items() if val is None]

    if missing_params:
        print("\n" + "!"*70)
        # Bold Red for the Error Header
        print(f"❌ {Colors.RED}{Colors.BOLD}ERROR: MISSING REQUIRED PARAMETERS{Colors.END}")
        print("The following parameters are required to run the detection:")
        for param in missing_params:
            print(f"  - {param}")
        
        # Blue for the Hint and Italic for the function name
        print(f"\n💡 {Colors.BLUE}HINT: Use {BOLD_ITALIC}help(timeseries_anomaly_detection){Colors.END} {Colors.BLUE}to see detailed{Colors.END}")
        print(f"{Colors.BLUE}descriptions and expected formats for each parameter.{Colors.END}")
        print("!"*70 + "\n")
        return # Exit early
    
    
    # --- 2. MANDATORY COLUMN VALIDATION ---
    mandatory_cols = group_columns + [variable, date_column]
    missing_cols = [col for col in mandatory_cols if col not in master_data.columns]
    
    if missing_cols:
        print("\n" + "!"*70)
        print(f"❌ {Colors.RED}{Colors.BOLD}ERROR: MANDATORY COLUMNS MISSING{Colors.END}")
        print(f"The following columns were not found in {BOLD_ITALIC}master_data{RESET}:")
        for col in missing_cols:
            print(f"  - {col}")
        
        print(f"\n💡 {Colors.BLUE}HINT: Check for typos or case-sensitivity.{Colors.END}")
        print(f"Available columns are: {', '.join(list(master_data.columns)[:5])}...")
        print("!"*70 + "\n")
        return  # Exit early without raising a traceback
    
    # Check if the variable is numeric
    if not pd.api.types.is_numeric_dtype(master_data[variable]):
        raise TypeError(f"CRITICAL: The variable '{variable}' must be numeric, but found {master_data[variable].dtype}.")
        
    # --- 3. EXECUTE PIPELINE ---
    # Store results in a local variable first
    final_df, evaluation_report, exclusion_report = run_pipeline(
        master_data=master_data,
        group_columns=group_columns,
        variable=variable,
        date_column=date_column,
        freq=freq,
        min_records=min_records,
        max_records=max_records, 
        contamination=contamination,
        random_state=random_state,
        alpha=alpha,
        sigma=sigma,
        eval_period=eval_period,
        prophet_CI=prophet_CI,
        mad_threshold=mad_threshold, 
        mad_scale_factor=mad_scale_factor,
        interpolation_method = interpolation_method
    )

    import inspect
    # Inside your timeseries_anomaly_detection function:
    # 1. Get the line of code that called this function
    frame = inspect.currentframe().f_back
    call_line = ""
    if frame and inspect.getframeinfo(frame).code_context:
        call_line = inspect.getframeinfo(frame).code_context[0].strip()

    # 2. Check if the user assigned the result to variables
    # We split by the function name and check the part before it (index 0)
    is_assigned = False
    if "timeseries_anomaly_detection" in call_line:
        prefix = call_line.split("timeseries_anomaly_detection")[0]
        # If there is exactly one '=', it's an assignment
        if prefix.count("=") == 1:
            is_assigned = True

    # 3. If NOT assigned, trigger the "Auto-Save" to the global namespace
    if not is_assigned:
        from IPython import get_ipython
        shell = get_ipython()
        if shell:
            shell.user_ns['final_results'] = final_df
            shell.user_ns['evaluation_report'] = evaluation_report
            shell.user_ns['exclusion_report'] = exclusion_report

            print("\n" + "*"*60)
            print("📀NOTE: Since variables are not assigned from the user")
            print("the outputs are saved for you in these three data frames:")
            print("final_results, evaluation_report, exclusion_report")
            print("*"*60 + "\n")

    # 4. Final return logic
    if is_assigned:
        # Determine if the user assigned to a single variable or multiple
        prefix = call_line.split("=")[0].strip()
        
        # If there's no comma in the assignment prefix, they used a single variable
        if "," not in prefix:
            print(f"\n💡 INFO: You assigned the output to a single variable: '{prefix}'")
            print(f"   This variable is a tuple containing 3 DataFrames. Access them via:")
            print(f"   1. Results Data:    {prefix}[0]")
            print(f"   2. evaluation_report:  {prefix}[1]")
            print(f"   3. Exclusion List:  {prefix}[2]")
            print(f"   Or unpack them: final_df, evaluation_report, exclusion_report = {prefix}\n")
        
        return final_df, evaluation_report, exclusion_report
    else:
        # Return None so Jupyter doesn't print the "wall of text"
        return None
    
"""    
    
class Colors:
    BLUE = '\033[94m'
    RED = '\033[91m'
    END = '\033[0m'
    BOLD = '\033[1m'
    BOLD_ITALIC = "\033[1;3m"
    RESET = "\033[0m"

def timeseries_anomaly_detection(
    master_data=None, 
    group_columns = None, 
    variable= None,
    date_column = None,
    freq="W-MON",
    min_records=None,  
    max_records =None,
    contamination=0.03, 
    random_state=42,
    alpha=0.3, 
    sigma=1.5, 
    eval_period=1,
    prophet_CI=0.90, 
    mad_threshold=2, 
    mad_scale_factor=0.6745,
    interpolation_method="linear"
):
    
    """
    Performs ensemble-based anomaly detection on grouped time-series data.
    
    This function acts as a high-level wrapper for the `run_pipeline` logic, 
    combining 8 statistical and machine learning models to provide a robust 
    Consensus Anomaly Score.

    ----------------------------------------------------------------------------
    💡 DOCUMENTATION & HELP:
    For detailed model descriptions or usage guides, run:
    >>> from quant_research import help_anomaly
    >>> help_anomaly()
    ----------------------------------------------------------------------------

    📌 MANDATORY ARGUMENTS
    ----------------------
    master_data (pd.DataFrame) : Input data containing variables, dates, and groups.
    group_columns (list[str])  : List of columns used to segment data (e.g., ['key', 'channel']).
    variable (str)             : The numerical target column name to analyze.
    date_column (str)          : The datetime column representing the time axis.

    ⚙️ OPTIONAL ARGUMENTS
    ---------------------
    freq (str)           : Pandas offset alias. 'W-MON' (default), 'D', or 'MS'.
    min_records (int)    : Min history per group. Defaults to (1 Year + eval_period).
    max_records (int)    : Max history to retain per group (filters for most recent N).
    contamination (float): Expected % of outliers (0 to 0.5). Default: 0.03.
    random_state (int)   : Seed for model reproducibility. Default: 42.
    alpha (float)        : Smoothing factor for EWMA trend calculations. Default: 0.3.
    sigma (float)        : Standard deviation multiplier for thresholding. Default: 1.5.
    eval_period (int)    : Number of trailing records to evaluate for anomalies. Default: 1.
    prophet_CI (float)   : Confidence interval for Prophet forecasting (0-1). Default: 0.9.
    mad_threshold (int)  : Median Absolute Deviation sensitivity. Default: 2.
    interpolation_method : Technique to fill missing time units. Default: 'linear'.

    📤 RETURNS
    ----------
    tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
        1. final_results     : The main DF including `is_Anomaly` and `Anomaly_Votes`.
        2. success_report   : Summary of interpolation %, record counts, and anomaly rates.
        3. exclusion_report : Diagnostic list of groups dropped due to data quality issues.
        
    """
        
    # making robust with input parameters
    if isinstance(group_columns, str):
        group_columns = [group_columns]
    
    
        
    # --- 1. MANDATORY PARAMETER VALIDATION ---
    required_params = {
        "master_data": master_data,
        "group_columns": group_columns,
        "variable": variable,
        "date_column": date_column
    }

    missing_params = [name for name, val in required_params.items() if val is None]

    if missing_params:
        print("\n" + "!"*70)
        # Bold Red for the Error Header
        print(f"❌ {Colors.RED}{Colors.BOLD}ERROR: MISSING REQUIRED PARAMETERS{Colors.END}")
        print("The following parameters are required to run the detection:")
        for param in missing_params:
            print(f"  - {param}")
        
        # Blue for the Hint and Bold/Italic for the help command
        print(f"\n💡 {Colors.BLUE}HINT: Use the following for details:{Colors.END}")
        print(f"   {Colors.BOLD_ITALIC}from anomaly_pipeline import help_anomaly{Colors.END}")
        print(f"   {Colors.BOLD_ITALIC}help_anomaly(){Colors.END}")
        print(f"{Colors.BLUE}descriptions and expected formats for each parameter.{Colors.END}")
        print("!"*70 + "\n")
        return # Exit early
    
    
    # --- 2. MANDATORY COLUMN VALIDATION ---
    mandatory_cols = group_columns + [variable, date_column]
    missing_cols = [col for col in mandatory_cols if col not in master_data.columns]
    
    if missing_cols:
        print("\n" + "!"*70)
        print(f"❌ {Colors.RED}{Colors.BOLD}ERROR: MANDATORY COLUMNS MISSING{Colors.END}")
        print(f"The following columns were not found in {Colors.BOLD_ITALIC}master_data{Colors.END}:")
        for col in missing_cols:
            print(f"  - {col}")
        
        # Blue for the Hint and Bold/Italic for the help command
        print(f"\n💡 {Colors.BLUE}HINT: Ensure columns match the spelling in your file.{Colors.END}")
        #print(f"{Colors.BLUE}Available columns: {Colors.END}{list(master_data.columns)[:5]}...")
        
        print(f"\n{Colors.BLUE}Use the following for detailed documentation:{Colors.END}")
        print(f"   {Colors.BOLD_ITALIC}from anomaly_pipeline import help_anomaly{Colors.END}")
        print(f"   {Colors.BOLD_ITALIC}help_anomaly(){Colors.END}")
        print("!"*70 + "\n")
        return # Exit early
    
    # Check if the variable is numeric
    if not pd.api.types.is_numeric_dtype(master_data[variable]):
        raise TypeError(f"CRITICAL: The variable '{variable}' must be numeric, but found {master_data[variable].dtype}.")
        
    # --- 3. EXECUTE PIPELINE ---
    # Store results in a local variable first
    final_df, evaluation_report, exclusion_report = run_pipeline(
        master_data=master_data,
        group_columns=group_columns,
        variable=variable,
        date_column=date_column,
        freq=freq,
        min_records=min_records,
        max_records=max_records, 
        contamination=contamination,
        random_state=random_state,
        alpha=alpha,
        sigma=sigma,
        eval_period=eval_period,
        prophet_CI=prophet_CI,
        mad_threshold=mad_threshold, 
        mad_scale_factor=mad_scale_factor,
        interpolation_method = interpolation_method
    )

    import inspect
    # Inside your timeseries_anomaly_detection function:
    # 1. Get the line of code that called this function
    frame = inspect.currentframe().f_back
    call_line = ""
    if frame and inspect.getframeinfo(frame).code_context:
        call_line = inspect.getframeinfo(frame).code_context[0].strip()

    # 2. Check if the user assigned the result to variables
    # We split by the function name and check the part before it (index 0)
    is_assigned = False
    if "timeseries_anomaly_detection" in call_line:
        prefix = call_line.split("timeseries_anomaly_detection")[0]
        # If there is exactly one '=', it's an assignment
        if prefix.count("=") == 1:
            is_assigned = True

    # 3. If NOT assigned, trigger the "Auto-Save" to the global namespace
    if not is_assigned:
        from IPython import get_ipython
        shell = get_ipython()
        if shell:
            shell.user_ns['final_results'] = final_df
            shell.user_ns['evaluation_report'] = evaluation_report
            shell.user_ns['exclusion_report'] = exclusion_report

            print("\n" + "*"*60)
            print("📀NOTE: Since variables are not assigned from the user")
            print("the outputs are saved for you in these three data frames:")
            print("final_results, evaluation_report, exclusion_report")
            print("*"*60 + "\n")

    # 4. Final return logic
    if is_assigned:
        # Determine if the user assigned to a single variable or multiple
        prefix = call_line.split("=")[0].strip()
        
        # If there's no comma in the assignment prefix, they used a single variable
        if "," not in prefix:
            print(f"\n📀 INFO: You assigned the output to a single variable: '{prefix}'")
            print(f"   This variable is a tuple containing 3 DataFrames. Access them via:")
            print(f"   1. Results Data:    {prefix}[0]")
            print(f"   2. evaluation_report:  {prefix}[1]")
            print(f"   3. Exclusion List:  {prefix}[2]")
            print(f"   Or unpack them: final_df, evaluation_report, exclusion_report = {prefix}\n")
        
        return final_df, evaluation_report, exclusion_report
    else:
        # Return None so Jupyter doesn't print the "wall of text"
        return None