"""DTC Binary protocol client for Sierra Chart (default: localhost:11099).

Binary wire format (no pragma pack → default C alignment):
  Every message: [uint16 Size][uint16 Type][payload...]
  Size = total byte count including the header.

Struct offsets used here match MSVC/GCC x64 default alignment:
  - int64/double are 8-byte aligned (padding inserted before them when needed)

Supported messages:
  Send:    LOGON_REQUEST, HEARTBEAT, HISTORICAL_ORDER_FILLS_REQUEST,
           OPEN_ORDERS_REQUEST
  Receive: LOGON_RESPONSE, HEARTBEAT, HISTORICAL_ORDER_FILL_RESPONSE,
           ORDER_UPDATE
"""

import re
import socket
import struct
import threading
import time
from datetime import datetime

# ── DTC message type constants ────────────────────────────────────────────────

_LOGON_REQUEST   = 1
_LOGON_RESPONSE  = 2
_HEARTBEAT       = 3
_OPEN_ORDERS_REQ = 300
_ORDER_UPDATE    = 301
_HIST_FILLS_REQ  = 303
_HIST_FILL_RESP  = 304

_PROTOCOL_VER  = 8
_LOGON_SUCCESS = 1
_BUY           = 1
_SELL          = 2


# ── Encoding helpers ──────────────────────────────────────────────────────────

def _s(text, n):
    """Return exactly n bytes: ASCII-encoded, null-padded."""
    b = (text or "").encode("ascii", errors="replace")[:n]
    return b.ljust(n, b"\x00")


def _unpack_str(data, offset, n):
    return data[offset:offset + n].rstrip(b"\x00").decode("ascii", errors="replace")


def _unpack_ts(data, offset):
    """Read int64 Unix timestamp at offset → datetime or None."""
    raw = struct.unpack_from("<q", data, offset)[0]
    try:
        return datetime.fromtimestamp(raw) if 0 < raw < 4_102_444_800 else None
    except (OSError, OverflowError):
        return None


def _base_symbol(sc_symbol, instruments):
    """'MNQM6' → 'MNQ' if 'MNQ' is in instruments, else None."""
    m = re.search(r'([A-Z]{2,4})[A-Z]\d', sc_symbol)
    if m and m.group(1) in instruments:
        return m.group(1)
    return sc_symbol if sc_symbol in instruments else None


# ── Message builders ──────────────────────────────────────────────────────────

# Format strings (little-endian, explicit padding to match C struct alignment)
_FMT_LOGON      = "<HHi32s32s64siiii32s64s32si"  # 284 bytes
_FMT_HEARTBEAT  = "<HHIq"                          # 16 bytes
_FMT_HIST_FILLS = "<HHi32si32sB7xq"               # 92 bytes  (7x = padding before int64)
_FMT_OPEN_ORD   = "<HHii"                          # 12 bytes


def _build_logon(heartbeat_secs=10):
    size = struct.calcsize(_FMT_LOGON)
    return struct.pack(
        _FMT_LOGON,
        size, _LOGON_REQUEST, _PROTOCOL_VER,
        _s("", 32), _s("", 32), _s("GizerBridge", 64),
        0, 0, heartbeat_secs, 0,
        _s("", 32), _s("", 64), _s("gizerbridge", 32), 0,
    )


def _build_heartbeat():
    size = struct.calcsize(_FMT_HEARTBEAT)
    return struct.pack(_FMT_HEARTBEAT, size, _HEARTBEAT, 0, int(time.time()))


def _build_hist_request(req_id, account, num_days):
    size = struct.calcsize(_FMT_HIST_FILLS)
    return struct.pack(
        _FMT_HIST_FILLS,
        size, _HIST_FILLS_REQ,
        req_id,
        _s("", 32),       # ServerOrderID – empty = all orders
        num_days,
        _s(account, 32),  # TradeAccount
        1,                # RequestAllHistoricalFills = true
        0,                # StartDateTime = 0 (unused when num_days > 0)
    )


