from collections import Counter
import pandas as pd


def _validate_inputs(
    dataframe: pd.DataFrame,
    columns: list[str],
    rounding_digits: int,
    top_n: int,
) -> None:
    if not isinstance(dataframe, pd.DataFrame):
        raise TypeError("Expected a pandas DataFrame for argument 'dataframe'.")

    for column in columns:
        if column not in dataframe.columns:
            raise ValueError(f"Column '{column}' not found in DataFrame. Please provide a valid column name.")

    if not isinstance(rounding_digits, int) or rounding_digits < 0:
        raise ValueError("The 'rounding_digits' parameter must be a non-negative integer.")

    if not isinstance(top_n, int) or top_n <= 0:
        raise ValueError("The 'top_n' parameter must be a positive integer.")




def identify_frequent_price_levels(
        dataframe,
        high_col='high',
        low_col='low',
        close_col='close',
        top_n=5,
        rounding_digits=2,
        show_output=False
):

    """
    Identifies the most frequent price levels in the given financial data for highs, lows, and closes.

    ====== Parameters ======
    dataframe : pd.DataFrame
        Input DataFrame containing price data
    high_col : str, optional
        Name of the high price column (default 'high')
    low_col : str, optional
        Name of the low price column (default 'low')
    close_col : str, optional
        Name of the close price column (default 'close')
    rounding_digits : int, optional
        Number of decimal places to round to (default 2)
    top_n : int, optional
        Number of top levels to return (default 5)
    show_output : bool, optional
        Whether to print results (default False)

    ====== Returns ======
    Dictionary containing:
        - 'highs': List of (price, count) tuples for highs
        - 'lows': List of (price, count) tuples for lows
        - 'closes': List of (price, count) tuples for closes
        - 'all_levels': List of (price, count) tuples for all combined levels
    """

    _validate_inputs(
        dataframe=dataframe,
        columns=[high_col, low_col, close_col],
        top_n=top_n,
        rounding_digits=rounding_digits
    )

    rounded_high = dataframe[high_col].dropna().round(rounding_digits)
    rounded_low = dataframe[low_col].dropna().round(rounding_digits)
    rounded_close = dataframe[close_col].dropna().round(rounding_digits)


    all_counter = Counter(rounded_high) + Counter(rounded_low) + Counter(rounded_close)
    high_counter = Counter(rounded_high)
    low_counter = Counter(rounded_low)
    close_counter = Counter(rounded_close)


    frequent_highs = high_counter.most_common(top_n)
    frequent_lows = low_counter.most_common(top_n)
    frequent_closes = close_counter.most_common(top_n)
    frequent_global =  all_counter.most_common(top_n)


    if show_output:
        print(f"\n=== Top {top_n} Most Frequent Highs ===")
        for level, count in frequent_highs:
            print(f"Price: {level:.{rounding_digits}f}  |  Occurrence: {count}")

        print(f"\n=== Top {top_n} Most Frequent Lows ===")
        for level, count in frequent_lows:
            print(f"Price: {level:.{rounding_digits}f}  |  Occurrence: {count}")

        print(f"\n=== Top {top_n} Most Frequent Closes ===")
        for level, count in frequent_closes:
            print(f"Price: {level:.{rounding_digits}f}  |  Occurrence: {count}")

        print(f"\n=== Top {top_n} Most Frequent Price Levels (Combined) ===")
        for level, count in frequent_global:
            print(f"Price: {level:.{rounding_digits}f}  |  Occurrence: {count}")


    return {
        'highs': frequent_highs,
        'lows': frequent_lows,
        'closes': frequent_closes,
        'all_levels': frequent_global
    }


#_______________________________________________________________________________________________________________


