#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

from datetime import datetime, date
from decimal import Decimal
from enum import Enum
from typing import Dict, Iterable, List, Optional, Union, TypeVar
from urllib.parse import quote as urlencode

from backtesterrb30.libs.xnt.models.http_jto import dc, extract_to_model, timestamp_to_dt, str_to_dt
from backtesterrb30.libs.xnt.models.http_jto import Numeric, Serializable


class InstrumentType(Enum):
    STOCK = 'STOCK'
    FUTURE = 'FUTURE'
    BOND = 'BOND'
    FOREX = 'CURRENCY'
    FUND = 'FUND'
    OPTION = 'OPTION'
    CFD = 'CFD'
    CALENDAR_SPREAD = 'CALENDAR_SPREAD'
    FX_SPOT = 'FX_SPOT'


class Durations(Enum):
    day = 'day'
    good_till_cancel = 'good_till_cancel'
    good_till_time = 'good_till_time'
    immediate_or_cancel = 'immediate_or_cancel'
    fill_or_kill = 'fill_or_kill'
    at_the_opening = 'at_the_opening'
    at_the_close = 'at_the_close'
    unknown = 'unknown'


class OrderTypes(Enum):
    market = 'market'
    limit = 'limit'
    stop = 'stop'
    stop_limit = 'stop_limit'
    twap = 'twap'
    iceberg = 'iceberg'
    trailing_stop = 'trailing_stop'
    unknown = 'unknown'


class Side(Enum):
    BUY = 'buy'
    SELL = 'sell'


class PermissionStatus(Enum):
    blocked = "Blocked"
    full_access = 'Full'
    read_only = 'ReadOnly'
    close_only = 'CloseOnly'

    def __eq__(self, other) -> bool:
        return self.name == other.name

    def __ne__(self, other) -> bool:
        return self.name != other.name


class OptionRight(Enum):
    PULL = 'PUT'
    CALL = 'CALL'
    BOTH = "BOTH"


class CandleDurations(Enum):
    DAY1 = 86400
    HOUR1 = 3600
    MIN30 = 1800
    MIN5 = 300
    MIN1 = 60


class DataType(Enum):
    QUOTES = "quotes"
    TRADES = "trades"


class FeedLevel(Enum):
    BP = "best_price"
    MD = "market_depth"
    BEST_PRICE = "best_price"
    MARKET_DEPTH = "market_depth"


class Ordering(Enum):
    ASC = "ASC"
    DESC = "DESC"


class OrderStatuses(Enum):
    created = 'created'
    accepted = 'accepted'
    pending = 'pending'
    placing = 'placing'
    working = 'working'
    cancelled = 'cancelled'
    filled = 'filled'
    rejected = 'rejected'

    @staticmethod
    def terminated(other: Enum):
        return other in (OrderStatuses.filled, OrderStatuses.rejected, OrderStatuses.cancelled)

    @staticmethod
    def active(other: Enum):
        return not OrderStatuses.terminated(other)


class ModifyAction(Enum):
    REPLACE = 'replace'
    CANCEL = 'cancel'


class Scopes(Enum):
    symbols = "symbols"
    feed = "feed"
    change = "change"
    ohlc = "ohlc"
    crossrates = "crossrates"
    summary = "summary"
    orders = "orders"
    transactions = "transactions"
    accounts = "accounts"


class AuthMethods(Enum):
    JWT = "jwt"
    BASIC = "basic"


class QuoteV1(Serializable):

    def __init__(self, symbol_id: str, timestamp: int, bid: Numeric, ask: Numeric) -> None:
        self.timestamp = timestamp_to_dt(timestamp)
        self.symbol_id = symbol_id
        self.bid = dc(bid)
        self.ask = dc(ask)

    @property
    def mid(self) -> Optional[Decimal]:
        if (self.bid is None) or (self.ask is None):
            return None
        else:
            return (self.bid + self.ask) / dc(2)


