#!/usr/bin/env python3
"""
LuckySt Syndicate - Game Scheduler with Base On-Chain Commitment

Schedules a LuckBot Trader to trade a list of specific games in the next 24 hours.
The schedule is committed to Base as an immutable self-transfer transaction,
then stored in Redis for the execution engine to pick up and deploy at game time.

Flow:
1. User provides list of games (tickers + start times + optional config)
2. Schedule commitment is written to Base (calldata = JSON schedule)
3. Schedule entries stored in Redis sorted set (score = start timestamp)
4. Background poller checks Redis every 30s and deploys instances at game time
5. When session ends, on-chain report is generated (via mm_core report hook)
"""

import os
import json
import uuid
import asyncio
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone, timedelta
from typing import Optional

import redis

from agentic.syndicate.chain_writer import (
    write_report_to_chain,
    LUCKYST_BUILDER_SUFFIX,
    BASE_RPC_URL,
    BASE_CHAIN_ID,
)


# Redis keys
SCHEDULE_SET = "luckyst:schedule:pending"       # sorted set: score=start_ts, value=schedule_id
SCHEDULE_META = "luckyst:schedule:meta:"        # hash per schedule: {schedule_id} -> JSON
SCHEDULE_ACTIVE = "luckyst:schedule:active"     # set of currently running schedule IDs
SCHEDULE_DONE = "luckyst:schedule:done"         # set of completed schedule IDs


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

@dataclass
class GameEntry:
    """A single game to be traded."""
    markets: list[str]               # 1 or 2 Kalshi market tickers
    start_time: str                  # ISO-8601 UTC when to deploy
    stop_time: Optional[str] = None  # ISO-8601 UTC when to stop (None = manual)
    contract_size: int = 1
    higher_first: bool = False


@dataclass
class ScheduleCommitment:
    """Full schedule committed to Base."""
    schedule_id: str
    games: list[GameEntry] = field(default_factory=list)
    created_at: str = ""
    base_tx: Optional[str] = None
    author_address: str = ""

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2)

    @staticmethod
    def from_json(raw: str) -> "ScheduleCommitment":
        data = json.loads(raw)
        games = [GameEntry(**g) for g in data.get("games", [])]
        return ScheduleCommitment(
            schedule_id=data["schedule_id"],
            games=games,
            created_at=data.get("created_at", ""),
            base_tx=data.get("base_tx"),
            author_address=data.get("author_address", ""),
        )


# ---------------------------------------------------------------------------
# Schedule creation + on-chain commitment
# ---------------------------------------------------------------------------

async def create_schedule(
    games: list[dict],
    redis_client: redis.Redis,
    base_address: Optional[str] = None,
    base_key: Optional[str] = None,
) -> ScheduleCommitment:
    """
    Create a game schedule, commit to Base, and store in Redis.

    Args:
        games: List of game dicts with keys: markets, start_time, stop_time,
               contract_size, higher_first
        redis_client: Redis connection
        base_address: Base address for on-chain commitment (optional)
        base_key: Private key for signing (optional)

    Returns:
        ScheduleCommitment with schedule_id and optional base_tx
    """
    schedule_id = str(uuid.uuid4())[:8]
    now = datetime.now(timezone.utc)

    game_entries = []
    for g in games:
        entry = GameEntry(
            markets=g["markets"],
            start_time=g["start_time"],
            stop_time=g.get("stop_time"),
            contract_size=g.get("contract_size", 1),
            higher_first=g.get("higher_first", False),
        )
        game_entries.append(entry)

    commitment = ScheduleCommitment(
        schedule_id=schedule_id,
        games=game_entries,
        created_at=now.isoformat(),
        author_address=base_address or "",
    )

    # Write to Base if credentials are available
    if base_address and base_key:
        try:
            schedule_bytes = commitment.to_json().encode("utf-8")
            # Prefix with a schedule marker byte (0x02 = schedule, vs 0x01 = report)
            chain_data = b"\x02" + schedule_bytes
            result = await write_report_to_chain(chain_data, base_key, base_address)
            if result.success and result.tx_hash:
                commitment.base_tx = result.tx_hash
                print(f"✓ Schedule committed to Base: {result.tx_hash}")
            else:
                print(f"⚠️ Base commit failed: {result.error}")
        except Exception as e:
            print(f"⚠️ Base commit error: {e}")

    # Store each game in Redis sorted set (score = start timestamp)
    for i, game in enumerate(game_entries):
        try:
            start_dt = datetime.fromisoformat(game.start_time)
            start_ts = start_dt.timestamp()
        except Exception:
            start_ts = now.timestamp()

        game_id = f"{schedule_id}:{i}"
        game_meta = {
            "schedule_id": schedule_id,
            "game_index": i,
            "markets": game.markets,
            "start_time": game.start_time,
            "stop_time": game.stop_time,
            "contract_size": game.contract_size,
            "higher_first": game.higher_first,
            "status": "pending",
            "base_tx": commitment.base_tx,
        }

        # Add to sorted set for time-based polling
        redis_client.zadd(SCHEDULE_SET, {game_id: start_ts})
        # Store metadata
        redis_client.set(f"{SCHEDULE_META}{game_id}", json.dumps(game_meta))

    # Store full commitment
    redis_client.set(f"{SCHEDULE_META}{schedule_id}", commitment.to_json())

    return commitment


# ---------------------------------------------------------------------------
# Schedule polling (called from background task)
# ---------------------------------------------------------------------------

