# 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

"""SMS, Browser, Voice, Healthcare, WebSearch, GitHub, and Slack wizards."""

from __future__ import annotations

import logging
import os
from pathlib import Path

import yaml

from ..formatting import fmt_bold, fmt_code, fmt_italic
from ._helpers import (
    _check_pygithub_lib,
    _check_slack_sdk_lib,
    _check_twilio_lib,
    _service_doc_blurb,
    _test_http_service,
    _update_env_file,
    _update_yaml_config,
)
from ._protocol import WizardHost

logger = logging.getLogger(__name__)


# ── SMS (Twilio) wizard ──────────────────────────────────────────


async def wizard_sms(host: WizardHost, recipient_id: str, args: list) -> None:
    m = host.format_mode
    sub = args[0] if args else ""

    if sub == "test":
        to_number = args[1] if len(args) > 1 else ""
        await wizard_sms_test(host, recipient_id, to_number)
        return

    if sub.startswith("AC") and len(args) >= 3:
        await wizard_sms_save(host, recipient_id, args[0], args[1], args[2])
        return

    has_sid = bool(os.environ.get("TWILIO_ACCOUNT_SID"))
    has_token = bool(os.environ.get("TWILIO_AUTH_TOKEN"))
    has_phone = bool(os.environ.get("TWILIO_PHONE_NUMBER"))
    has_lib = _check_twilio_lib()

    if has_sid and has_token and has_phone:
        phone = os.environ.get("TWILIO_PHONE_NUMBER", "")
        _icon = "\u2705" if has_lib else "\u274c"
        await host.wizard_send(
            recipient_id,
            f"\U0001f4f1 {fmt_bold('SMS Configuration', m)}\n\n"
            f"  \u2705 Twilio SID: "
            f"{fmt_code(os.environ.get('TWILIO_ACCOUNT_SID', '')[:8] + '...', m)}\n"
            f"  \u2705 Auth Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Phone: {fmt_code(phone, m)}\n"
            f"  {_icon} twilio package\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect sms test +1XXXXXXXXXX', m)}"
            f" \u2014 send test\n\n"
            f"Cost: ~$1/month + $0.0079/SMS",
        )
        return

    blurb = _service_doc_blurb("sms", m)
    await host.wizard_send(
        recipient_id,
        f"\U0001f4f1 {fmt_bold('Connect SMS (Twilio)', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Create account at https://www.twilio.com\n"
        f"2. Get a phone number (~$1/month)\n"
        f"3. Find credentials on Twilio Console dashboard\n\n"
        f"{fmt_bold('One-shot setup (3 values, space-separated):', m)}\n"
        f"  {fmt_code('/connect sms <SID> <TOKEN> <PHONE>', m)}\n\n"
        f"{fmt_bold('Example:', m)}\n"
        f"  {fmt_code('/connect sms ACe0a988d7... 9f3c8b... +15551234567', m)}\n\n"
        f"  \u2022 SID starts with {fmt_bold('AC', m)}"
        f" (from Console > Account Info)\n"
        f"  \u2022 Token is the 32-char hex string below it\n"
        f"  \u2022 Phone is your Twilio number"
        f" (not your personal number)\n\n"
        f"Cost: ~$1/month + $0.0079/SMS sent",
    )


async def wizard_sms_save(
    host: WizardHost,
    recipient_id: str,
    sid: str,
    token: str,
    phone: str,
) -> None:
    m = host.format_mode
    env_vars = {
        "TWILIO_ACCOUNT_SID": sid,
        "TWILIO_AUTH_TOKEN": token,
        "TWILIO_PHONE_NUMBER": phone,
    }
    for k, v in env_vars.items():
        os.environ[k] = v
    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, env_vars)
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Failed to save: {e}",
        )
        return

    has_lib = _check_twilio_lib()
    _icon = "\u2705" if has_lib else "\u274c (auto-installs on first use)"
    await host.wizard_send(
        recipient_id,
        f"\u2705 {fmt_bold('SMS configured!', m)}\n\n"
        f"  SID: {fmt_code(sid[:8] + '...', m)}\n"
        f"  Phone: {fmt_code(phone, m)}\n"
        f"  twilio package: {_icon}\n\n"
        f"Test: {fmt_code('/connect sms test +1XXXXXXXXXX', m)}",
    )