class QuoteV2(Serializable):
    class Level(Serializable):
        def __init__(self, value: str, size: str) -> None:
            self.value = dc(value)
            self.size = dc(size)

        @property
        def price(self) -> Decimal:
            return self.value

    def __init__(self, symbol_id: str, timestamp: int, bid: List[Dict[str, Numeric]],
                 ask: List[Dict[str, Numeric]]) -> None:
        self.timestamp = timestamp_to_dt(timestamp)
        self.symbol_id = symbol_id
        self.bid = extract_to_model(bid, self.Level)
        self.ask = extract_to_model(ask, self.Level)

    @property
    def mid(self) -> Optional[Decimal]:
        if (self.bid is None) or (self.ask is None):
            return None
        else:
            return (self.bid[0].price + self.ask[0].price) / dc(2)


class QuoteV3(Serializable):
    class Level(Serializable):
        def __init__(self, price: str, size: str) -> None:
            self.price = dc(price)
            self.size = dc(size)

    def __init__(self, symbol_id: str, timestamp: int, bid: List[Dict[str, Numeric]],
                 ask: List[Dict[str, Numeric]]) -> None:
        self.timestamp = timestamp_to_dt(timestamp)
        self.symbol_id = symbol_id
        self.bid = extract_to_model(bid, self.Level)
        self.ask = extract_to_model(ask, self.Level)

    @property
    def mid(self) -> Optional[Decimal]:
        if (self.bid is None) or (self.ask is None):
            return None
        else:
            return (self.bid[0].price + self.ask[0].price) / dc(2)


QuoteType = TypeVar('QuoteType', QuoteV1, QuoteV2, QuoteV3, covariant=True)


class TradeV1(Serializable):
    def __init__(self, timestamp: int, symbol_id: str, value: str, size: str) -> None:
        self.timestamp = timestamp_to_dt(timestamp)
        self.symbol_id = symbol_id
        self.value = dc(value)
        self.size = dc(size)


class TradeV2(TradeV1):
    pass


class TradeV3(Serializable):
    def __init__(self, timestamp: int, symbol_id: str, price: str, size: str) -> None:
        self.timestamp = timestamp_to_dt(timestamp)
        self.symbol_id = symbol_id
        self.price = dc(price)
        self.size = dc(size)


TradeType = TypeVar('TradeType', TradeV1, TradeV2, TradeV3, covariant=True)


class ChangeV1(Serializable):
    def __init__(self, base_price: str, daily_change: str, symbol_id: str) -> None:
        self.base_price = dc(base_price)
        self.daily_change = dc(daily_change)
        self.symbol_id = symbol_id


class ChangeV2(ChangeV1):
    pass


class ChangeV3(Serializable):
    def __init__(self, last_session_close_price: str, daily_change: str, symbol_id: str) -> None:
        self.last_session_close_price = dc(last_session_close_price)
        self.daily_change = dc(daily_change)
        self.symbol_id = symbol_id


ChangeType = TypeVar('ChangeType', ChangeV1, ChangeV2, ChangeV3, covariant=True)


class OHLCQuotes(Serializable):
    def __init__(self, open_: Numeric, low: Numeric, high: Numeric, close: Numeric, timestamp: int) -> None:
        self.open_ = dc(open_)
        self.low = dc(low)
        self.high = dc(high)
        self.close = dc(close)
        self.timestamp = timestamp_to_dt(timestamp)


class OHLCTrades(OHLCQuotes):
    def __init__(self, open_: Numeric, low: Numeric, high: Numeric, close: Numeric, timestamp: int,
                 volume: Numeric) -> None:
        super().__init__(open_, low, high, close, timestamp)
        self.volume = dc(volume)


class Crossrate(Serializable):
    def __init__(self, pair: str, symbol_id: str, rate: str) -> None:
        self.pair = pair
        self.symbol_id = symbol_id
        self.rate = rate


