import json
import logging
import uuid
import datetime as dt
from dataclasses import dataclass
from typing import Any, Dict

import pandas as pd
from DataCache_v3 import DataCache

# Configure logging
logger = logging.getLogger(__name__)


class IndicatorWrapper:
    """
    Wrapper to make indicator dict data accessible via .properties attribute.

    This bridges the gap between get_indicator_list() return format
    (flat dict with properties merged in) and the expected access pattern
    (indicator.properties.get(prop_name)).
    """
    def __init__(self, data: dict):
        self.properties = data


@dataclass()
class Signal:
    """Class for individual signal properties and state."""
    name: str
    source1: str
    prop1: str
    source2: str
    prop2: str
    operator: str
    state: bool = False
    value1: float = None
    value2: float = None
    range: float = None
    tbl_key: str = None
    creator: int = None
    public: bool = False

    def set_value1(self, value):
        self.value1 = value

    def set_value2(self, value):
        self.value2 = value

    def compare(self):
        if self.value1 is None:
            raise ValueError('Signal: Cannot compare: value1 not set')

        previous_state = self.state

        # Handle pattern operators (only need value1)
        if self.operator == 'is_bullish':
            # Bullish patterns return positive values (typically 100)
            self.state = self.value1 > 0
        elif self.operator == 'is_bearish':
            # Bearish patterns return negative values (typically -100)
            self.state = self.value1 < 0
        elif self.operator == 'is_detected':
            # Any pattern (bullish or bearish) returns non-zero
            self.state = self.value1 != 0
        elif self.operator == '+/-':
            # Range comparison requires both values
            if self.value2 is None:
                raise ValueError('Signal: Cannot compare: value2 not set')
            if self.range is None:
                raise ValueError('Signal: Cannot compare: range not set')
            self.state = abs(self.value1 - self.value2) < self.range
        else:
            # Standard comparison operators (>, <, ==)
            if self.value2 is None:
                raise ValueError('Signal: Cannot compare: value2 not set')
            string = str(self.value1) + self.operator + str(self.value2)
            logger.debug(f"Signal comparison: {string}")
            self.state = eval(string)

        state_change = self.state != previous_state
        return state_change