async def wizard_sms_test(
    host: WizardHost,
    recipient_id: str,
    to_number: str,
) -> None:
    m = host.format_mode
    if not to_number:
        await host.wizard_send(
            recipient_id,
            f"Usage: {fmt_code('/connect sms test +1XXXXXXXXXX', m)}",
        )
        return
    if not os.environ.get("TWILIO_ACCOUNT_SID"):
        await host.wizard_send(
            recipient_id,
            "\u26a0\ufe0f Twilio not configured. Use /connect sms",
        )
        return
    try:
        from twilio.rest import Client
    except ImportError:
        from familiar.core.deps import SMS_PACKAGES, ensure_packages

        ok, _ = ensure_packages(SMS_PACKAGES)
        if not ok:
            await host.wizard_send(
                recipient_id,
                "\u274c twilio installation failed. Check server logs.",
            )
            return
        from twilio.rest import Client

    try:
        client = Client(
            os.environ["TWILIO_ACCOUNT_SID"],
            os.environ["TWILIO_AUTH_TOKEN"],
        )
        message = client.messages.create(
            body="Hello from Familiar! \U0001f40d SMS is working.",
            from_=os.environ["TWILIO_PHONE_NUMBER"],
            to=to_number,
        )
        await host.wizard_send(
            recipient_id,
            f"\u2705 Test SMS sent to {to_number}\nSID: {message.sid}",
        )
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u274c SMS failed: {e}",
        )


# ── Browser (Playwright) checker ─────────────────────────────────


async def wizard_browser(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    has_playwright = False
    has_chromium = False

    try:
        import playwright  # noqa: F401

        has_playwright = True
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    if has_playwright:
        # Check for Chromium binary on disk — sync_playwright() cannot
        # run inside an async event loop (Telegram, Discord, etc.)
        _pw_dir = Path.home() / ".cache" / "ms-playwright"
        if _pw_dir.exists():
            has_chromium = any(
                _pw_dir.glob("chromium-*/chrome-linux*/chrome"),
            )

    ck, cr = "\u2705", "\u274c"
    if has_playwright and has_chromium:
        await host.wizard_send(
            recipient_id,
            f"\U0001f310 {fmt_bold('Browser Automation', m)}\n\n"
            f"  {ck} playwright package\n"
            f"  {ck} Chromium browser\n\n"
            f'Ready! Try: "Search the web for..."\n\n'
            f"{fmt_italic('Requires TRUSTED or OWNER trust level.', m)}",
        )
        return

    blurb = _service_doc_blurb("browser", m)
    lines = [
        f"\U0001f310 {fmt_bold('Browser Automation Setup', m)}\n",
        blurb,
        f"  {ck if has_playwright else cr} playwright package",
        f"  {ck if has_chromium else cr} Chromium browser\n",
    ]
    if not has_playwright:
        lines.append(
            f"Tap {fmt_bold('Install now', m)} to install playwright + Chromium.",
        )
    elif not has_chromium:
        lines.append(
            f"Tap {fmt_bold('Install now', m)} to install Chromium browser.",
        )

    lines.append(
        fmt_italic(
            "Chromium requires ~150MB and system libraries.",
            m,
        ),
    )

    if host.supports_buttons:
        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            [
                ("browser_install_cmd", "\U0001f4e6 Install now"),
                ("browser_recheck", "\U0001f504 Re-check installation"),
            ],
        )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


# ── Voice (Whisper/TTS) checker ──────────────────────────────────


async def wizard_voice(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    has_faster_whisper = False
    has_whisper = False
    has_piper = False

    try:
        import faster_whisper  # noqa: F401

        has_faster_whisper = True
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    try:
        import whisper  # noqa: F401

        has_whisper = True
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    try:
        import piper  # noqa: F401

        has_piper = True
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    has_openai_tts = bool(os.environ.get("OPENAI_API_KEY"))
    ck, em = "\u2705", "\u2b1c"
    has_any_stt = has_faster_whisper or has_whisper
    has_any_tts = has_piper or has_openai_tts

    blurb = _service_doc_blurb("voice", m)
    lines = [
        f"\U0001f3a4 {fmt_bold('Voice & Transcription', m)}\n",
        blurb,
        f"{fmt_bold('Speech-to-Text:', m)}",
        f"  {ck if has_faster_whisper else em} faster-whisper (recommended, fast)",
        f"  {ck if has_whisper else em} openai-whisper (original, slower)",
        "",
        f"{fmt_bold('Text-to-Speech:', m)}",
        f"  {ck if has_piper else em} piper-tts (local, fast)",
        f"  {ck if has_openai_tts else em} OpenAI TTS (cloud, high quality)",
        "",
    ]

    if has_any_stt:
        engine = "faster-whisper" if has_faster_whisper else "openai-whisper"
        lines.append(f"\u2705 Transcription ready ({engine})")
    else:
        lines.append(
            f"Tap {fmt_bold('Install now', m)} to install faster-whisper (STT).",
        )

    if not has_any_tts:
        lines.append(
            f"Tap {fmt_bold('Install now', m)} to also install piper-tts (TTS).",
        )

    lines.append(
        f"\n{fmt_italic('Whisper models download on first use (~75MB for tiny).', m)}",
    )

    fully_installed = has_any_stt and has_any_tts
    if host.supports_buttons and not fully_installed:
        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            [
                ("voice_install_cmd", "\U0001f4e6 Install now"),
                ("voice_recheck", "\U0001f504 Re-check installation"),
            ],
        )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