class SummaryV1(Serializable):
    class CurrencyPos(Serializable):
        def __init__(self, code: str, value: float, converted_value: float) -> None:
            self.code = code
            self.value = dc(value)
            self.converted_value = dc(converted_value)

    class Position(Serializable):
        def __init__(self, id_: str, symbol_type: str, currency: str, price: float, average_price: float,
                     quantity: float, value: float, converted_value: float, pnl: float, converted_pnl: float) -> None:
            self.id_ = id_
            self.symbol_type = Serializable.to_enum(symbol_type, InstrumentType)
            self.currency = currency
            self.price = dc(price)
            self.average_price = dc(average_price)
            self.quantity = dc(quantity)
            self.value = dc(value)
            self.converted_value = dc(converted_value)
            self.pnl = dc(pnl)
            self.converted_pnl = dc(converted_pnl)

    def __init__(self, account: str, timestamp: int, currency: str, margin_utilization: float, free_money: float,
                 net_asset_value: float, money_used_for_margin: float, session_date: str, currencies: List[str],
                 positions: List[Dict[str, Union[str, float]]]) -> None:
        self.account = account
        self.positions = extract_to_model(positions, self.Position)
        self.currencies = extract_to_model(currencies, self.CurrencyPos)
        self.session_date = session_date
        self.free_money = dc(free_money)
        self.timestamp = timestamp_to_dt(timestamp)
        self.net_asset_value = dc(net_asset_value)
        self.margin_utilization = dc(margin_utilization)
        self.money_used_for_margin = dc(money_used_for_margin)
        self.currency = currency


class SummaryV2(Serializable):
    class CurrencyPos(Serializable):
        def __init__(self, code: str, value: str, converted_value: str) -> None:
            self.code = code
            self.value = dc(value)
            self.converted_value = dc(converted_value)

    class Position(Serializable):
        def __init__(self, id_: str, symbol_type: str, currency: str, price: str, average_price: str, quantity: str,
                     value: str, converted_value: str, pnl: str, converted_pnl: str) -> None:
            self.id_ = id_
            self.symbol_type = Serializable.to_enum(symbol_type, InstrumentType)
            self.currency = currency
            self.price = dc(price)
            self.average_price = dc(average_price)
            self.quantity = dc(quantity)
            self.value = dc(value)
            self.converted_value = dc(converted_value)
            self.pnl = dc(pnl)
            self.converted_pnl = dc(converted_pnl)

    def __init__(self, account: str, timestamp: int, currency: str, margin_utilization: str, free_money: str,
                 net_asset_value: str, money_used_for_margin: str, session_date: Optional[List[int]],
                 currencies: List[Dict[str, str]],  positions: List[Dict[str, str]]) -> None:
        self.account = account
        self.positions = extract_to_model(positions, self.Position)
        self.currencies = extract_to_model(currencies, self.CurrencyPos)
        self.session_date = datetime(*session_date) if session_date else None
        self.free_money = dc(free_money)
        self.timestamp = timestamp_to_dt(timestamp)
        self.net_asset_value = dc(net_asset_value)
        self.margin_utilization = dc(margin_utilization)
        self.money_used_for_margin = dc(money_used_for_margin)
        self.currency = currency


class SummaryV3(Serializable):
    class CurrencyPos(Serializable):
        def __init__(self, code: str, price: str, converted_value: str) -> None:
            self.code = code
            self.price = dc(price)
            self.converted_value = dc(converted_value)

    class Position(Serializable):
        def __init__(self, symbol_id: str, symbol_type: str, currency: str, price: str,
                     average_price: str, quantity: str, value: str, converted_value: str,
                     pnl: str, converted_pnl: str) -> None:
            self.symbol_id = symbol_id
            self.symbol_type = Serializable.to_enum(symbol_type, InstrumentType)
            self.currency = currency
            self.price = dc(price)
            self.average_price = dc(average_price)
            self.quantity = dc(quantity)
            self.value = dc(value)
            self.converted_value = dc(converted_value)
            self.pnl = dc(pnl)
            self.converted_pnl = dc(converted_pnl)

    def __init__(self, account_id: str, timestamp: int, currency: str, margin_utilization: float, free_money: float,
                 net_asset_value: float, money_used_for_margin: float, session_date: str, currencies: List[str],
                 positions: List[Dict[str, str]]) -> None:
        self.account_id = account_id
        self.positions = extract_to_model(positions, self.Position)
        self.currencies = extract_to_model(currencies, self.CurrencyPos)
        self.session_date = session_date
        self.free_money = dc(free_money)
        self.timestamp = timestamp_to_dt(timestamp)
        self.net_asset_value = dc(net_asset_value)
        self.margin_utilization = dc(margin_utilization)
        self.money_used_for_margin = dc(money_used_for_margin)
        self.currency = currency


