from datetime import datetime
import asyncio
from logging import getLogger
from typing import Callable

import MetaTrader5
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, \
    TradePosition, OrderSendResult, OrderCheckResult

from .constants import TimeFrame, CopyTicks, OrderType
from .errors import Error
from .config import Config

logger = getLogger()


class BaseMeta(type):
    def __new__(mcs, cls_name, bases, cls_dict):
        defaults = MetaTrader5.__dict__
        defaults = {f'_{key}': value for key, value in defaults.items() if not key.startswith('_')}
        cls_dict |= defaults
        return super().__new__(mcs, cls_name, bases, cls_dict)


class MetaTrader(metaclass=BaseMeta):
    _account_info: Callable
    _copy_rates_from: Callable
    _copy_rates_from_pos: Callable
    _copy_rates_range: Callable
    _copy_ticks_from: Callable
    _copy_ticks_range: Callable
    _history_deals_get: Callable
    _history_deals_total: Callable
    _history_orders_get: Callable
    _history_orders_total: Callable
    _initialize: Callable
    _last_error: Callable
    _login: Callable
    _market_book_add: Callable
    _market_book_get: Callable
    _market_book_release: Callable
    _order_calc_margin: Callable
    _order_calc_profit: Callable
    _order_check: Callable
    _order_send: Callable
    _orders_get: Callable
    _orders_total: Callable
    _positions_get: Callable
    _positions_total: Callable
    _shutdown: Callable
    _symbol_info: Callable
    _symbol_info_tick: Callable
    _symbol_select: Callable
    _symbols_get: Callable
    _symbols_total: Callable
    _terminal_info: Callable
    _version: Callable
    error: Error
    config: Config

    def __init__(self):
        self.config = Config()

    async def __aenter__(self) -> 'MetaTrader':
        """
        Async context manager entry point.
        Initializes the connection to the MetaTrader terminal.

        Returns:
            MetaTrader: An instance of the MetaTrader class.
        """
        await self.initialize(**Config().account_info())
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """
        Async context manager exit point. Closes the connection to the MetaTrader terminal.
        """
        await self.shutdown()

    async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool:
        """
        Connects to the MetaTrader terminal using the specified login, password and server.

        Args:
            login (int): The trading account number.
            password (str): The trading account password.
            server (str): The trading server name.
            timeout (int): The timeout for the connection in seconds.

        Returns:
            bool: True if successful, False otherwise.
        """
        return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout)

    async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
                         timeout: int | None = None, portable=False) -> bool:
        """
        Initializes the connection to the MetaTrader terminal. All parameters are optional.

        Keyword Args:
            path (str): The path to the MetaTrader terminal executable.
            login (int): The trading account number.
            password (str): The trading account password.
            server (str): The trading server name.
            timeout (int): The timeout for the connection in seconds.
            portable (bool): If True, the terminal will be launched in portable mode.

        Returns:
            bool: True if successful, False otherwise.
        """
        args = (str(path),) if path else ()
        kwargs = {key: value for key, value in (('login', login), ('password', password), ('server', server),
                                                ('timeout', timeout), ('portable', portable)) if value}
        return await asyncio.to_thread(self._initialize, *args, **kwargs)

    async def shutdown(self) -> None:
        """
        Closes the connection to the MetaTrader terminal.

        Returns:
            None: None
        """
        return await asyncio.to_thread(self._shutdown)

    async def last_error(self) -> tuple[int, str]:
        try:
            return await asyncio.to_thread(self._last_error)
        except Exception as err:
            logger.warning(f'Error in obtaining last error.')
            return -1, str(err)

    async def version(self) -> tuple[int, int, str] | None:
        """"""
        res = await asyncio.to_thread(self._version)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining version information.{self.error.description}')
        return res

    async def account_info(self) -> AccountInfo | None:
        """"""
        res = await asyncio.to_thread(self._account_info)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining account information.{self.error.description}')
        return res

    async def terminal_info(self) -> TerminalInfo | None:
        res = await asyncio.to_thread(self._terminal_info)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining terminal information.{self.error.description}')
            return res
        return res

    async def symbols_total(self) -> int:
        return await asyncio.to_thread(self._symbols_total)

    async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
        kwargs = {'group': group} if group else {}
        res = await asyncio.to_thread(self._symbols_get, **kwargs)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining symbols.{self.error.description}')
            return res
        return res

    async def symbol_info(self, symbol: str) -> SymbolInfo | None:
        res = await asyncio.to_thread(self._symbol_info, symbol)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining information for {symbol}.{self.error.description}')
            return res
        return res

    async def symbol_info_tick(self, symbol: str) -> Tick | None:
        res = await asyncio.to_thread(self._symbol_info_tick, symbol)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining tick for {symbol}.{self.error.description}')
            return res
        return res

    async def symbol_select(self, symbol: str, enable: bool) -> bool:
        return await asyncio.to_thread(self._symbol_select, symbol, enable)

    async def market_book_add(self, symbol: str) -> bool:
        return await asyncio.to_thread(self._market_book_add, symbol)

    async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
        res = await asyncio.to_thread(self._market_book_get, symbol)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining market depth content for {symbol}.{self.error.description}')
            return res
        return res

    async def market_book_release(self, symbol: str) -> bool:
        return await asyncio.to_thread(self._market_book_release, symbol)

    async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int):
        res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
            return res
        return res

    async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int):
        res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
            return res
        return res

    async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
                               date_to: datetime | float):
        res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
            return res
        return res

    async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks):
        res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
            return res
        return res

    async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
                               flags: CopyTicks):
        res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
            return res
        return res

    async def orders_total(self) -> int:
        return await asyncio.to_thread(self._orders_total)

    async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
        """Get active orders with the ability to filter by symbol or ticket. There are three call options.
           Call without parameters. Return active orders on all symbols

        Keyword Args:
            symbol (str): Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.

            group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function
                returns only active orders meeting a specified criteria for a symbol name.

            ticket (int): Order ticket (ORDER_TICKET). Optional named parameter.

        Returns:
            tuple[TradeOrder]: A list of active trade orders as TradeOrder objects
        """
        kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
        res = await asyncio.to_thread(self._orders_get, **kwargs)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining orders.{self.error.description}')
            return res
        return res

    async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
        res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in calculating margin.{self.error.description}')
            return res
        return res

    async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
                                price_close: float) -> float | None:
        res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in calculating profit.{self.error.description}')
            return res
        return res

    async def order_check(self, request: dict) -> OrderCheckResult:
        return await asyncio.to_thread(self._order_check, request)

    async def order_send(self, request: dict) -> OrderSendResult:
        return await asyncio.to_thread(self._order_send, request)

    async def positions_total(self) -> int:
        return await asyncio.to_thread(self._positions_total)

    async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None:
        kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
        res = await asyncio.to_thread(self._positions_get, **kwargs)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in obtaining open positions.{self.error.description}')
            return res
        return res

    async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
        return await asyncio.to_thread(self._history_orders_total, date_from, date_to)

    async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
                                 group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None:
        kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
        args = tuple(arg for arg in (date_from, date_to) if arg)
        res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in getting orders.{self.error.description}')
            return res
        return res

    async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
        return await asyncio.to_thread(self._history_deals_total, date_from, date_to)

    async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
                                group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None:
        kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
        args = tuple(arg for arg in (date_from, date_to) if arg)
        res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
        if res is None:
            err = await self.last_error()
            self.error = Error(*err)
            logger.warning(f'Error in getting deals.{self.error}')
            return res
        return res
