"""Discord slash commands — app_commands definitions for peon-mcp bot interactions.

All command groups call the peon-mcp REST API (PEON_API_URL / 127.0.0.1:8420) and
return formatted Discord embeds.  Access control respects the discord_config's
``guild_policy`` and ``guild_id`` fields.
"""

from __future__ import annotations

import logging
import os
from typing import Any

logger = logging.getLogger(__name__)

import discord
import discord.app_commands as app_commands
import httpx


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

EMBED_COLOR = 0x33CC33  # Warcraft green

PRIORITY_CHOICES = ["critical", "high", "medium", "low"]
STATUS_TASK_CHOICES = ["grooming", "todo", "in_progress", "done", "cancelled", "timeout"]
STATUS_FEATURE_CHOICES = ["planned", "in_progress", "done", "cancelled"]
MEMORY_CATEGORIES = ["env", "pattern", "gotcha", "test", "dependency", "general"]


# ---------------------------------------------------------------------------
# Embed helpers
# ---------------------------------------------------------------------------


def _progress_bar(progress: float, width: int = 10) -> str:
    """Return a text progress bar for *progress* (0.0–1.0)."""
    filled = int(progress * width)
    bar = "█" * filled + "░" * (width - filled)
    return f"{bar} {int(progress * 100)}%"


def _make_embed(
    title: str,
    description: str = "",
    color: int = EMBED_COLOR,
    fields: list[tuple[str, str, bool]] | None = None,
) -> "discord.Embed":
    """Build a :class:`discord.Embed` with the given attributes."""
    embed = discord.Embed(title=title, description=description, color=color)
    for name, value, inline in fields or []:
        embed.add_field(name=name, value=value or "—", inline=inline)
    return embed


def _error_embed(message: str) -> "discord.Embed":
    return discord.Embed(title="Error", description=message, color=0xFF4444)


# ---------------------------------------------------------------------------
# HTTP helpers (thin async wrappers around httpx)
# ---------------------------------------------------------------------------


def _api_base() -> str:
    return os.environ.get("PEON_API_URL", "http://127.0.0.1:8420").rstrip("/")


def _api_headers() -> dict[str, str]:
    """Return headers dict, including X-API-Key when PEON_API_KEY is set."""
    key = os.environ.get("PEON_API_KEY", "")
    if key:
        return {"X-API-Key": key}
    return {}


async def _http_get(path: str, api_base: str, params: dict | None = None) -> Any:
    async with httpx.AsyncClient(headers=_api_headers()) as client:
        resp = await client.get(f"{api_base}{path}", params=params, timeout=10.0)
        resp.raise_for_status()
        return resp.json()


async def _http_post(path: str, api_base: str, json: dict | None = None) -> Any:
    async with httpx.AsyncClient(headers=_api_headers()) as client:
        resp = await client.post(f"{api_base}{path}", json=json or {}, timeout=30.0)
        resp.raise_for_status()
        return resp.json()


# ---------------------------------------------------------------------------
# Project resolution helper
# ---------------------------------------------------------------------------


async def _resolve_project_id(api_base: str, config_id: int, channel_id: str) -> str:
    """Resolve the project_id for a channel from its channel mapping.

    Calls ``GET /api/discord/configs/:id/channels`` and finds the mapping
    whose ``channel_id`` matches.  Returns empty string if not found.
    """
    if not config_id or not channel_id:
        return ""
    try:
        channels = await _http_get(f"/api/discord/configs/{config_id}/channels", api_base)
        mapping = next(
            (c for c in channels if c.get("channel_id") == channel_id), None
        )
        return mapping.get("project_id", "") if mapping else ""
    except Exception as exc:
        logger.warning("Failed to resolve project_id for config %d channel %s: %s", config_id, channel_id, exc)
        return ""


# ---------------------------------------------------------------------------
# Access control helper
# ---------------------------------------------------------------------------


async def _check_interaction_access(
    interaction: discord.Interaction,
    api_base: str,
    config_id: int,
) -> tuple[bool, str]:
    """Check if an interaction user has access based on guild_policy.

    Returns (allowed, reason).  Fails open on API errors so that a temporarily
    unavailable backend does not lock out all slash commands.
    """
    if not config_id:
        return True, ""
    try:
        config = await _http_get(f"/api/discord/configs/{config_id}", api_base)
        guild_policy = config.get("guild_policy", "open")
        if guild_policy == "disabled":
            return False, "Bot commands are disabled in this guild"
        if guild_policy == "open":
            return True, ""
        if guild_policy == "allowlist":
            channel_id = str(interaction.channel_id) if interaction.channel_id else ""
            channels = await _http_get(
                f"/api/discord/configs/{config_id}/channels", api_base
            )
            mapping = next(
                (c for c in channels if c.get("channel_id") == channel_id), None
            )
            from peon_mcp.grukk.discord.access import AccessController

            ac = AccessController()
            member_roles = [str(r.id) for r in getattr(interaction.user, "roles", [])]
            allowed, reason = ac.check_guild_access(
                channel_id,
                str(interaction.user.id),
                member_roles,
                guild_policy,
                mapping,
            )
            return allowed, reason
    except Exception as exc:
        logger.warning(
            "Command access check failed for config %d: %s", config_id, exc
        )
    return True, ""