# ── Healthcare / Compliance ──────────────────────────────────────


async def wizard_healthcare(
    host: WizardHost,
    recipient_id: str,
    args: list,
) -> None:
    """``/connect healthcare [hipaa|pii|disable]``"""
    m = host.format_mode

    agent = getattr(host, "agent", None)
    compliance_mode = getattr(agent, "compliance_mode", None)
    compliance_str = (
        compliance_mode.value
        if hasattr(compliance_mode, "value")
        else str(compliance_mode or "none")
    )
    pii_cfg = getattr(getattr(agent, "config", None), "agent", None)
    pii_on = getattr(pii_cfg, "enable_pii_detection", False) if pii_cfg else False

    if not args:
        # Show current status + options menu
        if compliance_str == "hipaa":
            current = "HIPAA mode active (encryption + PII blocking + audit logging)"
        elif pii_on:
            current = "PII detection enabled (redact mode)"
        else:
            current = "Not enabled"

        lines = [
            f"\U0001f3e5 {fmt_bold('Healthcare / Compliance', m)}\n",
            f"{fmt_bold('Current:', m)} {current}\n",
            f"{fmt_bold('Options:', m)}",
            f"  \u2022 {fmt_bold('HIPAA', m)} \u2014 encryption + PII blocking + audit logging",
            f"  \u2022 {fmt_bold('PII only', m)}"
            f" \u2014 redact sensitive data (SSN, email, phone),"
            f" no full HIPAA",
            f"  \u2022 {fmt_bold('Disable', m)} \u2014 no compliance features\n",
            "Commands:",
            f"  {fmt_code('/connect healthcare hipaa', m)}",
            f"  {fmt_code('/connect healthcare pii', m)}",
            f"  {fmt_code('/connect healthcare disable', m)}",
        ]

        options = [
            ("healthcare_hipaa", "\U0001f3e5 Enable HIPAA"),
            ("healthcare_pii", "\U0001f50d PII Only"),
            ("healthcare_disable", "\u274c Disable"),
        ]

        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            options,
        )
        return

    action = args[0].lower()

    if action == "hipaa":
        try:
            _update_yaml_config(
                {
                    "compliance": {
                        "mode": "hipaa",
                        "require_encryption": True,
                        "log_phi_access": True,
                    },
                    "agent": {"enable_pii_detection": True},
                }
            )
            # Live-update agent config if available
            if agent:
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "compliance",
                ):
                    agent.config.compliance.mode = "hipaa"
                    agent.config.compliance.require_encryption = True
                    agent.config.compliance.log_phi_access = True
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "agent",
                ):
                    agent.config.agent.enable_pii_detection = True
                if hasattr(agent, "guardrails"):
                    agent.guardrails.set_compliance_mode("hipaa")
            await host.wizard_send(
                recipient_id,
                f"{fmt_bold('HIPAA mode enabled', m)} \u2705\n\n"
                f"\u2022 PII detection:"
                f" {fmt_bold('BLOCK', m)}"
                f" (SSN, email, phone, credit card)\n"
                f"\u2022 Encryption: required\n"
                f"\u2022 PHI audit logging: on\n\n"
                f"Saved to {fmt_code('~/.familiar/config.yaml', m)}",
            )
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Failed to enable HIPAA mode: {e}",
            )

    elif action == "pii":
        try:
            _update_yaml_config(
                {
                    "compliance": {"mode": "none"},
                    "agent": {"enable_pii_detection": True},
                }
            )
            if agent:
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "compliance",
                ):
                    agent.config.compliance.mode = "none"
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "agent",
                ):
                    agent.config.agent.enable_pii_detection = True
                # Ensure guardrails has a PII detector in redact mode
                if hasattr(agent, "guardrails"):
                    g = agent.guardrails
                    if not g._pii_detector:
                        from familiar.core.guardrails import (
                            PIIConfig,
                            PIIDetector,
                        )

                        g._pii_detector = PIIDetector(PIIConfig())
                        g.pii_config = g._pii_detector.config
            await host.wizard_send(
                recipient_id,
                f"{fmt_bold('PII detection enabled', m)} \u2705\n\n"
                f"\u2022 Action:"
                f" {fmt_bold('REDACT', m)}"
                f" (SSN, email, phone, credit card)\n"
                f"\u2022 Compliance mode:"
                f" none (no HIPAA overhead)\n\n"
                f"Saved to {fmt_code('~/.familiar/config.yaml', m)}",
            )
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Failed to enable PII detection: {e}",
            )

    elif action == "disable":
        try:
            _update_yaml_config(
                {
                    "compliance": {
                        "mode": "none",
                        "require_encryption": False,
                        "log_phi_access": False,
                    },
                    "agent": {"enable_pii_detection": False},
                }
            )
            if agent:
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "compliance",
                ):
                    agent.config.compliance.mode = "none"
                    agent.config.compliance.require_encryption = False
                    agent.config.compliance.log_phi_access = False
                if hasattr(agent, "config") and hasattr(
                    agent.config,
                    "agent",
                ):
                    agent.config.agent.enable_pii_detection = False
                if hasattr(agent, "guardrails"):
                    agent.guardrails._pii_detector = None
                    agent.guardrails.pii_config = None
            await host.wizard_send(
                recipient_id,
                f"{fmt_bold('Healthcare / compliance disabled', m)}"
                f" \u2705\n\n"
                f"\u2022 PII detection: off\n"
                f"\u2022 Compliance mode: none\n\n"
                f"Saved to {fmt_code('~/.familiar/config.yaml', m)}",
            )
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Failed to disable compliance: {e}",
            )

    else:
        await host.wizard_send(
            recipient_id,
            f"Unknown healthcare option: {action}\n\n"
            f"Use: {fmt_code('/connect healthcare hipaa', m)}, "
            f"{fmt_code('/connect healthcare pii', m)}, or "
            f"{fmt_code('/connect healthcare disable', m)}",
        )


