import pandas as pd
import numpy as np
import logging
import os
import sys

# V7 Update: Import the fetcher
from .crypto_db_fetcher import CryptoDBFetcher

data_pipeline_logger = logging.getLogger(__name__)

# V7 Update: Add load_data_from_db function from V6
def load_data_from_db(
    db_dir: str, 
    ticker: str, 
    exchange: str, 
    start_date: str, 
    end_date: str,
    interval: str = "1min"
) -> pd.DataFrame:
    """
    Loads cryptocurrency OHLCV data from the local SQLite database using CryptoDBFetcher.
    Adapted from V6.

    Args:
        db_dir: Directory containing the SQLite database files.
        ticker: The trading pair symbol (e.g., 'BTC-USDT').
        exchange: The exchange name (e.g., 'COINBASE').
        start_date: Start date string (YYYY-MM-DD).
        end_date: End date string (YYYY-MM-DD).
        interval: The desired data interval (e.g., '1min', '5min', '1h').

    Returns:
        A Pandas DataFrame containing the OHLCV data, indexed by timestamp.
        Returns an empty DataFrame if data loading fails.
    """
    data_pipeline_logger.info(f"Loading data via DB: {ticker} from {exchange} ({start_date} to {end_date}, interval: {interval})")
    try:
        # Initialize fetcher (db_dir path is handled within fetcher now)
        fetcher = CryptoDBFetcher(db_dir=db_dir) 
        df = fetcher.fetch_data(
            ticker=ticker,
            start_date=start_date,
            end_date=end_date,
            interval=interval,
            exchange=exchange
        )
        if df.empty:
            data_pipeline_logger.warning(f"No data found for {ticker} in the specified DB range.")
        else:
            # Ensure index is datetime and timezone-aware (UTC)
            if not isinstance(df.index, pd.DatetimeIndex):
                 try:
                     df.index = pd.to_datetime(df.index, errors='coerce', utc=True)
                     if df.index.isnull().any():
                         data_pipeline_logger.warning("Dropping rows with invalid datetime index after conversion.")
                         df = df.dropna(subset=[df.index.name]) 
                 except Exception as idx_e:
                      data_pipeline_logger.error(f"Failed to convert index to DatetimeIndex: {idx_e}")
                      return pd.DataFrame()
            elif df.index.tz is None: # Ensure timezone if index is already datetime
                 df.index = df.index.tz_localize('utc')
            elif df.index.tz != 'UTC': # Convert to UTC if different timezone
                 df.index = df.index.tz_convert('utc')
                      
            df = df.sort_index()
            data_pipeline_logger.info(f"Successfully loaded {len(df)} rows from DB.")
            
        return df
    except Exception as e:
        data_pipeline_logger.error(f"Error loading data from database via fetcher: {e}", exc_info=True)
        return pd.DataFrame()

def create_data_pipeline(historical_data, split_ratios=[0.6, 0.2, 0.2]):
    """
    Prepare data pipeline for training both GRU and SAC models using chronological split.
    
    Args:
        historical_data: DataFrame with OHLCV data, sorted chronologically.
                         Must have a DatetimeIndex.
        split_ratios: Train/validation/test split ratios based on time.
    
    Returns:
        Tuple of (train_data, validation_data, test_data) DataFrames.
    """
    if historical_data is None or historical_data.empty:
        logging.error("Input data is empty, cannot create pipeline.")
        return None, None, None

    # Ensure data is sorted by time
    historical_data.sort_index(inplace=True)

    # V7 Change: Use index for duration calculation
    try:
        if not isinstance(historical_data.index, pd.DatetimeIndex):
             raise TypeError("Data index must be a DatetimeIndex for splitting.")
        total_duration = historical_data.index[-1] - historical_data.index[0]
    except IndexError:
        logging.error("Cannot calculate duration: Data has insufficient rows.")
        return None, None, None
    except TypeError as e:
        logging.error(f"Error calculating duration: {e}")
        return None, None, None

    train_ratio, val_ratio, test_ratio = split_ratios

    # Calculate split points based on time duration
    train_end_time = historical_data.index[0] + total_duration * train_ratio
    val_end_time = train_end_time + total_duration * val_ratio

    # Perform the split using time index
    # Ensure the split points are valid timestamps within the index range
    # Use searchsorted to find the index locations closest to the calculated times
    train_end_idx = historical_data.index.searchsorted(train_end_time)
    val_end_idx = historical_data.index.searchsorted(val_end_time)

    train_data = historical_data.iloc[:train_end_idx]
    val_data = historical_data.iloc[train_end_idx:val_end_idx]
    test_data = historical_data.iloc[val_end_idx:]

    logging.info(f"Data split complete:")
    logging.info(f"  Train: {len(train_data)} rows ({train_data.index.min()} to {train_data.index.max()})")
    logging.info(f"  Validation: {len(val_data)} rows ({val_data.index.min()} to {val_data.index.max()})")
    logging.info(f"  Test: {len(test_data)} rows ({test_data.index.min()} to {test_data.index.max()})")

    return train_data, val_data, test_data