# ---------------------------------------------------------------------------
# /task group
# ---------------------------------------------------------------------------

class TaskGroup(app_commands.Group, name="task", description="Task management commands"):
    """Slash command group for peon-mcp task operations."""

    def __init__(self, api_base: str, config_id: int = 0) -> None:
        super().__init__()
        self.api_base = api_base
        self.config_id = config_id

    async def _get_project_id(self, interaction: discord.Interaction) -> str:
        channel_id = str(interaction.channel_id) if interaction.channel_id else ""
        return await _resolve_project_id(self.api_base, self.config_id, channel_id)

    async def interaction_check(self, interaction: discord.Interaction) -> bool:
        allowed, reason = await _check_interaction_access(
            interaction, self.api_base, self.config_id
        )
        if not allowed:
            await interaction.response.send_message(
                embed=_error_embed(f"Access denied: {reason}"), ephemeral=True
            )
        return allowed

    # ------------------------------------------------------------------ #
    # /task list                                                           #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="list", description="List tasks with optional filters")
    @app_commands.describe(
        status="Filter by task status",
        priority="Filter by task priority",
    )
    async def list_cmd(
        self,
        interaction: discord.Interaction,
        status: str | None = None,
        priority: str | None = None,
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            params: dict = {"limit": 20}
            if status:
                params["status"] = status
            if priority:
                params["priority"] = priority
            data = await _http_get(
                f"/api/projects/{project_id}/tasks",
                self.api_base,
                params=params,
            )
            items = data.get("items", [])
            total = data.get("total", 0)
            if not items:
                await interaction.followup.send(
                    embed=_make_embed("Tasks", "No tasks found.")
                )
                return

            lines = []
            for t in items:
                bar = _progress_bar(t.get("progress", 0.0), width=6)
                lines.append(
                    f"**#{t['id']}** [{t['priority'][0].upper()}] {t['title'][:50]}\n"
                    f"  `{t['status']}` {bar}"
                )

            title = f"Tasks ({total} total)"
            if status:
                title += f" — {status}"
            if priority:
                title += f" — {priority}"

            embed = _make_embed(title, "\n".join(lines))
            await interaction.followup.send(embed=embed)

        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

    @list_cmd.autocomplete("status")
    async def _list_status_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=s, value=s)
            for s in STATUS_TASK_CHOICES
            if current.lower() in s
        ]

    @list_cmd.autocomplete("priority")
    async def _list_priority_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=p, value=p)
            for p in PRIORITY_CHOICES
            if current.lower() in p
        ]

    # ------------------------------------------------------------------ #
    # /task create                                                         #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="create", description="Create a new task")
    @app_commands.describe(
        title="Task title",
        priority="Task priority (default: medium)",
        description="Optional description",
    )
    async def create_cmd(
        self,
        interaction: discord.Interaction,
        title: str,
        priority: str = "medium",
        description: str | None = None,
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            body: dict = {"title": title, "priority": priority}
            if description:
                body["description"] = description
            data = await _http_post(
                f"/api/projects/{project_id}/tasks",
                self.api_base,
                json=body,
            )
            embed = _make_embed(
                f"Task #{data['id']} Created",
                data["title"],
                fields=[
                    ("Priority", data["priority"], True),
                    ("Status", data["status"], True),
                ],
            )
            await interaction.followup.send(embed=embed)
        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

    @create_cmd.autocomplete("priority")
    async def _create_priority_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=p, value=p)
            for p in PRIORITY_CHOICES
            if current.lower() in p
        ]

    # ------------------------------------------------------------------ #
    # /task status                                                         #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="status", description="Show task details with progress bar")
    @app_commands.describe(task_id="Task ID")
    async def status_cmd(
        self, interaction: discord.Interaction, task_id: int
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            data = await _http_get(f"/api/tasks/{task_id}", self.api_base)
            bar = _progress_bar(data.get("progress", 0.0))
            embed = _make_embed(
                f"Task #{data['id']}: {data['title'][:60]}",
                data.get("description", "")[:200] or "*No description*",
                fields=[
                    ("Status", data["status"], True),
                    ("Priority", data["priority"], True),
                    ("Progress", bar, False),
                ],
            )
            await interaction.followup.send(embed=embed)
        except httpx.HTTPStatusError as exc:
            if exc.response.status_code == 404:
                await interaction.followup.send(
                    embed=_error_embed(f"Task #{task_id} not found.")
                )
            else:
                await interaction.followup.send(embed=_error_embed(str(exc)))

    @status_cmd.autocomplete("task_id")
    async def _status_task_id_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[int]]:
        project_id = await self._get_project_id(interaction)
        return await _task_id_choices(self.api_base, project_id, current)

    # ------------------------------------------------------------------ #
    # /task next                                                           #
    # ------------------------------------------------------------------ #

    @app_commands.command(
        name="next", description="Trigger next-task assignment"
    )
    async def next_cmd(self, interaction: discord.Interaction) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            data = await _http_post(
                f"/api/projects/{project_id}/next-task",
                self.api_base,
                json={"commit_base": ""},
            )
            if not data.get("id"):
                await interaction.followup.send(
                    embed=_make_embed("No Tasks Available", "Queue is empty.")
                )
                return
            embed = _make_embed(
                f"Task #{data['id']} Started",
                data["title"],
                fields=[
                    ("Priority", data["priority"], True),
                    ("Status", data["status"], True),
                ],
            )
            await interaction.followup.send(embed=embed)
        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

