"""Core record types for the ante backtest harness.

Money and prices use ``Decimal`` (never binary float) so accounting is byte-reproducible.
Prices are probability prices in ``[0, 1]`` (Polymarket convention). The Kalshi cost model
works in integer cents internally; see ``costs``.

Two orderings are deliberately separate (see plan v6 "Event causality & ordering"):
  * ``event_index`` — deterministic PROCESSING order assigned at ingest.
  * causal fill-eligibility — a strictly-greater ``timestamp_utc`` (not sort order).
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum

# --- identifiers -----------------------------------------------------------

ConditionId = str  # market/condition id, e.g. "0x9634..."
TokenId = str  # outcome-token id (Polymarket clobTokenId / trade `asset`)


# --- enums -----------------------------------------------------------------


class Side(Enum):
    BUY = "BUY"
    SELL = "SELL"


class OrderType(Enum):
    MARKETABLE = "MARKETABLE"  # crosses the spread; taker
    POST_ONLY = "POST_ONLY"  # rests; maker-only


class TimeInForce(Enum):
    GTC = "GTC"  # remainder rests until end-of-data
    IOC = "IOC"  # remainder cancels immediately


class EventType(Enum):
    """Fixed cross-stream tie-break priority at equal timestamps (lower = earlier).

    Price updates are ordered before trades before resolution so that, within a single
    instant, state is observed (price) before it is acted upon (trade) before the market
    is settled (resolution). This is a determinism device, NOT a causal claim.
    """

    FEED = -1  # a ctx.data decision event (weather, ...) is observed before any same-instant price
    PRICE = 0
    TRADE = 1
    RESOLUTION = 2


# --- market events (the replay stream) -------------------------------------


@dataclass(frozen=True, slots=True)
class Market:
    condition_id: ConditionId
    question: str
    token_ids: tuple[TokenId, ...]  # binary v1: exactly two, positionally aligned with `outcomes`
    outcomes: tuple[str, ...]  # e.g. ("Yes", "No")


@dataclass(frozen=True, slots=True)
class TradeEvent:
    """A historical trade print (primary stream). Carries size + side for the liquidity cap."""

    timestamp_utc: datetime
    condition_id: ConditionId
    token_id: TokenId
    side: Side
    size: Decimal
    price: Decimal
    tx_hash: str  # stable per-event tie-break (`stable_raw_event_hash`)


@dataclass(frozen=True, slots=True)
class PriceEvent:
    """A price-series point (secondary stream)."""

    timestamp_utc: datetime
    condition_id: ConditionId
    token_id: TokenId
    price: Decimal
    source_seq: int  # provenance only; monotonic within this stream


@dataclass(frozen=True, slots=True)
class ResolutionEvent:
    timestamp_utc: datetime
    condition_id: ConditionId
    winning_token_id: TokenId


@dataclass(frozen=True, slots=True)
class FeedDecisionEvent:
    """A ctx.data feed value delivered at ``decision_at`` (plan §W1.5). Emitted once per market at
    the capture's sealed instant, ordered BEFORE any same-instant price so an order it triggers
    fills against a strictly-later price. ``feeds`` maps a feed name to its versioned payload dict
    for this market; a strategy reads it directly (and may re-read via ``ctx.data.fetch`` later).
    """

    timestamp_utc: datetime  # == decision_at
    condition_id: ConditionId
    feeds: tuple[tuple[str, str], ...]  # (feed_name, canonical_payload_json); hashable + deterministic

    def payload(self, feed: str) -> dict | None:
        import json

        for name, pj in self.feeds:
            if name == feed:
                return json.loads(pj)
        return None


# A market event is any of the four; the engine dispatches on type.
MarketEvent = TradeEvent | PriceEvent | ResolutionEvent | FeedDecisionEvent


@dataclass(frozen=True, slots=True)
class OrderedEvent:
    """A market event tagged with its normalized processing index (assigned by `replay`)."""

    event_index: int
    event: MarketEvent

    @property
    def timestamp_utc(self) -> datetime:
        return self.event.timestamp_utc

    @property
    def event_type(self) -> EventType:
        e = self.event
        if isinstance(e, FeedDecisionEvent):
            return EventType.FEED
        if isinstance(e, TradeEvent):
            return EventType.TRADE
        if isinstance(e, PriceEvent):
            return EventType.PRICE
        return EventType.RESOLUTION


# --- orders / fills / positions --------------------------------------------


@dataclass(frozen=True, slots=True)
class Order:
    """An order emitted by a strategy at a given event.

    ``placed_at_ts`` is the timestamp of the triggering event; fills are only eligible against
    events with a STRICTLY greater timestamp (no lookahead, no same-instant sibling fills).
    """

    order_id: str
    condition_id: ConditionId
    token_id: TokenId
    side: Side
    order_type: OrderType
    size: Decimal
    limit_price: Decimal  # for MARKETABLE, the worst acceptable price
    time_in_force: TimeInForce
    placed_at_ts: datetime
    max_size: Decimal | None = None  # per-order clip


@dataclass(frozen=True, slots=True)
class Fill:
    order_id: str
    condition_id: ConditionId
    token_id: TokenId
    side: Side
    size: Decimal
    price: Decimal
    fee: Decimal  # cents, per the cost model
    timestamp_utc: datetime
    is_maker: bool = False  # whether the model produced a maker (True) or taker (False) fill


@dataclass(slots=True)
class Position:
    """Token-keyed holding. Keyed by (condition_id, token_id) so NO/multi-outcome can't invert."""

    condition_id: ConditionId
    token_id: TokenId
    size: Decimal = Decimal(0)  # signed: +long token, -short
    cost_basis: Decimal = Decimal(0)  # total acquisition cost of the current holding

    @property
    def key(self) -> tuple[ConditionId, TokenId]:
        return (self.condition_id, self.token_id)


class LiquidityMode(Enum):
    """Whether a run had real size/side data or fell back to assumed liquidity.

    Surfaced in the report so a price-only run is never mistaken for a size-validated one.
    Polymarket v1 always runs FULL (trades carry size+side); PRICE_ONLY is for future venues.
    """

    FULL = "full"
    PRICE_ONLY = "price_only"