SummaryType = TypeVar('SummaryType', SummaryV1, SummaryV2, SummaryV3, covariant=True)


class TransactionV1(Serializable):
    def __init__(self, operation_type: str, id_: str, asset: Optional[str], when: int, sum_: float,
                 symbol_id: Optional[str] = None, account_id: Optional[str] = None,
                 order_id: Optional[str] = None, order_pos: Optional[int] = None,
                 uuid_: Optional[str] = None, value_date: Optional[str] = None) -> None:
        self.operation_type = operation_type
        self.id_ = id_
        self.asset = asset
        self.when = timestamp_to_dt(when)
        self.sum_ = sum_
        self.symbol_id = symbol_id
        self.account_id = account_id
        self.order_id = order_id
        self.order_pos = dc(order_pos)
        self.uuid = uuid_
        self.value_date = date.fromisoformat(value_date) if value_date else None  # type: date


class TransactionV2(TransactionV1):
    pass


class TransactionV3(Serializable):
    def __init__(self, operation_type: Optional[str] = None, id_: Optional[str] = None, timestamp: Optional[int] = None,
                 sum_: Optional[float] = None, asset: Optional[str] = None, symbol_id: Optional[str] = None,
                 account_id: Optional[str] = None, order_id: Optional[str] = None, order_pos: Optional[int] = None,
                 uuid_: Optional[str] = None, value_date: Optional[str] = None) -> None:
        self.operation_type = operation_type
        self.id_ = id_
        self.asset = asset
        self.timestamp = timestamp_to_dt(timestamp)
        self.sum_ = sum_
        self.symbol_id = symbol_id
        self.account_id = account_id
        self.order_id = order_id
        self.order_pos = dc(order_pos)
        self.uuid = uuid_
        self.value_date = date.fromisoformat(value_date) if value_date else None  # type: date


TransactionType = TypeVar('TransactionType', TransactionV1, TransactionV2, TransactionV3, covariant=True)


class UserAccount(Serializable):
    def __init__(self, status: Union[str, PermissionStatus], account_id: str) -> None:
        self.status = Serializable.to_enum(status, PermissionStatus)
        self.account_id = account_id


class Identifiers(Serializable):
    def __init__(self, isin: Optional[str] = None, figi: Optional[str] = None, cusip: Optional[str] = None,
                 ric: Optional[str] = None, sedol: Optional[str] = None) -> None:
        self.isin = isin
        self.figi = figi
        self.cusip = cusip
        self.ric = ric
        self.sedol = sedol


class SymbolV1(Serializable):
    class OptionData(Serializable):
        def __init__(self, option_group_id: str, right: str, strike_price: float) -> None:
            self.option_group_id = option_group_id
            self.right = Serializable.to_enum(right, OptionRight)
            self.strike_price = dc(strike_price)

    def __init__(self, name: str, description: str, country: str, exchange: str, id_: str,
                 currency: str, mpi: float, type_: str, ticker: str, group: Optional[str] = None,
                 option_data: Optional[Dict[str, str]] = None, expiration: Optional[int] = None) -> None:
        self.option_data = extract_to_model(option_data, self.OptionData)
        self.i18n = {}
        self.name = name
        self.description = description
        self.country = country
        self.exchange = exchange
        self.id_ = id_
        self.currency = currency
        self.mpi = dc(mpi)
        self.type_ = Serializable.to_enum(type_, InstrumentType)
        self.ticker = ticker
        self.expiration = timestamp_to_dt(expiration)
        self.group = group


