import pandas as pd
import numpy as np
import plotly.graph_objects as go
from typing import Tuple, List, Dict
from datetime import datetime
import yfinance as yf

class TrendChannel:
    def __init__(self, length: int = 100, multi: float = 3, extend: int = 50, 
                 color_up: str = '#16d897', color_dn: str = '#da853f'):
        if length <= 0:
            raise ValueError("`length` must be a positive integer.")
        if multi <= 0:
            raise ValueError("`multi` must be a positive float.")
        if extend <= 0:
            raise ValueError("`extend` must be a positive integer.")
        self.length = length
        self.multi = multi
        self.extend = extend
        self.color_up = color_up
        self.color_dn = color_dn
        
    def calculate_atr(self, high: pd.Series, low: pd.Series, close: pd.Series, 
                     period: int = 14) -> pd.Series:
        high = high.squeeze()
        low = low.squeeze()
        close = close.squeeze()
        
        tr1 = high - low
        tr2 = abs(high - close.shift(1))
        tr3 = abs(low - close.shift(1))
        tr = pd.DataFrame({'tr1': tr1, 'tr2': tr2, 'tr3': tr3}).max(axis=1)
        atr = tr.rolling(window=period).mean()
        
        return atr

    def calculate_trend(self, close: pd.Series, atr: pd.Series) -> pd.Series:
        close = close.squeeze()
        atr = atr.squeeze()
        
        sma = close.rolling(window=self.length).mean()
        upper = sma + atr
        lower = sma - atr
        trend = pd.Series(False, index=close.index)

        for i in range(1, len(close)):
            prev_trend = trend.iloc[i-1]
            if i >= self.length:
                signal_up = (close.iloc[i] > upper.iloc[i]) & (close.iloc[i-1] <= upper.iloc[i-1])
                signal_dn = (close.iloc[i] < lower.iloc[i]) & (close.iloc[i-1] >= lower.iloc[i-1])
                
                if signal_up:
                    trend.iloc[i] = True
                elif signal_dn:
                    trend.iloc[i] = False
                else:
                    trend.iloc[i] = prev_trend
            else:
                trend.iloc[i] = prev_trend
        return trend
    
    def calculate_future_price(self, x1: int, x2: int, y1: float, y2: float, future_num: int) -> float:
        """
        Calculate future price projection based on linear trend.
        """
        # Calculate slope and future price
        slope = (y2 - y1) / (x2 - x1) if (x2 - x1) != 0 else 0
        future_price = y2 + slope * (future_num - x2)
        return float(future_price)
    
    def generate_channels(self, df: pd.DataFrame) -> Tuple[List[Dict], List[Tuple]]:
        df = df.copy()
        
        # Calculate indicators
        df['atr'] = self.calculate_atr(df['High'], df['Low'], df['Close'])
        df['trend'] = self.calculate_trend(df['Close'], df['atr'])
        df['hl2'] = (df['High'] + df['Low']) / 2
        
        # Calculate channel boundaries
        df['high_src'] = df['hl2'] + df['atr'] * self.multi
        df['low_src'] = df['hl2'] - df['atr'] * self.multi  # Corrected typo here
        
        channels = []
        future_prices = []
        
        # Find trend changes
        trend_changes = (df['trend'] != df['trend'].shift(1)).fillna(False).astype(bool)
        trend_change_indices = df.index[trend_changes]
        
        # Debug: Print all trend_change_indices
        print("\nAll trend_change_indices:")
        print(trend_change_indices)
        
        # Filter trend_change_indices to ensure they are within the DataFrame's index
        trend_change_indices = trend_change_indices[trend_change_indices <= df.index[-1]]
        
        print("\nFiltered trend_change_indices (within DataFrame index):")
        print(trend_change_indices)
        
        for i in range(len(trend_change_indices)):
            start_idx = trend_change_indices[i]
            end_idx = (trend_change_indices[i + 1] 
                      if i < len(trend_change_indices) - 1 
                      else df.index[-1])
            
            # Debugging prints
            print(f"\nDebug - start_idx: {start_idx}, end_idx: {end_idx}")
            
            # Ensure end_idx is within DataFrame index
            if end_idx > df.index[-1]:
                print(f"Warning: end_idx {end_idx} exceeds DataFrame index. Skipping this channel.")
                continue

            # Use .at for scalar access to ensure single value
            try:
                y_mid_start = df.at[start_idx, 'hl2']
                y_mid_end = df.at[end_idx, 'hl2']
                y_high_start = df.at[start_idx, 'high_src']
                y_high_end = df.at[end_idx, 'high_src']
                y_low_start = df.at[start_idx, 'low_src']
                y_low_end = df.at[end_idx, 'low_src']
            except KeyError:
                print(f"Warning: start_idx or end_idx does not exist in DataFrame index.")
                continue
            
            # Ensure trend_value is a single scalar
            trend_value = df.at[start_idx, 'trend']
            trend = bool(trend_value)
            
            # Extracting scalar values correctly
            try:
                channel = {
                    'x_start': start_idx,
                    'x_end': end_idx,
                    'y_mid_start': float(y_mid_start) if not isinstance(y_mid_start, pd.Series) else y_mid_start.item(),
                    'y_mid_end': float(y_mid_end) if not isinstance(y_mid_end, pd.Series) else y_mid_end.item(),
                    'y_high_start': float(y_high_start) if not isinstance(y_high_start, pd.Series) else y_high_start.item(),
                    'y_high_end': float(y_high_end) if not isinstance(y_high_end, pd.Series) else y_high_end.item(),
                    'y_low_start': float(y_low_start) if not isinstance(y_low_start, pd.Series) else y_low_start.item(),
                    'y_low_end': float(y_low_end) if not isinstance(y_low_end, pd.Series) else y_low_end.item(),
                    'trend': trend
                }
            except Exception as e:
                print(f"Error converting channel values to float: {e}")
                continue

            # Debug: Print types of y values
            print("Channel y-values types:")
            for key in ['y_mid_start', 'y_mid_end', 'y_high_start', 'y_high_end', 'y_low_start', 'y_low_end']:
                print(f"{key}: {type(channel[key])} - {channel[key]}")
            
            channels.append(channel)
            
            # Calculate future_date by adding extend days to end_idx
            future_date = pd.Timestamp(end_idx) + pd.Timedelta(days=self.extend)
            print(f"Debug - future_index: {future_date}")
            
            # Get the integer locations
            try:
                x1_num = df.index.get_loc(start_idx)
                x2_num = df.index.get_loc(end_idx)
                future_num = x2_num + self.extend  # Project extend steps ahead
                print(f"x1_num: {x1_num}, x2_num: {x2_num}, future_num: {future_num}")
            except KeyError:
                print(f"Warning: Could not locate indices for projections.")
                continue
            
            # Check if future_num exceeds DataFrame length
            if future_num >= len(df):
                print(f"Warning: future_num {future_num} exceeds DataFrame's integer index range. Skipping future price projection.")
                continue

            # Extract y1 and y2 as floats correctly (already done above in channel dict)
            y1 = channel['y_mid_start']
            y2 = channel['y_mid_end']
            
            # Calculate future price without relying on future_index being present
            future_price = self.calculate_future_price(
                x1=x1_num,
                x2=x2_num,
                y1=y1,
                y2=y2,
                future_num=future_num
            )
            
            print(f"Calculated future_price: {future_price}")
            
            future_prices.append((future_date, future_price, trend))
        
        return channels, future_prices

    def plot(self, df: pd.DataFrame) -> go.Figure:
        channels, future_prices = self.generate_channels(df)
        
        fig = go.Figure()
        
        # Candlestick chart
        fig.add_trace(go.Candlestick(
            x=df.index,
            open=df['Open'],
            high=df['High'],
            low=df['Low'],
            close=df['Close'],
            name='Price'
        ))
        
        # Adding trend channels
        for channel in channels:
            color = self.color_up if channel['trend'] else self.color_dn
            
            # Mid Line
            fig.add_trace(go.Scatter(
                x=[channel['x_start'], channel['x_end']],
                y=[channel['y_mid_start'], channel['y_mid_end']],
                mode='lines',
                line=dict(color=color),
                name='Mid Line'
            ))
            
            # High and Low Lines
            for y_start, y_end in [(channel['y_high_start'], channel['y_high_end']), (channel['y_low_start'], channel['y_low_end'])]:
                fig.add_trace(go.Scatter(
                    x=[channel['x_start'], channel['x_end']],
                    y=[y_start, y_end],
                    mode='lines',
                    line=dict(color=color, dash='dash'),
                    showlegend=False
                ))
        
        # Adding future price projections
        for future_idx, price, trend in future_prices:
            color = self.color_up if trend else self.color_dn
            # Ensure future_idx is a pd.Timestamp
            if not isinstance(future_idx, pd.Timestamp):
                try:
                    future_idx = pd.Timestamp(future_idx)
                except Exception as e:
                    print(f"Error converting future_idx to Timestamp: {e}")
                    continue
            fig.add_trace(go.Scatter(
                x=[future_idx],
                y=[price],
                mode='markers+text',
                marker=dict(color=color, symbol='diamond', size=10),
                text=f'Future Price: {price:.2f}',
                textposition='top center',
                showlegend=False
            ))
        
        fig.update_layout(
            title='Future Trend Channel',
            yaxis_title='Price',
            xaxis_title='Date',
            template='plotly_dark',
            showlegend=True,
            xaxis_rangeslider_visible=False
        )
        
        return fig

    def extend_dataframe(self, df: pd.DataFrame, extend_days: int) -> pd.DataFrame:
        """
        (Optional) Extend the DataFrame with future dates by adding NaN rows.
        Useful if you want to visualize future projections seamlessly.
        """
        last_date = df.index[-1]
        future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=extend_days, freq='D')
        future_df = pd.DataFrame(index=future_dates, columns=df.columns)
        df_extended = pd.concat([df, future_df])
        return df_extended

