# 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

"""API service wizards — HubSpot, Todoist, Stripe, Square, MailerLite, Shippo,
Cal.com, Brave Search, Spoonacular, Reddit, Etsy, Clockify, QuickBooks."""

from __future__ import annotations

import logging
import os
from pathlib import Path

from ..formatting import fmt_bold, fmt_code, fmt_italic
from ._helpers import _service_doc_blurb, _test_http_service, _update_env_file
from ._protocol import WizardHost

logger = logging.getLogger(__name__)


# ── DRY helpers for single-token services ────────────────────────────


async def _save_single_token(
    host: WizardHost,
    recipient_id: str,
    env_var: str,
    token: str,
    test_fn,
) -> None:
    """Save one token to env + .env file, then run test."""
    os.environ[env_var] = token
    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, {env_var: token})
    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 connection...")
    await test_fn(host, recipient_id)


async def _handle_single_token_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
    service_key: str,
    env_var: str,
    test_fn,
) -> None:
    """Generic step handler for single-token services."""
    host._ensure_wizard_state()
    state = host._wizard_state.get(recipient_id)
    if not state:
        return
    text = text.strip()

    # 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)

    host._wizard_state.pop(recipient_id, None)
    await _save_single_token(host, recipient_id, env_var, text, test_fn)


# ── HubSpot ──────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(
            host, recipient_id, "HUBSPOT_ACCESS_TOKEN", sub, wizard_hubspot_test
        )
        return

    has_token = bool(os.environ.get("HUBSPOT_ACCESS_TOKEN"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4bc {fmt_bold('HubSpot Configuration', m)}\n\n"
            f"  \u2705 Access Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect hubspot test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("hubspot", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "hubspot", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your token will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4bc {fmt_bold('Connect HubSpot', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://app.hubspot.com \u2192 Settings \u2192 Integrations \u2192 Private Apps\n"
        f"2. Create a private app with CRM scopes\n"
        f"3. Copy the access token\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect hubspot <TOKEN>', m)}\n\n"
        f"Or paste your token below:"
        f"{warning}",
    )


async def hubspot_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "hubspot",
        "HUBSPOT_ACCESS_TOKEN",
        wizard_hubspot_test,
    )