class SymbolV2(Serializable):
    class OptionData(Serializable):
        def __init__(self, option_group_id: str, right: str, strike_price: str) -> None:
            self.option_group_id = option_group_id
            self.right = Serializable.to_enum(right, OptionRight)
            self.strike_price = dc(strike_price)

    def __init__(self, name: str, description: str, country: str, exchange: str, id_: str,
                 currency: str, mpi: str, type_: str, ticker: str, group: Optional[str] = None,
                 option_data: Optional[Dict[str, str]] = None, expiration: Optional[int] = None,
                 identifiers: Optional[Dict[str, str]] = None) -> None:
        self.option_data = extract_to_model(option_data, self.OptionData)
        self.i18n = {}
        self.name = name
        self.description = description
        self.country = country
        self.exchange = exchange
        self.id_ = id_
        self.currency = currency
        self.mpi = dc(mpi)
        self.type_ = Serializable.to_enum(type_, InstrumentType)
        self.ticker = ticker
        self.expiration = timestamp_to_dt(expiration)
        self.group = group
        self.identifiers = extract_to_model(identifiers, Identifiers)


class SymbolV3(Serializable):
    class OptionData(Serializable):
        def __init__(self, option_group_id: str, option_right: str, strike_price: str) -> None:
            self.option_group_id = option_group_id
            self.option_right = Serializable.to_enum(option_right, OptionRight)
            self.strike_price = dc(strike_price)

    def __init__(self, name: str, description: str, country: str, exchange: str, symbol_id: str, currency: str,
                 min_price_increment: str, symbol_type: str, ticker: str, group: str, expiration: Optional[int] = None,
                 option_data: Optional[Dict[str, str]] = None, underlying_symbol_id: Optional[str] = None,
                 identifiers: Optional[Dict[str, str]] = None, icon: Optional[str] = None) -> None:
        self.option_data = extract_to_model(option_data, self.OptionData)
        self.name = name
        self.description = description
        self.country = country
        self.exchange = exchange
        self.symbol_id = symbol_id
        self.currency = currency
        self.min_price_increment = dc(min_price_increment)
        self.symbol_type = Serializable.to_enum(symbol_type, InstrumentType)
        self.ticker = ticker
        self.expiration = timestamp_to_dt(expiration)
        self.group = group
        self.underlying_symbol_id = underlying_symbol_id
        self.identifiers = extract_to_model(identifiers, Identifiers)
        self.icon = icon


SymbolType = TypeVar('SymbolType', SymbolV1, SymbolV2, SymbolV3, covariant=True)


class SymbolSpecification(Serializable):
    def __init__(self, leverage: Numeric, contract_multiplier: Numeric, price_unit: Numeric, units: str,
                 lot_size: Numeric) -> None:
        self.leverage = dc(leverage)
        self.contract_multiplier = dc(contract_multiplier)
        self.price_unit = dc(price_unit)
        self.units = units
        self.lot_size = dc(lot_size)


class Exchange(Serializable):
    def __init__(self, id_: str, name: str, country: str) -> None:
        self.id_ = id_
        self.name = name
        self.country = country


class Group(Serializable):
    def __init__(self, group: str, name: str, types: List[str], exchange: str) -> None:
        self.group = group
        self.name = name
        self.types = [Serializable.to_enum(ty, InstrumentType) for ty in types]
        self.exchange = exchange


class Schedule(Serializable):
    class Interval(Serializable):
        class Period(Serializable):
            def __init__(self, start: int, end: int) -> None:
                self.start = timestamp_to_dt(start)
                self.end = timestamp_to_dt(end)

        def __init__(self, name: str, period: Dict[str, int], order_types: Optional[Dict[str, str]] = None) -> None:
            self.name = name
            self.period = extract_to_model(period, self.Period)
            self.order_types = order_types

        @property
        def start(self) -> datetime:
            return self.period.start

        @property
        def end(self) -> datetime:
            return self.period.end

    def __init__(self, intervals: list) -> None:
        self.intervals = extract_to_model(intervals, self.Interval)


class OrderSentV1(Serializable):
    order_type = None  # type: OrderTypes

    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None) -> None:
        self.account = account
        self.instrument = instrument
        self.side = side
        self.quantity = dc(quantity)
        self.duration = duration
        self.client_tag = client_tag
        self.oco_group = oco_group
        self.if_done_parent_id = if_done_parent_id


