﻿"""
ggbro Python SDK (single-file helper)
Build bots on the ggbro platform with Socket.IO + REST.

Requirements:
    pip install python-socketio[client] requests

Download:
    https://ggbro.app/developers/ggbro.py

Usage:
    from ggbro import Bot, SlashCommand

    bot = Bot(token='YOUR_BOT_TOKEN')
    bot.register_commands([
        SlashCommand(name='ping', description='Check bot latency'),
        SlashCommand(
            name='play',
            description='Play a song',
            options=[{
                'name': 'query',
                'description': 'Song name or URL',
                'type': 'string',  # or 3
                'required': True,
            }],
        ),
    ])

    @bot.command('ping')
    def ping(ctx):
        ctx.reply('Pong!')

    @bot.command('play')
    def play(ctx):
        # Prefer typed options â€” never trust raw args alone for multi-word values
        query = ctx.get_option('query', ctx.args)
        ctx.reply(f'Searching for **{query}**...')

    bot.run()

Slash interactions (interaction:create, type=slash_command):
    - options  Typed map from option definitions (USER/CHANNEL/ROLE IDs unwrapped).
    - args     Clean positional text after the command (e.g. "ovetto 7amra").
               Current clients send positional values, not `--query "â€¦"`.
               Older named wires are normalized server-side to the same shape.
    - Prefer ctx.get_option('name') / ctx.options over ctx.args.
    - --__bot routing is stripped before delivery; bots never see it.
"""

import socketio
import requests
import logging
import os
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Union

__version__ = '1.2.0'

logger = logging.getLogger('ggbro')


@dataclass
class SlashCommand:
    """Represents a slash command to register with the API.

    options: up to 25 Discord-compatible option defs.
      type codes/aliases: 3 string, 4 integer, 5 boolean, 6 user,
      7 channel, 8 role, 9 mentionable, 10 number
      (aliases: string/str/text, integer/int, boolean/bool, user/member,
       channel, role, mentionable, number/float/double)
    """
    name: str
    description: str = ''
    options: List[Dict[str, Any]] = field(default_factory=list)
    server_id: Optional[str] = None

    def to_dict(self) -> dict:
        d = {'name': self.name, 'description': self.description, 'options': self.options}
        if self.server_id:
            d['server_id'] = self.server_id
        return d


class CommandContext:
    """Context object passed to slash command handlers.

    Fields mirror interaction:create (type=slash_command):
      command, args, options, channel_id, server_id, user, interaction_id, message_id

    Always prefer options / get_option() over raw args for typed values.
    """

    def __init__(self, bot: 'Bot', interaction: dict):
        self._bot = bot
        self._interaction = interaction
        self.command: str = interaction.get('command', '')
        # Positional human text, e.g. "ovetto 7amra" â€” never includes --__bot
        self.args: str = interaction.get('args', '') or ''
        # Typed map: strings / ints / floats / bools; entity IDs already unwrapped
        self.options: Dict[str, Any] = dict(interaction.get('options') or {})
        self.channel_id: str = interaction.get('channel_id', '')
        self.server_id: str = interaction.get('server_id', '')
        self.user: dict = interaction.get('user', {}) or {}
        self.interaction_id: str = interaction.get('id', '')
        self.message_id: str = interaction.get('message_id', '')

    def get_option(self, name: str, default: Any = None) -> Any:
        """Return a typed option value, falling back to default (often ctx.args)."""
        if name in self.options and self.options[name] is not None and self.options[name] != '':
            return self.options[name]
        return default

    def reply(self, content: str = '', embeds: Optional[List[dict]] = None) -> dict:
        """Reply in the same channel where the command was used."""
        return self._bot.respond_interaction(
            interaction_id=self.interaction_id,
            channel_id=self.channel_id,
            content=content,
            embeds=embeds,
        )

    def edit_message(self, message_id: str, content: Optional[str] = None, embeds: Optional[List[dict]] = None) -> dict:
        """Edit an existing bot-authored message in the current channel."""
        return self._bot.edit_message(
            message_id=message_id,
            channel_id=self.channel_id,
            content=content,
            embeds=embeds,
        )