def create_sequences_v2(features_scaled, targets_scaled, start_price_unscaled, seq_length=60):
    """
    Create sequences for GRU training, handling potential mismatches in indices.
    
    Args:
        features_scaled: Scaled feature DataFrame
        targets_scaled: Scaled target Series
        start_price_unscaled: Unscaled starting price Series
        seq_length: Sequence length for GRU
        
    Returns:
        Tuple of (X sequences, y targets, starting prices) or (None, None, None) if creation fails
    """
    data_pipeline_logger.info(f"Creating sequences (v2) with length {seq_length}...")
    
    try:
        # Type checking and conversion for features
        if features_scaled is None:
            data_pipeline_logger.error("features_scaled is None")
            return None, None, None
            
        if not isinstance(features_scaled, pd.DataFrame):
            data_pipeline_logger.warning(f"features_scaled is not DataFrame but {type(features_scaled)}")
            try:
                if isinstance(features_scaled, pd.Series):
                    # Try to convert Series to DataFrame
                    features_scaled = pd.DataFrame(features_scaled)
                else:
                    # Try to convert numpy array to DataFrame
                    features_scaled = pd.DataFrame(features_scaled)
            except Exception as e:
                data_pipeline_logger.error(f"Failed to convert features_scaled to DataFrame: {e}")
                return None, None, None
                
        # Type checking and conversion for targets
        if targets_scaled is None:
            data_pipeline_logger.error("targets_scaled is None")
            return None, None, None
            
        if not isinstance(targets_scaled, pd.Series):
            data_pipeline_logger.warning(f"targets_scaled is not Series but {type(targets_scaled)}")
            try:
                if isinstance(targets_scaled, pd.DataFrame) and targets_scaled.shape[1] == 1:
                    # Convert single-column DataFrame to Series
                    targets_scaled = targets_scaled.iloc[:, 0]
                elif isinstance(targets_scaled, np.ndarray) and targets_scaled.ndim == 1:
                    # Convert 1D array to Series
                    targets_scaled = pd.Series(targets_scaled)
                else:
                    data_pipeline_logger.error(f"targets_scaled shape is not compatible: {getattr(targets_scaled, 'shape', 'unknown')}")
                    return None, None, None
            except Exception as e:
                data_pipeline_logger.error(f"Failed to convert targets_scaled to Series: {e}")
                return None, None, None
                
        # Type checking and conversion for prices
        if start_price_unscaled is None:
            data_pipeline_logger.error("start_price_unscaled is None")
            return None, None, None
            
        if not isinstance(start_price_unscaled, pd.Series):
            data_pipeline_logger.warning(f"start_price_unscaled is not Series but {type(start_price_unscaled)}")
            try:
                if isinstance(start_price_unscaled, pd.DataFrame) and start_price_unscaled.shape[1] == 1:
                    # Convert single-column DataFrame to Series
                    start_price_unscaled = start_price_unscaled.iloc[:, 0]
                elif isinstance(start_price_unscaled, np.ndarray) and start_price_unscaled.ndim == 1:
                    # Convert 1D array to Series
                    start_price_unscaled = pd.Series(start_price_unscaled)
                else:
                    data_pipeline_logger.error(f"start_price_unscaled shape is not compatible: {getattr(start_price_unscaled, 'shape', 'unknown')}")
                    return None, None, None
            except Exception as e:
                data_pipeline_logger.error(f"Failed to convert start_price_unscaled to Series: {e}")
                return None, None, None
                
        # Log input info
        data_pipeline_logger.info(f"Features index type: {type(features_scaled.index)}, length: {len(features_scaled)}")
        data_pipeline_logger.info(f"Targets index type: {type(targets_scaled.index)}, length: {len(targets_scaled)}")
        data_pipeline_logger.info(f"Start price index type: {type(start_price_unscaled.index)}, length: {len(start_price_unscaled)}")
        
        # Check for index compatibility 
        if (len(features_scaled) != len(targets_scaled) or 
            len(features_scaled) != len(start_price_unscaled)):
            data_pipeline_logger.warning(f"Input lengths don't match! Features: {len(features_scaled)}, Targets: {len(targets_scaled)}, Prices: {len(start_price_unscaled)}")
            
            # Try to align on common index if all have DatetimeIndex
            if (isinstance(features_scaled.index, pd.DatetimeIndex) and 
                isinstance(targets_scaled.index, pd.DatetimeIndex) and
                isinstance(start_price_unscaled.index, pd.DatetimeIndex)):
                
                # Find common dates
                common_index = features_scaled.index.intersection(
                    targets_scaled.index.intersection(start_price_unscaled.index)
                )
                
                # Check if we have any overlap
                if len(common_index) < seq_length:
                    data_pipeline_logger.error(f"Not enough common indices ({len(common_index)}) for sequence length ({seq_length})")
                    
                    # If we don't have enough common indices, create synthetic indices
                    # First find the shortest length
                    min_len = min(len(features_scaled), len(targets_scaled), len(start_price_unscaled))
                    
                    # If we have enough data for at least one sequence
                    if min_len >= seq_length:
                        data_pipeline_logger.warning(f"Using the shortest dataset length ({min_len})")
                        
                        # Convert features to numpy
                        features_np = features_scaled.values[:min_len]
                        
                        # Try to keep targets a Series and preserve its index
                        if len(targets_scaled) > min_len:
                            targets_scaled = targets_scaled.iloc[:min_len]
                        
                        # Try to keep prices a Series and preserve its index
                        if len(start_price_unscaled) > min_len:
                            start_price_unscaled = start_price_unscaled.iloc[:min_len]
                        
                        # Create sequences from numpy arrays
                        X_sequences, y_targets, starting_prices = [], [], []
                        for i in range(len(features_np) - seq_length):
                            X_sequences.append(features_np[i:i+seq_length])
                            y_targets.append(targets_scaled.iloc[i+seq_length])
                            starting_prices.append(start_price_unscaled.iloc[i+seq_length])
                            
                        if len(X_sequences) == 0:
                            data_pipeline_logger.error("No sequences created")
                            return None, None, None
                            
                        return np.array(X_sequences), np.array(y_targets), np.array(starting_prices)
                    else:
                        data_pipeline_logger.error(f"Cannot create sequences: Not enough data for sequence length {seq_length}")
                        return None, None, None
                
                # Align on common index
                data_pipeline_logger.info(f"Aligning on {len(common_index)} common indices")
                features_scaled = features_scaled.loc[common_index]
                targets_scaled = targets_scaled.loc[common_index]
                start_price_unscaled = start_price_unscaled.loc[common_index]
            else:
                data_pipeline_logger.error("Cannot create sequences v2: No common index found.")
                return None, None, None
        
        # Convert features to numpy array for sequence creation
        features_np = features_scaled.values
        
        # Create sequences
        X_sequences, y_targets, starting_prices = [], [], []
        for i in range(len(features_np) - seq_length):
            X_sequences.append(features_np[i:i+seq_length])
            y_targets.append(targets_scaled.iloc[i+seq_length])
            starting_prices.append(start_price_unscaled.iloc[i+seq_length])
            
        if len(X_sequences) == 0:
            data_pipeline_logger.error("No sequences created, check sequence length vs data length")
            return None, None, None
            
        data_pipeline_logger.info(f"Created {len(X_sequences)} sequences of length {seq_length}")
        return np.array(X_sequences), np.array(y_targets), np.array(starting_prices)
    except Exception as e:
        data_pipeline_logger.error(f"Error creating sequences: {e}", exc_info=True)
        return None, None, None 