#!/usr/bin/env python3
"""
Mentions Market Maker - Single Market YES/NO Maker
Interactive keyboard controls + Telegram commands.
Market ID loaded from .env (MENTIONS_MARKET_ID).
"""

import os
import sys
import time
import base64
import asyncio
from dataclasses import dataclass
from typing import Optional, Dict
from dotenv import load_dotenv
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import aiohttp

load_dotenv()

from telegram import TelegramNotifier


def _parse_ob_side(ob_fp_side: list) -> list:
    return [
        [int(round(float(p) * 100)), int(float(s))]
        for p, s in ob_fp_side
    ]


@dataclass
class Config:
    api_base: str = "https://api.elections.kalshi.com/trade-api/v2"


@dataclass
class MarketState:
    market_id: str
    yes_bid: Optional[float] = None
    yes_bid_size: int = 0
    yes_second_bid: Optional[float] = None
    no_bid: Optional[float] = None
    no_bid_size: int = 0
    no_second_bid: Optional[float] = None

    def update_orderbook(self, orderbook_data: dict):
        ob = orderbook_data.get("orderbook_fp", {})

        yes_offers = _parse_ob_side(ob.get("yes_dollars", []))
        if yes_offers:
            yes_sorted = sorted(yes_offers, key=lambda x: x[0], reverse=True)
            self.yes_bid = yes_sorted[0][0] / 100
            self.yes_bid_size = yes_sorted[0][1]
            self.yes_second_bid = yes_sorted[1][0] / 100 if len(yes_sorted) > 1 else None
        else:
            self.yes_bid = None
            self.yes_bid_size = 0
            self.yes_second_bid = None

        no_offers = _parse_ob_side(ob.get("no_dollars", []))
        if no_offers:
            no_sorted = sorted(no_offers, key=lambda x: x[0], reverse=True)
            self.no_bid = no_sorted[0][0] / 100
            self.no_bid_size = no_sorted[0][1]
            self.no_second_bid = no_sorted[1][0] / 100 if len(no_sorted) > 1 else None
        else:
            self.no_bid = None
            self.no_bid_size = 0
            self.no_second_bid = None


