"""
REST API client for exchange-data-manager.
"""

import asyncio
import logging
from datetime import datetime
from typing import Optional, List
from urllib.parse import urlencode

import aiohttp
import pandas as pd
import requests

from .exceptions import EdmConnectionError, EdmTimeoutError, EdmApiError
from .models import Candle, EdmConfig

logger = logging.getLogger(__name__)


class EdmClient:
    """
    REST client for exchange-data-manager API.

    Provides both synchronous and asynchronous methods for fetching candle data
    from the EDM service.
    """

    def __init__(
        self,
        base_url: str = "http://localhost:8080",
        timeout: float = 30.0,
        config: Optional[EdmConfig] = None
    ):
        """
        Initialize EDM client.

        Args:
            base_url: Base URL for EDM REST API
            timeout: Request timeout in seconds
            config: Optional EdmConfig object (overrides base_url and timeout)
        """
        if config:
            self.base_url = config.rest_url.rstrip('/')
            self.timeout = config.timeout
        else:
            self.base_url = base_url.rstrip('/')
            self.timeout = timeout

        self._session: Optional[aiohttp.ClientSession] = None

    # -------------------------------------------------------------------------
    # Synchronous Methods
    # -------------------------------------------------------------------------

    def get_candles_sync(
        self,
        exchange: str,
        symbol: str,
        timeframe: str,
        start: Optional[datetime] = None,
        end: Optional[datetime] = None,
        limit: Optional[int] = None,
        session_id: Optional[str] = None
    ) -> pd.DataFrame:
        """
        Fetch candles from EDM REST API (synchronous).

        Args:
            exchange: Exchange name (e.g., 'binance')
            symbol: Trading pair (e.g., 'BTC/USDT')
            timeframe: Candle timeframe (e.g., '1h', '15m')
            start: Start datetime (optional)
            end: End datetime (optional)
            limit: Maximum number of candles (optional)
            session_id: EDM session ID for authenticated exchanges (optional)

        Returns:
            DataFrame with columns: time, open, high, low, close, volume

        Raises:
            EdmConnectionError: If unable to connect to EDM
            EdmTimeoutError: If request times out
            EdmApiError: If EDM returns an error response
        """
        params = {
            'exchange': exchange,
            'symbol': symbol,
            'timeframe': timeframe,
        }

        if start:
            # EDM expects Unix timestamp in seconds
            params['start'] = int(start.timestamp()) if isinstance(start, datetime) else start
        if end:
            # EDM expects Unix timestamp in seconds
            params['end'] = int(end.timestamp()) if isinstance(end, datetime) else end
        if limit:
            params['limit'] = limit
        if session_id:
            params['session_id'] = session_id

        url = f"{self.base_url}/candles?{urlencode(params)}"

        try:
            logger.debug(f"Fetching candles from EDM: {url}")
            response = requests.get(url, timeout=self.timeout)

            if response.status_code != 200:
                raise EdmApiError(response.status_code, response.text)

            data = response.json()
            candles = data.get('candles', [])

            if not candles:
                logger.warning(f"No candles returned from EDM for {exchange}:{symbol}:{timeframe}")
                return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])

            df = pd.DataFrame(candles)

            # Ensure expected columns exist
            expected_cols = ['time', 'open', 'high', 'low', 'close', 'volume']
            for col in expected_cols:
                if col not in df.columns:
                    df[col] = 0.0 if col != 'time' else 0

            # Sort by time and reset index
            df = df.sort_values('time').reset_index(drop=True)

            logger.debug(f"Received {len(df)} candles from EDM")
            return df[expected_cols]

        except requests.exceptions.ConnectionError as e:
            logger.error(f"Failed to connect to EDM at {self.base_url}: {e}")
            raise EdmConnectionError(f"Failed to connect to EDM: {e}")
        except requests.exceptions.Timeout as e:
            logger.error(f"EDM request timed out after {self.timeout}s: {e}")
            raise EdmTimeoutError(f"Request timed out after {self.timeout}s")
        except requests.exceptions.RequestException as e:
            logger.error(f"EDM request failed: {e}")
            raise EdmConnectionError(f"Request failed: {e}")

    def get_exchanges_sync(self) -> List[str]:
        """
        Get list of available exchanges (synchronous).

        Returns:
            List of exchange names
        """
        url = f"{self.base_url}/exchanges"

        try:
            response = requests.get(url, timeout=self.timeout)
            if response.status_code != 200:
                raise EdmApiError(response.status_code, response.text)
            return response.json().get('exchanges', [])
        except requests.exceptions.RequestException as e:
            raise EdmConnectionError(f"Failed to get exchanges: {e}")

    def get_symbols_sync(self, exchange: str) -> List[str]:
        """
        Get list of symbols for an exchange (synchronous).

        Args:
            exchange: Exchange name

        Returns:
            List of trading pair symbols
        """
        url = f"{self.base_url}/exchanges/{exchange}/symbols"

        try:
            response = requests.get(url, timeout=self.timeout)
            if response.status_code != 200:
                raise EdmApiError(response.status_code, response.text)
            return response.json().get('symbols', [])
        except requests.exceptions.RequestException as e:
            raise EdmConnectionError(f"Failed to get symbols: {e}")

    def health_check_sync(self) -> bool:
        """
        Check if EDM service is healthy (synchronous).

        Returns:
            True if healthy, False otherwise
        """
        try:
            response = requests.get(f"{self.base_url}/health", timeout=5.0)
            return response.status_code == 200
        except requests.exceptions.RequestException:
            return False

    # -------------------------------------------------------------------------
    # Asynchronous Methods
    # -------------------------------------------------------------------------

    async def _get_session(self) -> aiohttp.ClientSession:
        """Get or create aiohttp session."""
        if self._session is None or self._session.closed:
            timeout = aiohttp.ClientTimeout(total=self.timeout)
            self._session = aiohttp.ClientSession(timeout=timeout)
        return self._session

    async def close(self):
        """Close the aiohttp session."""
        if self._session and not self._session.closed:
            await self._session.close()
            self._session = None

    # -------------------------------------------------------------------------
    # Session Management (for authenticated exchanges)
    # -------------------------------------------------------------------------

    def create_session_sync(self) -> str:
        """
        Create a new EDM session for authenticated exchange access.

        Returns:
            Session ID string
        """
        url = f"{self.base_url}/sessions"
        try:
            response = requests.post(url, timeout=self.timeout)
            if response.status_code not in (200, 201):
                raise EdmApiError(response.status_code, response.text)
            return response.json().get('session_id')
        except requests.exceptions.RequestException as e:
            raise EdmConnectionError(f"Failed to create session: {e}")

    def add_credentials_sync(
        self,
        session_id: str,
        exchange: str,
        api_key: str,
        api_secret: str,
        passphrase: Optional[str] = None,
        testnet: bool = False
    ) -> bool:
        """
        Add exchange credentials to an EDM session.

        Args:
            session_id: EDM session ID
            exchange: Exchange name
            api_key: API key
            api_secret: API secret
            passphrase: Optional passphrase (for exchanges like KuCoin)
            testnet: Whether to use testnet

        Returns:
            True if successful
        """
        url = f"{self.base_url}/sessions/{session_id}/credentials"
        payload = {
            'exchange': exchange,
            'api_key': api_key,
            'api_secret': api_secret,
            'testnet': testnet
        }
        if passphrase:
            payload['passphrase'] = passphrase

        try:
            response = requests.post(url, json=payload, timeout=self.timeout)
            if response.status_code not in (200, 201):
                raise EdmApiError(response.status_code, response.text)
            return True
        except requests.exceptions.RequestException as e:
            raise EdmConnectionError(f"Failed to add credentials: {e}")

    def delete_session_sync(self, session_id: str) -> bool:
        """
        Delete an EDM session.

        Args:
            session_id: EDM session ID

        Returns:
            True if successful
        """
        url = f"{self.base_url}/sessions/{session_id}"
        try:
            response = requests.delete(url, timeout=self.timeout)
            return response.status_code in (200, 204)
        except requests.exceptions.RequestException:
            return False

    async def get_candles(
        self,
        exchange: str,
        symbol: str,
        timeframe: str,
        start: Optional[datetime] = None,
        end: Optional[datetime] = None,
        limit: Optional[int] = None,
        session_id: Optional[str] = None
    ) -> pd.DataFrame:
        """
        Fetch candles from EDM REST API (asynchronous).

        Args:
            exchange: Exchange name (e.g., 'binance')
            symbol: Trading pair (e.g., 'BTC/USDT')
            timeframe: Candle timeframe (e.g., '1h', '15m')
            start: Start datetime (optional)
            end: End datetime (optional)
            limit: Maximum number of candles (optional)
            session_id: EDM session ID for authenticated exchanges (optional)

        Returns:
            DataFrame with columns: time, open, high, low, close, volume
        """
        params = {
            'exchange': exchange,
            'symbol': symbol,
            'timeframe': timeframe,
        }

        if start:
            # EDM expects Unix timestamp in seconds
            params['start'] = int(start.timestamp()) if isinstance(start, datetime) else start
        if end:
            # EDM expects Unix timestamp in seconds
            params['end'] = int(end.timestamp()) if isinstance(end, datetime) else end
        if limit:
            params['limit'] = limit
        if session_id:
            params['session_id'] = session_id

        url = f"{self.base_url}/candles?{urlencode(params)}"
        aio_session = await self._get_session()

        try:
            logger.debug(f"Fetching candles from EDM (async): {url}")
            async with aio_session.get(url) as response:
                if response.status != 200:
                    text = await response.text()
                    raise EdmApiError(response.status, text)

                data = await response.json()
                candles = data.get('candles', [])

                if not candles:
                    return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])

                df = pd.DataFrame(candles)
                expected_cols = ['time', 'open', 'high', 'low', 'close', 'volume']
                for col in expected_cols:
                    if col not in df.columns:
                        df[col] = 0.0 if col != 'time' else 0

                df = df.sort_values('time').reset_index(drop=True)
                return df[expected_cols]

        except aiohttp.ClientConnectionError as e:
            raise EdmConnectionError(f"Failed to connect to EDM: {e}")
        except asyncio.TimeoutError:
            raise EdmTimeoutError(f"Request timed out after {self.timeout}s")

    async def get_exchanges(self) -> List[str]:
        """Get list of available exchanges (asynchronous)."""
        session = await self._get_session()
        url = f"{self.base_url}/exchanges"

        try:
            async with session.get(url) as response:
                if response.status != 200:
                    text = await response.text()
                    raise EdmApiError(response.status, text)
                data = await response.json()
                return data.get('exchanges', [])
        except aiohttp.ClientConnectionError as e:
            raise EdmConnectionError(f"Failed to get exchanges: {e}")

    async def get_symbols(self, exchange: str) -> List[str]:
        """Get list of symbols for an exchange (asynchronous)."""
        session = await self._get_session()
        url = f"{self.base_url}/exchanges/{exchange}/symbols"

        try:
            async with session.get(url) as response:
                if response.status != 200:
                    text = await response.text()
                    raise EdmApiError(response.status, text)
                data = await response.json()
                return data.get('symbols', [])
        except aiohttp.ClientConnectionError as e:
            raise EdmConnectionError(f"Failed to get symbols: {e}")

    async def health_check(self) -> bool:
        """Check if EDM service is healthy (asynchronous)."""
        try:
            session = await self._get_session()
            async with session.get(f"{self.base_url}/health") as response:
                return response.status == 200
        except Exception:
            return False

    def __del__(self):
        """Cleanup on deletion."""
        if self._session and not self._session.closed:
            try:
                loop = asyncio.get_event_loop()
                if loop.is_running():
                    loop.create_task(self.close())
                else:
                    loop.run_until_complete(self.close())
            except Exception:
                pass