# ── WebSearch wizard ──────────────────────────────────────────────


async def wizard_websearch(
    host: WizardHost,
    recipient_id: str,
    args: list,
) -> None:
    """WebSearch backend configuration wizard."""
    m = host.format_mode
    sub = args[0].lower() if args else ""

    if sub == "test":
        await wizard_websearch_test(host, recipient_id)
        return

    if sub == "searxng" and len(args) > 1:
        url = args[1]
        await wizard_websearch_save_searxng(host, recipient_id, url)
        return

    if sub in ("duckduckgo", "ddg"):
        # Reset to default DuckDuckGo backend
        _update_yaml_config(
            {"websearch": {"backend": "duckduckgo", "searxng_url": ""}},
        )
        await host.wizard_send(
            recipient_id,
            f"\u2705 WebSearch backend reset to {fmt_bold('DuckDuckGo', m)} (default).",
        )
        return

    # Show current status
    config_path = Path.home() / ".familiar" / "config.yaml"
    backend = "duckduckgo"
    searxng_url = ""
    try:
        if config_path.exists():
            cfg = yaml.safe_load(config_path.read_text()) or {}
            ws = cfg.get("websearch", {})
            backend = ws.get("backend", "duckduckgo")
            searxng_url = ws.get("searxng_url", "")
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    searxng_line = ""
    if searxng_url:
        searxng_line = f"\nSearXNG URL: {fmt_code(searxng_url, m)}"

    blurb = _service_doc_blurb("websearch", m)
    await host.wizard_send(
        recipient_id,
        f"\U0001f50d {fmt_bold('WebSearch Configuration', m)}\n"
        f"{blurb}\n"
        f"Active backend: {fmt_bold(backend, m)}{searxng_line}\n\n"
        f"{fmt_bold('Commands:', m)}\n"
        f"/connect websearch test \u2014 test current backend\n"
        f"/connect websearch searxng <url>"
        f" \u2014 use SearXNG instance\n"
        f"/connect websearch duckduckgo \u2014 reset to default\n\n"
        f"{fmt_bold('Backends:', m)}\n"
        f"  \u2022 {fmt_bold('DuckDuckGo', m)}"
        f" \u2014 works out of the box, no setup needed\n"
        f"  \u2022 {fmt_bold('SearXNG', m)}"
        f" \u2014 self-hosted, private, configurable\n\n"
        f"{fmt_bold('SearXNG setup:', m)}\n"
        f"  1. Deploy SearXNG (Docker recommended):\n"
        f"     {fmt_code('docker run -d -p 8080:8080 searxng/searxng', m)}\n"
        f"  2. Configure:"
        f" {fmt_code('/connect websearch searxng http://localhost:8080', m)}\n"
        f"  3. Test: {fmt_code('/connect websearch test', m)}",
    )