async def wizard_hubspot_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("HUBSPOT_ACCESS_TOKEN", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No HubSpot token configured.\nRun: {fmt_code('/connect hubspot', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.hubapi.com/crm/v3/objects/contacts",
        headers={"Authorization": f"Bearer {token}"},
        params={"limit": "1"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('HubSpot connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Todoist ──────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(host, recipient_id, "TODOIST_API_TOKEN", sub, wizard_todoist_test)
        return

    has_token = bool(os.environ.get("TODOIST_API_TOKEN"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Todoist Configuration', m)}\n\n"
            f"  \u2705 API Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect todoist test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("todoist", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "todoist", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your token will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\u2705 {fmt_bold('Connect Todoist', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://todoist.com/app/settings/integrations/developer\n"
        f"2. Copy your API token\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect todoist <TOKEN>', m)}\n\n"
        f"Or paste your token below:"
        f"{warning}",
    )


async def todoist_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "todoist",
        "TODOIST_API_TOKEN",
        wizard_todoist_test,
    )


async def wizard_todoist_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("TODOIST_API_TOKEN", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Todoist token configured.\nRun: {fmt_code('/connect todoist', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.todoist.com/rest/v2/projects",
        headers={"Authorization": f"Bearer {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Todoist connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Stripe ───────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(host, recipient_id, "STRIPE_SECRET_KEY", sub, wizard_stripe_test)
        return

    has_token = bool(os.environ.get("STRIPE_SECRET_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4b3 {fmt_bold('Stripe Configuration', m)}\n\n"
            f"  \u2705 Secret Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect stripe test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("stripe", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "stripe", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4b3 {fmt_bold('Connect Stripe', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://dashboard.stripe.com/apikeys\n"
        f"2. Copy your Secret key (starts with {fmt_code('sk_', m)})\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect stripe <SECRET_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def stripe_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "stripe",
        "STRIPE_SECRET_KEY",
        wizard_stripe_test,
    )


async def wizard_stripe_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("STRIPE_SECRET_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Stripe key configured.\nRun: {fmt_code('/connect stripe', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.stripe.com/v1/balance",
        headers={"Authorization": f"Bearer {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Stripe connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Square ───────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(host, recipient_id, "SQUARE_ACCESS_TOKEN", sub, wizard_square_test)
        return

    has_token = bool(os.environ.get("SQUARE_ACCESS_TOKEN"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\u2b1b {fmt_bold('Square Configuration', m)}\n\n"
            f"  \u2705 Access Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect square test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("square", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "square", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your token will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\u2b1b {fmt_bold('Connect Square', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://developer.squareup.com/apps\n"
        f"2. Create an application\n"
        f"3. Copy the Access Token from Credentials\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect square <TOKEN>', m)}\n\n"
        f"Or paste your token below:"
        f"{warning}",
    )


async def square_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "square",
        "SQUARE_ACCESS_TOKEN",
        wizard_square_test,
    )


async def wizard_square_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("SQUARE_ACCESS_TOKEN", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Square token configured.\nRun: {fmt_code('/connect square', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://connect.squareup.com/v2/locations",
        headers={"Authorization": f"Bearer {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Square connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── MailerLite ───────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(
            host, recipient_id, "MAILERLITE_API_KEY", sub, wizard_mailerlite_test
        )
        return

    has_token = bool(os.environ.get("MAILERLITE_API_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4e8 {fmt_bold('MailerLite Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect mailerlite test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("mailerlite", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "mailerlite", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4e8 {fmt_bold('Connect MailerLite', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://dashboard.mailerlite.com/integrations/api\n"
        f"2. Generate a new API key\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect mailerlite <API_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def mailerlite_handle_step(
    host: WizardHost, recipient_id: str, text: str, message_ref
) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "mailerlite",
        "MAILERLITE_API_KEY",
        wizard_mailerlite_test,
    )


async def wizard_mailerlite_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("MAILERLITE_API_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No MailerLite key configured.\nRun: {fmt_code('/connect mailerlite', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://connect.mailerlite.com/api/subscribers",
        headers={"Authorization": f"Bearer {token}"},
        params={"limit": "1"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('MailerLite connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Shippo ───────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(host, recipient_id, "SHIPPO_API_KEY", sub, wizard_shippo_test)
        return

    has_token = bool(os.environ.get("SHIPPO_API_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4e6 {fmt_bold('Shippo Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect shippo test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("shippo", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "shippo", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4e6 {fmt_bold('Connect Shippo', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://apps.goshippo.com/settings/api\n"
        f"2. Copy your API Live Token\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect shippo <API_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def shippo_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "shippo",
        "SHIPPO_API_KEY",
        wizard_shippo_test,
    )


async def wizard_shippo_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("SHIPPO_API_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Shippo key configured.\nRun: {fmt_code('/connect shippo', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.goshippo.com/carriers",
        headers={"Authorization": f"ShippoToken {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Shippo connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Cal.com ──────────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(host, recipient_id, "CALCOM_API_KEY", sub, wizard_calcom_test)
        return

    has_token = bool(os.environ.get("CALCOM_API_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4c5 {fmt_bold('Cal.com Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect calcom test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("calcom", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "calcom", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4c5 {fmt_bold('Connect Cal.com', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://app.cal.com/settings/developer/api-keys\n"
        f"2. Create a new API key\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect calcom <API_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def calcom_handle_step(host: WizardHost, recipient_id: str, text: str, message_ref) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "calcom",
        "CALCOM_API_KEY",
        wizard_calcom_test,
    )


async def wizard_calcom_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("CALCOM_API_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Cal.com key configured.\nRun: {fmt_code('/connect calcom', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.cal.com/v2/me",
        headers={"Authorization": f"Bearer {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Cal.com connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Brave Search ─────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(
            host, recipient_id, "BRAVE_SEARCH_API_KEY", sub, wizard_brave_search_test
        )
        return

    has_token = bool(os.environ.get("BRAVE_SEARCH_API_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f981 {fmt_bold('Brave Search Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect brave_search test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("brave_search", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "brave_search", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f981 {fmt_bold('Connect Brave Search', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://brave.com/search/api/\n"
        f"2. Subscribe to a plan (free tier available)\n"
        f"3. Copy your API key\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect brave_search <API_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def brave_search_handle_step(
    host: WizardHost, recipient_id: str, text: str, message_ref
) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "brave_search",
        "BRAVE_SEARCH_API_KEY",
        wizard_brave_search_test,
    )


async def wizard_brave_search_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("BRAVE_SEARCH_API_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Brave Search key configured.\nRun: {fmt_code('/connect brave_search', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.search.brave.com/res/v1/web/search",
        headers={"X-Subscription-Token": token},
        params={"q": "test", "count": "1"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Brave Search connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Spoonacular ──────────────────────────────────────────────────────


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

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

    if sub and sub != "test":
        await _save_single_token(
            host, recipient_id, "SPOONACULAR_API_KEY", sub, wizard_spoonacular_test
        )
        return

    has_token = bool(os.environ.get("SPOONACULAR_API_KEY"))
    if has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f372 {fmt_bold('Spoonacular Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect spoonacular test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("spoonacular", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "spoonacular", "step": "token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f372 {fmt_bold('Connect Spoonacular', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://spoonacular.com/food-api/console\n"
        f"2. Sign up and get your API key\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect spoonacular <API_KEY>', m)}\n\n"
        f"Or paste your key below:"
        f"{warning}",
    )


async def spoonacular_handle_step(
    host: WizardHost, recipient_id: str, text: str, message_ref
) -> None:
    await _handle_single_token_step(
        host,
        recipient_id,
        text,
        message_ref,
        "spoonacular",
        "SPOONACULAR_API_KEY",
        wizard_spoonacular_test,
    )


async def wizard_spoonacular_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("SPOONACULAR_API_KEY", "")
    if not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Spoonacular key configured.\nRun: {fmt_code('/connect spoonacular', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.spoonacular.com/recipes/random",
        params={"number": "1", "apiKey": token},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Spoonacular connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Reddit (two-token) ──────────────────────────────────────────────


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

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

    # One-shot: /connect reddit <client_id> <client_secret>
    if sub and sub != "test" and len(args) >= 2:
        await wizard_reddit_save(host, recipient_id, sub, args[1])
        return

    has_id = bool(os.environ.get("REDDIT_CLIENT_ID"))
    has_secret = bool(os.environ.get("REDDIT_CLIENT_SECRET"))
    if has_id and has_secret:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4e2 {fmt_bold('Reddit Configuration', m)}\n\n"
            f"  \u2705 Client ID: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Client Secret: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect reddit test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("reddit", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "reddit", "step": "client_id"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your credentials will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4e2 {fmt_bold('Connect Reddit (1/2)', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://www.reddit.com/prefs/apps\n"
        f"2. Create a 'script' type application\n"
        f"3. Note the client ID (under app name) and secret\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect reddit <CLIENT_ID> <CLIENT_SECRET>', m)}\n\n"
        f"Or enter your {fmt_bold('Client ID', m)} below:"
        f"{warning}",
    )


async def reddit_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 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)

    if step == "client_id":
        state["client_id"] = text
        state["step"] = "client_secret"
        await host.wizard_send(
            recipient_id,
            f"\u2705 Client ID received.\n\n"
            f"{fmt_bold('Reddit Setup (2/2)', m)}\n\n"
            f"Enter your {fmt_bold('Client Secret', m)}:",
        )

    elif step == "client_secret":
        client_id = state.get("client_id", "")
        host._wizard_state.pop(recipient_id, None)
        await wizard_reddit_save(host, recipient_id, client_id, text)


async def wizard_reddit_save(
    host: WizardHost, recipient_id: str, client_id: str, client_secret: str
) -> None:
    env_vars = {
        "REDDIT_CLIENT_ID": client_id,
        "REDDIT_CLIENT_SECRET": client_secret,
    }
    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 Reddit connection...")
    await wizard_reddit_test(host, recipient_id)


async def wizard_reddit_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    client_id = os.environ.get("REDDIT_CLIENT_ID", "")
    client_secret = os.environ.get("REDDIT_CLIENT_SECRET", "")
    if not client_id or not client_secret:
        await host.wizard_send(
            recipient_id,
            f"\u274c Reddit not configured.\nRun: {fmt_code('/connect reddit', m)}",
        )
        return

    # Reddit uses OAuth2 client credentials for app-only access
    try:
        import httpx

        with httpx.Client(timeout=10) as client:
            resp = client.post(
                "https://www.reddit.com/api/v1/access_token",
                auth=(client_id, client_secret),
                data={"grant_type": "client_credentials"},
                headers={"User-Agent": "Familiar/1.0"},
            )
            if resp.status_code == 200:
                data = resp.json()
                if "access_token" in data:
                    await host.wizard_send(
                        recipient_id,
                        f"\u2705 {fmt_bold('Reddit connected!', m)}\n"
                        f"  OAuth token acquired successfully.\n\n"
                        f"Saved to: {fmt_code('~/.familiar/.env', m)}",
                    )
                    return
                error = data.get("error", "unknown error")
                await host.wizard_send(
                    recipient_id,
                    f"\u274c {fmt_bold('Connection failed', m)}\n  OAuth error: {error}",
                )
            else:
                await host.wizard_send(
                    recipient_id,
                    f"\u274c {fmt_bold('Connection failed', m)}\n  HTTP {resp.status_code}",
                )
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {e}",
        )


# ── Etsy (two-token) ────────────────────────────────────────────────


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

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

    # One-shot: /connect etsy <api_key> <shop_id>
    if sub and sub != "test" and len(args) >= 2:
        await wizard_etsy_save(host, recipient_id, sub, args[1])
        return

    has_key = bool(os.environ.get("ETSY_API_KEY"))
    has_shop = bool(os.environ.get("ETSY_SHOP_ID"))
    if has_key and has_shop:
        shop_id = os.environ.get("ETSY_SHOP_ID", "")
        await host.wizard_send(
            recipient_id,
            f"\U0001f3a8 {fmt_bold('Etsy Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Shop ID: {fmt_code(shop_id, m)}\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect etsy test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("etsy", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "etsy", "step": "api_key"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f3a8 {fmt_bold('Connect Etsy (1/2)', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://www.etsy.com/developers/your-apps\n"
        f"2. Create or select an app\n"
        f"3. Copy the Keystring (API Key)\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect etsy <API_KEY> <SHOP_ID>', m)}\n\n"
        f"Or enter your {fmt_bold('API Key', m)} below:"
        f"{warning}",
    )


async def etsy_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 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)

    if step == "api_key":
        state["api_key"] = text
        state["step"] = "shop_id"
        await host.wizard_send(
            recipient_id,
            f"\u2705 API Key received.\n\n"
            f"{fmt_bold('Etsy Setup (2/2)', m)}\n\n"
            f"Enter your {fmt_bold('Shop ID', m)}:\n\n"
            f"{fmt_italic('Find it in your shop URL: etsy.com/shop/YourShopName', m)}",
        )

    elif step == "shop_id":
        api_key = state.get("api_key", "")
        host._wizard_state.pop(recipient_id, None)
        await wizard_etsy_save(host, recipient_id, api_key, text)


async def wizard_etsy_save(host: WizardHost, recipient_id: str, api_key: str, shop_id: str) -> None:
    env_vars = {
        "ETSY_API_KEY": api_key,
        "ETSY_SHOP_ID": shop_id,
    }
    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 Etsy connection...")
    await wizard_etsy_test(host, recipient_id)


async def wizard_etsy_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    api_key = os.environ.get("ETSY_API_KEY", "")
    shop_id = os.environ.get("ETSY_SHOP_ID", "")
    if not api_key or not shop_id:
        await host.wizard_send(
            recipient_id,
            f"\u274c Etsy not configured.\nRun: {fmt_code('/connect etsy', m)}",
        )
        return
    ok, msg = _test_http_service(
        f"https://openapi.etsy.com/v3/application/shops/{shop_id}",
        headers={"x-api-key": api_key},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Etsy connected!', m)}\n"
            f"  Shop: {fmt_code(shop_id, m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── Clockify (two-token) ────────────────────────────────────────────


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

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

    # One-shot: /connect clockify <api_key> <workspace_id>
    if sub and sub != "test" and len(args) >= 2:
        await wizard_clockify_save(host, recipient_id, sub, args[1])
        return

    has_key = bool(os.environ.get("CLOCKIFY_API_KEY"))
    has_ws = bool(os.environ.get("CLOCKIFY_WORKSPACE_ID"))
    if has_key and has_ws:
        ws_id = os.environ.get("CLOCKIFY_WORKSPACE_ID", "")
        await host.wizard_send(
            recipient_id,
            f"\u23f0 {fmt_bold('Clockify Configuration', m)}\n\n"
            f"  \u2705 API Key: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Workspace: {fmt_code(ws_id, m)}\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect clockify test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("clockify", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "clockify", "step": "api_key"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your key will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\u23f0 {fmt_bold('Connect Clockify (1/2)', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://app.clockify.me/user/settings \u2192 API\n"
        f"2. Generate an API key\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect clockify <API_KEY> <WORKSPACE_ID>', m)}\n\n"
        f"Or enter your {fmt_bold('API Key', m)} below:"
        f"{warning}",
    )


async def clockify_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 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)

    if step == "api_key":
        state["api_key"] = text
        state["step"] = "workspace_id"
        await host.wizard_send(
            recipient_id,
            f"\u2705 API Key received.\n\n"
            f"{fmt_bold('Clockify Setup (2/2)', m)}\n\n"
            f"Enter your {fmt_bold('Workspace ID', m)}:\n\n"
            f"{fmt_italic('Find it in Settings > Workspace > General (the ID in the URL).', m)}",
        )

    elif step == "workspace_id":
        api_key = state.get("api_key", "")
        host._wizard_state.pop(recipient_id, None)
        await wizard_clockify_save(host, recipient_id, api_key, text)


async def wizard_clockify_save(
    host: WizardHost, recipient_id: str, api_key: str, workspace_id: str
) -> None:
    env_vars = {
        "CLOCKIFY_API_KEY": api_key,
        "CLOCKIFY_WORKSPACE_ID": workspace_id,
    }
    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 Clockify connection...")
    await wizard_clockify_test(host, recipient_id)


async def wizard_clockify_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    api_key = os.environ.get("CLOCKIFY_API_KEY", "")
    if not api_key:
        await host.wizard_send(
            recipient_id,
            f"\u274c Clockify not configured.\nRun: {fmt_code('/connect clockify', m)}",
        )
        return
    ok, msg = _test_http_service(
        "https://api.clockify.me/api/v1/user",
        headers={"X-Api-Key": api_key},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Clockify connected!', m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


# ── QuickBooks (two-token) ──────────────────────────────────────────


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

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

    # One-shot: /connect quickbooks <access_token> <realm_id>
    if sub and sub != "test" and len(args) >= 2:
        await wizard_quickbooks_save(host, recipient_id, sub, args[1])
        return

    has_token = bool(os.environ.get("QUICKBOOKS_ACCESS_TOKEN"))
    has_realm = bool(os.environ.get("QUICKBOOKS_REALM_ID"))
    if has_token and has_realm:
        realm = os.environ.get("QUICKBOOKS_REALM_ID", "")
        await host.wizard_send(
            recipient_id,
            f"\U0001f4b0 {fmt_bold('QuickBooks Configuration', m)}\n\n"
            f"  \u2705 Access Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n"
            f"  \u2705 Realm ID: {fmt_code(realm, m)}\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect quickbooks test', m)} \u2014 test connection\n",
        )
        return

    blurb = _service_doc_blurb("quickbooks", m)
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "quickbooks", "step": "access_token"}
    warning = ""
    if not host.supports_message_deletion:
        warning = f"\n\n\u26a0\ufe0f {fmt_italic('This channel cannot delete messages. Your token will remain visible.', m)}"
    await host.wizard_send(
        recipient_id,
        f"\U0001f4b0 {fmt_bold('Connect QuickBooks (1/2)', m)}\n"
        f"{blurb}\n"
        f"{fmt_bold('Setup:', m)}\n"
        f"1. Go to https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization\n"
        f"2. Create an app and obtain OAuth2 access token\n"
        f"3. Note your Company/Realm ID from the URL\n\n"
        f"{fmt_bold('One-shot:', m)} {fmt_code('/connect quickbooks <TOKEN> <REALM_ID>', m)}\n\n"
        f"Or enter your {fmt_bold('Access Token', m)} below:"
        f"{warning}",
    )


async def quickbooks_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 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)

    if step == "access_token":
        state["access_token"] = text
        state["step"] = "realm_id"
        await host.wizard_send(
            recipient_id,
            f"\u2705 Access Token received.\n\n"
            f"{fmt_bold('QuickBooks Setup (2/2)', m)}\n\n"
            f"Enter your {fmt_bold('Realm ID', m)} (Company ID):\n\n"
            f"{fmt_italic('Find it in the URL when logged into QuickBooks Online.', m)}",
        )

    elif step == "realm_id":
        access_token = state.get("access_token", "")
        host._wizard_state.pop(recipient_id, None)
        await wizard_quickbooks_save(host, recipient_id, access_token, text)


async def wizard_quickbooks_save(
    host: WizardHost, recipient_id: str, access_token: str, realm_id: str
) -> None:
    env_vars = {
        "QUICKBOOKS_ACCESS_TOKEN": access_token,
        "QUICKBOOKS_REALM_ID": realm_id,
    }
    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 QuickBooks connection...")
    await wizard_quickbooks_test(host, recipient_id)


async def wizard_quickbooks_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    token = os.environ.get("QUICKBOOKS_ACCESS_TOKEN", "")
    realm_id = os.environ.get("QUICKBOOKS_REALM_ID", "")
    if not token or not realm_id:
        await host.wizard_send(
            recipient_id,
            f"\u274c QuickBooks not configured.\nRun: {fmt_code('/connect quickbooks', m)}",
        )
        return
    ok, msg = _test_http_service(
        f"https://quickbooks.api.intuit.com/v3/company/{realm_id}/companyinfo/{realm_id}",
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/json",
        },
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('QuickBooks connected!', m)}\n"
            f"  Realm: {fmt_code(realm_id, m)}\n  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Connection failed', m)}\n  {msg}",
        )


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


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

    Returns ``True`` if the *key* was handled, ``False`` otherwise.
    """
    if key == "hubspot_menu":
        await wizard_hubspot(host, recipient_id, [])
        return True
    if key == "todoist_menu":
        await wizard_todoist(host, recipient_id, [])
        return True
    if key == "stripe_menu":
        await wizard_stripe(host, recipient_id, [])
        return True
    if key == "square_menu":
        await wizard_square(host, recipient_id, [])
        return True
    if key == "mailerlite_menu":
        await wizard_mailerlite(host, recipient_id, [])
        return True
    if key == "shippo_menu":
        await wizard_shippo(host, recipient_id, [])
        return True
    if key == "calcom_menu":
        await wizard_calcom(host, recipient_id, [])
        return True
    if key == "brave_search_menu":
        await wizard_brave_search(host, recipient_id, [])
        return True
    if key == "spoonacular_menu":
        await wizard_spoonacular(host, recipient_id, [])
        return True
    if key == "reddit_menu":
        await wizard_reddit(host, recipient_id, [])
        return True
    if key == "etsy_menu":
        await wizard_etsy(host, recipient_id, [])
        return True
    if key == "clockify_menu":
        await wizard_clockify(host, recipient_id, [])
        return True
    if key == "quickbooks_menu":
        await wizard_quickbooks(host, recipient_id, [])
        return True
    return False