class OrderSentV2(Serializable):
    order_type = None  # type: OrderTypes

    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        self.account_id = account_id
        self.instrument = instrument
        self.side = side
        self.quantity = dc(quantity)
        self.duration = duration
        self.client_tag = client_tag
        self.oco_group = oco_group
        self.if_done_parent_id = if_done_parent_id
        self.take_profit = take_profit
        self.stop_loss = stop_loss


class OrderSentV3(Serializable):
    order_type = None  # type: OrderTypes

    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        self.account_id = account_id
        self.symbol_id = symbol_id
        self.side = side
        self.quantity = dc(quantity)
        self.duration = duration
        self.client_tag = client_tag
        self.oco_group = oco_group
        self.if_done_parent_id = if_done_parent_id
        self.take_profit = take_profit
        self.stop_loss = stop_loss


OrderSentType = TypeVar('OrderSentType', OrderSentV1, OrderSentV2, OrderSentV3, covariant=True)


class OrderMarketV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.market


class OrderMarketV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.market


class OrderMarketV3(OrderSentV3):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.market


OrderMarketType = TypeVar("OrderMarketType", OrderMarketV1, OrderMarketV2, OrderMarketV3, covariant=True)


class OrderLimitV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.limit
        self.limit_price = dc(limit_price)


class OrderLimitV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.limit
        self.limit_price = dc(limit_price)


class OrderLimitV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.limit
        self.limit_price = limit_price


OrderLimitType = TypeVar("OrderLimitType", OrderLimitV1, OrderLimitV2, OrderLimitV3, covariant=True)


class OrderStopV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 stop_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.stop
        self.stop_price = stop_price


class OrderStopV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 stop_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.stop
        self.stop_price = stop_price


class OrderStopV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 stop_price: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.stop
        self.stop_price = stop_price


OrderStopType = TypeVar("OrderStopType", OrderStopV1, OrderStopV2, OrderStopV3, covariant=True)


class OrderStopLimitV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, stop_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.stop_limit
        self.limit_price = limit_price
        self.stop_price = stop_price


class OrderStopLimitV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, stop_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.stop_limit
        self.limit_price = limit_price
        self.stop_price = stop_price


class OrderStopLimitV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 limit_price: Numeric, stop_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.stop_limit
        self.limit_price = limit_price
        self.stop_price = stop_price


OrderStopLimitType = TypeVar("OrderStopLimitType", OrderStopLimitV1, OrderStopLimitV2, OrderStopLimitV3, covariant=True)


class OrderTrailingStopV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 price_distance: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.trailing_stop
        self.price_distance = price_distance


class OrderTrailingStopV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 price_distance: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.trailing_stop
        self.price_distance = price_distance


class OrderTrailingStopV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 price_distance: Numeric, client_tag: Optional[str] = None, oco_group: Optional[str] = None,
                 if_done_parent_id: Optional[str] = None, take_profit: Optional[Numeric] = None,
                 stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.trailing_stop
        self.price_distance = price_distance


OrderTrailingStopType = TypeVar("OrderTrailingStopType", OrderTrailingStopV1, OrderTrailingStopV2, OrderTrailingStopV3,
                                covariant=True)


class OrderTwapV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, place_interval: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.twap
        self.part_quantity = part_quantity
        self.place_interval = place_interval


class OrderTwapV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, place_interval: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.twap
        self.part_quantity = part_quantity
        self.place_interval = place_interval


class OrderTwapV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, place_interval: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.twap
        self.part_quantity = part_quantity
        self.place_interval = place_interval


OrderTwapType = TypeVar("OrderTwapType", OrderTwapV1, OrderTwapV2, OrderTwapV3, covariant=True)


class OrderIcebergV1(OrderSentV1):
    def __init__(self, account: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, limit_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None) -> None:
        super().__init__(account, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id)
        self.order_type = OrderTypes.iceberg
        self.limit_price = limit_price
        self.part_quantity = part_quantity