# ---------------------------------------------------------------------------
# /feature group
# ---------------------------------------------------------------------------

class FeatureGroup(app_commands.Group, name="feature", description="Feature commands"):
    """Slash command group for peon-mcp feature operations."""

    def __init__(self, api_base: str, config_id: int = 0) -> None:
        super().__init__()
        self.api_base = api_base
        self.config_id = config_id

    async def _get_project_id(self, interaction: discord.Interaction) -> str:
        channel_id = str(interaction.channel_id) if interaction.channel_id else ""
        return await _resolve_project_id(self.api_base, self.config_id, channel_id)

    async def interaction_check(self, interaction: discord.Interaction) -> bool:
        allowed, reason = await _check_interaction_access(
            interaction, self.api_base, self.config_id
        )
        if not allowed:
            await interaction.response.send_message(
                embed=_error_embed(f"Access denied: {reason}"), ephemeral=True
            )
        return allowed

    # ------------------------------------------------------------------ #
    # /feature list                                                        #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="list", description="List features")
    @app_commands.describe(status="Filter by feature status")
    async def list_cmd(
        self,
        interaction: discord.Interaction,
        status: str | None = None,
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            params: dict = {"limit": 20}
            if status:
                params["status"] = status
            data = await _http_get(
                f"/api/projects/{project_id}/features",
                self.api_base,
                params=params,
            )
            items = data.get("items", [])
            total = data.get("total", 0)
            if not items:
                await interaction.followup.send(
                    embed=_make_embed("Features", "No features found.")
                )
                return

            lines = [
                f"**#{f['id']}** `{f['status']}` {f['name'][:60]}"
                for f in items
            ]
            title = f"Features ({total} total)"
            if status:
                title += f" — {status}"
            await interaction.followup.send(
                embed=_make_embed(title, "\n".join(lines))
            )
        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

    @list_cmd.autocomplete("status")
    async def _list_status_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=s, value=s)
            for s in STATUS_FEATURE_CHOICES
            if current.lower() in s
        ]

    # ------------------------------------------------------------------ #
    # /feature status                                                      #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="status", description="Show feature with task breakdown")
    @app_commands.describe(feature_id="Feature ID")
    async def status_cmd(
        self, interaction: discord.Interaction, feature_id: int
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            feat = await _http_get(f"/api/features/{feature_id}", self.api_base)
            tasks_data = await _http_get(
                f"/api/features/{feature_id}/tasks", self.api_base
            )
            tasks = tasks_data.get("tasks", []) if isinstance(tasks_data, dict) else []
            done = sum(1 for t in tasks if t.get("status") == "done")
            total_tasks = len(tasks)
            embed = _make_embed(
                f"Feature #{feat['id']}: {feat['name'][:60]}",
                feat.get("description", "")[:200] or "*No description*",
                fields=[
                    ("Status", feat["status"], True),
                    (
                        "Tasks",
                        f"{done}/{total_tasks} done" if total_tasks else "None",
                        True,
                    ),
                    ("Branch", feat.get("branch") or "—", False),
                ],
            )
            await interaction.followup.send(embed=embed)
        except httpx.HTTPStatusError as exc:
            if exc.response.status_code == 404:
                await interaction.followup.send(
                    embed=_error_embed(f"Feature #{feature_id} not found.")
                )
            else:
                await interaction.followup.send(embed=_error_embed(str(exc)))

    @status_cmd.autocomplete("feature_id")
    async def _status_feature_id_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[int]]:
        project_id = await self._get_project_id(interaction)
        return await _feature_id_choices(self.api_base, project_id, current)

