import pandas as pd

def filter_timestamp(df, column, min_value, max_value):
    """
    Filters the DataFrame based on a "range" date filter.

    This function filters the DataFrame based on a fixed date range, corresponding to a DATE_FACET Dashboard Filter type and RANGE subtype.
    The function takes a DataFrame, a column name, and a minimum and maximum value.
    The function returns a filtered DataFrame that only includes rows where the specified column's value is within the given range.
    The function also handles the case where either the minimum or maximum value is None, in which case it filters based on the other value.
    The function uses the 'case' column to identify unique cases in the DataFrame.
    
    Args:
        df (pd.DataFrame): The DataFrame to filter.
        column (str): The column name to filter on.
        min_value (float): The minimum value for the range.
        max_value (float): The maximum value for the range.
    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    if min_value is None and max_value is None:
        return df
    elif min_value is None:
        unique_ids = df[df[column].lt(max_value)]['case'].unique()
    elif max_value is None:
        unique_ids = df[df[column].gt(min_value)]['case'].unique()
    else:
        unique_ids = df[df[column].gt(min_value) & df[column].lt(max_value)]['case'].unique()
    return df[df['case'].isin(unique_ids)]

def filter_timestamp_part(df, column, part, excluded_values):
    """
    Filters the DataFrame based on a "part" date filter.

    This function filters the DataFrame based on a specific part of the timestamp (e.g., year, month, etc.) and excludes certain values, corresponding to a DATE_FACET Dashboard Filter type and PART subtype.
    The function takes a DataFrame, a column name, the part to filter on, and a list of excluded values.
    The function returns a filtered DataFrame that only includes rows where the specified part of the timestamp is not in the excluded values.

    Args:
        df (pd.DataFrame): The DataFrame to filter.
        column (str): The column name to filter on.
        part (str): The part of the timestamp to filter on. Should be one of the following:
            - YEAR
            - QUARTER_OF_YEAR
            - MONTH_OF_YEAR
            - WEEK_OF_YEAR
            - DAY_OF_MONTH
            - DAY_OF_WEEK
            - HOUR_OF_DAY
        excluded_values (list): A list of values to exclude from the filter.

    Raises:
        ValueError: If the part specified is not valid.

    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    excluded_values = list(map(int, excluded_values))
    
    if part == 'YEAR':
        unique_ids = df[~df[column].dt.year.isin(excluded_values)]['case'].unique()
    elif part == 'QUARTER_OF_YEAR':
        unique_ids = df[~df[column].dt.quarter.isin(excluded_values)]['case'].unique()
    elif part == 'MONTH_OF_YEAR':
        unique_ids = df[~df[column].dt.month.isin(excluded_values)]['case'].unique()
    elif part == 'WEEK_OF_YEAR':
        unique_ids = df[~df[column].dt.isocalendar().week.isin(excluded_values)]['case'].unique()
    elif part == 'DAY_OF_MONTH':
        unique_ids = df[~df[column].dt.day.isin(excluded_values)]['case'].unique()
    elif part == 'DAY_OF_WEEK':
        unique_ids = df[~df[column].dt.weekday.isin(excluded_values)]['case'].unique()
    elif part == 'HOUR_OF_DAY':
        unique_ids = df[~df[column].dt.hour.isin(excluded_values)]['case'].unique()
    else:
        raise ValueError("Invalid part specified.")
    
    return df[df['case'].isin(unique_ids)]

