#!/usr/bin/env python3
"""
OpenClawd v2 - LuckySt Conversational Trading Agent

Natural language trading via Telegram. Claude decides when to deploy,
jump, pause, and stop based on your instructions.

Usage:
    python3 telegram_bot.py
"""

import os
import sys
import re
import json
import uuid
import logging
import httpx
from pathlib import Path
from datetime import datetime, timedelta, timezone
from dataclasses import dataclass
from typing import Optional
from dotenv import load_dotenv

from telegram import Update
from telegram.ext import (
    Application,
    CommandHandler,
    MessageHandler,
    ConversationHandler,
    ContextTypes,
    filters,
)

# Shared trading core
from trading_core import (
    api_request,
    load_session,
    save_session,
    get_token_and_creds,
    get_active_instance,
    get_trading_context,
    execute_tool as core_execute_tool,
    run_claude_agentic_loop,
    OPENCLAWD_SYSTEM,
    HAS_ANTHROPIC,
    ANTHROPIC_API_KEY,
    API_BASE,
)

# Load backend .env for defaults
load_dotenv(Path(__file__).parent.parent / "backend" / ".env")

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger("openclawd")

# ============================================================================
# CONFIG
# ============================================================================

TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
AUTHORIZED_USER_ID = int(os.environ.get("TELEGRAM_USER_ID", "0"))

# Conversation states for /creds flow
WAITING_API_KEY, WAITING_RSA_KEY = range(2)


# ============================================================================
# AUTH CHECK
# ============================================================================

def authorized(func):
    """Only allow the authorized Telegram user"""
    async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE):
        user_id = update.effective_user.id
        if AUTHORIZED_USER_ID and user_id != AUTHORIZED_USER_ID:
            await update.message.reply_text("Unauthorized. This bot is locked to one operator.")
            return
        return await func(update, context)
    return wrapper


# ============================================================================
# SLASH COMMANDS (backwards-compatible)
# ============================================================================

@authorized
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    session = load_session()
    logged_in = "jwt_token" in session
    has_creds = "kalshi_api_key" in session

    msg = (
        "Welcome to OpenClawd - LuckySt Trading Terminal\n"
        "Syndicate eternal. Spreads temporary. Edge ours.\n\n"
    )

    if not logged_in:
        msg += "First, login: /login <username> <password>\n\n"
    else:
        msg += "Logged in.\n\n"

    if not has_creds:
        msg += "Then store Kalshi creds: /creds\n\n"
    else:
        msg += "Kalshi credentials stored.\n\n"

    msg += (
        "You can use slash commands or just talk to me naturally.\n\n"
        "Examples:\n"
        "- 'deploy FOX vs GEN with 1 contract'\n"
        "- 'start trading in 30 minutes, stop after 40 minutes'\n"
        "- 'jump on market 1 when the spread looks wide'\n"
        "- 'what's a good spread for esports?'\n\n"
        "Commands: /deploy /deploy1 /status /jump /pause /fire /fair /z /stop /kill /monitor /balance /use /help"
    )
    await update.message.reply_text(msg)


@authorized
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await cmd_start(update, context)