async def wizard_websearch_save_searxng(
    host: WizardHost,
    recipient_id: str,
    url: str,
) -> None:
    """Save SearXNG as the active websearch backend."""
    m = host.format_mode
    _update_yaml_config(
        {"websearch": {"backend": "searxng", "searxng_url": url}},
    )

    await host.wizard_send(
        recipient_id,
        f"\u2705 {fmt_bold('SearXNG Configured', m)}\n\n"
        f"URL: {fmt_code(url, m)}\n"
        f"Backend: searxng\n"
        f"Saved to: {fmt_code('~/.familiar/config.yaml', m)}\n\n"
        f"Testing...",
    )

    # Auto-test after save
    await wizard_websearch_test(host, recipient_id)


async def wizard_websearch_test(
    host: WizardHost,
    recipient_id: str,
) -> None:
    """Test websearch backend reachability."""
    m = host.format_mode
    ck = "\u2705"
    cr = "\u274c"
    results = []

    # Always test DuckDuckGo
    ddg_ok, ddg_msg = _test_http_service(
        "https://html.duckduckgo.com/html/",
    )
    results.append(f"  {ck if ddg_ok else cr} DuckDuckGo: {ddg_msg}")

    # Test SearXNG if configured
    config_path = Path.home() / ".familiar" / "config.yaml"
    backend = "duckduckgo"
    searxng_url = ""
    try:
        if config_path.exists():
            cfg = yaml.safe_load(config_path.read_text()) or {}
            ws = cfg.get("websearch", {})
            backend = ws.get("backend", "duckduckgo")
            searxng_url = ws.get("searxng_url", "")
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    if searxng_url:
        sx_ok, sx_msg = _test_http_service(searxng_url)
        results.append(
            f"  {ck if sx_ok else cr} SearXNG ({searxng_url}): {sx_msg}",
        )

    active_marker = f"\n\nActive backend: {fmt_bold(backend, m)}"

    await host.wizard_send(
        recipient_id,
        f"\U0001f50d {fmt_bold('WebSearch Test', m)}\n\n" + "\n".join(results) + active_marker,
    )


# ── Menu-selection dispatcher ────────────────────────────────────


async def handle_menu_selection(
    host: WizardHost,
    recipient_id: str,
    key: str,
) -> bool:
    """Dispatch button callbacks for integration wizards.

    Returns ``True`` if the *key* was handled, ``False`` otherwise.
    """
    if key == "sms_menu":
        await wizard_sms(host, recipient_id, [])
        return True
    if key == "browser_menu":
        await wizard_browser(host, recipient_id)
        return True
    if key == "voice_menu":
        await wizard_voice(host, recipient_id)
        return True
    if key == "healthcare_menu":
        await wizard_healthcare(host, recipient_id, [])
        return True
    if key.startswith("healthcare_"):
        action = key[len("healthcare_") :]
        await wizard_healthcare(host, recipient_id, [action])
        return True
    if key == "websearch_menu":
        await wizard_websearch(host, recipient_id, [])
        return True
    if key == "github_menu":
        await wizard_github(host, recipient_id, [])
        return True
    if key == "slack_menu":
        await wizard_slack(host, recipient_id, [])
        return True
    return False


# ── GitHub wizard ───────────────────────────────────────────────


