"""
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 Literal

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


class _StreamRequest(BaseMessage, tag_field="op", tag=first_lower, frozen=True):
    """Common parent class for any stream request."""

    id: StreamRef | None = None  # Client generated unique id to link request with response (like json rpc)


class _StreamResponse(BaseMessage, tag_field="op", tag=str.lower, frozen=True):
    """Common parent class for any stream response."""

    id: StreamRef | None = 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: str | None = None  # Token value delta (received in MarketChangeMessage) for resuming a subscription
    conflate_ms: int | None = None  # The conflation rate (looped back on initial image: bounds are 0 to 120000)
    heartbeat_ms: int | None = None  # The heartbeat rate (looped back on initial image: bounds are 500 to 5000)
    initial_clk: str | None = 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 | None = None  # 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: str | None = None
    connections_available: int | None = None  # The number of connections available for this account at this moment
    error_code: StatusErrorCode | None = None
    error_message: str | None = 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: str | None = None  # Token value (non-None) should be stored for resuming in case of a disconnect
    conflate_ms: int | None = None  # The conflation rate (may differ from that requested if subscription is delayed)
    ct: ChangeType | None = None
    heartbeat_ms: int | None = None  # Heartbeat rate (may differ from requested: bounds are 500 to 30000)
    initial_clk: str | None = None  # Token value (non-None) should be stored for resuming in case of a disconnect
    pt: int  # Publish Time
    segment_type: SegmentType | None = None  # denotes the beginning and end of a segmentation
    status: int | None = 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.
    Market subscriptions are always in the underlying exchange currency - GBP.
    """

    mc: list[MarketChange] | None = 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.
    Order subscriptions are provided in the currency of the account that the orders are placed in.
    """

    oc: list[OrderMarketChange] | None = None

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