"""
Definition of the betfair streaming API messages as defined in:
- https://docs.developer.betfair.com/display/1smk3cen4v3lu3yomq5qye0ni/Exchange+Stream+API
- https://github.com/betfair/stream-api-sample-code/blob/master/ESASwaggerSchema.json
 """

from typing import List, Literal, Optional, Union

from betfair_parser.spec.common import BaseMessage
from betfair_parser.spec.streaming.enums import ChangeType, SegmentType, StatusErrorCode
from betfair_parser.spec.streaming.type_definitions import (
    MarketChange,
    MarketDataFilter,
    MarketFilter,
    OrderFilter,
    OrderMarketChange,
)


def first_lower(s: str) -> str:
    """Lower only the first character of a string."""
    return s[:1].lower() + s[1:]


class StreamRequest(BaseMessage, tag_field="op", tag=first_lower, omit_defaults=True, frozen=True):
    id: Optional[Union[int, str]] = None  # Client generated unique id to link request with response (like json rpc)


class StreamResponse(BaseMessage, tag_field="op", tag=str.lower, omit_defaults=True, frozen=True):
    id: Optional[Union[int, str]] = None  # Client generated unique id to link request with response (like json rpc)


class Authentication(StreamRequest, kw_only=True, frozen=True):
    """
    This message is the first message that the client must send on connecting to the server. You must
    be authenticated before any other request is processed.
    """

    app_key: str  # Application Key
    session: str  # Session Token generated by the API login


class _Subscription(StreamRequest, kw_only=True, frozen=True):
    """Common parent class for any Subscription request."""

    clk: Optional[str] = None  # Token value delta (received in MarketChangeMessage) for resuming a subscription
    conflate_ms: Optional[int] = None  # The conflation rate (looped back on initial image: bounds are 0 to 120000)
    heartbeat_ms: Optional[int] = None  # The heartbeat rate (looped back on initial image: bounds are 500 to 5000)
    initial_clk: Optional[str] = None  # Token value that should be passed to resume a subscription
    segmentation_enabled: bool = True  # allow server to send large sets of data in segments, instead of a single block


class MarketSubscription(_Subscription, kw_only=True, frozen=True):
    """
    This subscription type is used to receive price changes for one or more markets; your subscription
    criteria determine what you see. Limiting the amount of data that you consume will make your initial
    image much smaller (and faster) & suppress changes that are uninteresting to you.
    """

    market_filter: MarketFilter
    market_data_filter: MarketDataFilter


class OrderSubscription(_Subscription, kw_only=True, frozen=True):
    """This subscription type is used to receive order changes."""

    order_filter: OrderFilter  # Optional filter applied to order subscription


class Heartbeat(StreamRequest, frozen=True):
    """
    This is an explicit heartbeat request (in addition to the server heartbeat interval which is automatic).
    This functionality should not normally be necessary unless you need to keep a firewall open.
    """


class Connection(StreamResponse, kw_only=True, frozen=True):
    """This is received by the client when it successfully opens a connection to the server."""

    connection_id: str  # Unique identifier for support queries


class Status(StreamResponse, kw_only=True, frozen=True):
    """Every request receives a status response with a matching id."""

    connection_closed: bool
    connection_id: Optional[str] = None
    connections_available: Optional[int] = None  # The number of connections available for this account at this moment
    error_code: Optional[StatusErrorCode] = None
    error_message: Optional[str] = None  # Additional message in case of a failure
    status_code: Literal["SUCCESS", "FAILURE"]  # The status of the last request

    @property
    def is_error(self):
        return self.status_code != "SUCCESS"


class _ChangeMessage(StreamResponse, kw_only=True, frozen=True):
    """Common parent class for any ChangeMessage."""

    clk: Optional[str] = None  # Token value (non-None) should be stored for resuming in case of a disconnect
    conflate_ms: Optional[int] = None  # The conflation rate (may differ from that requested if subscription is delayed)
    ct: Optional[ChangeType] = None
    heartbeat_ms: Optional[int] = None  # Heartbeat rate (may differ from requested: bounds are 500 to 30000)
    initial_clk: Optional[str] = None  # Token value (non-None) should be stored for resuming in case of a disconnect
    pt: int  # Publish Time
    segment_type: Optional[SegmentType] = None  # denotes the beginning and end of a segmentation
    status: Optional[int] = None  # None if the stream is up-to-date and 503 if the services are experiencing latencies

    @property
    def is_heartbeat(self):
        return self.ct == ChangeType.HEARTBEAT

    @property
    def stream_unreliable(self):
        return self.status == 503

    @property
    def change_type(self):
        """Set to indicate the type of change (if None this is a delta)"""
        return self.ct

    @property
    def publish_time(self):
        """Time (in milliseconds since epoch) that the changes were generated"""
        return self.pt


class MCM(_ChangeMessage, kw_only=True, frozen=True):
    """This is the ChangeMessage stream of data Betfair sends back after subscribing to the market stream."""

    mc: Optional[List[MarketChange]] = None

    @property
    def market_changes(self):
        """The modifications to markets (will be None on a heartbeat)"""
        return self.mc


class OCM(_ChangeMessage, kw_only=True, frozen=True):
    """This is the ChangeMessage stream of data Betfair sends back after subscribing to the order stream."""

    oc: Optional[List[OrderMarketChange]] = None

    @property
    def order_market_changes(self):
        """The modifications to account's orders (will be None on a heartbeat)"""
        return self.oc