def filter_timestamp_relative(df, column, part, option):
    """
    Filters the DataFrame based on a relative date filter.

    This function filters the DataFrame based on a relative date range, corresponding to a DATE_FACET Dashboard Filter type and RELATIVE subtype.
    The function takes a DataFrame, a column name, the part to filter on, and an option.
    The function returns a filtered DataFrame that only includes rows where the specified column's value is within the given range.
    The function also handles the case where the option is 0, in which case it filters based on the current date.

    Args:
        df (pd.DataFrame): The DataFrame to filter.
        column (str): The column name to filter on.
        part (str): The part of the timestamp to filter on. Should be one of the following:
            - YEAR
            - QUARTER_OF_YEAR
            - MONTH_OF_YEAR
            - WEEK_OF_YEAR
            - DAY_OF_MONTH
            - DAY_OF_WEEK
            - HOUR_OF_DAY
        option (int): The option for the relative date filter. If 0, it uses the current date.
    Raises:
        ValueError: If the part specified is not valid.
    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    # Get the current date
    now = pd.Timestamp.now(tz='UTC')
    
    # Set the start and end dates based on the provided options
    if option == 0:
        # If option is 0, we take the current date part
        if part == 'YEAR':
            start_date = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
            end_date = start_date + pd.offsets.YearEnd()
        elif part == 'QUARTER_OF_YEAR':
            start_date = now - pd.offsets.QuarterBegin(startingMonth=1)
            end_date = start_date + pd.offsets.QuarterEnd()
        elif part == 'MONTH_OF_YEAR':
            start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
            end_date = (start_date + pd.DateOffset(months=1)) - pd.Timedelta(days=1)
        elif part == 'WEEK_OF_YEAR':
            start_date = now - pd.offsets.Week(weekday=0)  # Start from the most recent Monday
            end_date = start_date + pd.offsets.Week()
        elif part == 'DAY_OF_MONTH':
            start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
            end_date = start_date + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
        elif part == 'DAY_OF_WEEK':
            start_date = now - pd.Timedelta(days=now.weekday())
            end_date = start_date + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
        elif part == 'HOUR_OF_DAY':
            start_date = now.replace(minute=0, second=0, microsecond=0)
            end_date = start_date + pd.Timedelta(hours=1) - pd.Timedelta(seconds=1)
        else:
            raise ValueError("Invalid part specified. Use 'YEAR', 'QUARTER_OF_YEAR', 'MONTH_OF_YEAR', 'WEEK_OF_YEAR', 'DAY_OF_MONTH', 'DAY_OF_WEEK', or 'HOUR_OF_DAY'.")
    else:
        # Calculate the start date based on the option provided
        if part == 'YEAR':
            start_date = now - pd.DateOffset(years=option)
            end_date = now
        elif part == 'QUARTER_OF_YEAR':
            start_date = now - pd.DateOffset(months=option * 3)
            end_date = now
        elif part == 'MONTH_OF_YEAR':
            start_date = now - pd.DateOffset(months=option)
            end_date = now
        elif part == 'WEEK_OF_YEAR':
            start_date = now - pd.DateOffset(weeks=option)
            end_date = now
        elif part == 'DAY_OF_MONTH':
            start_date = now - pd.DateOffset(days=option)
            end_date = now
        elif part == 'DAY_OF_WEEK':
            start_date = now - pd.DateOffset(days=option * 7)
            end_date = now
        elif part == 'HOUR_OF_DAY':
            start_date = now - pd.DateOffset(hours=option)
            end_date = now
        else:
            raise ValueError("Invalid part specified. Use 'YEAR', 'QUARTER_OF_YEAR', 'MONTH_OF_YEAR', 'WEEK_OF_YEAR', 'DAY_OF_MONTH', 'DAY_OF_WEEK', or 'HOUR_OF_DAY'.")

    # Filter the dataframe based on the start and end dates
    unique_ids = df[(df[column] >= start_date) & (df[column] <= end_date)]['case'].unique()
    return df[df['case'].isin(unique_ids)]

def filter_numerical(df, column, min_value, max_value):
    """
    Filters the DataFrame based on a numerical range.

    This function filters the DataFrame based on a numerical range, corresponding to a NUMERICAL_FACET Dashboard Filter type. The function takes a DataFrame, a column name, and a minimum and maximum value.
    The function returns a filtered DataFrame that only includes rows where the specified column's value is within the given range.
    The function also handles the case where either the minimum or maximum value is None, in which case it filters based on the other value.
    Args:
        df (pd.DataFrame): The DataFrame to filter.
        column (str): The column name to filter on.
        min_value (float): The minimum value for the range.
        max_value (float): The maximum value for the range.
    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    if min_value is None and max_value is None:
        return df
    elif min_value is None:
        unique_ids = df[(pd.to_numeric(df[column]) <= max_value)]['case'].unique()
    elif max_value is None:
        unique_ids = df[(pd.to_numeric(df[column]) >= min_value)]['case'].unique()
    else:
        unique_ids = df[(pd.to_numeric(df[column]) >= min_value) & (pd.to_numeric(df[column]) <= max_value)]['case'].unique()
    return df[df['case'].isin(unique_ids)]