def identify_support_resistance_zones(
        dataframe,
        high_col='high',
        low_col='low',
        close_col='close',
        epsilon=0.1,
        chain_comparison=True,
        top_n=5,
        rounding_digits=2,
        show_output=False
):

    """
    Detects clusters of price levels that are within a specified proximity (epsilon) and form potential support or resistance zones.

    Parameters:
        dataframe : pd.DataFrame
            Input DataFrame containing price data.
        high_col : str, optional
            Name of the column for high prices (default 'high').
        low_col : str, optional
            Name of the column for low prices (default 'low').
        close_col : str, optional
            Name of the column for close prices (default 'close').
        epsilon : float, optional
            Maximum allowed price difference to consider prices part of the same zone (default 0.1).
        chain_comparison : bool, optional
            If True, compares each price to the last one in the group; if False, always compares to the group's first price (default True).
        top_n : int, optional
            Number of top levels to return (default 5)
        rounding_digits : int, optional
            Number of decimal places to round mean prices in the zones (default 2).
        show_output : bool, optional
            Whether to print the detected zones for each price type (default False).

    Returns:
        dict:
            A dictionary containing DataFrames of detected zones for each price type:
            - 'highs': Zones detected from high prices.
            - 'lows': Zones detected from low prices.
            - 'closes': Zones detected from close prices.
            - 'all_levels': Zones detected from combined price points of all three types.

    Each zone includes:
        - zone_min: The minimum price in the zone.
        - zone_max: The maximum price in the zone.
        - mean_price: The average price within the zone, rounded to specified decimals.
        - count: The number of price points contributing to the zone.
    """

    _validate_inputs(
        dataframe=dataframe,
        columns=[high_col, low_col, close_col],
        top_n=top_n,
        rounding_digits=rounding_digits
    )

    if epsilon <= 0:
        raise ValueError("The 'epsilon' parameter must be positive.")


    rounded_high = dataframe[high_col].round(rounding_digits).dropna().tolist()
    rounded_low = dataframe[low_col].round(rounding_digits).dropna().tolist()
    rounded_close = dataframe[close_col].round(rounding_digits).dropna().tolist()
    all_prices = rounded_high + rounded_low + rounded_close


    def _extract_zones(price_list):
        if not price_list:
            return []
        sorted_prices = sorted(price_list)
        zones = []
        current_zone = [sorted_prices[0]]
        if chain_comparison:
            for price in sorted_prices[1:]:
                if abs(price - current_zone[-1]) <= epsilon:
                    current_zone.append(price)
                else:
                    zones.append(current_zone)
                    current_zone = [price]
            zones.append(current_zone)
        else:
            for price in sorted_prices[1:]:
                if abs(price - current_zone[0]) <= epsilon:
                    current_zone.append(price)
                else:
                    zones.append(current_zone)
                    current_zone = [price]
            zones.append(current_zone)

        return pd.DataFrame([
            {
                'zone_min': min(zone),
                'zone_max': max(zone),
                'mean_price': round(sum(zone) / len(zone), rounding_digits),
                'count': len(zone)
            }
            for zone in zones
        ])


    zones_high = _extract_zones(rounded_high)
    zones_low = _extract_zones(rounded_low)
    zones_close = _extract_zones(rounded_close)
    zones_all = _extract_zones(all_prices)

    if show_output:
        def _print_zones(df, label):
            print(f"\n====== Top {top_n} Support/Resistance Zones for {label} ======")
            if df.empty:
                print("No zones detected.")
                return
            df = df.sort_values(by='count', ascending=False).head(top_n)
            for idx, row in df.iterrows():
                print(
                    f"Zone: {row['zone_min']:.{rounding_digits}f} - {row['zone_max']:.{rounding_digits}f} | "
                    f"Spread: {( row['zone_max'] - row['zone_min']):.{rounding_digits}f} | "
                    f"Mean: {row['mean_price']:.{rounding_digits}f} | Count: {row['count']:.0f}")

        _print_zones(zones_high, 'Highs')
        _print_zones(zones_low, 'Lows')
        _print_zones(zones_close, 'Closes')
        _print_zones(zones_all, 'All Levels')

    return {
        'highs': zones_high,
        'lows': zones_low,
        'closes': zones_close,
        'all_levels': zones_all
    }



