"""IdleMaxx Discord bot — slash command interface to the Game layer."""

from __future__ import annotations

import datetime
import logging
import os

import discord
from discord import app_commands

from idlemaxx.game import Game
from idlemaxx.models.models import Activity, Character, Item, Trade, TradeStatus, level_from_xp

log = logging.getLogger(__name__)


def _setup_logging() -> None:
    log_file = os.environ.get("IDLEMAXX_LOG_FILE", "idlemaxx_discord.log")
    fmt = logging.Formatter("%(asctime)s %(levelname)-8s %(name)s: %(message)s")

    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(fmt)

    file_handler = logging.FileHandler(log_file)
    file_handler.setFormatter(fmt)

    root = logging.getLogger()
    root.setLevel(logging.INFO)
    root.addHandler(stream_handler)
    root.addHandler(file_handler)


# ---------------------------------------------------------------------------
# Bot setup
# ---------------------------------------------------------------------------


class IdleMaxxBot(discord.Client):
    def __init__(self) -> None:
        intents = discord.Intents.default()
        super().__init__(intents=intents)
        self.tree = app_commands.CommandTree(self)
        db_url = os.environ.get("IDLEMAXX_DB_URL", "sqlite:///idlemaxx.db")
        self.game = Game(db_url)

    async def setup_hook(self) -> None:
        guild_id = os.environ.get("DISCORD_GUILD_ID")
        if guild_id:
            guild = discord.Object(id=int(guild_id))
            self.tree.copy_global_to(guild=guild)
            log.info("Syncing commands to guild %s", guild_id)
            await self.tree.sync(guild=guild)
        else:
            log.info("Syncing commands globally")
            await self.tree.sync()

    async def on_ready(self) -> None:
        assert self.user is not None
        log.info("Logged in as %s (id=%s)", self.user, self.user.id)


bot = IdleMaxxBot()

_seen_interactions: set[int] = set()


def _is_duplicate(interaction: discord.Interaction) -> bool:
    """Return True if this interaction ID has already been processed."""
    if interaction.id in _seen_interactions:
        log.warning("Duplicate interaction %s ignored", interaction.id)
        return True
    _seen_interactions.add(interaction.id)
    return False


# ---------------------------------------------------------------------------
# Character name modal
# ---------------------------------------------------------------------------


class CharacterNameModal(discord.ui.Modal, title="Create your character"):
    character_name = discord.ui.TextInput(
        label="Character name",
        placeholder="Enter a name for your character",
        min_length=1,
        max_length=32,
    )

    async def on_submit(self, interaction: discord.Interaction) -> None:
        name = self.character_name.value.strip()
        try:
            bot.game.create_character(name, discord_user_id=str(interaction.user.id))
            log.info("Character created: %r for user %s (%s)", name, interaction.user, interaction.user.id)
            await interaction.response.send_message(
                f"Character **{name}** created! Re-run your command to get started.",
            )
        except Exception as e:
            log.warning("Character creation failed for user %s: %s", interaction.user.id, e)
            await interaction.response.send_message(
                f"Could not create character: {e}",
                ephemeral=True,
            )


# ---------------------------------------------------------------------------
# Helper: get character or prompt creation
# ---------------------------------------------------------------------------


async def get_or_prompt_character(interaction: discord.Interaction) -> Character | None:
    """Return the Discord user's character, or send a creation modal and return None."""
    character = bot.game.get_character_by_discord_id(str(interaction.user.id))
    if character is None:
        log.info("No character for user %s (%s) — prompting creation", interaction.user, interaction.user.id)
        await interaction.response.send_modal(CharacterNameModal())
    return character


# ---------------------------------------------------------------------------
# Embed builders
# ---------------------------------------------------------------------------


def _format_elapsed(elapsed: datetime.timedelta) -> str:
    total = int(elapsed.total_seconds())
    h, rem = divmod(total, 3600)
    m, s = divmod(rem, 60)
    if h:
        return f"{h}h {m:02d}m {s:02d}s"
    return f"{m:02d}m {s:02d}s"


