# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Slack Channel (Socket Mode)

Run the Familiar as a Slack bot using Socket Mode (WebSocket — no public URL needed).

Features:
- Respond to DMs
- Respond to @mentions in channels
- Per-user conversation history
- Owner auto-promotion via OWNER_SLACK_ID

Setup:
1. Create a Slack app at https://api.slack.com/apps
2. Enable Socket Mode (Settings → Socket Mode → Enable)
3. Create an App-Level Token with connections:write scope
4. Add Bot Token Scopes: chat:write, app_mentions:read, im:history, im:read, im:write
5. Install app to workspace
6. Set SLACK_BOT_TOKEN (xoxb-...) and SLACK_APP_TOKEN (xapp-...) env vars
"""

import asyncio
import logging
import os
from typing import Optional

logger = logging.getLogger(__name__)

from .auth import ChannelType, ChannelUser, create_authenticator  # noqa: E402
from .connect_wizard import ConnectMixin  # noqa: E402
from .formatting import FormatMode  # noqa: E402


class SlackChannel(ConnectMixin):
    """
    Slack bot integration for Familiar via Socket Mode.

    Usage:
        channel = SlackChannel(agent)
        channel.run()  # Blocking
    """

    # ConnectMixin adapter properties
    format_mode = FormatMode.MARKDOWN
    supports_buttons = False
    supports_message_deletion = True

    def __init__(
        self,
        agent,
        bot_token: str = None,
        app_token: str = None,
        default_channel: str = None,
    ):
        """
        Initialize Slack channel.

        Args:
            agent: The Familiar instance
            bot_token: Slack bot token (xoxb-...) or use SLACK_BOT_TOKEN env var
            app_token: Slack app token (xapp-...) or use SLACK_APP_TOKEN env var
            default_channel: Default channel for scheduler delivery
        """
        self.agent = agent
        self.bot_token = bot_token or os.environ.get("SLACK_BOT_TOKEN", "")
        self.app_token = app_token or os.environ.get("SLACK_APP_TOKEN", "")
        self.default_channel = default_channel or os.environ.get("SLACK_DEFAULT_CHANNEL", "")

        self.web_client = None
        self.socket_client = None
        self.bot_user_id = None

        # Phase 1: Multi-user authentication
        self.authenticator = create_authenticator(
            config=agent.config if hasattr(agent, "config") else None,
        )

        # ConnectMixin: store channel refs for wizard routing
        self._wizard_channels: dict[str, str] = {}  # recipient_id -> channel_id

    # ── ConnectMixin adapter methods ────────────────────────────────

    async def wizard_send(self, recipient_id: str, text: str) -> None:
        if self.web_client:
            channel = self._wizard_channels.get(recipient_id, recipient_id)
            try:
                self.web_client.chat_postMessage(channel=channel, text=text)
            except Exception as e:
                logger.error(f"Wizard send failed: {e}")

    async def wizard_send_menu(
        self, recipient_id: str, text: str, options: list[tuple[str, str]]
    ) -> None:
        # No button support — fall back to numbered menu via ConnectMixin default
        await super().wizard_send_menu(recipient_id, text, options)

    async def wizard_delete_message(self, recipient_id: str, message_ref) -> bool:
        if self.web_client and message_ref:
            try:
                channel = message_ref.get("channel", "")
                ts = message_ref.get("ts", "")
                if channel and ts:
                    self.web_client.chat_delete(channel=channel, ts=ts)
                    return True
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        return False

    def _get_channel_user(
        self, slack_user_id: str, display_name: str = "User"
    ) -> Optional[ChannelUser]:
        """Get authenticated ChannelUser for Slack user."""
        return self.authenticator.authenticate(
            channel_type=ChannelType.SLACK,
            channel_id=str(slack_user_id),
            display_name=display_name,
        )

    def _setup_clients(self):
        """Set up Slack Web Client and Socket Mode Client."""
        try:
            from slack_sdk import WebClient
            from slack_sdk.socket_mode import SocketModeClient
            from slack_sdk.socket_mode.request import SocketModeRequest
            from slack_sdk.socket_mode.response import SocketModeResponse
        except ImportError:
            raise RuntimeError("slack-sdk not installed. Install with: pip install slack-sdk")

        self.web_client = WebClient(token=self.bot_token)

        # Verify auth and get bot user ID
        auth_response = self.web_client.auth_test()
        self.bot_user_id = auth_response["user_id"]
        logger.info(f"Slack bot authenticated as {auth_response['user']} ({self.bot_user_id})")

        self.socket_client = SocketModeClient(
            app_token=self.app_token,
            web_client=self.web_client,
        )

        def _socket_listener(client: SocketModeClient, req: SocketModeRequest):
            # Acknowledge immediately
            client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))

            if req.type == "events_api":
                event = req.payload.get("event", {})
                event_type = event.get("type", "")

                if event_type == "message":
                    # DMs and messages without subtype (not bot messages, edits, etc.)
                    if not event.get("subtype") and not event.get("bot_id"):
                        self._handle_message_sync(event, is_mention=False)

                elif event_type == "app_mention":
                    if not event.get("bot_id"):
                        self._handle_message_sync(event, is_mention=True)

        self.socket_client.socket_mode_request_listeners.append(_socket_listener)

    def _handle_message_sync(self, event: dict, is_mention: bool = False):
        """Handle an incoming Slack message event (sync, called from socket listener)."""
        user_id = event.get("user", "")
        channel = event.get("channel", "")
        text = event.get("text", "")
        ts = event.get("ts", "")

        if not user_id or not text:
            return

        # Ignore messages from self
        if user_id == self.bot_user_id:
            return

        # Strip bot mention from text
        if self.bot_user_id:
            text = text.replace(f"<@{self.bot_user_id}>", "").strip()

        if not text:
            return

        # Check if this is a DM (channel type C = public, D = DM, G = group DM)
        is_dm = channel.startswith("D")

        # Only respond to DMs and mentions
        if not is_dm and not is_mention:
            return

        # Get display name
        display_name = "User"
        try:
            user_info = self.web_client.users_info(user=user_id)
            profile = user_info.get("user", {}).get("profile", {})
            display_name = (
                profile.get("display_name")
                or profile.get("real_name")
                or user_info.get("user", {}).get("name", "User")
            )
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
        # Authenticate user
        channel_user = self._get_channel_user(user_id, display_name)
        if not channel_user:
            return

        # Auto-promote owner
        slack_owner = os.environ.get("OWNER_SLACK_ID")
        if slack_owner and str(user_id) == str(slack_owner):
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(f"slack:{user_id}", "slack")
                if session.trust_level != TrustLevel.OWNER:
                    session.set_trust_level(TrustLevel.OWNER)
                    session.daily_budget = 50.0
                    self.agent.sessions.save_session(session)
                    logger.info(f"Auto-promoted Slack owner {user_id}")
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
        # Intercept messages during wizard flows (ConnectMixin)
        rid = str(user_id)
        self._wizard_channels[rid] = channel

        # Slash command detection (connect, browse, search)
        if text.startswith("/"):
            parts = text[1:].split(maxsplit=1)
            cmd = parts[0].lower()
            cmd_args = parts[1] if len(parts) > 1 else ""
            if cmd in ("start", "tutorial"):
                loop = asyncio.new_event_loop()
                try:
                    if cmd == "start":
                        loop.run_until_complete(self.handle_start_command(rid))
                    else:
                        loop.run_until_complete(self.handle_tutorial_command(rid))
                finally:
                    loop.close()
                return

            if cmd in ("connect", "browse", "search"):
                try:
                    from ..core.security import TrustLevel

                    session = self.agent.sessions.get_or_create_session(f"slack:{user_id}", "slack")
                    if session.trust_level != TrustLevel.OWNER:
                        self.web_client.chat_postMessage(
                            channel=channel,
                            text=f"\U0001f512 /{cmd} requires OWNER trust level.",
                            thread_ts=ts if not is_dm else None,
                        )
                        return
                except ImportError as e:
                    logger.debug("Optional dependency not available: %s", e)
                loop = asyncio.new_event_loop()
                try:
                    if cmd == "connect":
                        args_list = cmd_args.split() if cmd_args else []
                        loop.run_until_complete(self.handle_connect_command(rid, args_list))
                    elif cmd == "browse":
                        loop.run_until_complete(self.handle_browse_command(rid))
                    elif cmd == "search":
                        args_list = cmd_args.split() if cmd_args else []
                        loop.run_until_complete(self.handle_search_command(rid, args_list))
                finally:
                    loop.close()
                return

        # Run wizard check async
        loop = asyncio.new_event_loop()
        try:
            message_ref = {"channel": channel, "ts": ts}
            intercepted = loop.run_until_complete(
                self.handle_wizard_message(rid, text, message_ref)
            )
            if intercepted:
                return
        finally:
            loop.close()

        # Get UserContext for linked users
        user_context = channel_user.to_context() if channel_user.is_linked else None
        agent_user_id = channel_user.user_id or f"slack:{user_id}"
        channel_id = f"slack:{channel}"

        logger.info(f"[Slack:{display_name}] {text[:50]}...")

        try:
            response = self.agent.chat(
                message=text,
                user_id=agent_user_id,
                channel=channel_id,
                user_context=user_context,
            )

            # Handle confirmation sentinels
            from familiar.core.confirmations import SENTINEL_PREFIX

            if isinstance(response, str) and response.startswith(SENTINEL_PREFIX):
                # Confirmations not supported in Slack yet — inform user
                self.web_client.chat_postMessage(
                    channel=channel,
                    text="This action requires confirmation. Please use the dashboard or CLI to confirm.",
                    thread_ts=ts if not is_dm else None,
                )
                return

            # Send response — reply in thread for channels, direct for DMs
            if response:
                self.web_client.chat_postMessage(
                    channel=channel,
                    text=response,
                    thread_ts=ts if not is_dm else None,
                )

        except Exception as e:
            logger.error(f"Error processing Slack message: {e}")
            try:
                self.web_client.chat_postMessage(
                    channel=channel,
                    text="Sorry, I encountered an error processing your message.",
                    thread_ts=ts if not is_dm else None,
                )
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
    def run(self):
        """Start the Slack bot (blocking)."""
        if not self.bot_token:
            print("❌ Slack bot token not set")
            print("   Set SLACK_BOT_TOKEN environment variable")
            return

        if not self.app_token:
            print("❌ Slack app token not set")
            print("   Set SLACK_APP_TOKEN environment variable")
            print("   Create one at: api.slack.com/apps → Basic Info → App-Level Tokens")
            return

        self._setup_clients()

        # Start scheduler delivery → Slack
        if hasattr(self, "agent") and hasattr(self.agent, "start_scheduler"):
            self.agent.start_scheduler()

        if self.agent.scheduler and self.default_channel:
            _web_ref = self.web_client

            def _deliver_to_slack(message, channel_name, chat_id):
                target = chat_id or self.default_channel
                try:
                    _web_ref.chat_postMessage(channel=target, text=f"📬 {message}")
                except Exception as e:
                    logger.error(f"Slack delivery failed: {e}")

            self.agent.scheduler.set_delivery_callback(_deliver_to_slack)
            logger.info(f"Scheduler delivery → Slack channel ({self.default_channel})")

        print("🚀 Starting Slack bot (Socket Mode)...")
        print(f"   Bot: {self.bot_user_id}")
        self.socket_client.connect()

        # Keep alive
        try:
            import time

            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            print("\nShutting down Slack bot...")
            self.socket_client.close()
