"""
TA-Lib indicator adapter.

Provides automatic registration of all TA-Lib technical indicators
as composable Indicator instances.

Installation Requirements:
--------------------------
TA-Lib requires both the C library and Python wrapper:

1. Install TA-Lib C library:
   - Ubuntu/Debian: sudo apt-get install libta-lib-dev
   - macOS: brew install ta-lib
   - From source: https://ta-lib.org/install.html

2. Install Python wrapper (already in requirements.txt):
   pip install TA-Lib

Usage:
------
    from indicator.talib_adapter import register_all_talib_indicators

    # Auto-register all TA-Lib indicators
    registry = IndicatorRegistry()
    register_all_talib_indicators(registry)

    # Now you can use any TA-Lib indicator
    sma = registry.create_instance("SMA", "sma_20", period=20)
    rsi = registry.create_instance("RSI", "rsi_14", timeperiod=14)
"""

import logging
from typing import Any, Dict, List, Optional

import numpy as np

try:
    import talib
    from talib import abstract
    TALIB_AVAILABLE = True
except ImportError:
    TALIB_AVAILABLE = False
    talib = None
    abstract = None

from datasource.schema import ColumnInfo

from .base import Indicator
from .schema import (
    ComputeContext,
    ComputeResult,
    IndicatorMetadata,
    IndicatorParameter,
    InputSchema,
    OutputSchema,
)

logger = logging.getLogger(__name__)


# Mapping of TA-Lib parameter types to our schema types
TALIB_TYPE_MAP = {
    "double": "float",
    "double[]": "float",
    "int": "int",
    "str": "string",
}

# Categorization of TA-Lib functions
TALIB_CATEGORIES = {
    "overlap": ["SMA", "EMA", "WMA", "DEMA", "TEMA", "TRIMA", "KAMA", "MAMA", "T3",
                "BBANDS", "MIDPOINT", "MIDPRICE", "SAR", "SAREXT", "HT_TRENDLINE"],
    "momentum": ["RSI", "MOM", "ROC", "ROCP", "ROCR", "ROCR100", "TRIX", "CMO", "DX",
                 "ADX", "ADXR", "APO", "PPO", "MACD", "MACDEXT", "MACDFIX", "MFI",
                 "STOCH", "STOCHF", "STOCHRSI", "WILLR", "CCI", "AROON", "AROONOSC",
                 "BOP", "MINUS_DI", "MINUS_DM", "PLUS_DI", "PLUS_DM", "ULTOSC"],
    "volume": ["AD", "ADOSC", "OBV"],
    "volatility": ["ATR", "NATR", "TRANGE"],
    "price": ["AVGPRICE", "MEDPRICE", "TYPPRICE", "WCLPRICE"],
    "cycle": ["HT_DCPERIOD", "HT_DCPHASE", "HT_PHASOR", "HT_SINE", "HT_TRENDMODE"],
    "pattern": ["CDL2CROWS", "CDL3BLACKCROWS", "CDL3INSIDE", "CDL3LINESTRIKE",
                "CDL3OUTSIDE", "CDL3STARSINSOUTH", "CDL3WHITESOLDIERS", "CDLABANDONEDBABY",
                "CDLADVANCEBLOCK", "CDLBELTHOLD", "CDLBREAKAWAY", "CDLCLOSINGMARUBOZU",
                "CDLCONCEALBABYSWALL", "CDLCOUNTERATTACK", "CDLDARKCLOUDCOVER", "CDLDOJI",
                "CDLDOJISTAR", "CDLDRAGONFLYDOJI", "CDLENGULFING", "CDLEVENINGDOJISTAR",
                "CDLEVENINGSTAR", "CDLGAPSIDESIDEWHITE", "CDLGRAVESTONEDOJI", "CDLHAMMER",
                "CDLHANGINGMAN", "CDLHARAMI", "CDLHARAMICROSS", "CDLHIGHWAVE", "CDLHIKKAKE",
                "CDLHIKKAKEMOD", "CDLHOMINGPIGEON", "CDLIDENTICAL3CROWS", "CDLINNECK",
                "CDLINVERTEDHAMMER", "CDLKICKING", "CDLKICKINGBYLENGTH", "CDLLADDERBOTTOM",
                "CDLLONGLEGGEDDOJI", "CDLLONGLINE", "CDLMARUBOZU", "CDLMATCHINGLOW",
                "CDLMATHOLD", "CDLMORNINGDOJISTAR", "CDLMORNINGSTAR", "CDLONNECK",
                "CDLPIERCING", "CDLRICKSHAWMAN", "CDLRISEFALL3METHODS", "CDLSEPARATINGLINES",
                "CDLSHOOTINGSTAR", "CDLSHORTLINE", "CDLSPINNINGTOP", "CDLSTALLEDPATTERN",
                "CDLSTICKSANDWICH", "CDLTAKURI", "CDLTASUKIGAP", "CDLTHRUSTING", "CDLTRISTAR",
                "CDLUNIQUE3RIVER", "CDLUPSIDEGAP2CROWS", "CDLXSIDEGAP3METHODS"],
    "statistic": ["BETA", "CORREL", "LINEARREG", "LINEARREG_ANGLE", "LINEARREG_INTERCEPT",
                  "LINEARREG_SLOPE", "STDDEV", "TSF", "VAR"],
    "math": ["ADD", "DIV", "MAX", "MAXINDEX", "MIN", "MININDEX", "MINMAX", "MINMAXINDEX",
             "MULT", "SUB", "SUM"],
}