class Signals:
    """Manages signals with database-backed storage via DataCache."""

    def __init__(self, data_cache: DataCache):
        """
        Initializes the Signals class.

        :param data_cache: Instance of DataCache to manage cache and database interactions.
        """
        self.data_cache = data_cache

        # Ensure the signals table exists in the database
        self._ensure_table_exists()

        # Create a cache for signals with necessary columns
        self.data_cache.create_cache(
            name='signals',
            cache_type='table',
            size_limit=500,
            eviction_policy='deny',
            default_expiration=dt.timedelta(hours=24),
            columns=[
                "creator",      # User ID who created the signal
                "name",         # Signal name
                "source1",      # First indicator source
                "prop1",        # First indicator property
                "source2",      # Second indicator source (or 'value')
                "prop2",        # Second indicator property (or the value itself)
                "operator",     # Comparison operator: >, <, ==, +/-
                "range",        # Range value for +/- operator
                "public",       # Whether signal is visible to other users
                "state",        # Current signal state (True/False)
                "last_value1",  # Last calculated value1
                "last_value2",  # Last calculated value2
                "tbl_key"       # Unique identifier
            ]
        )

        # In-memory cache of Signal objects for quick processing
        self.signals: list[Signal] = []

        # Load existing signals from database into memory
        self._load_signals_from_db()

    def _ensure_table_exists(self) -> None:
        """Create the signals table in the database if it doesn't exist."""
        try:
            if not self.data_cache.db.table_exists('signals'):
                create_sql = """
                    CREATE TABLE IF NOT EXISTS signals (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        creator INTEGER,
                        name TEXT NOT NULL,
                        source1 TEXT NOT NULL,
                        prop1 TEXT NOT NULL,
                        source2 TEXT NOT NULL,
                        prop2 TEXT NOT NULL,
                        operator TEXT NOT NULL,
                        range REAL,
                        public INTEGER DEFAULT 0,
                        state INTEGER DEFAULT 0,
                        last_value1 REAL,
                        last_value2 REAL,
                        tbl_key TEXT UNIQUE
                    )
                """
                self.data_cache.db.execute_sql(create_sql, params=[])
                logger.info("Created signals table in database")
        except Exception as e:
            logger.error(f"Error ensuring signals table exists: {e}", exc_info=True)

    def _load_signals_from_db(self) -> None:
        """Load all signals from database into memory."""
        try:
            signals_df = self.data_cache.get_all_rows_from_datacache(cache_name='signals')
            if signals_df is not None and not signals_df.empty:
                for _, row in signals_df.iterrows():
                    signal = Signal(
                        name=row.get('name', ''),
                        source1=row.get('source1', ''),
                        prop1=row.get('prop1', ''),
                        source2=row.get('source2', ''),
                        prop2=row.get('prop2', ''),
                        operator=row.get('operator', '=='),
                        state=bool(row.get('state', False)),
                        value1=row.get('last_value1'),
                        value2=row.get('last_value2'),
                        range=row.get('range'),
                        tbl_key=row.get('tbl_key'),
                        creator=row.get('creator'),
                        public=bool(row.get('public', False))
                    )
                    self.signals.append(signal)
                logger.info(f"Loaded {len(self.signals)} signals from database")
        except Exception as e:
            logger.error(f"Error loading signals from database: {e}", exc_info=True)

    def _save_signal(self, signal_data: dict) -> dict:
        """
        Saves a signal to the cache and database. Handles both creation and editing.

        :param signal_data: A dictionary containing signal data.
        :return: A dictionary containing success or failure information.
        """
        is_edit = 'tbl_key' in signal_data and signal_data['tbl_key']

        try:
            tbl_key = signal_data.get('tbl_key') if is_edit else str(uuid.uuid4())

            if is_edit:
                # Verify the existing signal
                existing_signal = self.data_cache.get_rows_from_datacache(
                    cache_name='signals',
                    filter_vals=[('tbl_key', tbl_key)],
                    include_tbl_key=True
                )
                if existing_signal.empty:
                    return {"success": False, "message": "Signal not found."}
            else:
                # Check for duplicate signal name for the same user
                creator = signal_data.get('creator')
                name = signal_data.get('name')
                if creator and name:
                    filter_conditions = [('creator', creator), ('name', name)]
                    existing_signal = self.data_cache.get_rows_from_datacache(
                        cache_name='signals',
                        filter_vals=filter_conditions,
                        include_tbl_key=True
                    )
                    if not existing_signal.empty:
                        return {"success": False, "message": "A signal with this name already exists"}

            # Validate required fields
            required_fields = ['name', 'source1', 'prop1', 'source2', 'prop2', 'operator']
            for field in required_fields:
                if not signal_data.get(field):
                    return {"success": False, "message": f"Missing required field: {field}"}

            # Prepare fields for database operations
            columns = (
                "creator", "name", "source1", "prop1", "source2", "prop2",
                "operator", "range", "public", "state", "last_value1", "last_value2", "tbl_key"
            )

            # Convert range to float if present
            range_val = signal_data.get('range')
            if range_val is not None:
                try:
                    range_val = float(range_val)
                except (TypeError, ValueError):
                    range_val = None

            values = (
                signal_data.get('creator'),
                signal_data['name'],
                signal_data['source1'],
                signal_data['prop1'],
                signal_data['source2'],
                signal_data['prop2'],
                signal_data['operator'],
                range_val,
                int(signal_data.get('public', 0)),
                int(signal_data.get('state', 0)),
                signal_data.get('value1') or signal_data.get('last_value1'),
                signal_data.get('value2') or signal_data.get('last_value2'),
                tbl_key
            )

            if is_edit:
                # Update the existing signal in database
                self.data_cache.modify_datacache_item(
                    cache_name='signals',
                    filter_vals=[('tbl_key', tbl_key)],
                    field_names=columns,
                    new_values=values,
                    key=tbl_key,
                    overwrite='tbl_key'
                )

                # Update in-memory signal
                for sig in self.signals:
                    if sig.tbl_key == tbl_key:
                        sig.name = signal_data['name']
                        sig.source1 = signal_data['source1']
                        sig.prop1 = signal_data['prop1']
                        sig.source2 = signal_data['source2']
                        sig.prop2 = signal_data['prop2']
                        sig.operator = signal_data['operator']
                        sig.range = range_val
                        sig.public = bool(signal_data.get('public', 0))
                        sig.creator = signal_data.get('creator')
                        break
            else:
                # Insert a new signal into database
                self.data_cache.insert_row_into_datacache(
                    cache_name='signals',
                    columns=columns,
                    values=values
                )

                # Add to in-memory signals
                new_signal = Signal(
                    name=signal_data['name'],
                    source1=signal_data['source1'],
                    prop1=signal_data['prop1'],
                    source2=signal_data['source2'],
                    prop2=signal_data['prop2'],
                    operator=signal_data['operator'],
                    range=range_val,
                    state=bool(signal_data.get('state', False)),
                    value1=signal_data.get('value1'),
                    value2=signal_data.get('value2'),
                    tbl_key=tbl_key,
                    creator=signal_data.get('creator'),
                    public=bool(signal_data.get('public', 0))
                )
                self.signals.append(new_signal)

            # Prepare the response
            response_signal = signal_data.copy()
            response_signal['tbl_key'] = tbl_key

            return {
                "success": True,
                "message": "Signal saved successfully" if is_edit else "Signal created successfully",
                "signal": response_signal,
                "updated_at": dt.datetime.now(dt.timezone.utc).isoformat()
            }

        except Exception as e:
            logger.error(f"Failed to save signal: {e}", exc_info=True)
            operation = "update" if is_edit else "create"
            return {"success": False, "message": f"Failed to {operation} signal: {str(e)}"}

    def new_signal(self, signal_data: dict) -> dict:
        """
        Add a new signal to the cache and database.

        :param signal_data: A dictionary containing signal data.
        :return: A dictionary containing success or failure information.
        """
        return self._save_signal(signal_data)

    def edit_signal(self, signal_data: dict) -> dict:
        """
        Updates an existing signal in the cache and database.

        :param signal_data: A dictionary containing the updated signal data.
        :return: A dictionary containing success or failure information.
        """
        return self._save_signal(signal_data)

    def delete_signal(self, tbl_key: str) -> dict:
        """
        Deletes a signal identified by its tbl_key.

        :param tbl_key: The unique identifier of the signal to delete.
        :return: A dictionary indicating success or failure.
        """
        try:
            # Get the signal name before deleting (for response)
            signal_name = None
            for sig in self.signals:
                if sig.tbl_key == tbl_key:
                    signal_name = sig.name
                    break

            # Remove from database
            self.data_cache.remove_row_from_datacache(
                cache_name='signals',
                filter_vals=[('tbl_key', tbl_key)]
            )

            # Remove from in-memory list
            self.signals = [sig for sig in self.signals if sig.tbl_key != tbl_key]

            return {
                "success": True,
                "message": "Signal deleted successfully.",
                "tbl_key": tbl_key,
                "name": signal_name
            }
        except Exception as e:
            logger.error(f"Failed to delete signal '{tbl_key}': {e}", exc_info=True)
            return {"success": False, "message": f"Failed to delete signal: {str(e)}"}

    def get_all_signals(self, user_id: int | None, form: str = 'dict') -> Any:
        """
        Return signals visible to the user (their own + public signals).

        :param user_id: The ID of the user making the request. If None, returns only public signals.
        :param form: The desired format ('df', 'json', 'dict', 'obj').
        :return: Signals in the requested format.
        """
        valid_forms = {'df', 'json', 'dict', 'obj'}
        if form not in valid_forms:
            raise ValueError(f"Invalid form '{form}'. Expected one of {valid_forms}.")

        try:
            # Fetch public signals
            public_df = self.data_cache.get_rows_from_datacache(
                cache_name='signals',
                filter_vals=[('public', 1)],
                include_tbl_key=True
            )

            if user_id is not None:
                # Fetch user's private signals
                user_df = self.data_cache.get_rows_from_datacache(
                    cache_name='signals',
                    filter_vals=[('creator', user_id), ('public', 0)],
                    include_tbl_key=True
                )
                # Concatenate the two DataFrames
                signals_df = pd.concat([public_df, user_df], ignore_index=True)
            else:
                signals_df = public_df

            # Return empty result if no signals found
            if signals_df is None or signals_df.empty:
                if form == 'df':
                    return pd.DataFrame()
                elif form == 'obj':
                    return []
                elif form == 'json':
                    return '[]'
                else:
                    return []

            # Return in requested format
            if form == 'df':
                return signals_df
            elif form == 'json':
                return signals_df.to_json(orient='records')
            elif form == 'dict':
                return signals_df.to_dict('records')
            elif form == 'obj':
                # Return in-memory Signal objects filtered for this user
                return [
                    sig for sig in self.signals
                    if sig.public or sig.creator == user_id
                ]

        except Exception as e:
            logger.error(f"Error getting signals: {e}", exc_info=True)
            if form == 'df':
                return pd.DataFrame()
            elif form == 'obj':
                return []
            elif form == 'json':
                return '[]'
            else:
                return []

    def get_signals(self, form: str, user_id: int = None) -> Any:
        """
        Legacy method for backwards compatibility.
        Return signals in the specified format.

        :param form: The desired format ('obj', 'json', 'dict').
        :param user_id: Optional user ID for filtering.
        :return: Signals in the requested format.
        """
        if form == 'obj':
            if user_id is not None:
                return [sig for sig in self.signals if sig.public or sig.creator == user_id]
            return self.signals
        elif form == 'json':
            sigs = self.signals if user_id is None else [
                sig for sig in self.signals if sig.public or sig.creator == user_id
            ]
            json_str = []
            for sig in sigs:
                json_str.append(json.dumps(sig.__dict__))
            return json_str
        elif form == 'dict':
            sigs = self.signals if user_id is None else [
                sig for sig in self.signals if sig.public or sig.creator == user_id
            ]
            return [sig.__dict__ for sig in sigs]
        return None

    def get_signal_by_name(self, name: str) -> Signal | None:
        """Get a signal by its name."""
        for signal in self.signals:
            if signal.name == name:
                return signal
        return None

    def get_signal_by_tbl_key(self, tbl_key: str) -> Signal | None:
        """Get a signal by its tbl_key."""
        for signal in self.signals:
            if signal.tbl_key == tbl_key:
                return signal
        return None

    def update_signal_state(self, tbl_key: str, state: bool, value1: float = None, value2: float = None) -> None:
        """
        Update a signal's state and values in both memory and database.

        :param tbl_key: The signal's unique identifier.
        :param state: The new state value.
        :param value1: Optional new value1.
        :param value2: Optional new value2.
        """
        try:
            # Update in-memory signal
            for sig in self.signals:
                if sig.tbl_key == tbl_key:
                    sig.state = state
                    if value1 is not None:
                        sig.value1 = value1
                    if value2 is not None:
                        sig.value2 = value2
                    break

            # Update in database
            update_values = {'state': int(state)}
            if value1 is not None:
                update_values['last_value1'] = value1
            if value2 is not None:
                update_values['last_value2'] = value2

            field_names = tuple(update_values.keys())
            new_values = tuple(update_values.values())

            self.data_cache.modify_datacache_item(
                cache_name='signals',
                filter_vals=[('tbl_key', tbl_key)],
                field_names=field_names,
                new_values=new_values,
                key=tbl_key,
                overwrite='tbl_key'
            )
        except Exception as e:
            logger.error(f"Error updating signal state: {e}", exc_info=True)

    def process_all_signals(self, indicators, external_sources=None, external_indicators=None) -> dict:
        """
        Loop through all signals and process them based on the last indicator results.

        :param indicators: The Indicators instance with calculated values.
        :param external_sources: Optional ExternalSources instance for custom signal types (real-time only).
        :param external_indicators: Optional ExternalIndicatorsManager for historical API indicators.
        :return: Dictionary of signals that changed state.
        """
        state_changes = {}

        # Cache indicator data per user to avoid repeated lookups
        user_indicator_cache: Dict[int, dict] = {}

        for signal in self.signals:
            # Get or fetch indicator data for this signal's creator
            user_id = signal.creator
            if user_id not in user_indicator_cache:
                try:
                    # Fetch indicator list for this user
                    indicator_list = indicators.get_indicator_list(user_id=user_id)
                    # Wrap each indicator's data so it has a .properties attribute
                    user_indicator_cache[user_id] = {
                        name: IndicatorWrapper(data)
                        for name, data in indicator_list.items()
                    }

                    # Add external sources as indicator-like entries (real-time)
                    if external_sources:
                        external_data = external_sources.get_sources_as_indicators(user_id)
                        for name, data in external_data.items():
                            user_indicator_cache[user_id][name] = IndicatorWrapper(data)

                    # Add external indicators as indicator-like entries (historical API)
                    if external_indicators:
                        ext_ind_data = external_indicators.get_indicators_as_indicator_list(user_id)
                        for name, data in ext_ind_data.items():
                            # For live trading, get the most recent value
                            ext_ind = external_indicators.get_indicator_by_name(name, user_id)
                            if ext_ind:
                                # Fetch today's value
                                from datetime import datetime
                                today_value = ext_ind.get_value_for_timestamp(datetime.now())
                                if today_value is not None:
                                    data['value'] = today_value
                            user_indicator_cache[user_id][name] = IndicatorWrapper(data)

                except Exception as e:
                    logger.debug(f"Could not fetch indicators for user {user_id}: {e}")
                    user_indicator_cache[user_id] = {}

            # Get the wrapped indicators for this user
            user_indicators = user_indicator_cache.get(user_id, {})

            change_in_state = self.process_signal(signal, user_indicators)
            if change_in_state:
                state_changes[signal.name] = signal.state
                # Persist state change to database
                if signal.tbl_key:
                    self.update_signal_state(
                        signal.tbl_key,
                        signal.state,
                        signal.value1,
                        signal.value2
                    )
        return state_changes

    def process_signal(self, signal: Signal, indicator_data: dict, candles=None) -> bool:
        """
        Process a signal by comparing indicator values.

        :param signal: The signal to process.
        :param indicator_data: Dict mapping indicator names to IndicatorWrapper objects.
        :param candles: Optional candles for recalculation.
        :return: True if the signal state changed, False otherwise.
        """
        if candles is None:
            # Get the source of the first signal
            source_1 = signal.source1
            # Ask the indicator for the last result
            if source_1 in indicator_data:
                signal.value1 = indicator_data[source_1].properties.get(signal.prop1)
            else:
                logger.debug(f'Could not calculate signal: source indicator "{source_1}" not found.')
                return False

            # Get the source of the second signal
            source_2 = signal.source2
            # If the source is a set value it will be stored in prop2
            if source_2 == 'value':
                try:
                    signal.value2 = float(signal.prop2)
                except (TypeError, ValueError):
                    signal.value2 = signal.prop2
            else:
                # Ask the indicator for the last result
                if source_2 in indicator_data:
                    signal.value2 = indicator_data[source_2].properties.get(signal.prop2)
                else:
                    logger.debug(f'Could not calculate signal: source2 indicator "{source_2}" not found.')
                    return False

            # Compare the retrieved values
            try:
                state_change = signal.compare()
                return state_change
            except ValueError as e:
                logger.debug(f"Signal comparison error: {e}")
                return False

        return False
