import logging
import sqlite3
from typing import List, Any, Dict, TYPE_CHECKING
import pandas as pd
import ccxt
import config
from Exchange import Exchange
from DataCache_v3 import DataCache

logger = logging.getLogger(__name__)


# Utility function to add a row to a DataFrame
def add_row(df: pd.DataFrame, dic: Dict[str, Any]) -> pd.DataFrame:
    return pd.concat([df, pd.DataFrame([dic])], ignore_index=True)


class ExchangeInterface:
    """
    Connects, maintains, and routes data requests to/from multiple exchanges.
    """

    def __init__(self, cache_manager: DataCache, edm_client=None):
        self.cache_manager = cache_manager
        self.edm_client = edm_client
        self.cache_manager.create_cache(
            name='exchange_data',
            cache_type='table',
            size_limit=100,
            eviction_policy='deny',
            columns=['user', 'name', 'reference', 'balances']
        )
        self.exchange_data_db_columns = self._ensure_exchange_data_schema()

        self.available_exchanges = self.get_ccxt_exchanges()

        self.default_ex_name = 'binance'
        self.default_exchange = None

    @staticmethod
    def _ensure_exchange_data_schema() -> set[str]:
        """
        Ensure the on-disk exchange_data table has columns required by current code.
        This handles older migrated databases that stored `saved_state` instead of
        `reference`.
        """
        try:
            with sqlite3.connect(config.DB_FILE) as conn:
                cur = conn.cursor()
                cur.execute("PRAGMA table_info(exchange_data)")
                table_info = cur.fetchall()
                if not table_info:
                    return set()

                cols = {row[1] for row in table_info}

                if 'reference' not in cols:
                    cur.execute("ALTER TABLE exchange_data ADD COLUMN reference BLOB")
                    if 'saved_state' in cols:
                        cur.execute(
                            "UPDATE exchange_data SET reference = saved_state WHERE reference IS NULL"
                        )

                if 'balances' not in cols:
                    cur.execute("ALTER TABLE exchange_data ADD COLUMN balances BLOB")

                conn.commit()
                cur.execute("PRAGMA table_info(exchange_data)")
                return {row[1] for row in cur.fetchall()}
        except Exception as e:
            logger.warning("exchange_data schema check skipped: %s", e)
            return set()

    def connect_default_exchange(self):
        if self.default_exchange is not None:
            return
        # Create a default user and exchange for unsigned requests
        self.connect_exchange(exchange_name=self.default_ex_name, user_name='default')
        self.default_exchange = self.get_exchange(ename=self.default_ex_name, uname='default')

    @staticmethod
    def get_ccxt_exchanges() -> List[str]:
        """Retrieve the list of available exchanges from CCXT."""
        return ccxt.exchanges

    @staticmethod
    def get_public_exchanges() -> List[str]:
        """Return a list of public exchanges available from CCXT."""
        from pathlib import Path

        public_list = []
        # Cross-platform path resolution
        file_path = Path(__file__).parent / 'working_public_exchanges.txt'

        try:
            with open(file_path, 'r') as file:
                public_list = [line.strip() for line in file.readlines()]
        except FileNotFoundError:
            print(f"Error: The file {file_path} was not found.")
        except Exception as e:
            print(f"An error occurred: {e}")

        return public_list

    def connect_exchange(self, exchange_name: str, user_name: str, api_keys: Dict[str, str] = None,
                         testnet: bool = False) -> bool:
        """
        Initialize and store a reference to the specified exchange.

        :param exchange_name: The name of the exchange.
        :param user_name: The name of the user connecting the exchange.
        :param api_keys: Optional API keys for the exchange.
        :param testnet: Whether to use testnet/sandbox mode.
        :return: True if successful, False otherwise.
        """
        try:
            # Get existing exchange to preserve EDM session ID for cleanup
            existing = None
            old_session_id = None
            try:
                existing = self.get_exchange(exchange_name, user_name)
                old_session_id = existing.edm_session_id if hasattr(existing, 'edm_session_id') else None
                old_testnet = getattr(existing, 'testnet', 'unknown')
                logger.info(f"Replacing existing {exchange_name} for {user_name} (old testnet={old_testnet}, new testnet={testnet})")
            except Exception:
                pass  # No existing entry, that's fine

            # Create new exchange with explicit testnet setting
            logger.info(f"Creating {exchange_name} for {user_name} with testnet={testnet}")
            exchange = Exchange(name=exchange_name, api_keys=api_keys, exchange_id=exchange_name.lower(),
                               testnet=testnet)

            # Create EDM session for authenticated exchange access
            if self.edm_client and api_keys and api_keys.get('key') and api_keys.get('secret'):
                try:
                    session_id = self.edm_client.create_session_sync()
                    self.edm_client.add_credentials_sync(
                        session_id=session_id,
                        exchange=exchange_name.lower(),
                        api_key=api_keys['key'],
                        api_secret=api_keys['secret'],
                        passphrase=api_keys.get('passphrase') or api_keys.get('password'),
                        testnet=testnet
                    )
                    exchange.edm_session_id = session_id
                    logger.info(f"Created EDM session {session_id} for {exchange_name}")
                except Exception as e:
                    logger.warning(f"Failed to create EDM session for {exchange_name}: {e}")

            # ALWAYS try to remove existing entry before adding new one
            # This prevents duplicate entries even if get_exchange failed
            # Use tbl_key for precise targeting, with fallback to user+name filter
            tbl_key = f"{user_name}:{exchange_name}"
            try:
                self.cache_manager.remove_row_from_datacache(
                    cache_name='exchange_data',
                    filter_vals=[('user', user_name), ('name', exchange_name)],
                    key=tbl_key
                )
                logger.info(f"Removed old exchange entry for {user_name}/{exchange_name}")
            except Exception as e:
                logger.debug(f"No existing entry to remove for {user_name}/{exchange_name}: {e}")

            # Clean up old EDM session if we had one
            if old_session_id and self.edm_client:
                try:
                    self.edm_client.delete_session_sync(old_session_id)
                except Exception as e:
                    logger.warning(f"Failed to delete old EDM session: {e}")

            self.add_exchange(user_name, exchange)
            logger.info(f"Connected {exchange_name} for {user_name} (testnet={testnet}, balances={len(exchange.balances)} assets)")
            return True
        except Exception as e:
            logger.error(f"Failed to connect user '{user_name}' to exchange '{exchange_name}': {str(e)}")
            return False

    def add_exchange(self, user_name: str, exchange: Exchange):
        """
        Add an exchange to the user's list of exchanges.

        :param user_name: The name of the user.
        :param exchange: The Exchange object to add.
        """
        try:
            # Generate a unique tbl_key to prevent duplicates
            tbl_key = f"{user_name}:{exchange.name}"

            row_data = {
                'user': user_name,
                'name': exchange.name,
                'reference': exchange,
                'balances': exchange.balances
            }
            # Backwards compatibility for migrated databases that still enforce
            # a NOT NULL legacy `saved_state` column.
            if 'saved_state' in self.exchange_data_db_columns:
                row_data['saved_state'] = exchange

            row = pd.DataFrame([row_data])

            # Pass key to let serialized_datacache_insert add the tbl_key column
            self.cache_manager.serialized_datacache_insert(cache_name='exchange_data', data=row, key=tbl_key)
        except Exception as e:
            logger.error(f"Couldn't create an instance of the exchange! {str(e)}")
            raise

    def get_exchange(self, ename: str, uname: str) -> Exchange:
        """
        Get a reference to the specified exchange for a user.

        :param ename: The name of the exchange.
        :param uname: The name of the user.
        :return: The Exchange object.
        """
        if not ename or not uname:
            raise ValueError('Missing argument!')

        exchange_data = self.cache_manager.get_serialized_datacache(cache_name='exchange_data',
                                                                    filter_vals=[('name', ename), ('user', uname)])

        if exchange_data.empty:
            raise ValueError('No matching exchange found.')
        # todo check this
        return exchange_data.at[exchange_data.index[0], 'reference']

    def get_connected_exchanges(self, user_name: str) -> List[str]:
        """
        Get a list of connected exchanges for a user.

        :param user_name: The name of the user.
        :return: A list of connected exchange names.
        """
        exchanges = self.cache_manager.get_serialized_datacache(
            cache_name='exchange_data', filter_vals=[('user', user_name)])
        return exchanges['name'].tolist()

    def get_available_exchanges(self) -> List[str]:
        """Get a list of available exchanges."""
        return self.available_exchanges

    def get_exchange_balances(self, user_name: str, name: str) -> pd.Series:
        """
        Get the balances of a specified exchange for a specific user.

        :param user_name: The name of the user.
        :param name: The name of the exchange.
        :return: A Series containing the balances.
        """
        exchange = self.cache_manager.get_serialized_datacache(
            cache_name='exchange_data',
            filter_vals=[('user', user_name), ('name', name)]
        )

        if not exchange.empty:
            return exchange.iloc[0]['balances']
        else:
            return pd.Series(dtype='object')  # Return an empty Series if no match is found

    def get_all_balances(self, user_name: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Get the balances of all connected exchanges for a user.

        :param user_name: The name of the user.
        :return: A dictionary containing the balances of all connected exchanges.
        """
        # Query exchange data for the given user
        exchanges = self.cache_manager.get_serialized_datacache(
            cache_name='exchange_data',
            filter_vals=[('user', user_name)]
        )

        # Select 'name' and 'balances' columns for all rows
        filtered_data = exchanges.loc[:, ['name', 'balances']]

        # Return a dictionary where exchange 'name' is the key and 'balances' is the value
        return {row['name']: row['balances'] for _, row in filtered_data.iterrows()}

    def refresh_all_balances(self, user_name: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Refresh balances from all connected exchanges for a user.

        :param user_name: The name of the user.
        :return: A dictionary containing the refreshed balances of all connected exchanges.
        """
        exchanges = self.cache_manager.get_serialized_datacache(
            cache_name='exchange_data',
            filter_vals=[('user', user_name)]
        )

        if exchanges.empty:
            return {}

        result = {}
        for _, row in exchanges.iterrows():
            exchange_name = row['name']
            exchange_ref = row['reference']
            try:
                # Refresh balances from the exchange
                logger.info(f"Refreshing {exchange_name} for user {user_name}, testnet={getattr(exchange_ref, 'testnet', 'unknown')}")
                new_balances = exchange_ref.refresh_balances()
                result[exchange_name] = new_balances

                # Update the cache with new balances
                self.cache_manager.modify_datacache_item(
                    cache_name='exchange_data',
                    filter_vals=[('user', user_name), ('name', exchange_name)],
                    field_names=('balances',),
                    new_values=(new_balances,)
                )
                logger.info(f"Refreshed balances for {exchange_name}: {len(new_balances)} assets")
            except Exception as e:
                logger.warning(f"Failed to refresh balances for {exchange_name}: {e}")
                result[exchange_name] = row.get('balances', [])

        return result

    def get_all_activated(self, user_name: str, fetch_type: str = 'trades') -> Dict[str, List[Dict[str, Any]]]:
        """
        Get active trades or open orders for all connected exchanges.

        :param user_name: The name of the user.
        :param fetch_type: The type of data to fetch ('trades' or 'orders').
        :return: A dictionary indexed by exchange name with lists of active trades or open orders.
        """
        exchanges = self.cache_manager.get_serialized_datacache(
            cache_name='exchange_data',
            filter_vals=[('user', user_name)]
        )

        # Select the 'name' and 'reference' columns
        filtered_data = exchanges.loc[:, ['name', 'reference']]

        if filtered_data.empty:
            return {}

        data_dict = {}

        # Iterate over the filtered data
        for name, reference in filtered_data.itertuples(index=False):
            if pd.isna(reference):
                continue

            try:
                # Fetch active trades or open orders based on the fetch_type
                if fetch_type == 'trades':
                    data = reference.get_active_trades()
                elif fetch_type == 'orders':
                    data = reference.get_open_orders()
                else:
                    logger.error(f"Invalid fetch type: {fetch_type}")
                    return {}

                data_dict[name] = data
            except Exception as e:
                logger.error(f"Error retrieving data for {name}: {str(e)}")

        return data_dict

    def get_order(self, symbol: str, order_id: str, exchange_name: str, user_name: str) -> Any:
        """
        Get an order from a specified exchange.

        :param symbol: The trading symbol.
        :param order_id: The order ID.
        :param exchange_name: The name of the exchange.
        :param user_name: The name of the user.
        :return: The order details.
        """
        exchange = self.get_exchange(ename=exchange_name, uname=user_name)
        return exchange.get_order(symbol=symbol, order_id=order_id)

    def get_trade_info(self, trade, user_name: str, info_type: str) -> Dict[str, Any] | None:
        """
        Get information about a trade (status, executed quantity, executed price).

        :param trade: The trade object.
        :param user_name: The name of the user.
        :param info_type: The type of information ('status', 'executed_qty', 'executed_price').
        :return: The requested information or None if the order is not found.
        """
        exchange = self.get_exchange(ename=trade.target, uname=user_name)
        if exchange.configured is False:
            logger.error("Must configure API keys to request trade info.")
            return None

        order = exchange.get_order(symbol=trade.symbol, order_id=trade.order.orderId)

        if order is None:
            logger.error(f"Order {trade.order.orderId} for {trade.symbol} not found.")
            return None

        if isinstance(order, dict):
            if info_type == 'status':
                return order.get('status')
            elif info_type == 'executed_qty':
                return order.get('filled')
            elif info_type == 'executed_price':
                return order.get('average')
            else:
                logger.error(f"Invalid info type: {info_type}")
                return None
        else:
            logger.error("Order object is not a dictionary")
            return None

    def get_price(self, symbol: str, exchange_name: str = None) -> float:
        """
        Get the current price of a trading pair from a specific exchange.

        :param symbol: The trading symbol.
        :param exchange_name: Optional exchange name to fetch price from.
                             If None, uses the default exchange.
        :return: The current price.
        """
        if exchange_name is None:
            self.connect_default_exchange()
            return self.default_exchange.get_price(symbol=symbol)
        else:
            return self._fetch_public_price(exchange_name, symbol)

    def _fetch_public_price(self, exchange_name: str, symbol: str) -> float:
        """
        Fetch price from an exchange using public endpoints (no auth required).

        :param exchange_name: The exchange name (e.g., 'binance', 'kucoin').
        :param symbol: The trading symbol (e.g., 'BTC/USDT').
        :return: The current price, or 0.0 if fetch fails.
        """
        import ccxt
        try:
            # Create a public ccxt client for the exchange
            exchange_class = getattr(ccxt, exchange_name.lower(), None)
            if exchange_class is None:
                logger.warning(f"Unknown exchange: {exchange_name}")
                return 0.0

            # Use cached public exchange clients to avoid creating new ones each time
            if not hasattr(self, '_public_exchanges'):
                self._public_exchanges = {}

            if exchange_name.lower() not in self._public_exchanges:
                self._public_exchanges[exchange_name.lower()] = exchange_class({
                    'enableRateLimit': True,
                    'timeout': 10000,
                })

            client = self._public_exchanges[exchange_name.lower()]
            ticker = client.fetch_ticker(symbol)
            return float(ticker['last'])

        except Exception as e:
            logger.warning(f"Error fetching price from {exchange_name} for {symbol}: {e}")
            return 0.0

    def get_trading_fees(self, symbol: str, user_name: str = None,
                         exchange_name: str = None) -> Dict[str, Any]:
        """
        Get trading fees for a symbol.

        This method routes to the appropriate exchange and returns fee information.
        For authenticated users, it will try to get user-specific fees (based on
        volume/VIP tier). Falls back to market defaults for unauthenticated requests.

        :param symbol: The trading symbol (e.g., 'BTC/USDT').
        :param user_name: Optional user name for user-specific fees.
        :param exchange_name: Optional exchange name (defaults to default exchange).
        :return: Dict with 'maker', 'taker', and 'source' keys.
        """
        default_fees = {'maker': 0.001, 'taker': 0.001, 'source': 'default'}

        try:
            # Try to get user-specific exchange first
            if user_name and exchange_name:
                exchange = self.get_exchange(ename=exchange_name, uname=user_name)
                if exchange:
                    return exchange.get_trading_fees(symbol)

            # Fall back to default exchange
            self.connect_default_exchange()
            if self.default_exchange:
                return self.default_exchange.get_trading_fees(symbol)

        except Exception as e:
            logger.warning(f"Could not fetch trading fees for {symbol}: {e}")

        return default_fees

    def get_margin_info(self, symbol: str, user_name: str = None,
                        exchange_name: str = None) -> Dict[str, Any]:
        """
        Get margin trading information for a symbol.

        :param symbol: The trading symbol (e.g., 'BTC/USDT').
        :param user_name: Optional user name.
        :param exchange_name: Optional exchange name.
        :return: Dict with margin info (margin_enabled, max_leverage, margin_modes).
        """
        default_info = {'margin_enabled': False, 'max_leverage': 1, 'margin_modes': []}

        try:
            if user_name and exchange_name:
                exchange = self.get_exchange(ename=exchange_name, uname=user_name)
                if exchange:
                    return exchange.get_margin_info(symbol)

            self.connect_default_exchange()
            if self.default_exchange:
                return self.default_exchange.get_margin_info(symbol)

        except Exception as e:
            logger.warning(f"Could not fetch margin info for {symbol}: {e}")

        return default_info

    def get_trade_status(self, trade) -> str:
        """
        Get the status of a trade order.

        For paper/backtest modes, assumes immediate fill (returns 'FILLED').
        For live mode, should query the exchange.

        :param trade: The trade object.
        :return: Status string ('FILLED', 'PARTIALLY_FILLED', 'PENDING', etc.)
        """
        # If order is None, it hasn't been placed yet
        if trade.order is None:
            return 'PENDING'

        # For paper trading / backtesting, assume immediate fill
        # TODO: For live trading, query exchange via get_trade_info
        return 'FILLED'

    def get_trade_executed_qty(self, trade) -> float:
        """
        Get the executed quantity of a trade order.

        For paper/backtest modes, returns the full order quantity (assumes full fill).

        :param trade: The trade object.
        :return: Executed quantity.
        """
        # For paper trading / backtesting, assume full fill
        return trade.base_order_qty

    @staticmethod
    def _positive_price(value: Any) -> float | None:
        """Normalize a value to a strictly positive float price."""
        try:
            price = float(value)
            return price if price > 0 else None
        except (TypeError, ValueError):
            return None

    def get_trade_executed_price(self, trade, fallback_price: float | None = None) -> float:
        """
        Get the executed price of a trade order.

        For paper/backtest modes, resolves a best-effort execution price and
        never silently returns 0.0.

        :param trade: The trade object.
        :param fallback_price: Optional known-good price from caller context
                               (e.g. current tick price in Trades.update()).
        :return: Executed price.
        """
        # 1) Explicit order price (e.g. limit orders)
        order_price = self._positive_price(getattr(trade, 'order_price', None))
        if order_price is not None:
            return order_price

        # 2) Caller-provided fallback (e.g. current candle/tick close)
        resolved_fallback = self._positive_price(fallback_price)
        if resolved_fallback is not None:
            return resolved_fallback

        # 3) Trade object fallbacks from known internal state
        for attr_name in ('entry_price',):
            attr_price = self._positive_price(getattr(trade, attr_name, None))
            if attr_price is not None:
                return attr_price

        stats = getattr(trade, 'stats', None)
        if isinstance(stats, dict):
            for key in ('current_price', 'opening_price', 'settled_price'):
                stat_price = self._positive_price(stats.get(key))
                if stat_price is not None:
                    return stat_price

        # 4) Exchange lookup as last resort
        try:
            market_price = self._positive_price(self.get_price(trade.symbol))
            if market_price is not None:
                return market_price
        except Exception as e:
            logger.warning(
                "Failed to resolve executed price from exchange for trade %s (%s): %s",
                getattr(trade, 'unique_id', 'unknown'),
                getattr(trade, 'symbol', 'unknown'),
                e
            )

        raise ValueError(
            f"Unable to resolve executed price for trade "
            f"{getattr(trade, 'unique_id', 'unknown')} "
            f"({getattr(trade, 'symbol', 'unknown')})"
        )

    def get_user_balance(self, user_id: int) -> float:
        """
        Get the balance for a user.

        Stub implementation for paper trading.

        :param user_id: The user ID.
        :return: Balance amount (default 10000.0 for paper trading).
        """
        # TODO: Implement proper balance tracking per user
        # For paper trading, return a default balance
        return 10000.0

    def disconnect_exchange(self, user_name: str, exchange_name: str) -> bool:
        """
        Disconnect an exchange for a user, cleaning up EDM session if present.

        :param user_name: The name of the user.
        :param exchange_name: The name of the exchange.
        :return: True if successful, False otherwise.
        """
        try:
            exchange = self.get_exchange(ename=exchange_name, uname=user_name)

            # Clean up EDM session
            if exchange.edm_session_id and self.edm_client:
                try:
                    self.edm_client.delete_session_sync(exchange.edm_session_id)
                    logger.info(f"Deleted EDM session {exchange.edm_session_id} for {exchange_name}")
                except Exception as e:
                    logger.warning(f"Failed to delete EDM session: {e}")

            # Remove from cache
            self.cache_manager.remove_row_from_datacache(
                cache_name='exchange_data',
                filter_vals=[('user', user_name), ('name', exchange_name)]
            )
            return True
        except Exception as e:
            logger.error(f"Failed to disconnect exchange {exchange_name} for {user_name}: {e}")
            return False