def _status_idle_embed(character: Character) -> discord.Embed:
    embed = discord.Embed(title=f"{character.name}", color=discord.Color.light_grey())
    embed.add_field(name="Status", value="Idle", inline=False)
    return embed


def _status_active_embed(character: Character) -> discord.Embed:
    activity_record = character.current_activity
    if activity_record is None:
        return _status_idle_embed(character)

    activity = activity_record.activity
    now = datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
    elapsed = now - activity_record.started_at
    actions_so_far = int(elapsed.total_seconds() // activity.action_time)

    embed = discord.Embed(
        title=f"{activity.category.icon} {activity.value.name.replace('_', ' ').title()}",
        color=discord.Color.green(),
    )
    embed.add_field(name="Character", value=character.name, inline=True)
    embed.add_field(name="Elapsed", value=_format_elapsed(elapsed), inline=True)
    if activity_record.action_limit is not None:
        embed.add_field(
            name="Actions",
            value=f"{actions_so_far} / {activity_record.action_limit}",
            inline=True,
        )

    if activity.skill_rewards:
        xp_lines = "\n".join(f"+{r.xp * actions_so_far} {r.skill.value.title()}" for r in activity.skill_rewards)
        embed.add_field(name="XP so far", value=xp_lines or "—", inline=False)

    if activity.item_rewards:
        item_lines = "\n".join(
            f"+{r.quantity * actions_so_far} {r.item.value.replace('_', ' ').title()}" for r in activity.item_rewards
        )
        embed.add_field(name="Items so far", value=item_lines or "—", inline=False)

    queue = bot.game.get_activity_queue(character)
    if queue:
        queue_lines = "\n".join(
            f"{i + 1}. {q.activity.value.name.replace('_', ' ').title()}"
            + (f" ({q.action_limit} actions)" if q.action_limit else "")
            for i, q in enumerate(queue)
        )
        embed.add_field(name="Queue", value=queue_lines, inline=False)

    return embed


def _end_embed(character: Character, rewards: list) -> discord.Embed:
    from collections import defaultdict
    from idlemaxx.game import ActivityReward

    rewards: list[ActivityReward]

    # Merge totals across all chained activities
    total_actions = sum(r.actions_completed for r in rewards)
    total_xp: dict = defaultdict(int)
    total_items: dict = defaultdict(int)
    total_mats: dict = defaultdict(int)
    for r in rewards:
        for skill, xp in r.xp_gained.items():
            total_xp[skill] += xp
        for item, qty in r.items_gained.items():
            total_items[item] += qty
        for item, qty in r.materials_consumed.items():
            total_mats[item] += qty

    title = "Activity Complete" if len(rewards) == 1 else f"Activity Complete ({len(rewards)} activities)"
    embed = discord.Embed(title=title, color=discord.Color.gold())
    embed.add_field(name="Actions completed", value=str(total_actions), inline=False)

    if total_xp:
        xp_lines = "\n".join(f"+{xp} {skill.value.title()}" for skill, xp in total_xp.items())
        embed.add_field(name="XP gained", value=xp_lines, inline=False)

    if total_items:
        item_lines = "\n".join(f"+{qty} {item.value.replace('_', ' ').title()}" for item, qty in total_items.items())
        embed.add_field(name="Items gained", value=item_lines, inline=False)

    if total_mats:
        mat_lines = "\n".join(f"-{qty} {item.value.replace('_', ' ').title()}" for item, qty in total_mats.items())
        embed.add_field(name="Materials consumed", value=mat_lines, inline=False)

    return embed


# ---------------------------------------------------------------------------
# Slash commands
# ---------------------------------------------------------------------------


@bot.tree.command(name="status", description="Check your current activity or idle status")
async def status_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/status called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    if character.current_activity is None:
        log.info("/status: %r is idle", character.name)
        embed = _status_idle_embed(character)
    else:
        log.info("/status: %r is doing %s", character.name, character.current_activity.activity.value.name)
        embed = _status_active_embed(character)

    await interaction.response.send_message(embed=embed)


async def activity_autocomplete(
    interaction: discord.Interaction,
    current: str,
) -> list[app_commands.Choice[str]]:
    matches = [
        app_commands.Choice(
            name=a.value.name.replace("_", " ").title(),
            value=a.name,
        )
        for a in Activity
        if current.lower() in a.value.name.replace("_", " ")
    ]
    return matches[:25]


@bot.tree.command(name="start", description="Start an activity")
@app_commands.describe(activity="Which activity to start", limit="Stop after this many actions (optional)")
@app_commands.autocomplete(activity=activity_autocomplete)
async def start_command(interaction: discord.Interaction, activity: str, limit: int | None = None) -> None:
    if _is_duplicate(interaction):
        return
    log.info(
        "/start called by %s (%s) with activity=%r limit=%r", interaction.user, interaction.user.id, activity, limit
    )
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        act = Activity[activity]
    except KeyError:
        log.warning("/start: unknown activity %r from user %s", activity, interaction.user.id)
        await interaction.response.send_message(f"Unknown activity `{activity}`.", ephemeral=True)
        return

    try:
        bot.game.start_activity(character, act, action_limit=limit)
    except Exception as e:
        log.warning("/start: failed for %r (%s): %s", character.name, act.value.name, e)
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    log.info("/start: %r started %s (limit=%r)", character.name, act.value.name, limit)
    display = act.value.name.replace("_", " ").title()
    embed = discord.Embed(
        title=f"{act.category.icon} Started: {display}",
        color=discord.Color.blue(),
    )
    embed.add_field(name="Character", value=character.name, inline=True)
    if limit is not None:
        embed.add_field(name="Action limit", value=str(limit), inline=True)
    await interaction.response.send_message(embed=embed)


@bot.tree.command(name="end", description="End your current activity and collect rewards")
async def end_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/end called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    rewards = bot.game.end_activity(character)
    if not rewards:
        log.info("/end: %r had no active activity", character.name)
        await interaction.response.send_message("You are not doing anything right now.", ephemeral=True)
        return

    total_actions = sum(r.actions_completed for r in rewards)
    log.info(
        "/end: %r completed %d actions across %d activit%s",
        character.name,
        total_actions,
        len(rewards),
        "y" if len(rewards) == 1 else "ies",
    )
    embed = _end_embed(character, rewards)
    await interaction.response.send_message(embed=embed)


@bot.tree.command(name="skills", description="View your skill levels")
async def skills_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/skills called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    embed = discord.Embed(title=f"{character.name} — Skills", color=discord.Color.purple())
    if not character.skills:
        embed.description = "No skills yet."
    else:
        lines = []
        for cs in sorted(character.skills, key=lambda s: s.skill.value):
            level = level_from_xp(cs.experience)
            lines.append(f"**{cs.skill.value.title()}** — Level {level} ({cs.experience} XP)")
        embed.description = "\n".join(lines)

    await interaction.response.send_message(embed=embed)


@bot.tree.command(name="inventory", description="View your inventory")
async def inventory_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/inventory called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    items = [ci for ci in character.items if ci.quantity > 0]

    embed = discord.Embed(title=f"{character.name} — Inventory", color=discord.Color.orange())
    if not items:
        embed.description = "Your inventory is empty."
    else:
        lines = [
            f"**{ci.item.value.replace('_', ' ').title()}** — {ci.quantity}"
            for ci in sorted(items, key=lambda i: i.item.value)
        ]
        embed.description = "\n".join(lines)

    await interaction.response.send_message(embed=embed)


# ---------------------------------------------------------------------------
# Queue command group
# ---------------------------------------------------------------------------

queue_group = app_commands.Group(name="queue", description="Manage your activity queue")


@queue_group.command(name="add", description="Add an activity to your queue")
@app_commands.describe(activity="Activity to queue", limit="Stop after this many actions (optional)")
@app_commands.autocomplete(activity=activity_autocomplete)
async def queue_add_command(interaction: discord.Interaction, activity: str, limit: int | None = None) -> None:
    if _is_duplicate(interaction):
        return
    log.info(
        "/queue add called by %s (%s) activity=%r limit=%r", interaction.user, interaction.user.id, activity, limit
    )
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        act = Activity[activity]
    except KeyError:
        await interaction.response.send_message(f"Unknown activity `{activity}`.", ephemeral=True)
        return

    try:
        bot.game.queue_activity(character, act, action_limit=limit)
    except Exception as e:
        log.warning("/queue add: failed for %r (%s): %s", character.name, act.value.name, e)
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    display = act.value.name.replace("_", " ").title()
    queue = bot.game.get_activity_queue(character)
    log.info("/queue add: %r queued %s (limit=%r), queue length=%d", character.name, act.value.name, limit, len(queue))
    embed = discord.Embed(title=f"Queued: {display}", color=discord.Color.blue())
    embed.add_field(name="Character", value=character.name, inline=True)
    if limit is not None:
        embed.add_field(name="Action limit", value=str(limit), inline=True)
    embed.add_field(name="Position in queue", value=str(len(queue)), inline=True)
    await interaction.response.send_message(embed=embed)


@queue_group.command(name="view", description="View your current activity and queue")
async def queue_view_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/queue view called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    embed = discord.Embed(title=f"{character.name} — Queue", color=discord.Color.blurple())

    current = character.current_activity
    if current is None:
        embed.add_field(name="Now", value="Idle", inline=False)
    else:
        display = current.activity.value.name.replace("_", " ").title()
        icon = current.activity.category.icon
        limit_str = f" ({current.action_limit} actions)" if current.action_limit else ""
        embed.add_field(name="Now", value=f"{icon} {display}{limit_str}", inline=False)

    queue = bot.game.get_activity_queue(character)
    if queue:
        lines = [
            f"{i + 1}. {q.activity.category.icon} {q.activity.value.name.replace('_', ' ').title()}"
            + (f" ({q.action_limit} actions)" if q.action_limit else "")
            for i, q in enumerate(queue)
        ]
        embed.add_field(
            name=f"Up next ({len(queue)}/{bot.game.get_queue_limit(character)})", value="\n".join(lines), inline=False
        )
    else:
        embed.add_field(name="Up next", value="Nothing queued", inline=False)

    await interaction.response.send_message(embed=embed)


@queue_group.command(name="remove", description="Remove an activity from your queue by position")
@app_commands.describe(position="Position in the queue (1 = next up)")
async def queue_remove_command(interaction: discord.Interaction, position: int) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/queue remove called by %s (%s) position=%d", interaction.user, interaction.user.id, position)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    queue = bot.game.get_activity_queue(character)
    if not queue:
        await interaction.response.send_message("Your queue is empty.", ephemeral=True)
        return

    if position < 1 or position > len(queue):
        await interaction.response.send_message(
            f"Invalid position. Your queue has {len(queue)} item(s).", ephemeral=True
        )
        return

    item = queue[position - 1]
    display = item.activity.value.name.replace("_", " ").title()
    bot.game.remove_from_queue(character, item)
    log.info("/queue remove: %r removed %s from position %d", character.name, item.activity.value.name, position)
    await interaction.response.send_message(f"Removed **{display}** from your queue.")


bot.tree.add_command(queue_group)


# ---------------------------------------------------------------------------
# Trade command group
# ---------------------------------------------------------------------------


def _parse_items(items_str: str | None) -> dict[Item, int]:
    """Parse 'item_name:qty,item_name:qty' into dict[Item, int].

    Raises ValueError with a human-readable message on bad input.
    """
    if not items_str:
        return {}
    result: dict[Item, int] = {}
    for part in items_str.split(","):
        part = part.strip()
        if not part:
            continue
        if ":" not in part:
            raise ValueError(f"Invalid item format '{part}' — expected item_name:quantity")
        name, _, qty_str = part.partition(":")
        try:
            item = Item[name.strip().upper()]
        except KeyError:
            raise ValueError(f"Unknown item '{name.strip()}' — use the item's internal name (e.g. regular_log)")
        try:
            qty = int(qty_str.strip())
        except ValueError:
            raise ValueError(f"Invalid quantity '{qty_str.strip()}' for {name.strip()}")
        result[item] = qty
    return result


def _resolve_trade(character: Character, trade_id_prefix: str) -> Trade:
    """Find a trade by UUID prefix that involves character.

    Raises ValueError if not found or ambiguous.
    """
    all_trades = bot.game.get_pending_trades(character) + bot.game.get_outgoing_trades(character)
    matches = [t for t in all_trades if str(t.id).startswith(trade_id_prefix)]
    if not matches:
        raise ValueError(f"No active trade found with ID starting '{trade_id_prefix}'")
    if len(matches) > 1:
        raise ValueError(f"Ambiguous trade ID '{trade_id_prefix}' — provide more characters")
    return matches[0]


def _format_offer_sides(trade: Trade) -> tuple[str, str]:
    """Return (initiator_side_str, counterparty_side_str) for the current offer."""
    offer = bot.game.get_current_offer(trade)
    initiator_lines = []
    counterparty_lines = []
    for oi in offer.items:
        line = f"{oi.quantity}x {oi.item.value.replace('_', ' ').title()}"
        if oi.offered_by_id == trade.initiator_id:
            initiator_lines.append(line)
        else:
            counterparty_lines.append(line)
    return (
        "\n".join(initiator_lines) or "*(nothing)*",
        "\n".join(counterparty_lines) or "*(nothing)*",
    )


trade_group = app_commands.Group(name="trade", description="Player-to-player trading")


@trade_group.command(name="offer", description="Send a trade offer to another player")
@app_commands.describe(
    player="Character name of the player to trade with",
    give="Items you are offering (format: item_name:qty,item_name:qty)",
    want="Items you want from them (format: item_name:qty,item_name:qty)",
)
async def trade_offer_command(
    interaction: discord.Interaction,
    player: str,
    give: str | None = None,
    want: str | None = None,
) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade offer called by %s (%s) player=%r", interaction.user, interaction.user.id, player)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        counterparty = bot.game.get_character_by_name(player)
    except Exception:
        await interaction.response.send_message(f"No character named **{player}** found.", ephemeral=True)
        return

    try:
        initiator_items = _parse_items(give)
        counterparty_items = _parse_items(want)
        trade = bot.game.create_trade(character, counterparty, initiator_items, counterparty_items)
    except ValueError as e:
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    short_id = str(trade.id)[:8]
    initiator_str, counterparty_str = _format_offer_sides(trade)
    embed = discord.Embed(title="Trade Offer Sent", color=discord.Color.blue())
    embed.add_field(name="Trade ID", value=f"`{short_id}`", inline=False)
    embed.add_field(name=f"You offer ({character.name})", value=initiator_str, inline=True)
    embed.add_field(name=f"You want ({counterparty.name})", value=counterparty_str, inline=True)
    embed.set_footer(text=f"Waiting on {counterparty.name} to respond.")
    log.info("/trade offer: %r → %r (trade %s)", character.name, counterparty.name, short_id)
    await interaction.response.send_message(embed=embed)


@trade_group.command(name="view", description="List your pending incoming and outgoing trades")
async def trade_view_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade view called by %s (%s)", interaction.user, interaction.user.id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    pending = bot.game.get_pending_trades(character)
    outgoing = bot.game.get_outgoing_trades(character)

    embed = discord.Embed(title=f"{character.name} — Trades", color=discord.Color.teal())

    if pending:
        lines = []
        for t in pending:
            other_id = t.counterparty_id if t.initiator_id == character.id else t.initiator_id
            other = bot.game._session.get(Character, other_id)
            lines.append(f"`{str(t.id)[:8]}` — with **{other.name if other else '?'}** *(your turn)*")
        embed.add_field(name="Awaiting your response", value="\n".join(lines), inline=False)
    else:
        embed.add_field(name="Awaiting your response", value="None", inline=False)

    if outgoing:
        lines = []
        for t in outgoing:
            other_id = t.counterparty_id if t.initiator_id == character.id else t.initiator_id
            other = bot.game._session.get(Character, other_id)
            lines.append(f"`{str(t.id)[:8]}` — with **{other.name if other else '?'}**")
        embed.add_field(name="Waiting on others", value="\n".join(lines), inline=False)
    else:
        embed.add_field(name="Waiting on others", value="None", inline=False)

    await interaction.response.send_message(embed=embed)


@trade_group.command(name="show", description="Show trade details and current offer")
@app_commands.describe(trade_id="Trade ID or prefix (from /trade view)")
async def trade_show_command(interaction: discord.Interaction, trade_id: str) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade show called by %s (%s) trade_id=%r", interaction.user, interaction.user.id, trade_id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        trade = _resolve_trade(character, trade_id)
    except ValueError as e:
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    initiator = bot.game._session.get(Character, trade.initiator_id)
    counterparty = bot.game._session.get(Character, trade.counterparty_id)
    initiator_name = initiator.name if initiator else "?"
    counterparty_name = counterparty.name if counterparty else "?"

    current_turn = bot.game._session.get(Character, trade.current_turn_id)
    turn_name = current_turn.name if current_turn else "?"

    initiator_str, counterparty_str = _format_offer_sides(trade)

    status_colors = {
        TradeStatus.PENDING: discord.Color.yellow(),
        TradeStatus.ACCEPTED: discord.Color.green(),
        TradeStatus.REJECTED: discord.Color.red(),
    }
    embed = discord.Embed(
        title=f"Trade: {initiator_name} ↔ {counterparty_name}",
        color=status_colors.get(trade.status, discord.Color.greyple()),
    )
    embed.add_field(name="Trade ID", value=f"`{str(trade.id)[:8]}`", inline=True)
    embed.add_field(name="Status", value=trade.status.value.title(), inline=True)
    if trade.status == TradeStatus.PENDING:
        embed.add_field(name="Waiting on", value=turn_name, inline=True)
    embed.add_field(name=f"{initiator_name} offers", value=initiator_str, inline=True)
    embed.add_field(name=f"{counterparty_name} offers", value=counterparty_str, inline=True)
    embed.set_footer(text=f"{len(trade.offers)} revision(s)")
    await interaction.response.send_message(embed=embed)


@trade_group.command(name="accept", description="Accept the current trade offer")
@app_commands.describe(trade_id="Trade ID or prefix (from /trade view)")
async def trade_accept_command(interaction: discord.Interaction, trade_id: str) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade accept called by %s (%s) trade_id=%r", interaction.user, interaction.user.id, trade_id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        trade = _resolve_trade(character, trade_id)
        initiator_str, counterparty_str = _format_offer_sides(trade)
        bot.game.accept_trade(trade, character)
    except ValueError as e:
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    initiator = bot.game._session.get(Character, trade.initiator_id)
    counterparty = bot.game._session.get(Character, trade.counterparty_id)
    initiator_name = initiator.name if initiator else "?"
    counterparty_name = counterparty.name if counterparty else "?"

    embed = discord.Embed(title="Trade Accepted!", color=discord.Color.green())
    embed.add_field(name=f"{initiator_name} gave", value=initiator_str, inline=True)
    embed.add_field(name=f"{counterparty_name} gave", value=counterparty_str, inline=True)
    log.info("/trade accept: trade %s accepted by %r", str(trade.id)[:8], character.name)
    await interaction.response.send_message(embed=embed)


@trade_group.command(name="reject", description="Reject and cancel a trade")
@app_commands.describe(trade_id="Trade ID or prefix (from /trade view)")
async def trade_reject_command(interaction: discord.Interaction, trade_id: str) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade reject called by %s (%s) trade_id=%r", interaction.user, interaction.user.id, trade_id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        trade = _resolve_trade(character, trade_id)
        bot.game.reject_trade(trade, character)
    except ValueError as e:
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    log.info("/trade reject: trade %s rejected by %r", str(trade.id)[:8], character.name)
    await interaction.response.send_message(f"Trade `{str(trade.id)[:8]}` has been rejected.")


@trade_group.command(name="counter", description="Send a counter-offer on a trade")
@app_commands.describe(
    trade_id="Trade ID or prefix (from /trade view)",
    give="Items you are now offering (format: item_name:qty,item_name:qty)",
    want="Items you want from them (format: item_name:qty,item_name:qty)",
)
async def trade_counter_command(
    interaction: discord.Interaction,
    trade_id: str,
    give: str | None = None,
    want: str | None = None,
) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/trade counter called by %s (%s) trade_id=%r", interaction.user, interaction.user.id, trade_id)
    character = await get_or_prompt_character(interaction)
    if character is None:
        return

    try:
        trade = _resolve_trade(character, trade_id)
        initiator = bot.game._session.get(Character, trade.initiator_id)
        counterparty = bot.game._session.get(Character, trade.counterparty_id)
        initiator_items = _parse_items(give) if character.id == trade.initiator_id else _parse_items(want)
        counterparty_items = _parse_items(want) if character.id == trade.initiator_id else _parse_items(give)
        bot.game.counter_trade(trade, character, initiator_items, counterparty_items)
    except ValueError as e:
        await interaction.response.send_message(str(e), ephemeral=True)
        return

    initiator_name = initiator.name if initiator else "?"
    counterparty_name = counterparty.name if counterparty else "?"
    initiator_str, counterparty_str = _format_offer_sides(trade)

    other_id = trade.counterparty_id if character.id == trade.initiator_id else trade.initiator_id
    other = bot.game._session.get(Character, other_id)
    other_name = other.name if other else "?"

    embed = discord.Embed(title="Counter-Offer Sent", color=discord.Color.orange())
    embed.add_field(name="Trade ID", value=f"`{str(trade.id)[:8]}`", inline=False)
    embed.add_field(name=f"{initiator_name} offers", value=initiator_str, inline=True)
    embed.add_field(name=f"{counterparty_name} offers", value=counterparty_str, inline=True)
    embed.set_footer(text=f"Waiting on {other_name} to respond.")
    log.info("/trade counter: %r counter-offered on trade %s", character.name, str(trade.id)[:8])
    await interaction.response.send_message(embed=embed)


bot.tree.add_command(trade_group)


# ---------------------------------------------------------------------------
# Help command
# ---------------------------------------------------------------------------


@bot.tree.command(name="help", description="Show all available commands")
async def help_command(interaction: discord.Interaction) -> None:
    if _is_duplicate(interaction):
        return
    log.info("/help called by %s (%s)", interaction.user, interaction.user.id)
    embed = discord.Embed(
        title="IdleMaxx — Commands",
        color=discord.Color.blurple(),
    )
    embed.add_field(
        name="Activity",
        value=(
            "`/status` — Check your current activity or idle status\n"
            "`/start <activity> [limit]` — Start an activity, optionally capped at N actions\n"
            "`/end` — Stop your current activity and collect rewards"
        ),
        inline=False,
    )
    embed.add_field(
        name="Skills & Inventory",
        value=("`/skills` — View your skill levels\n`/inventory` — View your inventory"),
        inline=False,
    )
    embed.add_field(
        name="Queue",
        value=(
            "`/queue add <activity> [limit]` — Add an activity to your queue\n"
            "`/queue view` — View your current activity and queue\n"
            "`/queue remove <position>` — Remove an activity from your queue by position"
        ),
        inline=False,
    )
    embed.add_field(
        name="Trading",
        value=(
            "`/trade offer <player> [give] [want]` — Send a trade offer (items: `item_name:qty,...`)\n"
            "`/trade view` — List your pending incoming and outgoing trades\n"
            "`/trade show <id>` — Show trade details and current offer\n"
            "`/trade counter <id> [give] [want]` — Send a counter-offer\n"
            "`/trade accept <id>` — Accept the current offer\n"
            "`/trade reject <id>` — Reject and cancel a trade"
        ),
        inline=False,
    )
    await interaction.response.send_message(embed=embed, ephemeral=True)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------


def main() -> None:
    token = os.environ.get("DISCORD_BOT_TOKEN")
    if not token:
        raise SystemExit("DISCORD_BOT_TOKEN environment variable is required")
    _setup_logging()
    log.info("Starting IdleMaxx Discord bot")
    bot.run(token, log_handler=None)