class MentionsMaker:
    """YES/NO bid market maker for mentions markets"""

    SIDES = ["yes", "no"]

    def __init__(self, api_key: str, api_secret: str, market_id: str, config: Config, telegram: TelegramNotifier):
        self.api_key = api_key
        self.market_id = market_id
        self.config = config
        self.telegram = telegram
        self.telegram.trader = self

        # Market state
        self.market_state = MarketState(market_id=market_id)

        # Order tracking
        self.order_ids: Dict[str, Optional[str]] = {"yes": None, "no": None}
        self.last_prices: Dict[str, Optional[float]] = {"yes": None, "no": None}

        # Fill tracking
        self.current_increment: Dict[str, int] = {"yes": 0, "no": 0}
        self.cycle_start_ts: int = 0

        # Cached data
        self.cached_resting: Dict[str, Optional[int]] = {"yes": None, "no": None}
        self.cached_position: Dict[str, Optional[int]] = {"yes": None, "no": None}
        self.cached_queue_position: Dict[str, Optional[int]] = {"yes": None, "no": None}
        self.cached_fills: Dict[str, int] = {"yes": 0, "no": 0}
        self.fill_prices: Dict[str, Optional[float]] = {"yes": None, "no": None}

        # Control
        self.running = False
        self.active = False
        self.paused = True
        self.stopping = False
        self.waiting_for_manual_resume = True
        self.emergency_stop = False
        self.contract_increment = 3
        self.one_side_first_mode = False
        self.active_side: Optional[str] = None
        self.side_selection: Optional[str] = None
        self.is_rebalancing = False
        self.bump_enabled: Dict[str, bool] = {"yes": False, "no": False}
        self.bump_target: Dict[str, Optional[int]] = {"yes": None, "no": None}
        self.single_fire_mode = False
        self.single_fire_sides_completed = 0
        self.trading_task: Optional[asyncio.Task] = None

        # Manual fill mode
        self.manual_fill_mode = False
        self.manual_fill_side: Optional[str] = None
        self.manual_fill_count: int = 0
        self.manual_fill_start_position: int = 0

        # Load private key
        self.private_key = None
        if api_secret:
            try:
                if os.path.isfile(api_secret):
                    with open(api_secret, 'r') as f:
                        key_data = f.read()
                else:
                    key_data = api_secret

                self.private_key = serialization.load_pem_private_key(
                    key_data.encode() if isinstance(key_data, str) else key_data,
                    password=None,
                    backend=default_backend()
                )
                print("✓ Private key loaded")
            except Exception as e:
                print(f"❌ Failed to load private key: {e}")
                sys.exit(1)

    def _sign_request(self, timestamp: str, method: str, path: str) -> str:
        if not self.private_key:
            return ""
        path_clean = path.split('?')[0]
        msg = timestamp + method + "/trade-api/v2" + path_clean
        sig = self.private_key.sign(
            msg.encode(),
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.DIGEST_LENGTH
            ),
            hashes.SHA256()
        )
        return base64.b64encode(sig).decode()

    async def _request(self, method: str, endpoint: str, data=None) -> dict:
        url = f"{self.config.api_base}{endpoint}"
        timestamp = str(int(time.time() * 1000))
        sig = self._sign_request(timestamp, method, endpoint)
        headers = {
            'KALSHI-ACCESS-KEY': self.api_key,
            'KALSHI-ACCESS-SIGNATURE': sig,
            'KALSHI-ACCESS-TIMESTAMP': timestamp,
            'Content-Type': 'application/json'
        }
        try:
            timeout = aiohttp.ClientTimeout(total=3)
            async with aiohttp.ClientSession(timeout=timeout) as session:
                if method == "GET":
                    async with session.get(url, headers=headers) as resp:
                        return await resp.json()
                elif method == "POST":
                    async with session.post(url, json=data, headers=headers) as resp:
                        return await resp.json()
                elif method == "DELETE":
                    async with session.delete(url, headers=headers) as resp:
                        return await resp.json()
        except Exception:
            return {}

    async def refresh_market_data(self):
        results = await asyncio.gather(
            self._request("GET", f"/markets/{self.market_id}/orderbook"),
            self._request("GET", f"/portfolio/orders?ticker={self.market_id}&status=resting"),
            self._request("GET", f"/portfolio/positions?ticker={self.market_id}&count_filter=position"),
            self._request("GET", f"/portfolio/orders/queue_positions?market_tickers={self.market_id}"),
            self._request("GET", f"/portfolio/fills?ticker={self.market_id}&min_ts={self.cycle_start_ts}&limit=200"),
            return_exceptions=True
        )

        if results[0] and not isinstance(results[0], Exception):
            self.market_state.update_orderbook(results[0])

        if results[1] and not isinstance(results[1], Exception):
            orders = results[1].get("orders", [])
            self.cached_resting["yes"] = sum(int(float(o.get("remaining_count_fp", o.get("count_fp", "0")))) for o in orders if o.get("side") == "yes")
            self.cached_resting["no"] = sum(int(float(o.get("remaining_count_fp", o.get("count_fp", "0")))) for o in orders if o.get("side") == "no")

        if results[2] and not isinstance(results[2], Exception):
            self.cached_position["yes"] = 0
            self.cached_position["no"] = 0
            for pos in results[2].get("market_positions", []):
                if pos.get("ticker") == self.market_id:
                    position_val = int(float(pos.get("position_fp", "0")))
                    if position_val > 0:
                        self.cached_position["yes"] = position_val
                    elif position_val < 0:
                        self.cached_position["no"] = abs(position_val)
                    break

        self.cached_queue_position["yes"] = None
        self.cached_queue_position["no"] = None
        if results[3] and not isinstance(results[3], Exception):
            queue_positions = results[3].get("queue_positions")
            if queue_positions and isinstance(queue_positions, list):
                for qp in queue_positions:
                    if qp.get("market_ticker") == self.market_id:
                        order_id = qp.get("order_id")
                        for side in ["yes", "no"]:
                            if self.order_ids.get(side) == order_id:
                                self.cached_queue_position[side] = int(float(qp.get("queue_position_fp", qp.get("queue_position", "0"))))
                                break

        self.cached_fills = {"yes": 0, "no": 0}
        if results[4] and not isinstance(results[4], Exception):
            for fill in results[4].get("fills", []):
                fill_side = fill.get("side")
                fill_count = int(float(fill.get("count_fp", "0")))
                if fill_side in ["yes", "no"]:
                    self.cached_fills[fill_side] += fill_count

        # One-side-first: track which side is expensive
        if self.one_side_first_mode and self.side_selection in ["expensive", "cheap"]:
            yes_bid = self.market_state.yes_bid or 0
            no_bid = self.market_state.no_bid or 0
            expensive = "yes" if yes_bid >= no_bid else "no"
            yes_pos = self.cached_position.get("yes") or 0
            no_pos = self.cached_position.get("no") or 0
            if yes_pos == 0 and no_pos == 0:
                if self.side_selection == "expensive":
                    self.active_side = expensive
                else:
                    self.active_side = "no" if expensive == "yes" else "yes"

    async def place_order(self, side: str, price: float, count: int) -> Optional[str]:
        order_data = {
            "ticker": self.market_id,
            "side": side,
            "action": "buy",
            "count": count,
            "type": "limit",
            "client_order_id": f"{self.market_id}-{side}-{int(time.time() * 1000)}",
            f"{side}_price": int(round(price * 100))
        }
        result = await self._request("POST", "/portfolio/orders", order_data)
        return result.get("order", {}).get("order_id") if result else None

    async def cancel_order(self, order_id: str) -> bool:
        if order_id:
            result = await self._request("DELETE", f"/portfolio/orders/{order_id}")
            return bool(result)
        return False

    async def cancel_all_orders(self):
        tasks = []
        for side in self.SIDES:
            if self.order_ids[side]:
                tasks.append(self.cancel_order(self.order_ids[side]))
                self.order_ids[side] = None
                self.last_prices[side] = None
        if tasks:
            await asyncio.gather(*tasks, return_exceptions=True)

    async def modify_order(self, side: str, new_price: float) -> Optional[str]:
        old_order_id = self.order_ids[side]

        if not old_order_id:
            if self.manual_fill_mode:
                count = self.manual_fill_count
            else:
                count = self.contract_increment
            new_order_id = await self.place_order(side, new_price, count)
            if new_order_id:
                self.order_ids[side] = new_order_id
                self.last_prices[side] = new_price
            return new_order_id

        resting_count = self.cached_resting[side] or 0
        if resting_count == 0:
            self.order_ids[side] = None
            self.last_prices[side] = None
            return None

        amend_data = {
            "ticker": self.market_id,
            "side": side,
            "action": "buy",
            "count": resting_count,
            f"{side}_price": int(round(new_price * 100))
        }
        result = await self._request("POST", f"/portfolio/orders/{old_order_id}/amend", amend_data)

        if result and result.get("order") and result["order"].get("status") != "canceled":
            self.last_prices[side] = new_price
            return old_order_id
        else:
            self.order_ids[side] = None
            self.last_prices[side] = None
            return None

    def get_bid_info(self, side: str):
        if side == "yes":
            return self.market_state.yes_bid, self.market_state.yes_bid_size, self.market_state.yes_second_bid
        else:
            return self.market_state.no_bid, self.market_state.no_bid_size, self.market_state.no_second_bid

    def get_market_spread(self) -> Optional[int]:
        if self.market_state.yes_bid is None or self.market_state.no_bid is None:
            return None
        return 100 - round(self.market_state.yes_bid * 100) - round(self.market_state.no_bid * 100)

    def check_target_price(self, side: str) -> Optional[float]:
        bid, bid_size, second_bid = self.get_bid_info(side)
        current_price = self.last_prices[side]
        our_resting = self.cached_resting.get(side) or 0

        if bid is None:
            return None

        bid_cents = round(bid * 100)
        current_cents = round(current_price * 100) if current_price is not None else None

        if bid_size > our_resting:
            others_best_cents = bid_cents
        elif second_bid is not None:
            others_best_cents = round(second_bid * 100)
        else:
            others_best_cents = None

        if self.bump_enabled.get(side, False):
            if self.bump_target[side] is None and others_best_cents is not None:
                self.bump_target[side] = others_best_cents + 1

            target_cents = self.bump_target[side]

            if target_cents is not None:
                if others_best_cents is not None and others_best_cents >= target_cents:
                    print(f"\n⚠️ {side.upper()}: Bump disabled - others bidding at/above ${target_cents/100:.2f}")
                    self.bump_enabled[side] = False
                    self.bump_target[side] = None
                else:
                    other_side = "no" if side == "yes" else "yes"
                    other_bid, _, _ = self.get_bid_info(other_side)

                    if other_bid:
                        other_bid_cents = round(other_bid * 100)
                        if target_cents + other_bid_cents <= 99 and target_cents <= 99:
                            return target_cents / 100

                    print(f"\n⚠️ {side.upper()}: Bump disabled - spread too tight")
                    self.bump_enabled[side] = False
                    self.bump_target[side] = None

        if current_cents is not None and bid_cents > current_cents:
            return bid

        if bid_size > our_resting:
            return bid

        if second_bid is not None:
            return second_bid

        return bid

    async def initialize_orders(self) -> bool:
        success = True
        if self.manual_fill_mode:
            sides = [self.manual_fill_side]
        elif self.one_side_first_mode:
            sides = [self.active_side]
        else:
            sides = self.SIDES

        for side in sides:
            existing = self.cached_resting[side]
            if existing is None:
                success = False
                continue
            if existing > 0:
                continue

            bid, _, _ = self.get_bid_info(side)
            if bid is not None:
                count = self.manual_fill_count if self.manual_fill_mode else self.contract_increment
                order_id = await self.place_order(side, bid, count)
                if order_id:
                    self.order_ids[side] = order_id
                    self.last_prices[side] = bid
                    print(f"✓ {side.upper()}: Placed {count} @ ${bid:.2f}")
                    await asyncio.sleep(0.2)
                else:
                    success = False
            else:
                success = False
        return success

    def check_fills(self):
        for side in self.SIDES:
            fills_count = self.cached_fills[side]
            if fills_count > self.current_increment[side]:
                new_fills = fills_count - self.current_increment[side]
                yes_pos = self.cached_position.get("yes") or 0
                no_pos = self.cached_position.get("no") or 0
                self.telegram.notify_fill(side, self.last_prices[side] or 0, new_fills, yes_pos, no_pos)
                if self.current_increment[side] < self.contract_increment and fills_count >= self.contract_increment:
                    self.fill_prices[side] = self.last_prices[side]
                    if self.bump_enabled[side]:
                        self.bump_enabled[side] = False
                        self.bump_target[side] = None
                        print(f"\n✓ {side.upper()} filled - bump disabled")
                self.current_increment[side] = fills_count

    def both_filled(self) -> bool:
        if self.manual_fill_mode:
            side = self.manual_fill_side
            fills_count = self.cached_fills[side]
            if fills_count >= self.manual_fill_count:
                return True
            return False

        if self.one_side_first_mode:
            yes_pos = self.cached_position["yes"] or 0
            no_pos = self.cached_position["no"] or 0
            yes_rest = self.cached_resting["yes"] or 0
            no_rest = self.cached_resting["no"] or 0

            if (yes_pos == 0 and no_pos == 0 and yes_rest == 0 and no_rest == 0 and
                self.current_increment["yes"] >= self.contract_increment and
                self.current_increment["no"] >= self.contract_increment):
                return True
            return self.current_increment[self.active_side] >= self.contract_increment

        if self.is_rebalancing:
            yes_pos = self.cached_position["yes"]
            no_pos = self.cached_position["no"]
            if yes_pos is None or no_pos is None:
                return False
            if yes_pos == no_pos:
                self.is_rebalancing = False
                return True
            return False

        if not all(self.current_increment[s] >= self.contract_increment for s in self.SIDES):
            return False

        yes_pos = self.cached_position["yes"]
        no_pos = self.cached_position["no"]
        if yes_pos is None or no_pos is None:
            return False
        return yes_pos == no_pos

    async def rebalance(self, yes_pos: int, no_pos: int):
        print(f"\n⚠️ Rebalancing: YES={yes_pos}, NO={no_pos}")
        await self.cancel_all_orders()

        if yes_pos < no_pos:
            lagging_side = "yes"
            diff = no_pos - yes_pos
        else:
            lagging_side = "no"
            diff = yes_pos - no_pos

        self.cycle_start_ts = int(time.time() * 1000)
        self.current_increment = {"yes": 0, "no": 0}
        self.order_ids = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}

        bid, _, _ = self.get_bid_info(lagging_side)
        if bid is not None:
            order_id = await self.place_order(lagging_side, bid, diff)
            if order_id:
                self.order_ids[lagging_side] = order_id
                self.last_prices[lagging_side] = bid
                self.is_rebalancing = True
                self.telegram.notify_rebalance(lagging_side, diff, bid)

    async def start_new_cycle(self):
        yes_pos = self.cached_position.get("yes") or 0
        no_pos = self.cached_position.get("no") or 0

        if self.manual_fill_mode:
            print(f"\n✓ Manual fill complete: {self.manual_fill_count} {self.manual_fill_side.upper()}")
            await self.cancel_all_orders()
            self.manual_fill_mode = False
            self.paused = True
            self.waiting_for_manual_resume = True
            self.telegram.notify_paused()
            return

        if self.one_side_first_mode:
            print(f"\n✓ {self.active_side.upper()} filled — switching sides")
            new_side = "no" if self.active_side == "yes" else "yes"
            self.current_increment[new_side] = 0
            self.active_side = new_side
        else:
            self.telegram.notify_cycle_complete(yes_pos, no_pos, self.contract_increment)
            self.current_increment = {"yes": 0, "no": 0}

        self.cycle_start_ts = int(time.time() * 1000)
        self.order_ids = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}
        self.bump_enabled = {"yes": False, "no": False}
        self.bump_target = {"yes": None, "no": None}

        if self.single_fire_mode:
            if self.one_side_first_mode:
                self.single_fire_sides_completed += 1
                if self.single_fire_sides_completed >= 2:
                    self.paused = True
                    self.single_fire_mode = False
                    self.single_fire_sides_completed = 0
                    self.waiting_for_manual_resume = True
                    print("\n✓ Single fire complete — paused")
                    self.telegram.notify_paused()
                    return
            else:
                self.paused = True
                self.single_fire_mode = False
                self.waiting_for_manual_resume = True
                print("\n✓ Single fire complete — paused")
                self.telegram.notify_paused()
                return

        if await self.initialize_orders():
            print("\n✓ New cycle started")
        else:
            if not self.one_side_first_mode and yes_pos != no_pos:
                await self.rebalance(yes_pos, no_pos)

    async def update_orders(self):
        if self.manual_fill_mode:
            sides = [self.manual_fill_side]
        elif self.one_side_first_mode:
            sides = [self.active_side]
        else:
            sides = self.SIDES

        for side in sides:
            resting = self.cached_resting[side]
            if resting is None or resting == 0:
                continue

            target_price = self.check_target_price(side)
            if target_price is None:
                continue

            last_price = self.last_prices[side]
            if last_price is not None:
                if round(target_price * 100) != round(last_price * 100):
                    new_order_id = await self.modify_order(side, target_price)
                    if new_order_id:
                        direction = "↑" if target_price > last_price else "↓"
                        spread = self.get_market_spread()
                        spread_str = f" [Spread: {spread}c]" if spread is not None else ""
                        bump_str = " [BUMP]" if self.bump_enabled[side] else ""
                        print(f"\n{direction} {side.upper()}: ${last_price:.2f} → ${target_price:.2f}{spread_str}{bump_str}")

    def print_status(self):
        yes_bid = f"${self.market_state.yes_bid:.2f}" if self.market_state.yes_bid else "N/A"
        no_bid = f"${self.market_state.no_bid:.2f}" if self.market_state.no_bid else "N/A"
        yes_pos = self.cached_position.get("yes") or 0
        no_pos = self.cached_position.get("no") or 0
        yes_queue = self.cached_queue_position["yes"]
        no_queue = self.cached_queue_position["no"]
        yes_queue_str = str(yes_queue) if yes_queue is not None else "?"
        no_queue_str = str(no_queue) if no_queue is not None else "?"
        spread = self.get_market_spread()
        spread_str = f" Sp:{spread}c" if spread is not None else ""
        side_str = f"[{self.active_side.upper()}]" if self.one_side_first_mode else ""
        rebal_str = "[REBAL]" if self.is_rebalancing else ""
        pause_str = "PAUSED " if self.paused or self.waiting_for_manual_resume else ""
        bump_yes = "↑" if self.bump_enabled["yes"] else ""
        bump_no = "↑" if self.bump_enabled["no"] else ""

        print(f"\r{pause_str}{side_str}{rebal_str} Y{bump_yes}:{yes_bid} Q:{yes_queue_str} ({self.current_increment['yes']}/{self.contract_increment}) "
              f"N{bump_no}:{no_bid} Q:{no_queue_str} ({self.current_increment['no']}/{self.contract_increment}) "
              f"Pos:Y={yes_pos},N={no_pos}{spread_str}", end="")
        sys.stdout.flush()

    async def trading_loop(self):
        last_status = time.time()

        while self.running and self.active:
            if self.emergency_stop:
                print("\n⛔ Emergency stop")
                await self.cancel_all_orders()
                self.active = False
                self.emergency_stop = False
                self.telegram.notify_stopped()
                break

            await self.refresh_market_data()
            self.check_fills()

            if self.stopping:
                if self.both_filled():
                    print("\n✓ Cycle complete — stopping")
                    await self.cancel_all_orders()
                    self.active = False
                    self.stopping = False
                    self.telegram.notify_stopped()
                    break

            elif self.paused:
                await self.update_orders()

            elif self.waiting_for_manual_resume:
                self.cycle_start_ts = int(time.time() * 1000)
                self.current_increment = {"yes": 0, "no": 0}
                if await self.initialize_orders():
                    print("\n✓ Orders initialized")
                self.waiting_for_manual_resume = False

            else:
                if self.both_filled():
                    await self.start_new_cycle()
                else:
                    await self.update_orders()

            if time.time() - last_status >= 1:
                self.print_status()
                last_status = time.time()

            await asyncio.sleep(0.15)

        print("\n✓ Trading stopped")

    def toggle_pause(self):
        if not self.active:
            print("\n⚠️ Not trading — press G to start")
            return
        if self.paused or self.waiting_for_manual_resume:
            self.paused = False
            self.waiting_for_manual_resume = False
            print("\n▶️ Resumed")
            self.telegram.notify_resumed()
        else:
            self.paused = True
            print("\n⏸️ Paused")
            self.telegram.notify_paused()

    def single_fire(self):
        if not self.active:
            print("\n⚠️ Not trading — press G to start")
            return
        if not (self.paused or self.waiting_for_manual_resume):
            print("\n⚠️ Already running — pause first")
            return
        print("\n🎯 Single fire mode")
        self.single_fire_mode = True
        self.single_fire_sides_completed = 0
        self.paused = False
        self.waiting_for_manual_resume = False

    def toggle_bump(self, side: str):
        if not self.active:
            print(f"\n⚠️ Not trading — press G to start")
            return
        if self.one_side_first_mode and side != self.active_side:
            print(f"\n⚠️ Can only bump active side ({self.active_side.upper()}) in one-side-first mode")
            return
        self.bump_enabled[side] = not self.bump_enabled[side]
        if not self.bump_enabled[side]:
            self.bump_target[side] = None
        status = "ENABLED" if self.bump_enabled[side] else "DISABLED"
        print(f"\n{'↑' if self.bump_enabled[side] else '↓'} {side.upper()}: Bump mode {status}")

    async def start_close_position(self):
        await self.refresh_market_data()
        yes_pos = self.cached_position.get("yes") or 0
        no_pos = self.cached_position.get("no") or 0
        if yes_pos == no_pos:
            print(f"\n⚠️ No open position to close (YES={yes_pos}, NO={no_pos})")
            return
        if yes_pos > no_pos:
            close_side = "no"
            close_count = yes_pos - no_pos
        else:
            close_side = "yes"
            close_count = no_pos - yes_pos
        print(f"\n🔒 Closing position: buying {close_count} {close_side.upper()} (current: YES={yes_pos}, NO={no_pos})")
        await self.start_manual_fill(close_side, close_count)

    async def start_manual_fill(self, side: str, count: int):
        print(f"\n🎯 Manual fill: {count} {side.upper()} contract(s)")
        await self.cancel_all_orders()
        await self.refresh_market_data()

        self.manual_fill_mode = True
        self.manual_fill_side = side
        self.manual_fill_count = count
        self.manual_fill_start_position = self.cached_position[side] or 0

        self.cycle_start_ts = int(time.time() * 1000)
        self.current_increment = {"yes": 0, "no": 0}
        self.order_ids = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}
        self.bump_enabled = {"yes": False, "no": False}
        self.bump_target = {"yes": None, "no": None}

        self.active = True
        self.paused = False
        self.stopping = False
        self.waiting_for_manual_resume = False
        self.one_side_first_mode = False

        if await self.initialize_orders():
            print(f"✓ Manual fill order placed - waiting for fill")
        else:
            print(f"❌ Failed to place manual fill order")
            self.manual_fill_mode = False
            self.active = False
            return

        if not self.trading_task or self.trading_task.done():
            self.trading_task = asyncio.create_task(self.trading_loop())

    async def start_manual_fill_taker(self, side: str, count: int):
        print(f"\n🎯 Manual TAKER fill: {count} {side.upper()} contract(s)")
        await self.cancel_all_orders()
        await self.refresh_market_data()

        self.manual_fill_mode = True
        self.manual_fill_side = side
        self.manual_fill_count = count
        self.manual_fill_start_position = self.cached_position[side] or 0

        self.cycle_start_ts = int(time.time() * 1000)
        self.current_increment = {"yes": 0, "no": 0}
        self.order_ids = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}
        self.bump_enabled = {"yes": False, "no": False}
        self.bump_target = {"yes": None, "no": None}

        self.active = True
        self.paused = False
        self.stopping = False
        self.waiting_for_manual_resume = False
        self.one_side_first_mode = False

        other_side = "no" if side == "yes" else "yes"
        other_bid, _, _ = self.get_bid_info(other_side)
        if other_bid is not None:
            ask_cents = 100 - round(other_bid * 100)
            ask_price = ask_cents / 100
            order_id = await self.place_order(side, ask_price, count)
            if order_id:
                self.order_ids[side] = order_id
                self.last_prices[side] = ask_price
                print(f"✓ {side.upper()}: Placed {count} @ ${ask_price:.2f} (taker at ask)")
            else:
                print(f"❌ {side.upper()}: Failed to place taker order")
                self.manual_fill_mode = False
                self.active = False
                return
        else:
            print(f"❌ {side.upper()}: No opposite bid available for ask price")
            self.manual_fill_mode = False
            self.active = False
            return

        if not self.trading_task or self.trading_task.done():
            self.trading_task = asyncio.create_task(self.trading_loop())

    async def start_trading(self):
        if self.active:
            print("\n⚠️ Already running")
            return

        loop = asyncio.get_event_loop()

        # Contract size
        while True:
            try:
                val = await loop.run_in_executor(None, lambda: input("Contract increment (1-20): ").strip())
                increment = int(val)
                if 1 <= increment <= 20:
                    self.contract_increment = increment
                    break
                print("❌ Must be 1-20")
            except ValueError:
                print("❌ Enter a number")

        # One-side-first?
        osf = await loop.run_in_executor(None, lambda: input("Higher price side first? (y/n): ").strip().lower())
        if osf == 'y':
            self.one_side_first_mode = True
            self.side_selection = "expensive"
            await self.refresh_market_data()
            yes_bid = self.market_state.yes_bid or 0
            no_bid = self.market_state.no_bid or 0
            self.active_side = "yes" if yes_bid >= no_bid else "no"
            print(f"Starting with {self.active_side.upper()} (bid: ${yes_bid if self.active_side == 'yes' else no_bid:.2f})")
        else:
            self.one_side_first_mode = False
            self.active_side = None

        side_info = f" + HIGHER FIRST ({self.active_side.upper()})" if self.one_side_first_mode else ""
        confirm = await loop.run_in_executor(
            None, lambda: input(f"Start JOIN BID{side_info} with {self.contract_increment} contracts? (y/n): ").strip().lower()
        )
        if confirm != 'y':
            print("❌ Cancelled")
            return

        self.is_rebalancing = False
        self.manual_fill_mode = False
        print(f"\n🚀 Starting JOIN BID{side_info} ({self.contract_increment} contracts)...")

        await self.refresh_market_data()
        self.cycle_start_ts = int(time.time() * 1000)
        self.current_increment = {"yes": 0, "no": 0}
        self.order_ids = {"yes": None, "no": None}
        self.last_prices = {"yes": None, "no": None}
        self.fill_prices = {"yes": None, "no": None}
        self.bump_enabled = {"yes": False, "no": False}
        self.bump_target = {"yes": None, "no": None}

        self.active = True
        self.stopping = False
        self.emergency_stop = False
        self.paused = True
        self.waiting_for_manual_resume = True

        self.telegram.notify_startup(
            self.market_id, self.contract_increment,
            self.one_side_first_mode, self.active_side
        )

        self.trading_task = asyncio.create_task(self.trading_loop())
        print("✓ Ready — press R to begin trading")

    def stop_trading(self):
        if not self.active:
            print("\n⚠️ Not trading")
            return
        print("\n⏳ Stopping after current cycle...")
        self.stopping = True

    async def force_stop(self):
        if not self.active:
            print("\n⚠️ Not trading")
            return
        print("\n⛔ Emergency stop — cancelling all orders...")
        await self.cancel_all_orders()
        self.bump_enabled = {"yes": False, "no": False}
        self.bump_target = {"yes": None, "no": None}
        self.manual_fill_mode = False
        self.active = False
        self.stopping = False
        self.emergency_stop = False
        if self.trading_task:
            self.trading_task.cancel()
            try:
                await self.trading_task
            except asyncio.CancelledError:
                pass
        self.telegram.notify_stopped()
        print("✓ Stopped")