async def wizard_github(host: WizardHost, recipient_id: str, args: list) -> None:
    """``/connect github [test]``"""
    m = host.format_mode
    sub = args[0].lower() if args else ""

    if sub == "test":
        await wizard_github_test(host, recipient_id)
        return

    # One-shot: /connect github <token>
    if sub and sub.startswith("gh") and len(sub) > 10:
        await wizard_github_save(host, recipient_id, sub, args[1:])
        return

    has_token = bool(os.environ.get("GITHUB_TOKEN"))

    if has_token:
        repos = os.environ.get("GITHUB_REPOS", "")
        interval = os.environ.get("GITHUB_POLL_INTERVAL", "60")
        has_lib = _check_pygithub_lib()
        if not has_lib:
            from familiar.core.deps import GITHUB_PACKAGES, ensure_packages

            ok, _ = ensure_packages(GITHUB_PACKAGES)
            has_lib = ok
        _lib_icon = "\u2705" if has_lib else "\u274c (failed to install)"
        await host.wizard_send(
            recipient_id,
            f"\U0001f4bb {fmt_bold('GitHub Configuration', m)}\n\n"
            f"  \u2705 Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Repos: {fmt_code(repos or '(all)', m)}\n"
            f"  \u2705 Poll interval: {fmt_code(interval + 's', m)}\n"
            f"  {_lib_icon} PyGithub package\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect github test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("github", m)
    if host.supports_buttons:
        await _start_github_wizard(host, recipient_id)
    else:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4bb {fmt_bold('Connect GitHub', m)}\n"
            f"{blurb}\n"
            f"{fmt_bold('Setup:', m)}\n"
            f"1. Go to https://github.com/settings/tokens\n"
            f"2. Generate a new token (classic) with {fmt_bold('repo', m)}"
            f" + {fmt_bold('notifications', m)} scopes\n"
            f"3. Copy the token\n\n"
            f"{fmt_bold('Interactive setup:', m)}\n"
            f"  Send your token and the wizard will guide you through"
            f" optional repo filters.\n\n"
            f"{fmt_italic('Familiar polls GitHub notifications (mentions, assignments, review requests) and responds automatically.', m)}",
        )
        await _start_github_wizard(host, recipient_id)


async def _start_github_wizard(host: WizardHost, recipient_id: str) -> None:
    host._ensure_wizard_state()
    m = host.format_mode
    host._wizard_state[recipient_id] = {
        "type": "github",
        "step": "token",
    }
    blurb = _service_doc_blurb("github", m)
    warning = ""
    if not host.supports_message_deletion:
        pw_warning = (
            "This channel cannot delete messages. Your token will remain visible in chat history."
        )
        warning = f"\n\n\u26a0\ufe0f {fmt_italic(pw_warning, m)}"

    await host.wizard_send(
        recipient_id,
        f"\U0001f4bb {fmt_bold('GitHub Setup', m)}\n"
        f"{blurb}\n"
        f"Enter your {fmt_bold('GitHub Personal Access Token', m)}:\n\n"
        f"Create one at: https://github.com/settings/tokens\n"
        f"Required scopes: {fmt_bold('repo', m)}, {fmt_bold('notifications', m)}"
        f"{warning}"
        + (
            "\n\n"
            + fmt_italic(
                "Your message will be deleted immediately for safety.",
                m,
            )
            if host.supports_message_deletion
            else ""
        ),
    )


async def github_handle_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
) -> None:
    host._ensure_wizard_state()
    m = host.format_mode
    state = host._wizard_state.get(recipient_id)
    if not state:
        return
    step = state["step"]
    text = text.strip()

    if step == "token":
        # Delete the message containing the token
        if host.supports_message_deletion and message_ref is not None:
            try:
                await host.wizard_delete_message(recipient_id, message_ref)
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        state["token"] = text
        state["step"] = "repos"
        await host.wizard_send(
            recipient_id,
            f"\u2705 Token received.\n\n"
            f"Enter {fmt_bold('repository filter', m)} (optional):\n\n"
            f"  \u2022 Comma-separated: {fmt_code('owner/repo1, owner/repo2', m)}\n"
            f"  \u2022 Or type {fmt_bold('all', m)} for all repos\n\n"
            + fmt_italic('Press enter or type "all" to monitor all repos.', m),
        )

    elif step == "repos":
        token = state.get("token", "")
        repos_input = text.strip()
        host._wizard_state.pop(recipient_id, None)

        repos_list = []
        if repos_input.lower() not in ("all", "", "skip"):
            repos_list = [r.strip() for r in repos_input.split(",") if r.strip()]

        await wizard_github_save(host, recipient_id, token, repos_list)


async def wizard_github_save(
    host: WizardHost,
    recipient_id: str,
    token: str,
    repos: list,
) -> None:
    env_vars = {"GITHUB_TOKEN": token}
    if repos:
        env_vars["GITHUB_REPOS"] = ",".join(repos)

    for k, v in env_vars.items():
        os.environ[k] = v

    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, env_vars)
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Failed to save: {e}",
        )
        return

    await host.wizard_send(recipient_id, "Testing GitHub connection...")
    await wizard_github_test(host, recipient_id)