def _build_open_orders_request():
    size = struct.calcsize(_FMT_OPEN_ORD)
    return struct.pack(_FMT_OPEN_ORD, size, _OPEN_ORDERS_REQ, 1, 0)


# ── Message parsers ───────────────────────────────────────────────────────────

# HISTORICAL_ORDER_FILL_RESPONSE – Offsets empirisch bestätigt (480-Byte-Pakete, Rithmic):
#   0   uint16  Size
#   2   uint16  Type
#   4   int32   RequestID
#   8   int32   TotalNumberMessages
#  12   int32   MessageNumber
#  16   char[64] Symbol          (z.B. "MNQM6.CME")
#  80   char[16] Exchange        (leer bei Rithmic, Exchange steckt im Symbol)
#  96   char[32] FillIdentifier  (laufende Fill-Nr. als String, z.B. "592")
# 128   uint32  BuySell          (1=BUY, 2=SELL)
# 132   uint32  unbekannt        (immer 0)
# 136   double  Price × 100      (z.B. 2902225.0 → 29022.25 Punkte)
# 144   int64   DateTime         (Unix-Sekunden)
# 152   double  Volume/Qty       (Anzahl Kontrakte, z.B. 1.0)
# 160   char[32] TradeID         (Rithmic Order-ID, z.B. "1031001")
# 192   char[32] unbekannt
# 224   char[32] TradeAccount    (z.B. "4PRO-RQTPUT")
# 256   uint8   FillStatus       (1=partial, 2=final – KEIN NoOrderFills-Flag)
# 257   char[96] InfoText        (z.B. "Rithmic Direct - DTC (Filled)[final]")
# 353   char[64] unbekannt
# 416   char[64] UniqueExecutionID (z.B. "2537652419")

def _parse_fill_resp(data):
    if len(data) < 260:
        return None
    try:
        raw_price = struct.unpack_from("<d", data, 136)[0]
        price     = raw_price / 100.0          # Rithmic liefert price × 100
        qty       = struct.unpack_from("<d", data, 152)[0]
        buysell   = struct.unpack_from("<I", data, 128)[0]
        total     = struct.unpack_from("<i", data, 8)[0]
        msgno     = struct.unpack_from("<i", data, 12)[0]
        return {
            "req_id":    struct.unpack_from("<i", data, 4)[0],
            "total":     total,
            "msgno":     msgno,
            "symbol":    _unpack_str(data, 16, 64),
            "price":     price,
            "qty":       qty,
            "datetime":  _unpack_ts(data, 144),
            "buysell":   buysell,
            "account":   _unpack_str(data, 224, 32),
            "no_fills":  total > 0 and msgno >= total,
            "unique_id": _unpack_str(data, 416, 64) if len(data) >= 480 else "",
        }
    except struct.error:
        return None


# ORDER_UPDATE field offsets (C struct, default alignment):
#   0  uint16 Size
#   2  uint16 Type
#   4  int32  RequestID
#   8  int32  TotalNumMessages
#  12  int32  MessageNumber
#  16  char[64] Symbol
#  80  char[16] Exchange
#  96  char[32] PreviousServerOrderID
# 128  char[32] ServerOrderID
# 160  char[32] ClientOrderID
# 192  char[32] ExchangeOrderID
# 224  uint32 OrderStatus
# 228  uint32 OrderUpdateReason
# 232  uint32 OrderType
# 236  uint32 BuySell
# 240  double Price1            (240 % 8 == 0 ✓)
# 248  double Price2
# 256  uint32 TimeInForce
# 260  [4 bytes padding]
# 264  int64  GoodTillDateTime  (264 % 8 == 0 ✓)
# 272  double OrderQuantity
# 280  double FilledQuantity
# 288  double RemainingQuantity
# 296  double AverageFillPrice
# 304  double LastFillPrice
# 312  int64  LastFillDateTime
# 320  double LastFillQuantity
# 328  char[64] UniqueExecutionID
# 392  char[32] TradeAccount