if __name__ == "__main__":
    # Download sample data
    symbol = '^IBEX'  # Replace with your desired symbol
    df = yf.download(symbol, start='2024-01-01', end='2024-12-31')  # Adjust end date as needed
    
    # Check if DataFrame is empty
    if df.empty:
        print("Downloaded data is empty. Please check the symbol and date range.")
    else:
        # Ensure the DateTimeIndex is timezone-naive
        if df.index.tz is not None:
            print("Converting DataFrame index to timezone-naive.")
            try:
                df.index = df.index.tz_localize(None)
            except TypeError as e:
                print(f"Error converting timezone: {e}")
                # If tz_localize fails, try tz_convert
                try:
                    df.index = df.index.tz_convert(None)
                except Exception as ex:
                    print(f"Failed to convert timezone: {ex}")
                    # As a last resort, reset the index to be naive
                    df = df.reset_index(drop=True)
        
        # Verify index is now timezone-naive
        print(f"\nDataFrame index timezone: {df.index.tz}")  # Should print None
        print(f"DataFrame index type: {df.index.dtype}")  # Should be datetime64[ns]
        print(f"DataFrame index is unique: {df.index.is_unique}")  # Should be True
        print(f"DataFrame index is sorted: {df.index.is_monotonic_increasing}")  # Should be True
        
        # Additional Debug: Check index max and min
        print(f"DataFrame index starts at: {df.index.min()}")
        print(f"DataFrame index ends at: {df.index.max()}")
        
        # Create and plot trend channel
        tc = TrendChannel(length=100, multi=3, extend=50)
        try:
            fig = tc.plot(df)
            fig.show()
        except Exception as e:
            print(f"An error occurred during plotting: {e}")

Fix the error:

DataFrame index timezone: None
DataFrame index type: datetime64[ns]
DataFrame index is unique: True
DataFrame index is sorted: True
DataFrame index starts at: 2024-01-02 00:00:00
DataFrame index ends at: 2024-11-05 00:00:00

All trend_change_indices:
DatetimeIndex(['2024-01-02', '2024-07-01', '2024-08-01', '2024-08-23'], dtype='datetime64[ns]', name='Date', freq=None)

Filtered trend_change_indices (within DataFrame index):
DatetimeIndex(['2024-01-02', '2024-07-01', '2024-08-01', '2024-08-23'], dtype='datetime64[ns]', name='Date', freq=None)

Debug - start_idx: 2024-01-02 00:00:00, end_idx: 2024-07-01 00:00:00
An error occurred during plotting: only integer scalar arrays can be converted to a scalar index