def _get_function_category(func_name: str) -> str:
    """Determine the category of a TA-Lib function."""
    for category, functions in TALIB_CATEGORIES.items():
        if func_name in functions:
            return category
    return "other"


class TALibIndicator(Indicator):
    """
    Generic adapter for TA-Lib technical indicators.

    Wraps any TA-Lib function to work within the composable indicator framework.
    Handles parameter mapping, input validation, and output formatting.
    """

    # Class variable to store the TA-Lib function name
    talib_function_name: str = None

    def __init__(self, instance_name: str, **params):
        """
        Initialize a TA-Lib indicator.

        Args:
            instance_name: Unique name for this instance
            **params: TA-Lib function parameters
        """
        if not TALIB_AVAILABLE:
            raise ImportError(
                "TA-Lib is not installed. Please install the TA-Lib C library "
                "and Python wrapper. See indicator/talib_adapter.py for instructions."
            )

        super().__init__(instance_name, **params)
        self._talib_func = abstract.Function(self.talib_function_name)

    @classmethod
    def get_metadata(cls) -> IndicatorMetadata:
        """Get metadata from TA-Lib function info."""
        if not TALIB_AVAILABLE:
            raise ImportError("TA-Lib is not installed")

        func = abstract.Function(cls.talib_function_name)
        info = func.info

        # Build parameters list from TA-Lib function info
        parameters = []
        for param_name, param_info in info.get("parameters", {}).items():
            # Handle case where param_info is a simple value (int/float) instead of a dict
            if isinstance(param_info, dict):
                param_type = TALIB_TYPE_MAP.get(param_info.get("type", "double"), "float")
                default_value = param_info.get("default_value")
            else:
                # param_info is a simple value (default), infer type from the value
                if isinstance(param_info, int):
                    param_type = "int"
                elif isinstance(param_info, float):
                    param_type = "float"
                else:
                    param_type = "float"  # Default to float
                default_value = param_info

            parameters.append(
                IndicatorParameter(
                    name=param_name,
                    type=param_type,
                    description=f"TA-Lib parameter: {param_name}",
                    default=default_value,
                    required=False
                )
            )

        # Get function group/category
        category = _get_function_category(cls.talib_function_name)

        # Build display name (split camelCase or handle CDL prefix)
        display_name = cls.talib_function_name
        if display_name.startswith("CDL"):
            display_name = display_name[3:]  # Remove CDL prefix for patterns

        return IndicatorMetadata(
            name=cls.talib_function_name,
            display_name=display_name,
            description=info.get("display_name", f"TA-Lib {cls.talib_function_name} indicator"),
            category=category,
            parameters=parameters,
            use_cases=[f"Technical analysis using {cls.talib_function_name}"],
            references=["https://ta-lib.org/function.html"],
            tags=["talib", category, cls.talib_function_name.lower()]
        )

    @classmethod
    def get_input_schema(cls) -> InputSchema:
        """
        Get input schema from TA-Lib function requirements.

        Most TA-Lib functions use OHLCV data, but some use subsets.
        """
        if not TALIB_AVAILABLE:
            raise ImportError("TA-Lib is not installed")

        func = abstract.Function(cls.talib_function_name)
        info = func.info
        input_names = info.get("input_names", {})

        required_columns = []

        # Map TA-Lib input names to our schema
        if "prices" in input_names:
            price_inputs = input_names["prices"]
            if "open" in price_inputs:
                required_columns.append(
                    ColumnInfo(name="open", type="float", description="Opening price")
                )
            if "high" in price_inputs:
                required_columns.append(
                    ColumnInfo(name="high", type="float", description="High price")
                )
            if "low" in price_inputs:
                required_columns.append(
                    ColumnInfo(name="low", type="float", description="Low price")
                )
            if "close" in price_inputs:
                required_columns.append(
                    ColumnInfo(name="close", type="float", description="Closing price")
                )
            if "volume" in price_inputs:
                required_columns.append(
                    ColumnInfo(name="volume", type="float", description="Trading volume")
                )

        # Handle functions that take generic price arrays
        if "price" in input_names:
            required_columns.append(
                ColumnInfo(name="close", type="float", description="Price (typically close)")
            )

        # If no specific inputs found, assume close price
        if not required_columns:
            required_columns.append(
                ColumnInfo(name="close", type="float", description="Closing price")
            )

        return InputSchema(required_columns=required_columns)

    @classmethod
    def get_output_schema(cls, **params) -> OutputSchema:
        """Get output schema from TA-Lib function outputs."""
        if not TALIB_AVAILABLE:
            raise ImportError("TA-Lib is not installed")

        func = abstract.Function(cls.talib_function_name)
        info = func.info
        output_names = info.get("output_names", [])

        columns = []

        # Most TA-Lib functions output one or more float arrays
        if isinstance(output_names, list):
            for output_name in output_names:
                columns.append(
                    ColumnInfo(
                        name=output_name.lower(),
                        type="float",
                        description=f"{cls.talib_function_name} output: {output_name}",
                        nullable=True  # TA-Lib often has NaN for initial periods
                    )
                )
        else:
            # Single output, use function name
            columns.append(
                ColumnInfo(
                    name=cls.talib_function_name.lower(),
                    type="float",
                    description=f"{cls.talib_function_name} indicator value",
                    nullable=True
                )
            )

        return OutputSchema(columns=columns)

    def compute(self, context: ComputeContext) -> ComputeResult:
        """Compute indicator using TA-Lib."""
        # Extract input columns
        input_data = {}

        # Get the function's expected inputs
        info = self._talib_func.info
        input_names = info.get("input_names", {})

        # Prepare input arrays
        if "prices" in input_names:
            price_inputs = input_names["prices"]
            for price_type in price_inputs:
                column_data = context.get_column(price_type)
                # Convert to numpy array, replacing None with NaN
                input_data[price_type] = np.array(
                    [float(v) if v is not None else np.nan for v in column_data]
                )
        elif "price" in input_names:
            # Generic price input, use close
            column_data = context.get_column("close")
            input_data["price"] = np.array(
                [float(v) if v is not None else np.nan for v in column_data]
            )
        else:
            # Default to close if no inputs specified
            column_data = context.get_column("close")
            input_data["close"] = np.array(
                [float(v) if v is not None else np.nan for v in column_data]
            )

        # Set parameters on the function
        self._talib_func.parameters = self.params

        # Execute TA-Lib function
        try:
            output = self._talib_func(input_data)
        except Exception as e:
            logger.error(f"TA-Lib function {self.talib_function_name} failed: {e}")
            raise ValueError(f"TA-Lib computation failed: {e}")

        # Format output
        times = context.get_times()
        output_names = info.get("output_names", [])

        # Handle single vs multiple outputs
        if isinstance(output, np.ndarray):
            # Single output
            output_name = output_names[0].lower() if output_names else self.talib_function_name.lower()
            result_data = [
                {
                    "time": times[i],
                    output_name: float(output[i]) if not np.isnan(output[i]) else None
                }
                for i in range(len(times))
            ]
        elif isinstance(output, tuple):
            # Multiple outputs
            result_data = []
            for i in range(len(times)):
                row = {"time": times[i]}
                for j, output_array in enumerate(output):
                    output_name = output_names[j].lower() if j < len(output_names) else f"output_{j}"
                    row[output_name] = float(output_array[i]) if not np.isnan(output_array[i]) else None
                result_data.append(row)
        else:
            raise ValueError(f"Unexpected TA-Lib output type: {type(output)}")

        return ComputeResult(
            data=result_data,
            is_partial=context.is_incremental
        )