def _parse_order_update(data):
    if len(data) < 328:
        return None
    try:
        qty   = struct.unpack_from("<d", data, 320)[0]
        price = struct.unpack_from("<d", data, 304)[0] / 100.0
        if qty <= 0 or price <= 0:
            return None  # not a fill event
        uid = _unpack_str(data, 328, 64) if len(data) >= 392 else ""
        return {
            "symbol":    _unpack_str(data, 16, 64),
            "buysell":   struct.unpack_from("<I", data, 236)[0],
            "price":     price,
            "qty":       qty,
            "datetime":  _unpack_ts(data, 312),
            "account":   _unpack_str(data, 392, 32) if len(data) >= 424 else "",
            "unique_id": uid,
        }
    except struct.error:
        return None


# ── DTCClient ─────────────────────────────────────────────────────────────────

class DTCClient:
    """Low-level DTC Binary protocol client (not thread-safe for concurrent sends)."""

    def __init__(self, host="localhost", port=11099):
        self._host = host
        self._port = port
        self._sock = None
        self._buf  = b""
        self._lock = threading.Lock()

    def connect(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(10)
        sock.connect((self._host, self._port))
        sock.settimeout(None)
        self._sock = sock

    def disconnect(self):
        if self._sock:
            try:
                self._sock.close()
            except OSError:
                pass
            self._sock = None

    def logon(self, heartbeat_secs=10):
        """Send LOGON_REQUEST and block until LOGON_RESPONSE received."""
        self._send(_build_logon(heartbeat_secs))
        for msg_type, data in self._read_iter(timeout=10):
            if msg_type == _LOGON_RESPONSE and len(data) >= 16:
                result = struct.unpack_from("<i", data, 8)[0]
                text   = _unpack_str(data, 12, 96)
                if result != _LOGON_SUCCESS:
                    raise ConnectionError(f"DTC Logon abgelehnt: {text}")
                return
        raise TimeoutError("Kein LOGON_RESPONSE erhalten (Timeout)")

    def request_fills(self, account, num_days=30):
        """Request historical fills for account. Returns list of fill dicts."""
        req_id = int(time.time() * 1000) & 0x7FFFFFFF
        self._send(_build_hist_request(req_id, account, num_days))

        fills = []
        for msg_type, data in self._read_iter(timeout=15):
            if msg_type != _HIST_FILL_RESP:
                continue

            f = _parse_fill_resp(data)
            if not f or f["req_id"] != req_id:
                continue
            if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0 and f["price"] > 0:
                fills.append(f)
            if f["no_fills"]:
                break
        return fills

    def subscribe_order_updates(self):
        """Send OPEN_ORDERS_REQUEST so SC starts pushing ORDER_UPDATE messages."""
        self._send(_build_open_orders_request())

    def send_heartbeat(self):
        self._send(_build_heartbeat())

    def read_messages(self, timeout=5):
        """Non-blocking read: return all complete messages buffered within timeout."""
        self._sock.settimeout(timeout)
        msgs = []
        try:
            chunk = self._sock.recv(65536)
            if not chunk:
                raise ConnectionError("DTC-Verbindung vom Server geschlossen")
            self._buf += chunk
        except socket.timeout:
            pass
        while len(self._buf) >= 4:
            size = struct.unpack_from("<H", self._buf, 0)[0]
            if size < 4 or len(self._buf) < size:
                break
            msg_type = struct.unpack_from("<H", self._buf, 2)[0]
            msgs.append((msg_type, bytes(self._buf[:size])))
            self._buf = self._buf[size:]
        return msgs

    # ── Internal ──────────────────────────────────────────────────────────────

    def _send(self, data):
        with self._lock:
            self._sock.sendall(data)

    def _read_iter(self, timeout):
        """Generator yielding (msg_type, data) tuples until timeout."""
        deadline = time.time() + timeout
        while time.time() < deadline:
            remaining = max(0.1, deadline - time.time())
            self._sock.settimeout(remaining)
            try:
                chunk = self._sock.recv(65536)
                if not chunk:
                    raise ConnectionError("DTC-Verbindung geschlossen")
                self._buf += chunk
            except socket.timeout:
                pass
            while len(self._buf) >= 4:
                size = struct.unpack_from("<H", self._buf, 0)[0]
                if size < 4 or len(self._buf) < size:
                    break
                msg_type = struct.unpack_from("<H", self._buf, 2)[0]
                data = bytes(self._buf[:size])
                self._buf = self._buf[size:]
                yield msg_type, data


# ── Trade reconstruction from fills ──────────────────────────────────────────

def fills_to_trades(fills, instruments):
    """Convert list of fill dicts to completed round-trip trade dicts.

    Groups fills by (account, base_symbol), sorts by datetime, then matches
    buys and sells until the net position returns to zero – that's one trade.
    """
    groups = {}
    for f in sorted(fills, key=lambda x: x.get("datetime") or datetime.min):
        base = _base_symbol(f["symbol"], instruments)
        if not base:
            continue
        groups.setdefault((f["account"], base), []).append(f)

    trades = []
    for (account, symbol), group in groups.items():
        trades.extend(_match_fills(group, account, symbol, instruments))
    return trades


def _match_fills(fills, account, symbol, instruments):
    trades, pending, position = [], [], 0.0
    for f in fills:
        delta     = f["qty"] if f["buysell"] == _BUY else -f["qty"]
        position += delta
        pending.append(f)
        if abs(position) < 0.01:
            t = _pending_to_trade(pending, account, symbol, instruments)
            if t:
                trades.append(t)
            pending, position = [], 0.0
    return trades


def _pending_to_trade(fills, account, symbol, instruments):
    buys  = [f for f in fills if f["buysell"] == _BUY]
    sells = [f for f in fills if f["buysell"] == _SELL]
    if not buys or not sells:
        return None

    direction   = "Long" if fills[0]["buysell"] == _BUY else "Short"
    entry_fs    = buys  if direction == "Long" else sells
    exit_fs     = sells if direction == "Long" else buys

    e_qty = sum(f["qty"] for f in entry_fs)
    x_qty = sum(f["qty"] for f in exit_fs)
    ep    = sum(f["price"] * f["qty"] for f in entry_fs) / e_qty
    xp    = sum(f["price"] * f["qty"] for f in exit_fs)  / x_qty
    lots  = int(round(e_qty))

    _fmt      = "%Y-%m-%d %H:%M:%S"
    entry_ts  = min((f["datetime"] for f in entry_fs if f["datetime"]), default=None)
    exit_ts   = max((f["datetime"] for f in exit_fs  if f["datetime"]), default=None)
    entry_str = entry_ts.strftime(_fmt) if entry_ts else datetime.now().strftime(_fmt)
    exit_str  = exit_ts.strftime(_fmt)  if exit_ts  else datetime.now().strftime(_fmt)

    cfg       = instruments[symbol]
    tick      = cfg["tick_size"]
    tick_val  = cfg["tick_value"]
    pnl_ticks = round((xp - ep if direction == "Long" else ep - xp) / tick) * lots
    pnl       = round(pnl_ticks * tick_val, 2)

    if direction == "Long":
        execs = f"Buy {lots} @ {ep:.4f} / {entry_str},Sell {lots} @ {xp:.4f} / {exit_str}"
    else:
        execs = f"Sell {lots} @ {ep:.4f} / {entry_str},Buy {lots} @ {xp:.4f} / {exit_str}"

    # Trade-ID uses only actual fill datetimes — never datetime.now().
    # This makes IDs identical whether generated from the historical-fill path
    # or from the real-time ORDER_UPDATE path for the same trade.
    _id_ts  = entry_ts or exit_ts
    _id_str = _id_ts.strftime(_fmt) if _id_ts else ""

    return {
        "trade_id":       f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{_id_str}",
        "instrument":     symbol,
        "direction":      direction,
        "lots":           lots,
        "entry_price":    round(ep, 4),
        "exit_price":     round(xp, 4),
        "entry_datetime": entry_str,
        "exit_datetime":  exit_str,
        "pnl":            pnl,
        "pnl_ticks":      pnl_ticks,
        "executions":     execs,
        "account":        account,
    }


# ── Real-time monitor ─────────────────────────────────────────────────────────

def monitor_realtime(host, port, accounts, instruments, on_trade, stop_event, log_fn,
                     on_ready=None):
    """Connect to SC DTC server, load existing fills, then stream ORDER_UPDATE.

    Calls on_trade(trade_dict) for each newly completed round-trip trade.
    Calls on_ready(existing_ids) once the connection is established and the
    historical fill scan is complete – before entering the real-time loop.
    Returns set of trade IDs that were already present at startup (caller marks
    these as sent so they don't get re-uploaded after a restart).
    Returns None if the connection fails immediately.
    """
    client = DTCClient(host, port)
    try:
        client.connect()
        client.logon(heartbeat_secs=10)
    except Exception as e:
        log_fn(f"DTC-Verbindungsfehler: {e}")
        return None

    # ── Scan existing fills so we know what's already been uploaded ────────────
    existing_ids = set()
    all_fills    = []
    for account in accounts:
        try:
            fills = client.request_fills(account, num_days=30)
            all_fills.extend(fills)
        except Exception as e:
            log_fn(f"Historische Fills ({account}): {e}")

    for t in fills_to_trades(all_fills, instruments):
        existing_ids.add(t["trade_id"])

    # ── Subscribe to real-time ORDER_UPDATE messages ──────────────────────────
    try:
        client.subscribe_order_updates()
    except Exception as e:
        log_fn(f"ORDER_UPDATE-Subscription fehlgeschlagen: {e}")

    if on_ready:
        on_ready(existing_ids)

    # Per-(account, symbol) state for real-time fill accumulation
    rt_pending  = {}   # (account, symbol) → [fill dicts]
    rt_position = {}   # (account, symbol) → float
    seen_uids   = set()

    last_hb = time.time()

    while not stop_event.is_set():
        try:
            msgs = client.read_messages(timeout=3)
        except Exception as e:
            log_fn(f"DTC Lesefehler: {e}")
            break

        if time.time() - last_hb >= 10:
            try:
                client.send_heartbeat()
            except Exception:
                pass
            last_hb = time.time()

        for msg_type, data in msgs:
            if msg_type == _HEARTBEAT:
                try:
                    client.send_heartbeat()
                except Exception:
                    pass

            elif msg_type == _ORDER_UPDATE:
                f = _parse_order_update(data)
                if not f:
                    continue
                uid = f["unique_id"]
                if uid and uid in seen_uids:
                    continue
                if uid:
                    seen_uids.add(uid)

                base = _base_symbol(f["symbol"], instruments)
                if not base or not f["account"]:
                    continue

                key = (f["account"], base)
                rt_position.setdefault(key, 0.0)
                rt_pending.setdefault(key, [])

                rt_position[key] += f["qty"] if f["buysell"] == _BUY else -f["qty"]
                rt_pending[key].append(f)

                if abs(rt_position[key]) < 0.01:
                    group = rt_pending.pop(key)
                    rt_position[key] = 0.0
                    t = _pending_to_trade(group, f["account"], base, instruments)
                    if t and t["trade_id"] not in existing_ids:
                        existing_ids.add(t["trade_id"])
                        on_trade(t)

    client.disconnect()
    return existing_ids