async def check_and_deploy_scheduled_games(
    redis_client: redis.Redis,
    deploy_fn,
) -> list[str]:
    """
    Check for games that should start now and deploy them.

    Called periodically (every 30s) by a background task.

    Args:
        redis_client: Redis connection
        deploy_fn: async fn(markets, contract_size, higher_first) -> instance_id
                   Should call the /terminal/auto/deploy API endpoint

    Returns:
        List of deployed game IDs
    """
    now = datetime.now(timezone.utc).timestamp()
    deployed = []

    # Get all games whose start_time <= now
    due_games = redis_client.zrangebyscore(SCHEDULE_SET, "-inf", now)

    for game_id_raw in due_games:
        game_id = game_id_raw.decode() if isinstance(game_id_raw, bytes) else game_id_raw

        # Get metadata
        meta_raw = redis_client.get(f"{SCHEDULE_META}{game_id}")
        if not meta_raw:
            redis_client.zrem(SCHEDULE_SET, game_id)
            continue

        meta = json.loads(meta_raw if isinstance(meta_raw, str) else meta_raw.decode())

        if meta.get("status") != "pending":
            redis_client.zrem(SCHEDULE_SET, game_id)
            continue

        # Deploy the trading instance
        try:
            instance_id = await deploy_fn(
                markets=meta["markets"],
                contract_size=meta.get("contract_size", 1),
                higher_first=meta.get("higher_first", False),
            )

            # Update status
            meta["status"] = "running"
            meta["instance_id"] = instance_id
            meta["deployed_at"] = datetime.now(timezone.utc).isoformat()
            redis_client.set(f"{SCHEDULE_META}{game_id}", json.dumps(meta))
            redis_client.sadd(SCHEDULE_ACTIVE, game_id)

            # Remove from pending set
            redis_client.zrem(SCHEDULE_SET, game_id)

            deployed.append(game_id)
            print(f"✓ Scheduled game {game_id} deployed as instance #{instance_id}")

            # If stop_time is set, schedule a stop
            if meta.get("stop_time"):
                try:
                    stop_dt = datetime.fromisoformat(meta["stop_time"])
                    stop_ts = stop_dt.timestamp()
                    redis_client.zadd("luckyst:schedule:stops", {
                        json.dumps({"game_id": game_id, "instance_id": instance_id}): stop_ts
                    })
                except Exception:
                    pass

        except Exception as e:
            print(f"⚠️ Failed to deploy game {game_id}: {e}")
            meta["status"] = "failed"
            meta["error"] = str(e)
            redis_client.set(f"{SCHEDULE_META}{game_id}", json.dumps(meta))
            redis_client.zrem(SCHEDULE_SET, game_id)

    return deployed


async def check_and_stop_scheduled_games(
    redis_client: redis.Redis,
    stop_fn,
):
    """
    Check for games that should stop now and send stop commands.

    Args:
        redis_client: Redis connection
        stop_fn: async fn(instance_id) -> None
    """
    now = datetime.now(timezone.utc).timestamp()
    due_stops = redis_client.zrangebyscore("luckyst:schedule:stops", "-inf", now)

    for entry_raw in due_stops:
        entry = json.loads(entry_raw if isinstance(entry_raw, str) else entry_raw.decode())
        game_id = entry["game_id"]
        instance_id = entry["instance_id"]

        try:
            await stop_fn(instance_id)
            print(f"✓ Scheduled stop for game {game_id}, instance #{instance_id}")
        except Exception as e:
            print(f"⚠️ Failed to stop game {game_id}: {e}")

        redis_client.zrem("luckyst:schedule:stops", entry_raw)

        # Update status
        meta_raw = redis_client.get(f"{SCHEDULE_META}{game_id}")
        if meta_raw:
            meta = json.loads(meta_raw if isinstance(meta_raw, str) else meta_raw.decode())
            meta["status"] = "completed"
            meta["stopped_at"] = datetime.now(timezone.utc).isoformat()
            redis_client.set(f"{SCHEDULE_META}{game_id}", json.dumps(meta))
            redis_client.srem(SCHEDULE_ACTIVE, game_id)
            redis_client.sadd(SCHEDULE_DONE, game_id)


# ---------------------------------------------------------------------------
# List / query schedules
# ---------------------------------------------------------------------------

def list_pending_games(redis_client: redis.Redis) -> list[dict]:
    """List all pending scheduled games."""
    game_ids = redis_client.zrange(SCHEDULE_SET, 0, -1, withscores=True)
    results = []
    for game_id_raw, score in game_ids:
        game_id = game_id_raw.decode() if isinstance(game_id_raw, bytes) else game_id_raw
        meta_raw = redis_client.get(f"{SCHEDULE_META}{game_id}")
        if meta_raw:
            meta = json.loads(meta_raw if isinstance(meta_raw, str) else meta_raw.decode())
            meta["scheduled_start_ts"] = score
            results.append(meta)
    return results


def list_active_games(redis_client: redis.Redis) -> list[dict]:
    """List all currently running scheduled games."""
    game_ids = redis_client.smembers(SCHEDULE_ACTIVE)
    results = []
    for game_id_raw in game_ids:
        game_id = game_id_raw.decode() if isinstance(game_id_raw, bytes) else game_id_raw
        meta_raw = redis_client.get(f"{SCHEDULE_META}{game_id}")
        if meta_raw:
            results.append(json.loads(meta_raw if isinstance(meta_raw, str) else meta_raw.decode()))
    return results