async def main():
    market_id = os.getenv("MENTIONS_MARKET_ID")
    api_key = os.getenv("KALSHI_MOM_API_KEY")
    api_secret = os.getenv("KALSHI_MOM_API_SECRET")
    bot_token = os.getenv("TELEGRAM_MOM_BOT_TOKEN")
    chat_id = os.getenv("TELEGRAM_MOM_CHAT_ID")

    if not market_id:
        print("❌ Set MENTIONS_MARKET_ID in .env")
        sys.exit(1)
    if not all([api_key, api_secret]):
        print("❌ Missing KALSHI_MOM_API_KEY / KALSHI_MOM_API_SECRET in .env")
        sys.exit(1)
    if not all([bot_token, chat_id]):
        print("❌ Missing TELEGRAM_MOM_BOT_TOKEN / TELEGRAM_MOM_CHAT_ID in .env")
        sys.exit(1)

    config = Config()
    telegram = TelegramNotifier(bot_token, chat_id)
    trader = MentionsMaker(api_key, api_secret, market_id, config, telegram)
    trader.running = True

    # Start telegram listener
    telegram.start_listener()

    print("\n" + "=" * 60)
    print("MENTIONS MARKET MAKER")
    print(f"Market: {market_id}")
    print(f"Account: mom")
    print("=" * 60)
    print("\nControls:")
    print("  [G] Start trading (prompts for size & mode)")
    print("  [R] Pause / resume")
    print("  [F] Single fire (one cycle then pause)")
    print("  [1] Toggle YES bump (+1c above others' best bid)")
    print("  [2] Toggle NO bump (+1c above others' best bid)")
    print("  [7] Taker fill: YES (contract increment)")
    print("  [8] Taker fill: NO (contract increment)")
    print("  [9] Taker fill: YES (2x contract increment)")
    print("  [0] Taker fill: NO (2x contract increment)")
    print("  [Y] Maker fill: YES (contract increment)")
    print("  [U] Maker fill: NO (contract increment)")
    print("  [H] Maker fill: YES (2x contract increment)")
    print("  [J] Maker fill: NO (2x contract increment)")
    print("  [K] Close position (buy opposite side to flatten)")
    print("  [N] Cancel all orders")
    print("  [S] Stop after current cycle")
    print("  [Z] Emergency stop")
    print("=" * 60 + "\n")

    loop = asyncio.get_event_loop()

    try:
        while trader.running:
            choice = await loop.run_in_executor(None, lambda: input("Command: ").strip().upper())

            if choice == "G":
                await trader.start_trading()
            elif choice == "R":
                trader.toggle_pause()
            elif choice == "F":
                trader.single_fire()
            elif choice == "1":
                trader.toggle_bump("yes")
            elif choice == "2":
                trader.toggle_bump("no")
            elif choice == "7":
                await trader.start_manual_fill_taker("yes", trader.contract_increment)
            elif choice == "8":
                await trader.start_manual_fill_taker("no", trader.contract_increment)
            elif choice == "9":
                await trader.start_manual_fill_taker("yes", trader.contract_increment * 2)
            elif choice == "0":
                await trader.start_manual_fill_taker("no", trader.contract_increment * 2)
            elif choice == "Y":
                await trader.start_manual_fill("yes", trader.contract_increment)
            elif choice == "U":
                await trader.start_manual_fill("no", trader.contract_increment)
            elif choice == "H":
                await trader.start_manual_fill("yes", trader.contract_increment * 2)
            elif choice == "J":
                await trader.start_manual_fill("no", trader.contract_increment * 2)
            elif choice == "K":
                await trader.start_close_position()
            elif choice == "N":
                await trader.cancel_all_orders()
                print("\n✓ All orders cancelled")
            elif choice == "S":
                trader.stop_trading()
            elif choice == "Z":
                await trader.force_stop()
            else:
                print("❌ Invalid command")
    except KeyboardInterrupt:
        print("\n⛔ Interrupted")
        await trader.cancel_all_orders()
        telegram.notify_stopped()


if __name__ == "__main__":
    asyncio.run(main())