@authorized
async def cmd_login(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if not args or len(args) < 2:
        await update.message.reply_text("Usage: /login <username> <password>")
        return

    username, password = args[0], " ".join(args[1:])
    result = await api_request("POST", "/auth/login", {"username": username, "password": password})

    if "error" in result:
        await update.message.reply_text(f"Login failed: {result['error']}")
        return

    token = result.get("access_token")
    if not token:
        await update.message.reply_text("Login failed: no token returned")
        return

    session = load_session()
    session["jwt_token"] = token
    session["username"] = username
    save_session(session)

    try:
        await update.message.delete()
    except Exception:
        pass

    await update.message.reply_text(f"Logged in as {username}. (Password message deleted)")


@authorized
async def cmd_creds(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "Send your Kalshi API key (UUID format).\n"
        "It will be deleted from chat after saving."
    )
    return WAITING_API_KEY


@authorized
async def recv_api_key(update: Update, context: ContextTypes.DEFAULT_TYPE):
    api_key = update.message.text.strip()
    session = load_session()
    session["kalshi_api_key"] = api_key
    save_session(session)
    try:
        await update.message.delete()
    except Exception:
        pass
    await update.message.reply_text(
        "API key saved. Now send your RSA private key.\n"
        "Paste the full PEM key (including BEGIN/END lines)."
    )
    return WAITING_RSA_KEY


@authorized
async def recv_rsa_key(update: Update, context: ContextTypes.DEFAULT_TYPE):
    rsa_key = update.message.text.strip()
    if "BEGIN" not in rsa_key:
        await update.message.reply_text("Invalid RSA key. Must contain BEGIN PRIVATE KEY. Try again.")
        return WAITING_RSA_KEY

    session = load_session()
    session["rsa_key"] = rsa_key
    save_session(session)
    try:
        await update.message.delete()
    except Exception:
        pass
    await update.message.reply_text("RSA key saved. Credentials ready.")
    return ConversationHandler.END


async def creds_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("Cancelled.")
    return ConversationHandler.END


@authorized
async def cmd_deploy(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if not args or len(args) < 2:
        await update.message.reply_text("Usage: /deploy <market1> <market2> [size]")
        return
    market1, market2 = args[0].upper(), args[1].upper()
    size = int(args[2]) if len(args) > 2 else 1
    token, api_key, rsa_key = get_token_and_creds()
    if not token:
        await update.message.reply_text("Not logged in. /login first.")
        return
    if not api_key or not rsa_key:
        await update.message.reply_text("No Kalshi creds. /creds first.")
        return

    result = await api_request("POST", "/terminal/auto/deploy", {
        "num_markets": 2, "mode": "automated", "markets": [market1, market2],
        "kalshi_api_key": api_key, "rsa_key_path": rsa_key,
        "both_side": "no", "market_priority": "none", "side_priority": None,
        "min_spread": 2, "max_spread": 15, "m1_bounds": [1, 15, 85, 99],
        "m2_bounds": [1, 15, 85, 99], "position_increment": size,
        "max_position": 100, "join_only": True, "grid_mode": False,
        "grid_levels": None, "contract_increment": size,
    }, token)

    if "error" in result:
        await update.message.reply_text(f"Deploy failed: {result['error']}")
        return

    instance_id = result.get("id")
    session = load_session()
    session["active_instance"] = instance_id
    save_session(session)
    m1s, m2s = market1.split("-")[-1], market2.split("-")[-1]
    await update.message.reply_text(
        f"Deployed! #{instance_id}\n{m1s} vs {m2s} | {size} contracts | JOIN\n"
        f"/jump 1 /jump 2 /pause /fire /stop /status\n"
        f"Jump monitor OFF. Use /monitor to enable."
    )


@authorized
async def cmd_deploy1(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if not args:
        await update.message.reply_text("Usage: /deploy1 <market> [size]")
        return
    market = args[0].upper()
    size = int(args[1]) if len(args) > 1 else 1
    token, api_key, rsa_key = get_token_and_creds()
    if not token or not api_key or not rsa_key:
        await update.message.reply_text("Not logged in or no creds.")
        return

    result = await api_request("POST", "/terminal/auto/deploy", {
        "num_markets": 1, "mode": "automated", "markets": [market],
        "kalshi_api_key": api_key, "rsa_key_path": rsa_key,
        "both_side": None, "market_priority": None, "side_priority": "no",
        "min_spread": 2, "max_spread": 15, "m1_bounds": [1, 15, 85, 99],
        "m2_bounds": None, "position_increment": size,
        "max_position": 100, "join_only": True, "grid_mode": False,
        "grid_levels": None, "contract_increment": size,
    }, token)

    if "error" in result:
        await update.message.reply_text(f"Deploy failed: {result['error']}")
        return

    instance_id = result.get("id")
    session = load_session()
    session["active_instance"] = instance_id
    save_session(session)
    await update.message.reply_text(
        f"Deployed! #{instance_id} | {market} | {size} contracts | JOIN\n"
        f"Jump monitor OFF. Use /monitor to enable."
    )


@authorized
async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    token, _, _ = get_token_and_creds()
    if not token:
        await update.message.reply_text("Not logged in.")
        return
    result = await api_request("GET", "/terminal/auto/instances", token=token)
    if "error" in result:
        await update.message.reply_text(f"Error: {result['error']}")
        return
    instances = result.get("instances", [])
    if not instances:
        await update.message.reply_text("No active instances.")
        return
    for inst in instances:
        markets = inst.get("markets", [])
        market_str = " vs ".join(m.split("-")[-1] for m in markets)
        inc = inst.get("current_increment", {})
        fill_str = ""
        if "m1" in inc:
            fill_str = f"M1 {inc['m1'].get('filled',0)}/{inc['m1'].get('total',0)} | M2 {inc['m2'].get('filled',0)}/{inc['m2'].get('total',0)}"
        elif "yes" in inc:
            fill_str = f"YES {inc['yes'].get('filled',0)}/{inc['yes'].get('total',0)} | NO {inc['no'].get('filled',0)}/{inc['no'].get('total',0)}"
        msg = f"#{inst['id']} | {inst.get('status','?').upper()} | {inst.get('trade_mode','Join')}\n{market_str}\nPos: {inst.get('position',0)} | P&L: {inst.get('pnl','$0.00')}"
        if fill_str:
            msg += f"\n{fill_str}"
        await update.message.reply_text(msg)


@authorized
async def cmd_jump(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if not args:
        await update.message.reply_text("Usage: /jump <1|2>")
        return
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    result = await api_request("POST", f"/terminal/auto/instances/{instance_id}/jump", {"market_index": int(args[0]) - 1}, token)
    if "error" in result:
        await update.message.reply_text(f"Error: {result['error']}")
        return
    await update.message.reply_text(f"Market {args[0]} JUMP: {'ON' if result.get('jump_active') else 'OFF'}")


@authorized
async def cmd_pause(update: Update, context: ContextTypes.DEFAULT_TYPE):
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/control", {"action": "toggle_pause"}, token)
    await update.message.reply_text("Pause/Resume toggled.")


@authorized
async def cmd_fire(update: Update, context: ContextTypes.DEFAULT_TYPE):
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/control", {"action": "single_fire"}, token)
    await update.message.reply_text("Single fire activated.")


@authorized
async def cmd_fair(update: Update, context: ContextTypes.DEFAULT_TYPE):
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/fair_value", {}, token)
    await update.message.reply_text("Fair value toggled.")


@authorized
async def cmd_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Cancel all resting orders (z key equivalent)"""
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/control", {"action": "cancel_orders"}, token)
    await update.message.reply_text("All orders cancelled.")


@authorized
async def cmd_stop(update: Update, context: ContextTypes.DEFAULT_TYPE):
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/control", {"action": "stop"}, token)
    stop_jump_monitor(context.job_queue)
    await update.message.reply_text("Stop requested. Will complete current cycle then stop.")


@authorized
async def cmd_kill(update: Update, context: ContextTypes.DEFAULT_TYPE):
    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance.")
        return
    token, _, _ = get_token_and_creds()
    await api_request("POST", f"/terminal/auto/instances/{instance_id}/control", {"action": "force_stop"}, token)
    stop_jump_monitor(context.job_queue)
    await update.message.reply_text("Force stopped. Orders cancelled.")


@authorized
async def cmd_balance(update: Update, context: ContextTypes.DEFAULT_TYPE):
    wallet = "0x4085b0eA2598aF729205bC982b2A3419593BE24d"
    try:
        usdc_addr = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        selector = "0x70a08231"
        padded = wallet.lower().replace("0x", "").zfill(64)
        async with httpx.AsyncClient() as client:
            resp = await client.post("https://mainnet.base.org", json={
                "jsonrpc": "2.0", "method": "eth_call",
                "params": [{"to": usdc_addr, "data": selector + padded}, "latest"], "id": 1,
            })
            usdc = int(resp.json()["result"], 16) / 1e6
        await update.message.reply_text(f"Base Wallet: {wallet[:8]}...{wallet[-6:]}\nUSDC: {usdc:.2f}")
    except Exception as e:
        await update.message.reply_text(f"Error: {e}")


@authorized
async def cmd_use(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if not args:
        await update.message.reply_text("Usage: /use <instance_id>")
        return
    instance_id = int(args[0])
    session = load_session()
    session["active_instance"] = instance_id
    save_session(session)
    await update.message.reply_text(f"Active instance: #{instance_id}")


@authorized
async def cmd_monitor(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Toggle jump monitor on/off. Usage: /monitor [interval_seconds]"""
    running_jobs = context.job_queue.get_jobs_by_name("jump_monitor")
    if running_jobs:
        stop_jump_monitor(context.job_queue)
        await update.message.reply_text("Jump monitor OFF.")
        return

    instance_id = get_active_instance()
    if not instance_id:
        await update.message.reply_text("No active instance. Deploy first.")
        return

    args = context.args
    interval = int(args[0]) if args else 10
    interval = max(5, min(interval, 120))

    start_jump_monitor(context.job_queue, update.effective_chat.id, interval=interval)
    await update.message.reply_text(f"Jump monitor ON (every {interval}s).\n/monitor again to stop.")


# ============================================================================
# SCHEDULING SYSTEM
# ============================================================================

@dataclass
class TradingSession:
    session_id: str
    markets: list
    start_time: Optional[datetime]
    stop_time: Optional[datetime]
    contract_size: int = 1
    status: str = "pending"
    instance_id: Optional[int] = None
    chat_id: int = 0
    jump_instructions: str = ""


def parse_time(time_str: str) -> datetime:
    """Parse relative (+30m, +1h, +2h30m) or absolute (ISO-8601, HH:MM) times"""
    now = datetime.now(timezone.utc)

    if time_str.startswith("+"):
        total_minutes = 0
        parts = re.findall(r'(\d+)\s*(h|m)', time_str)
        for val, unit in parts:
            if unit == "h":
                total_minutes += int(val) * 60
            else:
                total_minutes += int(val)
        if not parts:
            digits = re.findall(r'\d+', time_str)
            if digits:
                total_minutes = int(digits[0])
        return now + timedelta(minutes=total_minutes)

    try:
        dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except ValueError:
        pass

    try:
        parts = time_str.split(":")
        hour, minute = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
        dt = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
        if dt <= now:
            dt += timedelta(days=1)
        return dt
    except (ValueError, IndexError):
        pass

    raise ValueError(f"Cannot parse time: {time_str}")


async def scheduled_start(context: ContextTypes.DEFAULT_TYPE):
    """APScheduler callback: deploy trading instance"""
    job_data = context.job.data
    session_id = job_data["session_id"]
    sessions = context.bot_data.get("sessions", {})
    sess = sessions.get(session_id)
    if not sess or sess.status != "pending":
        return

    token, api_key, rsa_key = get_token_and_creds()
    if not token or not api_key or not rsa_key:
        await context.bot.send_message(sess.chat_id, "Scheduled deploy failed: not logged in or no creds.")
        sess.status = "error"
        return

    num_markets = len(sess.markets)
    config = {
        "num_markets": num_markets, "mode": "automated", "markets": sess.markets,
        "kalshi_api_key": api_key, "rsa_key_path": rsa_key,
        "both_side": "no" if num_markets == 2 else None,
        "market_priority": "none" if num_markets == 2 else None,
        "side_priority": None if num_markets == 2 else "no",
        "min_spread": 2, "max_spread": 15, "m1_bounds": [1, 15, 85, 99],
        "m2_bounds": [1, 15, 85, 99] if num_markets == 2 else None,
        "position_increment": sess.contract_size, "max_position": 100,
        "join_only": True, "grid_mode": False, "grid_levels": None,
        "contract_increment": sess.contract_size,
    }

    result = await api_request("POST", "/terminal/auto/deploy", config, token)

    if "error" in result:
        await context.bot.send_message(sess.chat_id, f"Scheduled deploy FAILED: {result['error']}")
        sess.status = "error"
        return

    sess.instance_id = result.get("id")
    sess.status = "running"

    s = load_session()
    s["active_instance"] = sess.instance_id
    save_session(s)

    market_str = " vs ".join(m.split("-")[-1] for m in sess.markets)
    stop_str = f"\nAuto-stop: {sess.stop_time.strftime('%H:%M UTC')}" if sess.stop_time else ""
    await context.bot.send_message(
        sess.chat_id,
        f"Trading started! Instance #{sess.instance_id}\n"
        f"{market_str} | {sess.contract_size} contracts | JOIN{stop_str}"
    )


async def scheduled_stop(context: ContextTypes.DEFAULT_TYPE):
    """APScheduler callback: graceful stop"""
    job_data = context.job.data
    session_id = job_data["session_id"]
    sessions = context.bot_data.get("sessions", {})
    sess = sessions.get(session_id)
    if not sess or not sess.instance_id:
        return

    token, _, _ = get_token_and_creds()
    if token:
        await api_request("POST", f"/terminal/auto/instances/{sess.instance_id}/control", {"action": "stop"}, token)

    sess.status = "completed"
    await context.bot.send_message(
        sess.chat_id,
        f"Trading session ended (graceful stop).\nInstance #{sess.instance_id} completing final cycle."
    )


# ============================================================================
# JUMP MONITOR
# ============================================================================

async def check_jump_conditions(context: ContextTypes.DEFAULT_TYPE):
    """Monitor instance and trigger JUMP when conditions are met."""
    instance_id = get_active_instance()
    token, _, _ = get_token_and_creds()
    if not instance_id or not token:
        return

    try:
        result = await api_request(
            "GET", f"/terminal/auto/instances/{instance_id}/status", token=token
        )
        if "error" in result or "orderbook" not in result:
            return

        orderbook = result.get("orderbook", {})
        jump_active = result.get("jump_active", {})

        for market_idx, (market_id, ob) in enumerate(orderbook.items()):
            if jump_active.get(market_id, False):
                continue

            bids = ob.get("bids", [])
            asks = ob.get("asks", [])
            resting = ob.get("resting_order")
            queue = ob.get("queue_position")

            if not bids or not asks or not resting or not queue:
                continue

            spread = asks[0]["price"] - bids[0]["price"]
            if spread <= 1:
                continue

            total_at_bid = bids[0]["size"]
            if total_at_bid <= 0:
                continue
            queue_pct = queue["position"] / total_at_bid
            if queue_pct <= 0.75:
                continue

            resting_pct = resting["quantity"] / total_at_bid
            if resting_pct >= 0.25:
                continue

            await api_request(
                "POST", f"/terminal/auto/instances/{instance_id}/jump",
                {"market_index": market_idx}, token
            )
            chat_id = context.job.data.get("chat_id")
            if chat_id:
                label = market_id[-7:] if len(market_id) > 7 else market_id
                await context.bot.send_message(
                    chat_id,
                    f"jumping bid — {label} (spread {spread}c, queue {queue_pct:.0%}, resting {resting_pct:.0%})"
                )
    except Exception as e:
        logger.error(f"Jump monitor error: {e}")


def start_jump_monitor(job_queue, chat_id: int, interval: int = 10):
    """Start the jump condition monitor with configurable interval."""
    stop_jump_monitor(job_queue)
    job_queue.run_repeating(
        check_jump_conditions, interval=interval, first=interval,
        data={"chat_id": chat_id}, name="jump_monitor"
    )
    logger.info(f"Jump monitor started (every {interval}s)")


def stop_jump_monitor(job_queue):
    """Stop the jump condition monitor."""
    for job in job_queue.get_jobs_by_name("jump_monitor"):
        job.schedule_removal()


# ============================================================================
# AI CHAT WITH TOOL-USE (via trading_core)
# ============================================================================

async def send_telegram_reply(update: Update, text: str):
    """Send reply, splitting if too long for Telegram"""
    if len(text) > 4000:
        for i in range(0, len(text), 4000):
            await update.message.reply_text(text[i:i+4000])
    else:
        await update.message.reply_text(text)


@authorized
async def handle_chat(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle non-command text messages via Claude tool-use agentic loop"""
    if not HAS_ANTHROPIC or not ANTHROPIC_API_KEY:
        await update.message.reply_text("AI chat not configured. Set ANTHROPIC_API_KEY in .env.")
        return

    user_msg = update.message.text
    trading_ctx = await get_trading_context()
    chat_id = update.effective_chat.id

    # Build extra context
    extra = f"\n\nCurrent state:\n{trading_ctx}"
    sessions = context.bot_data.get("sessions", {})
    if sessions:
        session_lines = []
        for sid, sess in sessions.items():
            if sess.status in ("pending", "running"):
                market_str = " vs ".join(m.split("-")[-1] for m in sess.markets)
                session_lines.append(f"  {sid[:8]}: {sess.status} | {market_str}")
        if session_lines:
            extra += "\n\nScheduled sessions:\n" + "\n".join(session_lines)

    # Build Telegram-specific tool executor with scheduling support
    async def telegram_tool_executor(tool_name: str, tool_input: dict) -> str:
        async def schedule_fn(ti, cid):
            token, api_key, rsa_key = get_token_and_creds()
            if not token or not api_key or not rsa_key:
                return "Error: Not logged in or missing credentials."

            markets = ti["markets"]
            start_time = parse_time(ti["start_time"])
            stop_time = parse_time(ti["stop_time"]) if ti.get("stop_time") else None
            size = ti.get("contract_size", 1)
            jump_instr = ti.get("jump_instructions", "")

            sid = str(uuid.uuid4())
            sess = TradingSession(
                session_id=sid, markets=markets, start_time=start_time,
                stop_time=stop_time, contract_size=size, chat_id=cid,
                jump_instructions=jump_instr,
            )

            if "sessions" not in context.bot_data:
                context.bot_data["sessions"] = {}
            context.bot_data["sessions"][sid] = sess

            delay_start = (start_time - datetime.now(timezone.utc)).total_seconds()
            if delay_start < 0:
                delay_start = 0
            context.job_queue.run_once(scheduled_start, when=delay_start, data={"session_id": sid}, name=f"start_{sid[:8]}")

            if stop_time:
                delay_stop = (stop_time - datetime.now(timezone.utc)).total_seconds()
                if delay_stop > 0:
                    context.job_queue.run_once(scheduled_stop, when=delay_stop, data={"session_id": sid}, name=f"stop_{sid[:8]}")

            market_str = " vs ".join(m.split("-")[-1] for m in markets)
            start_str = start_time.strftime("%H:%M UTC")
            stop_str = stop_time.strftime("%H:%M UTC") if stop_time else "manual"
            return (
                f"Session scheduled: {sid[:8]}\n"
                f"Markets: {market_str} | {size} contracts\n"
                f"Start: {start_str} (in {int(delay_start/60)} min)\n"
                f"Stop: {stop_str}"
            )

        return await core_execute_tool(
            tool_name, tool_input,
            chat_id=chat_id,
            jump_monitor_fn=lambda cid, interval: start_jump_monitor(context.job_queue, cid, interval),
            stop_monitor_fn=lambda: stop_jump_monitor(context.job_queue),
            schedule_fn=schedule_fn,
            sessions=context.bot_data.get("sessions"),
        )

    history = context.user_data.get("chat_history", [])

    try:
        reply, updated_history = await run_claude_agentic_loop(
            user_message=user_msg,
            chat_history=history,
            tool_executor=telegram_tool_executor,
            system_prompt_extra=extra,
        )
        context.user_data["chat_history"] = updated_history
        await send_telegram_reply(update, reply)
    except Exception as e:
        logger.error(f"AI chat error: {e}")
        context.user_data["chat_history"] = []
        await update.message.reply_text(f"AI error (history reset): {e}")


# ============================================================================
# MAIN
# ============================================================================

def build_telegram_app() -> Application:
    """Build the Telegram Application with all handlers registered."""
    app = Application.builder().token(TELEGRAM_TOKEN).build()

    # Credential conversation handler
    creds_handler = ConversationHandler(
        entry_points=[CommandHandler("creds", cmd_creds)],
        states={
            WAITING_API_KEY: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_api_key)],
            WAITING_RSA_KEY: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_rsa_key)],
        },
        fallbacks=[CommandHandler("cancel", creds_cancel)],
    )

    app.add_handler(CommandHandler("start", cmd_start))
    app.add_handler(CommandHandler("help", cmd_help))
    app.add_handler(CommandHandler("login", cmd_login))
    app.add_handler(creds_handler)
    app.add_handler(CommandHandler("deploy", cmd_deploy))
    app.add_handler(CommandHandler("deploy1", cmd_deploy1))
    app.add_handler(CommandHandler("status", cmd_status))
    app.add_handler(CommandHandler("jump", cmd_jump))
    app.add_handler(CommandHandler("pause", cmd_pause))
    app.add_handler(CommandHandler("fire", cmd_fire))
    app.add_handler(CommandHandler("fair", cmd_fair))
    app.add_handler(CommandHandler("z", cmd_cancel))
    app.add_handler(CommandHandler("stop", cmd_stop))
    app.add_handler(CommandHandler("kill", cmd_kill))
    app.add_handler(CommandHandler("balance", cmd_balance))
    app.add_handler(CommandHandler("use", cmd_use))
    app.add_handler(CommandHandler("monitor", cmd_monitor))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_chat))

    return app


def main():
    if not TELEGRAM_TOKEN:
        print("Set TELEGRAM_BOT_TOKEN in backend/.env")
        sys.exit(1)

    if not AUTHORIZED_USER_ID:
        print("Set TELEGRAM_USER_ID in backend/.env")
        sys.exit(1)

    print("OpenClawd v2 starting...")
    print(f"API: {API_BASE}")
    print(f"Authorized user: {AUTHORIZED_USER_ID}")

    app = build_telegram_app()

    ai_status = "enabled" if (HAS_ANTHROPIC and ANTHROPIC_API_KEY) else "disabled (set ANTHROPIC_API_KEY)"
    print(f"AI chat: {ai_status}")
    print("Bot running. Send /start on Telegram.")
    app.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()
