# 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 = True
    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:
        """Send menu with Block Kit buttons."""
        if not self.web_client:
            return
        channel = self._wizard_channels.get(recipient_id, recipient_id)
        blocks = [
            {"type": "section", "text": {"type": "mrkdwn", "text": text}},
            {"type": "actions", "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": label[:75]},
                    "action_id": f"connect:{key}",
                    "value": key,
                }
                for key, label in options[:25]
            ]},
        ]
        try:
            self.web_client.chat_postMessage(channel=channel, text=text, blocks=blocks)
        except Exception as e:
            logger.error(f"Slack Block Kit menu failed: {e}")
            # Fallback to text
            await super().wizard_send_menu(recipient_id, text, options)

    async def wizard_send_card_menu(
        self,
        recipient_id: str,
        header: str,
        tab_content_text: str,
        tab_options: list[tuple[str, str]],
        active_tab: str,
    ) -> None:
        """Send tabbed card menu using Block Kit sections + buttons."""
        if not self.web_client:
            return
        channel = self._wizard_channels.get(recipient_id, recipient_id)

        from .connect_wizard._helpers import TAB_CATEGORIES, TAB_ORDER

        # Tab bar as button actions
        tab_elements = []
        for tid in TAB_ORDER:
            cat = TAB_CATEGORIES.get(tid, {})
            label = cat.get("label", tid)
            icon = cat.get("icon", "")
            style = "primary" if tid == active_tab else None
            btn = {
                "type": "button",
                "text": {"type": "plain_text", "text": f"{icon} {label}"},
                "action_id": f"ctab:{tid}",
                "value": tid,
            }
            if style:
                btn["style"] = style
            tab_elements.append(btn)

        # Service option buttons
        service_elements = [
            {
                "type": "button",
                "text": {"type": "plain_text", "text": label[:75]},
                "action_id": f"connect:{key}",
                "value": key,
            }
            for key, label in tab_options[:20]
        ]

        blocks = [
            {"type": "header", "text": {"type": "plain_text", "text": header}},
            {"type": "actions", "elements": tab_elements},
            {"type": "section", "text": {"type": "mrkdwn", "text": tab_content_text}},
        ]
        if service_elements:
            blocks.append({"type": "actions", "elements": service_elements})

        try:
            self.web_client.chat_postMessage(
                channel=channel, text=f"{header}\n{tab_content_text}", blocks=blocks
            )
        except Exception as e:
            logger.error(f"Slack card menu failed: {e}")
            await self.wizard_send(recipient_id, f"**{header}**\n\n{tab_content_text}")

    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":
                    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)

            elif req.type == "interactive":
                # Block Kit button callbacks
                payload = req.payload
                actions = payload.get("actions", [])
                user_id = payload.get("user", {}).get("id", "")
                ch = payload.get("channel", {}).get("id", "")
                rid = str(user_id)
                self._wizard_channels[rid] = ch

                for action in actions:
                    action_id = action.get("action_id", "")
                    value = action.get("value", "")
                    if action_id.startswith("ctab:"):
                        tab_id = action_id[5:]
                        loop = asyncio.new_event_loop()
                        try:
                            loop.run_until_complete(self._show_tab(rid, tab_id))
                        finally:
                            loop.close()
                    elif action_id.startswith("connect:"):
                        key = action_id[8:]
                        loop = asyncio.new_event_loop()
                        try:
                            loop.run_until_complete(
                                self.handle_connect_menu_selection(rid, key)
                            )
                        finally:
                            loop.close()

        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
        if text.startswith("/"):
            parts = text[1:].split(maxsplit=1)
            cmd = parts[0].lower()
            cmd_args = parts[1] if len(parts) > 1 else ""
            _reply = lambda msg: self.web_client.chat_postMessage(
                channel=channel, text=msg, thread_ts=ts if not is_dm else None
            )

            # Wizard commands
            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

            # Owner-gated commands
            if cmd in ("connect", "browse", "search", "grant", "revoke", "preauth"):
                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:
                        _reply(f"\U0001f512 /{cmd} requires OWNER trust level.")
                        return
                except ImportError as e:
                    logger.debug("Optional dependency not available: %s", e)

                loop = asyncio.new_event_loop()
                try:
                    if cmd == "connect":
                        loop.run_until_complete(self.handle_connect_command(rid, cmd_args.split() if cmd_args else []))
                    elif cmd == "browse":
                        loop.run_until_complete(self.handle_browse_command(rid))
                    elif cmd == "search":
                        loop.run_until_complete(self.handle_search_command(rid, cmd_args.split() if cmd_args else []))
                    elif cmd == "grant":
                        args = cmd_args.split()
                        if len(args) >= 2:
                            try:
                                from ..skills.user_management.skill import grant_preauth
                                grant_preauth(args[0], args[1])
                                _reply(f"\u2705 Granted **{args[1]}** to `{args[0]}`")
                            except Exception as e:
                                _reply(f"\u274c Grant failed: {e}")
                        else:
                            _reply("Usage: `/grant <user_id> <role>`")
                    elif cmd == "revoke":
                        if cmd_args:
                            try:
                                from ..skills.user_management.skill import revoke_preauth
                                revoke_preauth(cmd_args.strip())
                                _reply(f"\u2705 Revoked role for `{cmd_args.strip()}`")
                            except Exception as e:
                                _reply(f"\u274c Revoke failed: {e}")
                        else:
                            _reply("Usage: `/revoke <user_id>`")
                    elif cmd == "preauth":
                        try:
                            from ..skills.user_management.skill import list_preauths
                            preauths = list_preauths()
                            if preauths:
                                lines = [f"`{uid}` → *{role}*" for uid, role in preauths.items()]
                                _reply("\U0001f4cb *Pre-authorized users:*\n" + "\n".join(lines))
                            else:
                                _reply("No pre-authorizations configured.")
                        except Exception as e:
                            _reply(f"\u274c Error: {e}")
                finally:
                    loop.close()
                return

            # Session info commands
            session = self.agent.sessions.get_or_create_session(f"slack:{user_id}", "slack")

            if cmd == "trust":
                _reply(
                    f"\U0001f6e1\ufe0f *Trust Level:* {session.trust_level.value}\n"
                    f"Score: {session.trust_score:.1f} | Messages: {session.message_count}"
                )
                return
            if cmd == "budget":
                remaining = session.remaining_budget
                _reply(
                    f"\U0001f4b0 *Budget:* ${remaining:.2f} remaining today\n"
                    f"Spent: ${session.spent_today:.2f} / ${session.daily_budget:.2f}"
                )
                return
            if cmd == "caps":
                caps = sorted(session.capabilities) if session.capabilities else ["none"]
                _reply(f"\U0001f9e9 *Capabilities:* {', '.join(caps)}")
                return
            if cmd == "model":
                if cmd_args:
                    try:
                        self.agent.switch_provider(cmd_args.strip())
                        _reply(f"\u2705 Switched to: *{cmd_args.strip()}*")
                    except Exception as e:
                        _reply(f"\u274c Switch failed: {e}")
                else:
                    _reply(f"\U0001f916 *Active model:* {self.agent.provider.name}")
                return
            if cmd == "remember":
                args = cmd_args.split(maxsplit=1)
                if len(args) >= 2:
                    self.agent.remember(args[0], args[1], user_id=f"slack:{user_id}")
                    _reply(f"\u2705 Remembered: *{args[0]}*")
                else:
                    _reply("Usage: `/remember <key> <value>`")
                return
            if cmd == "recall":
                if cmd_args:
                    results = self.agent.recall(cmd_args, user_id=f"slack:{user_id}")
                    if results:
                        lines = [f"• *{r.get('key', '?')}*: {r.get('value', '')}" for r in results[:5]]
                        _reply("\U0001f50d *Recall results:*\n" + "\n".join(lines))
                    else:
                        _reply("No memories found.")
                else:
                    _reply("Usage: `/recall <query>`")
                return
            if cmd == "whoami":
                cu = self._get_channel_user(user_id, display_name)
                linked = "linked" if (cu and cu.is_linked) else "not linked"
                role = cu.role if cu else "guest"
                _reply(f"\U0001f464 *{display_name}*\nSlack ID: `{user_id}`\nAccount: {linked}\nRole: {role}")
                return
            if cmd == "link":
                if cmd_args:
                    try:
                        code = self.authenticator.start_link(ChannelType.SLACK, str(user_id), cmd_args.strip())
                        _reply(f"\U0001f4e7 Check your email for verification code, then send: `/confirm <code>`")
                    except Exception as e:
                        _reply(f"\u274c Link failed: {e}")
                else:
                    _reply("Usage: `/link <email>`")
                return
            if cmd == "confirm":
                if cmd_args:
                    try:
                        result = self.authenticator.confirm_link(ChannelType.SLACK, str(user_id), cmd_args.strip())
                        if result:
                            _reply("\u2705 Account linked!")
                        else:
                            _reply("\u274c Invalid or expired code.")
                    except Exception as e:
                        _reply(f"\u274c Confirm failed: {e}")
                else:
                    _reply("Usage: `/confirm <code>`")
                return
            if cmd == "unlink":
                try:
                    self.authenticator.unlink(ChannelType.SLACK, str(user_id))
                    _reply("\u2705 Account unlinked.")
                except Exception as e:
                    _reply(f"\u274c Unlink failed: {e}")
                return
            if cmd in ("help", "h"):
                _reply(
                    "\U0001f916 *Familiar Commands:*\n"
                    "`/start` — Guided setup\n"
                    "`/connect` — Connect services\n"
                    "`/trust` — Show trust level\n"
                    "`/budget` — Show spending budget\n"
                    "`/model` — Show/switch LLM\n"
                    "`/remember <key> <value>` — Store memory\n"
                    "`/recall <query>` — Search memories\n"
                    "`/link <email>` — Link account\n"
                    "`/whoami` — Account info\n"
                    "`/grant <id> <role>` — Grant access (owner)\n"
                    "`/help` — This message"
                )
                return
            if cmd in ("status", "s"):
                status = self.agent.get_status()
                _reply(
                    f"\U0001f4ca *Status:*\n"
                    f"Provider: {status.get('provider', '?')}\n"
                    f"Memory: {status.get('memory_entries', 0)} entries\n"
                    f"Skills: {status.get('skills_loaded', 0)} loaded"
                )
                return
            if cmd == "clear":
                self.agent.clear_history(f"slack:{user_id}")
                _reply("\u2705 Conversation history cleared.")
                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()