def create_talib_indicator_class(func_name: str) -> type:
    """
    Dynamically create an Indicator class for a TA-Lib function.

    Args:
        func_name: TA-Lib function name (e.g., 'SMA', 'RSI')

    Returns:
        Indicator class for this function
    """
    return type(
        f"TALib_{func_name}",
        (TALibIndicator,),
        {"talib_function_name": func_name}
    )


def register_all_talib_indicators(registry, only_tradingview_supported: bool = True) -> int:
    """
    Auto-register all available TA-Lib indicators with the registry.

    Args:
        registry: IndicatorRegistry instance
        only_tradingview_supported: If True, only register indicators that have
                                   TradingView equivalents (default: True)

    Returns:
        Number of indicators registered

    Raises:
        ImportError: If TA-Lib is not installed
    """
    if not TALIB_AVAILABLE:
        logger.warning(
            "TA-Lib is not installed. Skipping TA-Lib indicator registration. "
            "Install TA-Lib C library and Python wrapper to enable TA-Lib indicators."
        )
        return 0

    # Get list of supported indicators if filtering is enabled
    from .tv_mapping import is_indicator_supported

    # Get all TA-Lib functions
    func_groups = talib.get_function_groups()
    all_functions = []
    for group, functions in func_groups.items():
        all_functions.extend(functions)

    # Remove duplicates
    all_functions = sorted(set(all_functions))

    registered_count = 0
    skipped_count = 0

    for func_name in all_functions:
        try:
            # Skip if filtering enabled and indicator not supported in TradingView
            if only_tradingview_supported and not is_indicator_supported(func_name):
                skipped_count += 1
                logger.debug(f"Skipping TA-Lib function {func_name} - not supported in TradingView")
                continue

            # Create indicator class for this function
            indicator_class = create_talib_indicator_class(func_name)

            # Register with the registry
            registry.register(indicator_class)
            registered_count += 1

        except Exception as e:
            logger.warning(f"Failed to register TA-Lib function {func_name}: {e}")
            continue

    logger.info(f"Registered {registered_count} TA-Lib indicators (skipped {skipped_count} unsupported)")
    return registered_count


def get_talib_version() -> Optional[str]:
    """
    Get the installed TA-Lib version.

    Returns:
        Version string or None if not installed
    """
    if TALIB_AVAILABLE:
        return talib.__version__
    return None


def is_talib_available() -> bool:
    """Check if TA-Lib is available."""
    return TALIB_AVAILABLE
