"""
IBKR Adapter
============
BrokerAdapter implementation for Interactive Brokers via the CPZ AI Gateway Service.

This adapter communicates with the EC2-hosted IBKR Gateway Service, which manages
IB Gateway Docker containers and handles TWS API protocol communication.
"""

from __future__ import annotations

import os
import logging
from datetime import datetime
from typing import AsyncIterator, Iterable, Optional, Dict, Any, List

import requests

from ..enums import OrderSide, OrderType, TimeInForce
from ..interfaces import BrokerAdapter
from ..models import (
    OrderSubmitRequest,
    OrderReplaceRequest,
    Order,
    Account,
    Position,
    Quote,
    Bar,
)
from .mapping import (
    map_order_status,
    map_order_side,
    map_order_type,
    map_time_in_force,
    to_ibkr_side,
    to_ibkr_order_type,
    to_ibkr_tif,
)

logger = logging.getLogger(__name__)

# Default gateway URL
DEFAULT_GATEWAY_URL = "https://ibkr-gateway.cpz-lab.com"


class IBKRAdapter(BrokerAdapter):
    """
    Interactive Brokers adapter for CPZ Python SDK.
    
    Connects to the CPZ AI IBKR Gateway Service for trading operations.
    The gateway service manages IB Gateway Docker containers and handles
    authentication via IB Key mobile app 2FA.
    
    Usage:
        # Via BrokerRouter (recommended)
        from cpz.execution import BrokerRouter
        
        router = BrokerRouter.default()
        router.use_broker('ibkr', account_id='U1234567')
        
        account = router.get_account()
        positions = router.get_positions()
        
        # Direct instantiation
        from cpz.execution.ibkr import IBKRAdapter
        
        adapter = IBKRAdapter.create(account_id='U1234567')
        account = adapter.get_account()
    """
    
    def __init__(
        self,
        gateway_url: str,
        access_token: str,
        account_id: str,
        timeout: int = 30
    ) -> None:
        """
        Initialize IBKR Adapter.
        
        Args:
            gateway_url: URL of the IBKR Gateway Service
            access_token: Supabase JWT token for authentication
            account_id: IBKR account ID
            timeout: Request timeout in seconds
        """
        self._gateway_url = gateway_url.rstrip('/')
        self._access_token = access_token
        self._account_id = account_id
        self._timeout = timeout
        self._headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
    
    @staticmethod
    def create(**kwargs: object) -> "IBKRAdapter":
        """
        Create an IBKR adapter instance.
        
        Automatically retrieves credentials from the CPZ AI platform
        if not explicitly provided.
        
        Args:
            account_id: IBKR account ID (optional, will use first available)
            gateway_url: Override gateway URL (optional)
            access_token: Supabase JWT token (optional, fetched from CPZ platform)
            env: Environment 'paper' or 'live' (optional, for credential lookup)
            
        Returns:
            Configured IBKRAdapter instance
        """
        from ...common.cpz_ai import CPZAIClient
        
        # Get gateway URL from env or kwargs
        gateway_url = str(
            kwargs.get('gateway_url') or 
            os.getenv('IBKR_GATEWAY_URL', DEFAULT_GATEWAY_URL)
        )
        
        account_id = str(kwargs.get('account_id') or '')
        access_token = str(kwargs.get('access_token') or '')
        env = str(kwargs.get('env') or os.getenv('IBKR_ENV', 'paper'))
        
        # Get CPZ AI platform client for credentials
        platform = CPZAIClient.from_env()
        
        # Get access token if not provided
        if not access_token:
            # Use the platform's service token for gateway auth
            access_token = platform.get_service_token()
        
        # Get IBKR session/account info from platform
        if not account_id:
            try:
                # Try to get IBKR credentials from the platform
                creds = platform.get_broker_credentials(
                    broker='ibkr',
                    env=env or None,
                    account_id=None
                )
                if creds:
                    account_id = str(creds.get('account_id', ''))
            except Exception as exc:
                logger.warning(f"Failed to get IBKR credentials from platform: {exc}")
        
        if not account_id:
            raise ValueError(
                "IBKR account_id is required. Provide it directly or configure IBKR "
                "credentials in your CPZ AI account at https://ai.cpz-lab.com/execution"
            )
        
        return IBKRAdapter(
            gateway_url=gateway_url,
            access_token=access_token,
            account_id=account_id
        )
    
    def _request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Make a request to the IBKR Gateway Service.
        
        Args:
            method: HTTP method
            endpoint: API endpoint
            json: JSON body
            params: Query parameters
            
        Returns:
            Response data dict
            
        Raises:
            Exception on request failure
        """
        url = f"{self._gateway_url}{endpoint}"
        
        try:
            response = requests.request(
                method=method,
                url=url,
                headers=self._headers,
                json=json,
                params=params,
                timeout=self._timeout
            )
            
            if response.status_code >= 400:
                error_text = response.text
                try:
                    error_data = response.json()
                    error_text = error_data.get('error', error_text)
                except Exception:
                    pass
                raise Exception(f"IBKR Gateway error ({response.status_code}): {error_text}")
            
            return response.json() if response.text else {}
            
        except requests.exceptions.Timeout:
            raise Exception(f"IBKR Gateway request timeout after {self._timeout}s")
        except requests.exceptions.ConnectionError:
            raise Exception(f"Failed to connect to IBKR Gateway at {self._gateway_url}")
    
    def get_account(self) -> Account:
        """Get account information."""
        result = self._request('GET', f'/api/ibkr/account/{self._account_id}')
        
        data = result.get('data', result)
        
        return Account(
            id=str(data.get('account_id', self._account_id)),
            buying_power=float(data.get('buying_power', 0) or 0),
            equity=float(data.get('equity', 0) or 0),
            cash=float(data.get('cash', 0) or 0)
        )
    
    def get_positions(self) -> list[Position]:
        """Get all positions for the account."""
        result = self._request('GET', f'/api/ibkr/positions', params={'account_id': self._account_id})
        
        data = result.get('data', result)
        if not isinstance(data, list):
            data = []
        
        positions: list[Position] = []
        for p in data:
            positions.append(Position(
                symbol=str(p.get('symbol', '')),
                qty=float(p.get('qty', 0) or 0),
                avg_entry_price=float(p.get('avg_cost', p.get('avg_entry_price', 0)) or 0),
                market_value=float(p.get('market_value', 0) or 0)
            ))
        
        return positions
    
    def submit_order(self, req: OrderSubmitRequest) -> Order:
        """
        Submit an order via the IBKR Gateway.
        
        The gateway records the order to Supabase with strategy_id tracking
        before placing with IBKR.
        """
        order_data = {
            'account_id': self._account_id,
            'symbol': req.symbol,
            'side': to_ibkr_side(req.side),
            'quantity': int(req.qty),
            'order_type': to_ibkr_order_type(req.type),
            'time_in_force': to_ibkr_tif(req.time_in_force),
            'strategy_id': getattr(req, 'strategy_id', None) or '',
        }
        
        # Add limit price if applicable
        if req.type == OrderType.LIMIT and req.limit_price is not None:
            order_data['price'] = req.limit_price
        
        result = self._request('POST', '/api/ibkr/orders', json=order_data)
        
        # Extract order from response
        data = result.get('data', result)
        
        return self._map_order_response(data, req)
    
    def get_order(self, order_id: str) -> Order:
        """Get order by ID."""
        result = self._request('GET', f'/api/ibkr/orders/{order_id}', params={'account_id': self._account_id})
        data = result.get('data', result)
        return self._map_order_response(data)
    
    def cancel_order(self, order_id: str) -> Order:
        """Cancel an order."""
        result = self._request('DELETE', f'/api/ibkr/orders/{order_id}', params={'account_id': self._account_id})
        data = result.get('data', result)
        return self._map_order_response(data)
    
    def replace_order(self, order_id: str, req: OrderReplaceRequest) -> Order:
        """
        Replace/modify an order.
        
        Note: IBKR order modification is limited. This cancels and replaces.
        """
        # Cancel existing order
        self.cancel_order(order_id)
        
        # The replace request doesn't have full order details, so we need to
        # construct a new order submit request. This is a limitation.
        raise NotImplementedError(
            "IBKR order replacement requires full order details. "
            "Cancel the order and submit a new one instead."
        )
    
    def stream_quotes(self, symbols: Iterable[str]) -> AsyncIterator[Quote]:
        """
        Stream real-time quotes.
        
        Note: For real-time streaming, use WebSocket connection directly
        to the IBKR Gateway Service at wss://ibkr-gateway.cpz-lab.com/socket.io/
        """
        async def _gen() -> AsyncIterator[Quote]:
            # Fallback to polling quotes for now
            for symbol in symbols:
                try:
                    result = self._request('GET', f'/api/ibkr/quote/{symbol}')
                    data = result.get('data', result)
                    yield Quote(
                        symbol=str(data.get('symbol', symbol)),
                        bid=float(data.get('bid', 0) or 0),
                        ask=float(data.get('ask', 0) or 0),
                        bid_size=float(data.get('bid_size', 0) or 0),
                        ask_size=float(data.get('ask_size', 0) or 0),
                    )
                except Exception as e:
                    logger.warning(f"Failed to get quote for {symbol}: {e}")
                    yield Quote(symbol=symbol, bid=0.0, ask=0.0)
        
        return _gen()
    
    def get_quotes(self, symbols: list[str]) -> list[Quote]:
        """Get quotes for multiple symbols."""
        quotes: list[Quote] = []
        
        for symbol in symbols:
            try:
                result = self._request('GET', f'/api/ibkr/quote/{symbol}')
                data = result.get('data', result)
                quotes.append(Quote(
                    symbol=str(data.get('symbol', symbol)),
                    bid=float(data.get('bid', 0) or 0),
                    ask=float(data.get('ask', 0) or 0),
                    bid_size=float(data.get('bid_size', 0) or 0),
                    ask_size=float(data.get('ask_size', 0) or 0),
                ))
            except Exception as e:
                logger.warning(f"Failed to get quote for {symbol}: {e}")
                quotes.append(Quote(symbol=symbol, bid=0.0, ask=0.0))
        
        return quotes
    
    def get_historical_data(
        self,
        symbol: str,
        timeframe: str,
        limit: int = 100,
        start: Optional[datetime] = None,
        end: Optional[datetime] = None,
    ) -> list[Bar]:
        """
        Get historical bar data.
        
        Note: IBKR historical data requires market data subscriptions.
        This is a best-effort implementation.
        """
        params: Dict[str, Any] = {
            'symbol': symbol,
            'timeframe': timeframe,
            'limit': limit,
        }
        if start:
            params['start'] = start.isoformat()
        if end:
            params['end'] = end.isoformat()
        
        try:
            result = self._request('GET', '/api/ibkr/historical', params=params)
            data = result.get('data', [])
            
            bars: list[Bar] = []
            for bar in data:
                bars.append(Bar(
                    symbol=symbol,
                    open=float(bar.get('open', 0) or 0),
                    high=float(bar.get('high', 0) or 0),
                    low=float(bar.get('low', 0) or 0),
                    close=float(bar.get('close', 0) or 0),
                    volume=float(bar.get('volume', 0) or 0),
                    ts=datetime.fromisoformat(bar.get('timestamp', datetime.utcnow().isoformat()).replace('Z', '+00:00'))
                ))
            
            return bars
            
        except Exception as e:
            logger.warning(f"Failed to get historical data for {symbol}: {e}")
            return []
    
    def _map_order_response(
        self,
        data: Dict[str, Any],
        req: Optional[OrderSubmitRequest] = None
    ) -> Order:
        """Map gateway order response to Order model."""
        # Get order ID - prefer cpz_order_id for consistency
        order_id = str(
            data.get('cpz_order_id') or
            data.get('order_id') or
            data.get('broker_order_id') or
            ''
        )
        
        # Get symbol
        symbol = str(data.get('symbol', req.symbol if req else ''))
        
        # Map side
        side_str = str(data.get('side', req.side.value if req else 'buy'))
        side = map_order_side(side_str)
        
        # Get quantity
        qty = float(data.get('quantity', req.qty if req else 0) or 0)
        
        # Map order type
        type_str = str(data.get('order_type', req.type.value if req else 'market'))
        order_type = map_order_type(type_str)
        
        # Map time in force
        tif_str = str(data.get('time_in_force', req.time_in_force.value if req else 'DAY'))
        tif = map_time_in_force(tif_str)
        
        # Get status
        status = map_order_status(str(data.get('status', 'pending')))
        
        # Get optional fill details
        filled_qty = data.get('filled_qty')
        avg_fill_price = data.get('avg_fill_price')
        
        return Order(
            id=order_id,
            symbol=symbol,
            side=side,
            qty=qty,
            type=order_type,
            time_in_force=tif,
            status=status,
            filled_qty=float(filled_qty) if filled_qty is not None else None,
            average_fill_price=float(avg_fill_price) if avg_fill_price is not None else None,
        )