class ComponentContext:
    """Context object passed to message component handlers."""

    def __init__(self, bot: 'Bot', interaction: dict):
        self._bot = bot
        self._interaction = interaction
        self.channel_id: str = interaction.get('channel_id', '')
        self.server_id: str = interaction.get('server_id', '')
        self.user: dict = interaction.get('user', {}) or {}
        self.interaction_id: str = interaction.get('id', '')
        self.message_id: str = interaction.get('message_id', '')
        self.custom_id: str = interaction.get('custom_id', '')
        self.component_type: str = interaction.get('component_type', '')
        self.values: List[str] = interaction.get('values', []) or []
        self.selected_channels: List[dict] = interaction.get('selected_channels', []) or []

    def reply(self, content: str = '', embeds: Optional[List[dict]] = None) -> dict:
        return self._bot.respond_interaction(
            interaction_id=self.interaction_id,
            channel_id=self.channel_id,
            content=content,
            embeds=embeds,
        )

    def update_message(self, content: Optional[str] = None, embeds: Optional[List[dict]] = None) -> dict:
        return self._bot.edit_message(
            message_id=self.message_id,
            channel_id=self.channel_id,
            content=content,
            embeds=embeds,
        )


class Bot:
    """Main bot class for connecting to the ggbro gateway."""

    def __init__(self, token: str, api_url: Optional[str] = None, gateway_url: Optional[str] = None, request_timeout: int = 15):
        self.token = token
        self.api_url = (api_url or os.getenv('GGBRO_PUBLIC_URL', 'https://ggbro.app')).rstrip('/')
        self.gateway_url = (gateway_url or os.getenv('GGBRO_GATEWAY_URL', self.api_url)).rstrip('/')
        self.request_timeout = request_timeout
        self._http = requests.Session()
        self._http.headers.update({'User-Agent': f'ggbro-python-sdk/{__version__}'})
        self._sio = socketio.Client(
            reconnection=True,
            reconnection_attempts=0,
            reconnection_delay=1,
            reconnection_delay_max=5,
            randomization_factor=0.2,
        )
        self._command_handlers: Dict[str, Callable] = {}
        self._component_handlers: Dict[str, Callable] = {}
        self._event_handlers: Dict[str, Callable] = {}
        self._servers: List[dict] = []
        self._ready = False

        self._sio.on('connect', self._on_connect)
        self._sio.on('disconnect', self._on_disconnect)
        self._sio.on('interaction:create', self._on_interaction)
        self._sio.on('member:joined', self._on_member_joined)
        self._sio.on('message:new', self._on_message_new)

    # â”€â”€ Decorators â”€â”€

    def command(self, name: str) -> Callable:
        """Decorator to register a slash command handler."""
        def decorator(func: Callable) -> Callable:
            self._command_handlers[name.lower()] = func
            return func
        return decorator

    def event(self, name: str) -> Callable:
        """Decorator to register an event handler (ready, disconnect, member:joined, message:new, interaction:create)."""
        def decorator(func: Callable) -> Callable:
            self._event_handlers[name] = func
            return func
        return decorator

    def component(self, custom_id: str) -> Callable:
        """Decorator to register a message component handler."""
        def decorator(func: Callable) -> Callable:
            self._component_handlers[custom_id] = func
            return func
        return decorator

    # â”€â”€ Public API â”€â”€

    def register_commands(self, commands: List[SlashCommand]) -> dict:
        """Bulk-register slash commands (replaces existing global commands for the app). Max 50."""
        headers = {'Authorization': f'Bearer {self.token}', 'Content-Type': 'application/json'}
        resp = self._http.post(
            f'{self.api_url}/api/v1/oauth2/commands',
            json=[c.to_dict() for c in commands],
            headers=headers,
            timeout=self.request_timeout,
        )
        resp.raise_for_status()
        data = resp.json()
        logger.info('Registered %d commands', len(data.get('commands', [])))
        return data

    def _emit_with_ack(self, event: str, payload: dict) -> dict:
        """Emit a Socket.IO event and wait for the ack payload."""
        response = self._sio.call(event, payload, timeout=self.request_timeout)
        return response if isinstance(response, dict) else {'ok': False, 'response': response}

    def respond_interaction(self, interaction_id: str, channel_id: str, content: str = '', embeds: Optional[List[dict]] = None) -> dict:
        payload: dict = {
            'interaction_id': interaction_id,
            'channel_id': channel_id,
            'content': content,
        }
        if embeds is not None:
            payload['embeds'] = embeds
        return self._emit_with_ack('interaction:response', payload)

    def send_message(self, channel_id: str, content: str = '', embeds: Optional[List[dict]] = None) -> dict:
        """Send a message to a channel the bot is a member of."""
        payload: dict = {
            'channel_id': channel_id,
            'content': content,
        }
        if embeds is not None:
            payload['embeds'] = embeds
        return self._emit_with_ack('bot:send_message', payload)

    def edit_message(self, message_id: str, content: Optional[str] = None, embeds: Optional[List[dict]] = None, channel_id: Optional[str] = None) -> dict:
        """Edit a bot-authored channel message."""
        payload: dict = {
            'message_id': message_id,
        }
        if channel_id:
            payload['channel_id'] = channel_id
        if content is not None:
            payload['content'] = content
        if embeds is not None:
            payload['embeds'] = embeds
        return self._emit_with_ack('bot:edit_message', payload)

    def get_servers(self) -> List[dict]:
        """Get the list of servers the bot has joined."""
        headers = {'Authorization': f'Bearer {self.token}'}
        resp = self._http.get(f'{self.api_url}/api/v1/servers', headers=headers, timeout=self.request_timeout)
        resp.raise_for_status()
        data = resp.json()
        return data.get('servers', data if isinstance(data, list) else [])

    def get_channels(self, server_id: str) -> List[dict]:
        """Get channels in a server."""
        headers = {'Authorization': f'Bearer {self.token}'}
        resp = self._http.get(f'{self.api_url}/api/v1/channels/{server_id}', headers=headers, timeout=self.request_timeout)
        resp.raise_for_status()
        data = resp.json()
        return data.get('channels', data if isinstance(data, list) else [])

    # â”€â”€ REST API helpers â”€â”€

    def _auth_headers(self) -> dict:
        return {'Authorization': f'Bearer {self.token}', 'Content-Type': 'application/json'}

    def send_dm(self, user_id: str, content: str = '', embeds: Optional[List[dict]] = None) -> dict:
        """Send a DM to a user (bot must share a server with them)."""
        payload: dict = {'content': content}
        if embeds:
            payload['embeds'] = embeds
        resp = self._http.post(f'{self.api_url}/api/v1/oauth2/bot/dm/{user_id}', headers=self._auth_headers(), json=payload, timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('message', {})

    def send_channel_message(self, channel_id: str, content: str = '', embeds: Optional[List[dict]] = None, reply_to: Optional[str] = None) -> dict:
        """Send a message to a channel via REST."""
        payload: dict = {'content': content}
        if embeds:
            payload['embeds'] = embeds
        if reply_to:
            payload['reply_to'] = reply_to
        resp = self._http.post(f'{self.api_url}/api/v1/oauth2/bot/channels/{channel_id}/messages', headers=self._auth_headers(), json=payload, timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('message', {})

    def get_messages(self, channel_id: str, limit: int = 50, before: Optional[str] = None, after: Optional[str] = None) -> List[dict]:
        """Fetch messages from a channel."""
        params: Dict[str, str] = {'limit': str(limit)}
        if before:
            params['before'] = before
        if after:
            params['after'] = after
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/channels/{channel_id}/messages', headers=self._auth_headers(), params=params, timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('messages', [])

    def bulk_delete(self, channel_id: str, message_ids: List[str]) -> int:
        """Bulk delete up to 100 messages."""
        resp = self._http.post(f'{self.api_url}/api/v1/oauth2/bot/channels/{channel_id}/messages/bulk-delete', headers=self._auth_headers(), json={'message_ids': message_ids}, timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('deleted', 0)

    def get_server(self, server_id: str) -> dict:
        """Get server info."""
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('server', {})

    def get_members(self, server_id: str) -> List[dict]:
        """List server members."""
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/members', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('members', [])

    def get_member(self, server_id: str, user_id: str) -> dict:
        """Get a specific server member with roles."""
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/members/{user_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('member', {})

    def kick_member(self, server_id: str, user_id: str) -> None:
        """Kick a member from a server."""
        resp = self._http.delete(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/members/{user_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()

    def ban_member(self, server_id: str, user_id: str, reason: str = '') -> None:
        """Ban a member from a server."""
        resp = self._http.post(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/bans', headers=self._auth_headers(), json={'user_id': user_id, 'reason': reason}, timeout=self.request_timeout)
        resp.raise_for_status()

    def unban_member(self, server_id: str, user_id: str) -> None:
        """Unban a member from a server."""
        resp = self._http.delete(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/bans/{user_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()

    def get_roles(self, server_id: str) -> List[dict]:
        """List server roles."""
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/roles', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('roles', [])

    def add_role(self, server_id: str, user_id: str, role_id: str) -> None:
        """Assign a role to a member."""
        resp = self._http.post(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/members/{user_id}/roles/{role_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()

    def remove_role(self, server_id: str, user_id: str, role_id: str) -> None:
        """Remove a role from a member."""
        resp = self._http.delete(f'{self.api_url}/api/v1/oauth2/bot/servers/{server_id}/members/{user_id}/roles/{role_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()

    def set_presence(self, status: Optional[str] = None, activity_type: Optional[str] = None, activity_name: Optional[str] = None) -> None:
        """Set the bot's online status and activity."""
        payload: dict = {}
        if status:
            payload['status'] = status
        if activity_type and activity_name:
            payload['activity'] = {'type': activity_type, 'name': activity_name}
        resp = self._http.put(f'{self.api_url}/api/v1/oauth2/bot/presence', headers=self._auth_headers(), json=payload, timeout=self.request_timeout)
        resp.raise_for_status()

    def get_user(self, user_id: str) -> dict:
        """Get public profile of a user."""
        resp = self._http.get(f'{self.api_url}/api/v1/oauth2/bot/users/{user_id}', headers=self._auth_headers(), timeout=self.request_timeout)
        resp.raise_for_status()
        return resp.json().get('user', {})

    def run(self) -> None:
        """Connect to the gateway and block forever."""
        logger.info('Connecting to %s ...', self.gateway_url)
        self._sio.connect(
            self.gateway_url,
            auth={'token': self.token},
            transports=['websocket'],
            wait_timeout=self.request_timeout,
        )
        self._sio.wait()

    # â”€â”€ Internal handlers â”€â”€

    def _on_connect(self) -> None:
        logger.info('Connected to gateway')
        self._join_servers()
        self._ready = True
        handler = self._event_handlers.get('ready')
        if handler:
            handler()

    def _on_disconnect(self) -> None:
        logger.warning('Disconnected from gateway')
        self._ready = False
        handler = self._event_handlers.get('disconnect')
        if handler:
            handler()

    def _join_servers(self) -> None:
        """Join all server and channel rooms so the bot receives events."""
        try:
            servers = self.get_servers()
            self._servers = servers
            for server in servers:
                sid = server.get('id', '')
                self._sio.emit('server:join', sid)
                channels = self.get_channels(sid)
                for ch in channels:
                    if ch.get('type', 'text') == 'text':
                        self._sio.emit('channel:join', ch['id'])
            logger.info('Joined %d servers', len(servers))
        except Exception as e:
            logger.error('Failed to join servers: %s', e)

    def _on_interaction(self, data: dict) -> None:
        """Handle incoming slash command and component interactions."""
        interaction_type = data.get('type', '')

        if interaction_type == 'slash_command':
            cmd_name = data.get('command', '').lower()
            handler = self._command_handlers.get(cmd_name)
            if handler:
                ctx = CommandContext(self, data)
                try:
                    handler(ctx)
                except Exception as e:
                    logger.error('Error in command handler %s: %s', cmd_name, e)
                    ctx.reply(f'An error occurred while running /{cmd_name}.')
            return

        if interaction_type == 'message_component':
            custom_id = data.get('custom_id', '')
            handler = self._component_handlers.get(custom_id)
            if handler:
                ctx = ComponentContext(self, data)
                try:
                    handler(ctx)
                except Exception as e:
                    logger.error('Error in component handler %s: %s', custom_id, e)
                    ctx.reply('An error occurred while processing that interaction.')
            return

        handler = self._event_handlers.get('interaction:create')
        if handler:
            try:
                handler(data)
            except Exception as e:
                logger.error('Error in interaction:create handler: %s', e)

    def _on_member_joined(self, data: dict) -> None:
        handler = self._event_handlers.get('member:joined')
        if handler:
            try:
                handler(data)
            except Exception as e:
                logger.error('Error in member:joined handler: %s', e)

    def _on_message_new(self, data: dict) -> None:
        handler = self._event_handlers.get('message:new')
        if handler:
            try:
                handler(data)
            except Exception as e:
                logger.error('Error in message:new handler: %s', e)