def filter_categorical(df, column, excluded_values, selected_values):
    """
    Filters the DataFrame based on categorical values.

    This function filters the DataFrame based on categorical values, corresponding to an 'ALPHANUM_FACET' Dashboard Filter type. The function takes a DataFrame, a column name, and a list of excluded or selected values.
    The function returns a filtered DataFrame that only includes rows where the specified column's value is either in the excluded or selected values.
    Args:
        df (pd.DataFrame): The DataFrame to filter.
        column (str): The column name to filter on.
        excluded_values (list): A list of values to exclude from the filter.
        selected_values (list): A list of values to include in the filter.
    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    if excluded_values is not None:
        unique_ids = df[~df[column].apply(str).isin(excluded_values)]['case'].unique()
    elif selected_values is not None:
        unique_ids = df[df[column].apply(str).isin(selected_values)]['case'].unique()
    else:
        unique_ids = df['case'].unique()
    return df[df['case'].isin(unique_ids)]

def apply_dashboard_filters(filtered_df, filters):
    """
    Applies the specified filters to the DataFrame.

    This function takes a DataFrame and a list of filters, and applies each filter to the DataFrame.
    The function returns a filtered DataFrame that only includes rows that match all the specified filters.
    The filters can be of different types, including date filters, numerical filters, and categorical filters.

    Args:
        filtered_df (pd.DataFrame): The DataFrame to filter.
        filters (list): A list of dictionaries, where each dictionary represents a filter to apply.
            Each dictionary should have the following keys:
                - column: The column name to filter on.
                - filterType: The type of filter to apply. Can be one of the following:
                    - DATE_FACET
                    - NUMERICAL_FACET
                    - ALPHANUM_FACET
                - dateFilterType: The type of date filter to apply. Can be one of the following:
                    - RANGE
                    - PART
                    - RELATIVE
                - dateFilterPart: The part of the timestamp to filter on. Should be one of the following:
                    - YEAR
                    - QUARTER_OF_YEAR
                    - MONTH_OF_YEAR
                    - WEEK_OF_YEAR
                    - DAY_OF_MONTH
                    - DAY_OF_WEEK
                    - HOUR_OF_DAY
                - minValue: The minimum value for the range (for numerical and date filters).
                - maxValue: The maximum value for the range (for numerical and date filters).
                - excludedValues: A list of values to exclude from the filter (for categorical filters).
                - selectedValues: A list of values to include in the filter (for categorical filters).
    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    for filter_column in filters:
        try:
            if filter_column['column'] in filtered_df.columns:
                if filter_column['filterType'] == 'DATE_FACET':
                    if filter_column['dateFilterType'] == 'RANGE':
                        filtered_df = filter_timestamp(filtered_df, filter_column['column'], pd.to_datetime(filter_column['minValue'], unit='ms', utc=True) if 'minValue' in filter_column else None, pd.to_datetime(filter_column['maxValue'], unit='ms', utc=True) if 'maxValue' in filter_column else None)
                    elif filter_column['dateFilterType'] == 'PART':
                        filtered_df = filter_timestamp_part(filtered_df, filter_column['column'], filter_column['dateFilterPart'], list(filter_column['excludedValues'].keys()))
                    elif filter_column['dateFilterType'] == 'RELATIVE':
                        filtered_df = filter_timestamp_relative(filtered_df, filter_column['column'], filter_column['dateFilterPart'], filter_column['dateFilterOption']['last'])
                elif filter_column['filterType'] == 'NUMERICAL_FACET':
                    filtered_df = filter_numerical(filtered_df, filter_column['column'], filter_column['minValue'] if 'minValue' in filter_column else None, filter_column['maxValue'] if 'maxValue' in filter_column else None)
                elif filter_column['filterType'] == 'ALPHANUM_FACET':
                    excluded_values = list(filter_column['excludedValues'].keys()) if 'excludedValues' in filter_column else None
                    selected_values = list(filter_column['selectedValues'].keys()) if 'selectedValues' in filter_column else None
                    filtered_df = filter_categorical(filtered_df, filter_column['column'], excluded_values=excluded_values, selected_values=selected_values)
        except Exception as e:
            print(e)
    return filtered_df