async def wizard_github_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("GITHUB_TOKEN", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No GitHub token configured.\nRun: {fmt_code('/connect github', m)}",
        )
        return

    ok, msg = _test_http_service(
        "https://api.github.com/user",
        headers={"Authorization": f"token {token}"},
    )
    if ok:
        # Try to get username from the API
        username = ""
        try:
            import httpx

            with httpx.Client(timeout=10) as client:
                resp = client.get(
                    "https://api.github.com/user",
                    headers={"Authorization": f"token {token}"},
                )
                if resp.status_code == 200:
                    username = resp.json().get("login", "")
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
        repos = os.environ.get("GITHUB_REPOS", "")
        user_line = f"\n  User: {fmt_code(username, m)}" if username else ""
        repos_line = f"\n  Repos: {fmt_code(repos, m)}" if repos else "\n  Repos: all"
        has_lib = _check_pygithub_lib()
        if not has_lib:
            from familiar.core.deps import GITHUB_PACKAGES, ensure_packages

            ok, _ = ensure_packages(GITHUB_PACKAGES)
            has_lib = ok
        _lib_icon = "\u2705" if has_lib else "\u274c (failed to install)"
        lib_line = f"\n  PyGithub: {_lib_icon}"

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('GitHub connected!', m)}\n"
            f"{user_line}"
            f"{repos_line}\n"
            f"  Status: {msg}"
            f"{lib_line}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}\n\n"
            f"Start with: {fmt_code('python -m familiar --github', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n\n"
            f"  {msg}\n\n"
            f"Check your token and scopes (repo + notifications).",
        )


# ── Slack wizard ────────────────────────────────────────────────


async def wizard_slack(host: WizardHost, recipient_id: str, args: list) -> None:
    """``/connect slack [test]``"""
    m = host.format_mode
    sub = args[0].lower() if args else ""

    if sub == "test":
        await wizard_slack_test(host, recipient_id)
        return

    has_bot = bool(os.environ.get("SLACK_BOT_TOKEN"))
    has_app = bool(os.environ.get("SLACK_APP_TOKEN"))

    if has_bot and has_app:
        default_ch = os.environ.get("SLACK_DEFAULT_CHANNEL", "")
        has_lib = _check_slack_sdk_lib()
        if not has_lib:
            from familiar.core.deps import SLACK_PACKAGES, ensure_packages

            ok, _ = ensure_packages(SLACK_PACKAGES)
            has_lib = ok
        _lib_icon = "\u2705" if has_lib else "\u274c (failed to install)"
        ch_icon = "\u2705" if default_ch else "\u2b1c"
        await host.wizard_send(
            recipient_id,
            f"\U0001f4ac {fmt_bold('Slack Configuration', m)}\n\n"
            f"  \u2705 Bot Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 App Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  {ch_icon} Default channel:"
            f" {fmt_code(default_ch or '(not set)', m)}\n"
            f"  {_lib_icon} slack-sdk package\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect slack test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("slack", m)
    if host.supports_buttons:
        await _start_slack_wizard(host, recipient_id)
    else:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4ac {fmt_bold('Connect Slack', m)}\n"
            f"{blurb}\n"
            f"{fmt_bold('Setup:', m)}\n"
            f"1. Create a Slack app at https://api.slack.com/apps\n"
            f"2. Enable Socket Mode (Settings \u2192 Socket Mode)\n"
            f"3. Add Bot Token Scopes: {fmt_code('chat:write, app_mentions:read, im:history, im:read, im:write', m)}\n"
            f"4. Create an App-Level Token with"
            f" {fmt_code('connections:write', m)} scope\n"
            f"5. Install app to workspace\n\n"
            f"{fmt_italic('The wizard will guide you through entering both tokens.', m)}",
        )
        await _start_slack_wizard(host, recipient_id)


async def _start_slack_wizard(host: WizardHost, recipient_id: str) -> None:
    host._ensure_wizard_state()
    m = host.format_mode
    host._wizard_state[recipient_id] = {
        "type": "slack",
        "step": "bot_token",
    }

    warning = ""
    if not host.supports_message_deletion:
        pw_warning = (
            "This channel cannot delete messages. Your tokens will remain visible in chat history."
        )
        warning = f"\n\n\u26a0\ufe0f {fmt_italic(pw_warning, m)}"

    await host.wizard_send(
        recipient_id,
        f"\U0001f4ac {fmt_bold('Slack Setup (1/2)', m)}\n\n"
        f"Enter your {fmt_bold('Bot Token', m)} ({fmt_code('xoxb-...', m)}):\n\n"
        f"Find it at: api.slack.com/apps \u2192 Your App \u2192"
        f" OAuth & Permissions \u2192 Bot User OAuth Token"
        f"{warning}"
        + (
            "\n\n"
            + fmt_italic(
                "Your message will be deleted immediately for safety.",
                m,
            )
            if host.supports_message_deletion
            else ""
        ),
    )