# ---------------------------------------------------------------------------
# /memory group
# ---------------------------------------------------------------------------

class MemoryGroup(app_commands.Group, name="memory", description="Project memory commands"):
    """Slash command group for recalling and saving project memories."""

    def __init__(self, api_base: str, config_id: int = 0) -> None:
        super().__init__()
        self.api_base = api_base
        self.config_id = config_id

    async def _get_project_id(self, interaction: discord.Interaction) -> str:
        channel_id = str(interaction.channel_id) if interaction.channel_id else ""
        return await _resolve_project_id(self.api_base, self.config_id, channel_id)

    async def interaction_check(self, interaction: discord.Interaction) -> bool:
        allowed, reason = await _check_interaction_access(
            interaction, self.api_base, self.config_id
        )
        if not allowed:
            await interaction.response.send_message(
                embed=_error_embed(f"Access denied: {reason}"), ephemeral=True
            )
        return allowed

    # ------------------------------------------------------------------ #
    # /memory recall                                                       #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="recall", description="Recall project memories")
    @app_commands.describe(category="Filter by memory category")
    async def recall_cmd(
        self,
        interaction: discord.Interaction,
        category: str | None = None,
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            body: dict = {"limit": 10}
            if category:
                body["category"] = category
            memories = await _http_post(
                f"/api/projects/{project_id}/memories/recall",
                self.api_base,
                json=body,
            )
            if not memories:
                await interaction.followup.send(
                    embed=_make_embed("Memories", "No memories found.")
                )
                return
            lines = [
                f"**[{m['category']}]** {m['content'][:100]}"
                for m in memories
            ]
            title = "Memories"
            if category:
                title += f" ({category})"
            await interaction.followup.send(
                embed=_make_embed(title, "\n".join(lines))
            )
        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

    @recall_cmd.autocomplete("category")
    async def _recall_category_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=c, value=c)
            for c in MEMORY_CATEGORIES
            if current.lower() in c
        ]

    # ------------------------------------------------------------------ #
    # /memory save                                                         #
    # ------------------------------------------------------------------ #

    @app_commands.command(name="save", description="Save a new project memory")
    @app_commands.describe(
        content="Memory content (max 500 characters)",
        category="Memory category (default: general)",
    )
    async def save_cmd(
        self,
        interaction: discord.Interaction,
        content: str,
        category: str = "general",
    ) -> None:
        await interaction.response.defer(thinking=True)
        try:
            project_id = await self._get_project_id(interaction)
            if not project_id:
                await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                return
            data = await _http_post(
                f"/api/projects/{project_id}/memories",
                self.api_base,
                json={"content": content[:500], "category": category},
            )
            embed = _make_embed(
                "Memory Saved",
                data["content"],
                fields=[("Category", data["category"], True)],
            )
            await interaction.followup.send(embed=embed)
        except httpx.HTTPError as exc:
            await interaction.followup.send(embed=_error_embed(str(exc)))

    @save_cmd.autocomplete("category")
    async def _save_category_ac(
        self, interaction: discord.Interaction, current: str
    ) -> list[app_commands.Choice[str]]:
        return [
            app_commands.Choice(name=c, value=c)
            for c in MEMORY_CATEGORIES
            if current.lower() in c
        ]

# ---------------------------------------------------------------------------
# Autocomplete helpers (shared across groups)
# ---------------------------------------------------------------------------

async def _task_id_choices(
    api_base: str, project_id: str, current: str
) -> list[app_commands.Choice[int]]:
    """Return up to 10 task ID choices matching *current* (by ID prefix or title)."""
    try:
        data = await _http_get(
            f"/api/projects/{project_id}/tasks",
            api_base,
            params={"limit": 50, "status": "in_progress"},
        )
        items = data.get("items", [])
        # Also include todo tasks
        todo_data = await _http_get(
            f"/api/projects/{project_id}/tasks",
            api_base,
            params={"limit": 50, "status": "todo"},
        )
        items += todo_data.get("items", [])

        choices = []
        for t in items[:50]:
            label = f"#{t['id']}: {t['title'][:50]}"
            if not current or current in str(t["id"]) or current.lower() in t["title"].lower():
                choices.append(app_commands.Choice(name=label[:100], value=t["id"]))
            if len(choices) >= 10:
                break
        return choices
    except Exception:
        return []