class OrderIcebergV2(OrderSentV2):
    def __init__(self, account_id: str, instrument: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, limit_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, instrument, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.iceberg
        self.limit_price = limit_price
        self.part_quantity = part_quantity


class OrderIcebergV3(OrderSentV3):
    def __init__(self, account_id: str, symbol_id: str, side: Side, quantity: Numeric, duration: Durations,
                 part_quantity: Numeric, limit_price: Numeric, client_tag: Optional[str] = None,
                 oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                 take_profit: Optional[Numeric] = None, stop_loss: Optional[Numeric] = None) -> None:
        super().__init__(account_id, symbol_id, side, quantity, duration, client_tag, oco_group, if_done_parent_id,
                         take_profit, stop_loss)
        self.order_type = OrderTypes.iceberg
        self.limit_price = limit_price
        self.part_quantity = part_quantity


OrderIcebergType = TypeVar("OrderIcebergType", OrderIcebergV1, OrderIcebergV2, OrderIcebergV3, covariant=True)


class Fill(Serializable):
    def __init__(self, quantity: str, price: str, position: int, time: str) -> None:
        self.quantity = dc(quantity)
        self.price = dc(price)
        self.position = position
        self.time = str_to_dt(time)


class OrderState(Serializable):
    def __init__(self, last_update: str, status: str, fills: List[Dict[str, Union[str, int]]],
                 reason: Optional[str] = None):
        self.last_update = str_to_dt(last_update)
        self.status = Serializable.to_enum(status, OrderStatuses)
        self.fills = extract_to_model(fills, Fill)
        # TODO: rewrite it to normal Reject model
        self.reason = reason


class Reject(Serializable):
    def __init__(self, group: str, message: str):
        self.group = group
        self.message = message


class OrderV1(Serializable):
    class OrderParameters(Serializable):
        def __init__(self, side: str, duration: str, quantity: str, instrument: str, order_type: str,
                     oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                     limit_price: Optional[str] = None, stop_price: Optional[str] = None,
                     price_distance: Optional[str] = None, part_quantity: Optional[str] = None,
                     place_interval: Optional[str] = None) -> None:
            self.side = Serializable.to_enum(side, Side)
            self.duration = Serializable.to_enum(duration, Durations)
            self.quantity = dc(quantity)
            self.instrument = instrument
            self.order_type = Serializable.to_enum(order_type, OrderTypes)
            self.oco_group = oco_group
            self.if_done_parent_id = if_done_parent_id
            self.limit_price = dc(limit_price)
            self.stop_price = dc(stop_price)
            self.price_distance = dc(price_distance)
            self.part_quantity = dc(part_quantity)
            self.place_interval = dc(place_interval)

    def __init__(self, place_time: str, order_state: Dict[str, Union[List, str, None]], id_: str,
                 order_parameters: Dict[str, Optional[str]], current_modification_id: str, exante_account: str,
                 client_tag: Optional[str] = None, username: Optional[str] = None):
        self.place_time = str_to_dt(place_time)
        self.order_state = extract_to_model(order_state, OrderState)
        self.id_ = id_
        self.client_tag = client_tag
        self.order_parameters = extract_to_model(order_parameters, self.OrderParameters)
        self.current_modification_id = current_modification_id
        self.exante_account = exante_account
        self.username = username


class OrderV2(Serializable):
    class OrderParameters(Serializable):
        def __init__(self, side: str, duration: str, quantity: str, instrument: str, order_type: str,
                     oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                     limit_price: Optional[str] = None, stop_price: Optional[str] = None,
                     price_distance: Optional[str] = None, part_quantity: Optional[str] = None,
                     place_interval: Optional[str] = None) -> None:
            self.side = Serializable.to_enum(side, Side)
            self.duration = Serializable.to_enum(duration, Durations)
            self.quantity = dc(quantity)
            self.instrument = instrument
            self.order_type = Serializable.to_enum(order_type, OrderTypes)
            self.oco_group = oco_group
            self.if_done_parent_id = if_done_parent_id
            self.limit_price = dc(limit_price)
            self.stop_price = dc(stop_price)
            self.price_distance = dc(price_distance)
            self.part_quantity = dc(part_quantity)
            self.place_interval = dc(place_interval)

    def __init__(self, place_time: str, order_state: Dict, id_: str, order_parameters: Dict,
                 username: str, current_modification_id: str, account_id: str = None,
                 client_tag: Optional[str] = None) -> None:
        self.place_time = str_to_dt(place_time)
        self.order_state = extract_to_model(order_state, OrderState)
        self.id_ = id_
        self.client_tag = client_tag
        self.order_parameters = extract_to_model(order_parameters, self.OrderParameters)
        self.current_modification_id = current_modification_id
        self.account_id = account_id
        self.username = username


class OrderV3(Serializable):
    class OrderParameters(Serializable):
        def __init__(self, side: str, duration: str, quantity: str, symbol_id: str, order_type: str,
                     oco_group: Optional[str] = None, if_done_parent_id: Optional[str] = None,
                     limit_price: Optional[str] = None, stop_price: Optional[str] = None,
                     price_distance: Optional[str] = None, part_quantity: Optional[str] = None,
                     place_interval: Optional[str] = None) -> None:
            self.side = Serializable.to_enum(side, Side)
            self.duration = Serializable.to_enum(duration, Durations)
            self.quantity = dc(quantity)
            self.symbol_id = symbol_id
            self.order_type = Serializable.to_enum(order_type, OrderTypes)
            self.oco_group = oco_group
            self.if_done_parent_id = if_done_parent_id
            self.limit_price = dc(limit_price)
            self.stop_price = dc(stop_price)
            self.price_distance = dc(price_distance)
            self.part_quantity = dc(part_quantity)
            self.place_interval = dc(place_interval)

    def __init__(self, place_time: str, order_state: Dict, order_id: str, order_parameters: Dict, username: str,
                 current_modification_id: str, account_id: str, client_tag: Optional[str] = None) -> None:
        self.place_time = str_to_dt(place_time)
        self.order_state = extract_to_model(order_state, OrderState)
        self.order_id = order_id
        self.client_tag = client_tag
        self.order_parameters = extract_to_model(order_parameters, self.OrderParameters)
        self.current_modification_id = current_modification_id
        self.account_id = account_id
        self.username = username


OrderType = TypeVar('OrderType', OrderV1, OrderV2, OrderV3, covariant=True)


class ExOrderV1(Serializable):
    def __init__(self, quantity: str, order_id: str, event: str, price: str, position: str, time: str,) -> None:
        self.quantity = dc(quantity)
        self.order_id = order_id
        self.event = event
        self.price = dc(price)
        self.position = dc(position)
        self.time = str_to_dt(time)


class ExOrderV2(ExOrderV1):
    pass


class ExOrderV3(Serializable):
    def __init__(self, quantity: str, order_id: str, price: str, position: str, timestamp: str) -> None:
        self.quantity = dc(quantity)
        self.order_id = order_id
        self.price = dc(price)
        self.position = dc(position)
        self.timestamp = str_to_dt(timestamp)


ExOrderType = TypeVar("ExOrderType", ExOrderV1, ExOrderV2, ExOrderV3, covariant=True)


def resolve_model(version: str,
                  class_type: Union[TradeType, QuoteType, ChangeType, TradeType, SummaryType, OrderSentType,
                                    OrderMarketType, OrderLimitType, OrderStopType, OrderStopLimitType,
                                    OrderTrailingStopType, OrderTwapType, OrderIcebergType]):
    classes = list(filter(lambda x: f"{class_type.__name__[:-4]}V{version.split('.')[0]}" == x.__name__,
                          class_type.__constraints__))  # type: List[Serializable]
    if len(classes) > 1:
        raise Exception("Unexpected resolution!")
    else:
        return classes[0]


def resolve_symbol(symbol: Union[str, SymbolType, Iterable[str], Iterable[SymbolType]]) -> Optional[str]:
    if isinstance(symbol, SymbolV3):
        return urlencode(symbol.symbol_id, safe="")
    elif isinstance(symbol, (SymbolV1, SymbolV2)):
        return urlencode(symbol.id_, safe="")
    elif isinstance(symbol, Iterable) and not isinstance(symbol, str):
        return ",".join([resolve_symbol(x) for x in symbol])
    elif symbol:
        return urlencode(symbol, safe="")
    else:
        return symbol