async def slack_handle_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
) -> None:
    host._ensure_wizard_state()
    m = host.format_mode
    state = host._wizard_state.get(recipient_id)
    if not state:
        return
    step = state["step"]
    text = text.strip()

    if step == "bot_token":
        if not text.startswith("xoxb-"):
            await host.wizard_send(
                recipient_id,
                f"That doesn't look like a bot token. "
                f"Bot tokens start with {fmt_code('xoxb-', m)}.\n"
                f"Please try again:",
            )
            return

        # Delete the message containing the token
        if host.supports_message_deletion and message_ref is not None:
            try:
                await host.wizard_delete_message(recipient_id, message_ref)
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        state["bot_token"] = text
        state["step"] = "app_token"

        warning = ""
        if not host.supports_message_deletion:
            warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages.', m)}"

        await host.wizard_send(
            recipient_id,
            f"\u2705 Bot token received.\n\n"
            f"{fmt_bold('Slack Setup (2/2)', m)}\n\n"
            f"Enter your {fmt_bold('App-Level Token', m)}"
            f" ({fmt_code('xapp-...', m)}):\n\n"
            f"Find it at: api.slack.com/apps \u2192 Your App \u2192"
            f" Basic Information \u2192 App-Level Tokens"
            f"{warning}"
            + (
                "\n\n"
                + fmt_italic(
                    "Your message will be deleted immediately for safety.",
                    m,
                )
                if host.supports_message_deletion
                else ""
            ),
        )

    elif step == "app_token":
        if not text.startswith("xapp-"):
            await host.wizard_send(
                recipient_id,
                f"That doesn't look like an app token. "
                f"App tokens start with {fmt_code('xapp-', m)}.\n"
                f"Please try again:",
            )
            return

        # Delete the message containing the token
        if host.supports_message_deletion and message_ref is not None:
            try:
                await host.wizard_delete_message(recipient_id, message_ref)
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        bot_token = state.get("bot_token", "")
        host._wizard_state.pop(recipient_id, None)

        await wizard_slack_save(host, recipient_id, bot_token, text)


async def wizard_slack_save(
    host: WizardHost,
    recipient_id: str,
    bot_token: str,
    app_token: str,
) -> None:
    env_vars = {
        "SLACK_BOT_TOKEN": bot_token,
        "SLACK_APP_TOKEN": app_token,
    }
    for k, v in env_vars.items():
        os.environ[k] = v

    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, env_vars)
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Failed to save: {e}",
        )
        return

    await host.wizard_send(recipient_id, "Testing Slack connection...")
    await wizard_slack_test(host, recipient_id)


async def wizard_slack_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    bot_token = os.environ.get("SLACK_BOT_TOKEN", "")
    app_token = os.environ.get("SLACK_APP_TOKEN", "")

    if not bot_token or not app_token:
        missing = []
        if not bot_token:
            missing.append("SLACK_BOT_TOKEN")
        if not app_token:
            missing.append("SLACK_APP_TOKEN")
        await host.wizard_send(
            recipient_id,
            f"\u274c Missing: {', '.join(missing)}\nRun: {fmt_code('/connect slack', m)}",
        )
        return

    ok, msg = _test_http_service(
        "https://slack.com/api/auth.test",
        headers={"Authorization": f"Bearer {bot_token}"},
    )
    if ok:
        # Try to get workspace and bot user info
        workspace = ""
        bot_user = ""
        try:
            import httpx

            with httpx.Client(timeout=10) as client:
                resp = client.post(
                    "https://slack.com/api/auth.test",
                    headers={"Authorization": f"Bearer {bot_token}"},
                )
                if resp.status_code == 200:
                    data = resp.json()
                    if data.get("ok"):
                        workspace = data.get("team", "")
                        bot_user = data.get("user", "")
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
        ws_line = f"\n  Workspace: {fmt_code(workspace, m)}" if workspace else ""
        bot_line = f"\n  Bot user: {fmt_code(bot_user, m)}" if bot_user else ""
        has_lib = _check_slack_sdk_lib()
        if not has_lib:
            from familiar.core.deps import SLACK_PACKAGES, ensure_packages

            ok, _ = ensure_packages(SLACK_PACKAGES)
            has_lib = ok
        _lib_icon = "\u2705" if has_lib else "\u274c (failed to install)"
        lib_line = f"\n  slack-sdk: {_lib_icon}"

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Slack connected!', m)}\n"
            f"{ws_line}"
            f"{bot_line}\n"
            f"  Status: {msg}"
            f"{lib_line}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}\n\n"
            f"Start with: {fmt_code('python -m familiar --slack', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n\n"
            f"  {msg}\n\n"
            f"Check your bot token and app scopes.",
        )