async def _feature_id_choices(
    api_base: str, project_id: str, current: str
) -> list[app_commands.Choice[int]]:
    """Return up to 10 feature ID choices matching *current*."""
    try:
        data = await _http_get(
            f"/api/projects/{project_id}/features",
            api_base,
            params={"limit": 50},
        )
        items = data.get("items", [])
        choices = []
        for f in items:
            label = f"#{f['id']}: {f['name'][:50]}"
            if not current or current in str(f["id"]) or current.lower() in f["name"].lower():
                choices.append(app_commands.Choice(name=label[:100], value=f["id"]))
            if len(choices) >= 10:
                break
        return choices
    except Exception:
        return []

# ---------------------------------------------------------------------------
# CommandRegistry — assembles groups and /status into a CommandTree
# ---------------------------------------------------------------------------

class CommandRegistry:
    """Builds and registers all peon-mcp slash commands onto a CommandTree.

    Usage::

        registry = CommandRegistry(api_base, guild_id, config_id)
        registry.register(client.tree)
        # On bot ready:
        await registry.sync(client)
    """

    def __init__(
        self,
        api_base: str,
        guild_id: str = "",
        config_id: int = 0,
    ) -> None:
        self.api_base = api_base.rstrip("/")
        self.guild_id = guild_id
        self.config_id = config_id

    def register(self, tree: "app_commands.CommandTree") -> None:
        """Add all command groups and the /status command to *tree*."""
        tree.add_command(TaskGroup(self.api_base, self.config_id))
        tree.add_command(FeatureGroup(self.api_base, self.config_id))
        tree.add_command(MemoryGroup(self.api_base, self.config_id))

        # /status — standalone command
        api_base = self.api_base
        config_id = self.config_id

        @tree.command(name="status", description="Show project dashboard summary")
        async def status_cmd(interaction: discord.Interaction) -> None:
            allowed, reason = await _check_interaction_access(
                interaction, api_base, config_id
            )
            if not allowed:
                await interaction.response.send_message(
                    embed=_error_embed(f"Access denied: {reason}"), ephemeral=True
                )
                return
            await interaction.response.defer(thinking=True)
            try:
                channel_id = str(interaction.channel_id) if interaction.channel_id else ""
                project_id = await _resolve_project_id(api_base, config_id, channel_id)
                if not project_id:
                    await interaction.followup.send(embed=_error_embed("No project mapped to this channel."))
                    return
                stats = await _http_get(
                    f"/api/projects/{project_id}/stats", api_base
                )
                active = stats.get("tasks_in_progress", 0)
                total_tasks = stats.get("tasks_total", 0)
                done_tasks = stats.get("tasks_done", 0)
                features_total = stats.get("features_total", 0)
                features_done = stats.get("features_done", 0)
                embed = _make_embed(
                    f"Project Dashboard — {project_id}",
                    fields=[
                        ("Active Tasks", str(active), True),
                        (
                            "Tasks Done",
                            f"{done_tasks}/{total_tasks}",
                            True,
                        ),
                        (
                            "Features",
                            f"{features_done}/{features_total} done",
                            True,
                        ),
                    ],
                )
                await interaction.followup.send(embed=embed)
            except httpx.HTTPError as exc:
                await interaction.followup.send(embed=_error_embed(str(exc)))

    async def sync(self, client: Any) -> int:
        """Sync the CommandTree with Discord.

        If *guild_id* is set, performs a fast guild-specific sync.
        Otherwise performs a global sync (takes up to 1 hour to propagate).

        Returns the number of commands synced.
        """
        tree = getattr(client, "tree", None)
        if tree is None:
            logger.warning("CommandRegistry.sync: client has no tree attribute")
            return 0
        try:
            if self.guild_id:
                guild = discord.Object(id=int(self.guild_id))
                # Copy global commands to guild for faster testing
                tree.copy_global_to(guild=guild)
                synced = await tree.sync(guild=guild)
            else:
                synced = await tree.sync()
            count = len(synced)
            logger.info(
                "Synced %d slash commands (config=%s, guild=%s)",
                count,
                self.config_id,
                self.guild_id or "global",
            )
            return count
        except Exception as exc:
            logger.error("Failed to sync slash commands: %s", exc)
            return 0

