import pandas as pd
import numpy as np
def outlier_identifier(dataframe, columns=None, identifier = 'IQR', return_df=False):
    """
    A function that identify and summarize the count and range of based on the method the user choose
    Parameters
    ----------
    dataframe : pandas.core.frame.DataFrame
        The target dataframe where the function is performed.
    columns : list, default=None
        The target columns where the function needed to be performed. Default is None, the function will check all columns
    identifier : string, default='IQR'
        The method of identifying outliers.
        - if "Z_score" : Use z-test with threshold of 3
        - if "IQR" : Use IQR (Inter Quantile range) to identify outliers (default)
    return_df : bool, default=False
        Can be set to True if want output as dataframe identified with outliers in rows

    Returns
    -------
    pandas.core.frame.DataFrame 
    (a dataframe with the summary of the outlier identified by the method) if return_df = False , 
    (a dataframe with additional column having if row has outlier or not) if return_df = True
    
    Examples
    --------
    >>> import pandas as pd
        
    >>> df = pd.DataFrame({
    >>>    'SepalLengthCm' : [5.1, 4.9, 4.7, 5.5, 5.1, 50, 54, 5.0, 5.2, 5.3, 5.1],
    >>>    'SepalWidthCm' : [1.4, 1.4, 20, 2.0, 0.7, 1.6, 1.2, 1.4, 1.8, 1.5, 2.1],
    >>>    'PetalWidthCm' : [0.2, 0.2, 0.2, 0.3, 0.4, 0.5, 0.5, 0.6, 0.4, 0.2, 5]
    >>> })
    >>> outlier_identifier(df)
    	                SepalLengthCm SepalWidthCm PetalWidthCm
    outlier_count                  2            1            1
    outlier_percentage        18.18%        9.09%        9.09%
    mean                       13.63         3.19         0.77
    median                       5.1          1.5          0.4
    std                        18.99         5.59         1.41
    lower_range                  NaN          NaN          NaN
    upper_range         (50.0, 54.0)         20.0          5.0
    """

    # Handle dataframe type error (Check if dataframe is of type Pandas DataFrame)
    if not isinstance(dataframe, pd.DataFrame):
        raise TypeError(f"passed dataframe is of type {type(dataframe).__name__}, should be DataFrame")
        
    # Handle empty dataframe or dataframe with all NAN
    if dataframe.empty or dataframe.dropna().empty:
        raise ValueError("passed dataframe is None")
    
    # Handle columns type error (Check if columns are None or type list)
    if not columns is None and not isinstance(columns, list):
        raise TypeError(f"passed columns is of type {type(columns).__name__}, should be list or NoneType")
    
    # Handle identifier type error (Check if identifier is of type str)
    if not isinstance(identifier, str):
        raise TypeError(f"passed identifier is of type {type(identifier).__name__}, should be string with value 'Z_score' or 'IQR'")
    
    # Handle identifier Value error (Check if identifier has value 'Z_score' or 'IQR')
    if identifier!='Z_score' and identifier!='IQR':
        raise ValueError("passed identifier should have value 'Z_score' or 'IQR'")
        
    # Handle return_df type error (Check if identifier is of type bool)
    if not isinstance(return_df, bool):
        raise TypeError(f"passed return_df is of type {type(return_df).__name__}, should be bool with value as True or False")

    if columns is None:
        columns = dataframe.columns
    df_filtered = dataframe[columns]
    numeric_columns = df_filtered.select_dtypes('number').columns
    df_selected = df_filtered[numeric_columns]
    df_selected['outlier'] = False
    
    output = pd.DataFrame(columns=numeric_columns, index=['outlier_count', 'outlier_percentage', 'mean', 'median', 'std', 'lower_range', 'upper_range'])
    if identifier == 'Z_score':
        if return_df == False:
            for col in numeric_columns:
                std = df_selected[col].std()
                mean = df_selected[col].mean()
                col_outliers = df_selected[np.abs(df_selected[col] - mean) > std*3]
                output.loc['outlier_count', col] = len(col_outliers)
                output.loc['outlier_percentage', col] = str(round(len(col_outliers) * 100 / len(df_selected), 2)) + '%'
                output.loc['mean', col] = round(mean, 2)
                output.loc['median', col] = round(np.percentile(df_selected[col], 50), 2)
                output.loc['std', col] = round(std, 2)

                low_range = col_outliers[col_outliers[col] < -std*3]
                if not low_range.empty:
                    if len(low_range) == 1:
                        output.loc['lower_range', col] = min(low_range[col])
                    else:
                        output.loc['lower_range', col] = (min(low_range[col]), max(low_range[col]))
                upper_range = col_outliers[col_outliers[col] > std*3]
                if not upper_range.empty:
                    if len(upper_range) == 1:
                        output.loc['upper_range', col] = min(upper_range[col])
                    else:
                        output.loc['upper_range', col] = (min(upper_range[col]), max(upper_range[col]))
            return output
        else:
            for col in numeric_columns:
                std = df_selected[col].std()
                mean = df_selected[col].mean()
                df_selected.loc[np.abs((df_selected[col] - mean)) > std*3, 'outlier'] = True
            return df_selected


    else:
        if return_df == False:
            for col in numeric_columns:
                std = df_selected[col].std()
                mean = df_selected[col].mean()
                iqr = np.percentile(df_selected[col], 75) - np.percentile(df_selected[col], 25)
                col_outliers = df_selected[np.abs((df_selected[col] - mean) / std) > 1.5*iqr]
                output.loc['outlier_count', col] = len(col_outliers)
                output.loc['outlier_percentage', col] = str(round(len(col_outliers) * 100 / len(df_selected), 2)) + '%'
                output.loc['mean', col] = round(mean, 2)
                output.loc['median', col] = round(np.percentile(df_selected[col], 50), 2)
                output.loc['std', col] = round(std, 2)
                low_range = col_outliers[col_outliers[col] < -1.5*iqr]
                if not low_range.empty:
                    if len(low_range) == 1:
                        output.loc['lower_range', col] = min(low_range[col])
                    else:
                        output.loc['lower_range', col] = (min(low_range[col]), max(low_range[col]))
                upper_range = col_outliers[col_outliers[col] > 1.5*iqr]
                if not upper_range.empty:
                    if len(upper_range) == 1:
                        output.loc['upper_range', col] = min(upper_range[col])
                    else:
                        output.loc['upper_range', col] = (min(upper_range[col]), max(upper_range[col]))
            return output

        else:
            for col in numeric_columns:
                std = df_selected[col].std()
                mean = df_selected[col].mean()
                iqr = np.percentile(df_selected[col], 75) - np.percentile(df_selected[col], 25)
                df_selected.loc[np.abs((df_selected[col] - mean) / std) > 1.5*iqr, 'outlier'] = True
            
            return df_selected