# 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

"""Email, Calendar, Google Workspace, and Proton Bridge wizards."""

from __future__ import annotations

import asyncio
import logging
import os
from pathlib import Path

from ..formatting import fmt_bold, fmt_code, fmt_italic, fmt_link
from ._helpers import (
    EMAIL_PRESETS,
    _check_google_libs,
    _check_tcp_port,
    _password_help,
    _service_doc_blurb,
    _test_imap_connection,
    _update_env_file,
    caldav,
)
from ._protocol import WizardHost

logger = logging.getLogger(__name__)


# ── Email wizard ─────────────────────────────────────────────────────


async def wizard_email(host: WizardHost, recipient_id: str, args: list):
    m = host.format_mode
    sub = args[0].lower() if args else ""

    if sub == "test":
        await wizard_email_test(host, recipient_id)
        return
    if sub == "set" and len(args) >= 3:
        await wizard_email_set_credential(
            host,
            recipient_id,
            args[1].upper(),
            " ".join(args[2:]),
        )
        return
    if sub == "proton":
        if len(args) > 1 and "@" in args[1]:
            await wizard_email_setup_inline(
                host,
                recipient_id,
                "proton",
                args[1:],
            )
        else:
            await wizard_proton_bridge(host, recipient_id)
        return
    if sub in EMAIL_PRESETS:
        if len(args) > 1 and "@" in args[1]:
            await wizard_email_setup_inline(
                host,
                recipient_id,
                sub,
                args[1:],
            )
        else:
            await start_email_wizard(host, recipient_id, sub)
        return
    if sub == "custom":
        await wizard_email_custom(host, recipient_id)
        return

    # Default: status or picker
    from familiar.skills.email.accounts import get_all_accounts

    accounts = get_all_accounts()
    if accounts:
        acct_lines = []
        for acct in accounts:
            provider = acct.get("provider", "custom").capitalize()
            acct_lines.append(f"  \u2705 {provider}: {fmt_code(acct['address'], m)}")
        options = [
            ("email_gmail", "Gmail"),
            ("email_outlook", "Outlook"),
            ("email_yahoo", "Yahoo"),
            ("email_proton", "Proton Mail"),
            ("email_custom", "Custom IMAP"),
        ]
        await host.wizard_send_menu(
            recipient_id,
            f"\U0001f4e7 {fmt_bold('Email Configuration', m)}\n\n"
            + "\n".join(acct_lines)
            + "\n\n"
            + f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect email test', m)}"
            f" \u2014 verify all connections\n\n"
            f"{fmt_bold('Add another account:', m)}",
            options,
        )
        return

    # Not configured
    options = [
        ("email_gmail", "Gmail"),
        ("email_outlook", "Outlook"),
        ("email_yahoo", "Yahoo"),
        ("email_proton", "Proton Mail"),
        ("email_custom", "Custom IMAP"),
    ]
    await host.wizard_send_menu(
        recipient_id,
        f"\U0001f4e7 {fmt_bold('Connect Email', m)}\n\nChoose your email provider:",
        options,
    )


async def wizard_email_custom(host: WizardHost, recipient_id: str):
    m = host.format_mode
    await host.wizard_send(
        recipient_id,
        f"\U0001f4e7 {fmt_bold('Custom Email Setup', m)}\n\n"
        f"Set each value individually:\n\n"
        f"{fmt_code('/connect email set EMAIL_ADDRESS user@example.com', m)}\n"
        f"{fmt_code('/connect email set EMAIL_PASSWORD your-password', m)}\n"
        f"{fmt_code('/connect email set EMAIL_IMAP_SERVER imap.example.com', m)}\n"
        f"{fmt_code('/connect email set EMAIL_SMTP_SERVER smtp.example.com', m)}\n"
        f"{fmt_code('/connect email set EMAIL_IMAP_PORT 993', m)}\n"
        f"{fmt_code('/connect email set EMAIL_SMTP_PORT 587', m)}\n\n"
        f"Then test:\n"
        f"  {fmt_code('/connect email test', m)}",
    )


async def wizard_email_setup_inline(
    host: WizardHost,
    recipient_id: str,
    provider_key: str,
    extra_args: list,
):
    """Handle inline email setup: /connect email gmail user@gmail.com PASSWORD"""
    m = host.format_mode
    preset = {**EMAIL_PRESETS[provider_key], "_provider_key": provider_key}
    email_addr = extra_args[0]
    password = extra_args[1] if len(extra_args) > 1 else None

    if password:
        await email_save_and_test(
            host,
            recipient_id,
            email_addr,
            password,
            preset,
        )
        return

    preset_name = preset["name"]
    await host.wizard_send(
        recipient_id,
        f"\U0001f4e7 {fmt_bold(f'{preset_name} Setup', m)}\n\n"
        f"Address: {fmt_code(email_addr, m)}\n\n"
        f"{_password_help(provider_key, m)}\n"
        f"Send your password:\n"
        f"  {fmt_code(f'/connect email {provider_key} {email_addr} YOUR_PASSWORD', m)}",
    )


async def start_email_wizard(
    host: WizardHost,
    recipient_id: str,
    provider_key: str,
):
    """Start the interactive email setup wizard."""
    host._ensure_wizard_state()
    m = host.format_mode
    preset = EMAIL_PRESETS[provider_key]
    name = preset["name"]
    help_text = _password_help(provider_key, m)

    host._wizard_state[recipient_id] = {
        "type": "email",
        "step": "email",
        "provider": provider_key,
    }

    await host.wizard_send(
        recipient_id,
        f"\U0001f4e7 {fmt_bold(f'{name} Setup', m)}\n\n"
        f"{help_text}\n"
        f"Enter your {fmt_bold(f'{name} email address', m)}:",
    )


async def email_handle_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
):
    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 == "email":
        if "@" not in text or "." not in text:
            await host.wizard_send(
                recipient_id,
                "That doesn't look like an email address. Please try again:",
            )
            return
        state["email"] = text
        state["step"] = "password"

        warning = ""
        if not host.supports_message_deletion:
            pw_warning = (
                "This channel cannot delete messages. "
                "Your password will remain visible in chat history. "
                "Consider setting credentials via the server CLI instead."
            )
            warning = f"\n\n\u26a0\ufe0f {fmt_italic(pw_warning, m)}"

        preset_name = EMAIL_PRESETS[state["provider"]]["name"]
        await host.wizard_send(
            recipient_id,
            f"\U0001f510 Enter your {fmt_bold(f'{preset_name} password', m)}:"
            f"{warning}"
            + (
                "\n\n"
                + fmt_italic(
                    "Your message will be deleted immediately.",
                    m,
                )
                if host.supports_message_deletion
                else ""
            ),
        )

    elif step == "password":
        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)
        provider_key = state["provider"]
        email = state["email"]
        preset = {
            **EMAIL_PRESETS[provider_key],
            "_provider_key": provider_key,
        }
        host._wizard_state.pop(recipient_id, None)

        await host.wizard_send(
            recipient_id,
            f"\U0001f4e7 Credentials saved for {fmt_bold(email, m)}.\n"
            f"Testing IMAP connection to {preset['imap_server']}...",
        )
        await email_save_and_test(
            host,
            recipient_id,
            email,
            text,
            preset,
        )


async def email_save_and_test(
    host: WizardHost,
    recipient_id: str,
    email_addr: str,
    password: str,
    preset: dict,
):
    m = host.format_mode
    env_vars = {
        "EMAIL_ADDRESS": email_addr,
        "EMAIL_PASSWORD": password,
        "EMAIL_IMAP_SERVER": preset["imap_server"],
        "EMAIL_SMTP_SERVER": preset["smtp_server"],
        "EMAIL_IMAP_PORT": str(preset["imap_port"]),
        "EMAIL_SMTP_PORT": str(preset["smtp_port"]),
    }

    # Check if an email account already exists in the registry.
    # If so, only add to registry — don't overwrite .env (which holds
    # the primary account's credentials).
    provider_key = preset.get("_provider_key", "custom")
    is_first_account = True
    try:
        from familiar.skills.email.accounts import (
            _load_accounts_file,
            _unique_id,
            add_account,
        )

        data = _load_accounts_file()
        is_first_account = len(data["accounts"]) == 0

        # Find existing account with same address to upsert
        acct_id = None
        for existing in data["accounts"]:
            if existing.get("address") == email_addr:
                acct_id = existing["id"]
                break
        if acct_id is None:
            acct_id = _unique_id(provider_key, data["accounts"])
        account = {
            "id": acct_id,
            "address": email_addr,
            "password": password,
            "imap_server": preset["imap_server"],
            "smtp_server": preset["smtp_server"],
            "imap_port": preset["imap_port"],
            "smtp_port": preset["smtp_port"],
            "provider": provider_key,
        }
        add_account(account)
    except Exception as e:
        logger.warning(f"Failed to save to account registry: {e}")

    # Only write to .env if this is the first account or updating the
    # same address that's already in .env.  This prevents a second
    # account from clobbering the primary account's credentials.
    existing_addr = os.environ.get("EMAIL_ADDRESS", "")
    if is_first_account or existing_addr == email_addr or not existing_addr:
        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

    success, result = _test_imap_connection(env_vars)
    if success:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Email connected!', m)}\n\n"
            f"  Address: {fmt_code(email_addr, m)}\n"
            f"  IMAP: {fmt_code(preset['imap_server'], m)} \u2705\n"
            f"  {result}\n\n"
            f'Try: "Check my email" or "Search for messages from..."\n\n'
            f"{fmt_bold('Add another account?', m)}\n"
            f"  {fmt_code('/connect email', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f {fmt_bold('Credentials saved but connection failed', m)}\n\n"
            f"  Error: {result}\n\n"
            f"Check your password and try:\n"
            f"  {fmt_code('/connect email test', m)}",
        )


async def wizard_email_test(host: WizardHost, recipient_id: str):
    m = host.format_mode
    from familiar.skills.email.accounts import get_all_accounts

    accounts = get_all_accounts()
    if not accounts:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Email not configured.\nUse {fmt_code('/connect email', m)} to set up.",
        )
        return
    await host.wizard_send(
        recipient_id,
        f"Testing {len(accounts)} account(s)...",
    )
    results = []
    for acct in accounts:
        env_vars = {
            "EMAIL_ADDRESS": acct["address"],
            "EMAIL_PASSWORD": acct["password"],
            "EMAIL_IMAP_SERVER": acct["imap_server"],
            "EMAIL_SMTP_SERVER": acct["smtp_server"],
            "EMAIL_IMAP_PORT": str(acct["imap_port"]),
            "EMAIL_SMTP_PORT": str(acct["smtp_port"]),
        }
        success, result = _test_imap_connection(env_vars)
        provider = acct.get("provider", "custom").capitalize()
        if success:
            results.append(
                f"\u2705 {provider} ({acct['address']}): {result}",
            )
        else:
            results.append(
                f"\u274c {provider} ({acct['address']}): {result}",
            )
    await host.wizard_send(recipient_id, "\n".join(results))


async def wizard_email_set_credential(
    host: WizardHost,
    recipient_id: str,
    key: str,
    value: str,
):
    m = host.format_mode
    allowed = {
        "EMAIL_ADDRESS",
        "EMAIL_PASSWORD",
        "EMAIL_IMAP_SERVER",
        "EMAIL_SMTP_SERVER",
        "EMAIL_IMAP_PORT",
        "EMAIL_SMTP_PORT",
    }
    if key not in allowed:
        await host.wizard_send(
            recipient_id,
            f"Unknown key: {key}\nAllowed: {', '.join(sorted(allowed))}",
        )
        return
    os.environ[key] = value
    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, {key: value})
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Failed to save: {e}",
        )
        return
    is_sensitive = "PASSWORD" in key
    display_val = (
        "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" if is_sensitive else fmt_code(value, m)
    )
    await host.wizard_send(
        recipient_id,
        f"\u2705 Set {fmt_bold(key, m)} = {display_val}",
    )


# ── CalDAV wizard ────────────────────────────────────────────────────


async def wizard_calendar(
    host: WizardHost,
    recipient_id: str,
    args: list,
):
    m = host.format_mode
    sub = args[0].lower() if args else ""

    if sub == "test":
        await wizard_calendar_test(host, recipient_id)
        return
    if sub == "google":
        await start_google_calendar_wizard(host, recipient_id)
        return
    if sub == "caldav":
        if len(args) >= 4:
            await caldav_save_and_test(
                host,
                recipient_id,
                args[1],
                args[2],
                " ".join(args[3:]),
            )
            return
        await start_caldav_wizard(host, recipient_id)
        return

    # Default: status or picker (multi-account aware)
    try:
        from familiar.skills.calendar.accounts import get_all_accounts

        accounts = get_all_accounts()
    except Exception:
        accounts = []

    if accounts:
        acct_lines = []
        for acct in accounts:
            label = acct.get("label") or acct["id"]
            atype = acct.get("type", "unknown")
            if atype == "caldav":
                detail = f" ({acct.get('url', '')[:40]})"
            else:
                detail = ""
            acct_lines.append(
                f"  \u2705 {label}: {fmt_bold(atype.title() + detail, m)}",
            )
        await host.wizard_send(
            recipient_id,
            f"\U0001f4c5 {fmt_bold('Calendar Configuration', m)}\n\n"
            + "\n".join(acct_lines)
            + "\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect calendar test', m)}"
            f" \u2014 verify connection\n"
            f"  {fmt_code('/connect calendar caldav', m)}"
            f" \u2014 add CalDAV account\n"
            f"  {fmt_code('/connect calendar google', m)}"
            f" \u2014 add Google account\n\n"
            f'Try: "What\'s on my calendar today?"',
        )
        return

    options = [
        ("calendar_google", "Google Calendar"),
        ("calendar_caldav", "CalDAV"),
    ]
    await host.wizard_send_menu(
        recipient_id,
        f"\U0001f4c5 {fmt_bold('Calendar Setup', m)}\n\n"
        f"Current: {fmt_bold('Not configured', m)}\n\n"
        f"Choose your calendar provider:",
        options,
    )


async def start_caldav_wizard(host: WizardHost, recipient_id: str):
    host._ensure_wizard_state()
    m = host.format_mode
    host._wizard_state[recipient_id] = {"type": "caldav", "step": "url"}
    blurb = _service_doc_blurb("caldav", m)
    await host.wizard_send(
        recipient_id,
        f"\U0001f4c5 {fmt_bold('CalDAV Calendar Setup', m)}\n"
        f"{blurb}\n"
        f"CalDAV works with Nextcloud, Radicale, ownCloud, Synology, "
        f"Baikal, and most self-hosted calendar servers.\n\n"
        f"Enter your {fmt_bold('CalDAV server URL', m)}:\n\n"
        f"{fmt_italic('Example: https://cloud.example.org/remote.php/dav/calendars/alice/', m)}",
    )


async def caldav_handle_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
):
    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 == "url":
        if not text.startswith(("http://", "https://")):
            await host.wizard_send(
                recipient_id,
                "That doesn't look like a URL. "
                "Please enter a URL starting with http:// or https://:",
            )
            return
        state["url"] = text
        state["step"] = "user"
        await host.wizard_send(
            recipient_id,
            f"Enter your {fmt_bold('CalDAV username', m)}:",
        )

    elif step == "user":
        if not text:
            await host.wizard_send(
                recipient_id,
                "Username cannot be empty. Please try again:",
            )
            return
        state["user"] = text
        state["step"] = "password"

        warning = ""
        if not host.supports_message_deletion:
            pw_warning = (
                "This channel cannot delete messages. "
                "Your password will remain visible in chat history. "
                "Consider setting credentials via the server CLI instead."
            )
            warning = f"\n\n\u26a0\ufe0f {fmt_italic(pw_warning, m)}"

        await host.wizard_send(
            recipient_id,
            f"\U0001f510 Enter your {fmt_bold('CalDAV password', m)}:"
            f"{warning}"
            + (
                "\n\n"
                + fmt_italic(
                    "Your message will be deleted immediately for safety.",
                    m,
                )
                if host.supports_message_deletion
                else ""
            ),
        )

    elif step == "password":
        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)
        url = state["url"]
        user = state["user"]
        host._wizard_state.pop(recipient_id, None)
        await host.wizard_send(
            recipient_id,
            "Testing CalDAV connection...",
        )
        await caldav_save_and_test(host, recipient_id, url, user, text)


async def caldav_save_and_test(
    host: WizardHost,
    recipient_id: str,
    url: str,
    user: str,
    password: str,
):
    m = host.format_mode

    # Save to calendar account registry (upsert by url+user to prevent duplicates)
    is_first_account = True
    try:
        from familiar.skills.calendar.accounts import (
            _load_accounts_file,
            _unique_id,
            add_account,
        )

        data = _load_accounts_file()
        is_first_account = len(data["accounts"]) == 0
        # Find existing account with same url+user
        acct_id = None
        for existing in data["accounts"]:
            if (
                existing.get("type") == "caldav"
                and existing.get("url") == url
                and existing.get("user") == user
            ):
                acct_id = existing["id"]
                break
        if acct_id is None:
            acct_id = _unique_id("caldav", data["accounts"])
        account = {
            "id": acct_id,
            "type": "caldav",
            "label": "",
            "url": url,
            "user": user,
            "password": password,
        }
        add_account(account)
    except Exception as e:
        logger.warning(
            f"Failed to save calendar account to registry: {e}",
        )

    # Only write to .env if this is the first CalDAV account or updating
    # the same url+user already in .env.  Prevents a second account from
    # clobbering the primary account's credentials.
    existing_url = os.environ.get("CALDAV_URL", "")
    existing_user = os.environ.get("CALDAV_USER", "")
    if is_first_account or not existing_url or (existing_url == url and existing_user == user):
        os.environ["CALDAV_URL"] = url
        os.environ["CALDAV_USER"] = user
        os.environ["CALDAV_PASSWORD"] = password
        os.environ["CALENDAR_PROVIDER"] = "caldav"
        env_path = Path.home() / ".familiar" / ".env"
        try:
            _update_env_file(
                env_path,
                {
                    "CALDAV_URL": url,
                    "CALDAV_USER": user,
                    "CALDAV_PASSWORD": password,
                    "CALENDAR_PROVIDER": "caldav",
                },
            )
        except Exception as e:
            logger.error(f"Failed to write .env: {e}")
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Credentials set for this session but failed to persist: {e}",
            )
            return

    if caldav is not None:
        try:
            client = caldav.DAVClient(
                url=url,
                username=user,
                password=password,
            )
            principal = client.principal()
            calendars = principal.calendars()
            cal_names = [str(c) for c in calendars[:5]]

            await host.wizard_send(
                recipient_id,
                f"\u2705 {fmt_bold('CalDAV Connected!', m)}\n\n"
                f"Server: {fmt_code(url[:50], m)}\n"
                f"User: {fmt_code(user, m)}\n"
                f"Calendars found: {len(calendars)}\n"
                + ("\n".join(f"  \u2022 {n}" for n in cal_names) + "\n" if cal_names else "")
                + f"\nSaved to: {fmt_code('~/.familiar/.env', m)}\n\n"
                f"Add another account? Use "
                f"{fmt_code('/connect calendar google', m)} or "
                f"{fmt_code('/connect calendar caldav', m)}",
            )
            return
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Credentials saved but connection test "
                f"failed:\n{fmt_code(str(e), m)}\n\n"
                f"Check your URL, username, and password.\n"
                f"To retry: "
                f"{fmt_code('/connect calendar caldav', m)}",
            )
            return
    else:
        # Auto-install caldav + vobject
        from familiar.core.deps import (
            CALDAV_PACKAGES,
            ensure_packages_async,
        )

        await host.wizard_send(
            recipient_id,
            "Installing caldav libraries...",
        )
        ok, failed = await ensure_packages_async(CALDAV_PACKAGES)
        if ok:
            await host.wizard_send(
                recipient_id,
                f"\u2705 CalDAV libraries installed! Re-run "
                f"{fmt_code('/connect calendar caldav', m)} to test.",
            )
        else:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Credentials saved but CalDAV library "
                f"install failed: {', '.join(failed)}",
            )


async def wizard_calendar_test(host: WizardHost, recipient_id: str):
    m = host.format_mode

    # Try multi-account test first
    try:
        from familiar.skills.calendar.accounts import get_all_accounts

        accounts = get_all_accounts()
    except Exception:
        accounts = []

    if accounts:
        results = []
        for acct in accounts:
            label = acct.get("label") or acct["id"]
            atype = acct.get("type", "unknown")
            try:
                if atype == "caldav":
                    from familiar.skills.calendar.skill import (
                        _get_caldav_client_for_account,
                    )

                    client = _get_caldav_client_for_account(acct)
                    principal = client.principal()
                    calendars = principal.calendars()
                    results.append(
                        f"\u2705 {label} (CalDAV) \u2014 {len(calendars)} calendar(s)",
                    )
                elif atype == "google":
                    from familiar.skills.calendar.skill import (
                        _get_google_service_for_account,
                    )

                    _get_google_service_for_account(acct)
                    results.append(
                        f"\u2705 {label} (Google) \u2014 OK",
                    )
                else:
                    results.append(
                        f"\u26a0\ufe0f {label}: unknown type '{atype}'",
                    )
            except Exception as e:
                results.append(f"\u274c {label} ({atype}): {e}")
        await host.wizard_send(recipient_id, "\n".join(results))
        return

    # Legacy single-provider test
    try:
        from familiar.skills.calendar.skill import _calendar_provider

        provider = _calendar_provider()
    except ImportError:
        await host.wizard_send(
            recipient_id,
            "\U0001f4c5 Calendar module not available.",
        )
        return

    if provider == "none":
        await host.wizard_send(
            recipient_id,
            f"\U0001f4c5 No calendar configured.\n\n"
            f"Use {fmt_code('/connect calendar', m)} to set up.",
        )
        return

    if provider == "caldav":
        try:
            from familiar.skills.calendar.skill import _get_caldav_client

            client = _get_caldav_client()
            principal = client.principal()
            calendars = principal.calendars()
            await host.wizard_send(
                recipient_id,
                f"\u2705 CalDAV connection OK \u2014 {len(calendars)} calendar(s) found.",
            )
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u274c CalDAV connection failed: {e}",
            )
    else:
        try:
            from familiar.skills.calendar.skill import (
                _get_calendar_service,
            )

            _get_calendar_service()
            await host.wizard_send(
                recipient_id,
                "\u2705 Google Calendar connection OK.",
            )
        except Exception as e:
            await host.wizard_send(
                recipient_id,
                f"\u274c Google Calendar connection failed: {e}",
            )


# ── Google Calendar wizard ───────────────────────────────────────────


async def start_google_calendar_wizard(
    host: WizardHost,
    recipient_id: str,
):
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"
    creds_file = data_dir / "google_credentials.json"
    token_file = data_dir / "google_token.json"

    has_libs = _check_google_libs()
    has_creds = creds_file.exists()
    has_token = token_file.exists()

    if not has_libs:
        await host.wizard_send(
            recipient_id,
            "\U0001f4e6 Installing Google Calendar libraries...",
        )
        await wizard_google_install_libs(host, recipient_id)
        has_libs = _check_google_libs()
        if not has_libs:
            return

    if not has_creds:
        actual_path = str(creds_file)
        console_link = fmt_link(
            "Google Cloud Console",
            "https://console.cloud.google.com/apis/credentials",
            m,
        )
        await host.wizard_send(
            recipient_id,
            f"\U0001f4c5 {fmt_bold('Google Calendar Setup', m)}\n\n"
            f"\u2705 Libraries installed\n"
            f"\u274c OAuth credentials needed\n\n"
            f"\U0001f449 {fmt_bold('Easiest:', m)} Run the guided "
            f"setup script on the server:\n"
            f"  {fmt_code('python -m familiar.onboard.google_setup', m)}\n\n"
            f"Or follow these steps manually:\n"
            f"1. Go to {console_link}\n"
            f"2. Create project \u2192 Enable Calendar API\n"
            f"3. Create OAuth Client ID"
            f" \u2192 {fmt_bold('Desktop app', m)}\n"
            f"4. Download the JSON file\n\n"
            f"Then either:\n"
            f"  \u2022 Place it at: {fmt_code(actual_path, m)}\n"
            f"  \u2022 Or upload it directly in this chat",
        )
        return

    # Validate credentials type (must be Desktop app, not Web app)
    try:
        import json as _json

        creds_data = _json.loads(creds_file.read_text())
        if "web" in creds_data and "installed" not in creds_data:
            await host.wizard_send(
                recipient_id,
                f"\u274c {fmt_bold('Wrong credentials type', m)}\n\n"
                f"Your {fmt_code('credentials.json', m)} is for a "
                f"{fmt_bold('Web application', m)}, but Familiar needs "
                f"{fmt_bold('Desktop app', m)} credentials.\n\n"
                f"{fmt_bold('How to fix:', m)}\n"
                f"1. Go to "
                f"https://console.cloud.google.com/apis/credentials\n"
                f"2. Click {fmt_bold('+ CREATE CREDENTIALS', m)}"
                f" \u2192 {fmt_bold('OAuth client ID', m)}\n"
                f"3. Application type:"
                f" {fmt_bold('Desktop app', m)}\n"
                f"4. Download the new JSON file\n"
                f"5. Replace {fmt_code(str(creds_file), m)}\n\n"
                f"Or run: "
                f"{fmt_code('python -m familiar.onboard.google_setup', m)}",
            )
            return
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    if not has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f4c5 {fmt_bold('Google Calendar Setup', m)}\n\n"
            f"\u2705 Libraries installed\n"
            f"\u2705 credentials.json found\n"
            f"\u23f3 Starting OAuth authorization...",
        )
        await wizard_google_auth(host, recipient_id, "calendar")
        # Register after OAuth completes (token file now exists)
        if token_file.exists():
            register_google_calendar_account(host)
        return

    # Already authorized — ensure registered
    register_google_calendar_account(host)
    await host.wizard_send(
        recipient_id,
        f"\U0001f4c5 {fmt_bold('Google Calendar', m)}\n\n"
        f"\u2705 Libraries installed\n"
        f"\u2705 credentials.json present\n"
        f"\u2705 Calendar authorized\n\n"
        f"\U0001f389 Google Calendar is connected! "
        f'Try: "What\'s on my calendar?"\n\n'
        f"Add another account? Use "
        f"{fmt_code('/connect calendar caldav', m)} or "
        f"{fmt_code('/connect calendar google', m)}",
    )


def register_google_calendar_account(host: WizardHost):
    """Save Google Calendar account to the registry (idempotent)."""
    try:
        from familiar.skills.calendar.accounts import (
            _load_accounts_file,
            _unique_id,
            add_account,
        )

        data = _load_accounts_file()
        # Check if a google account already exists
        for acct in data["accounts"]:
            if acct.get("type") == "google":
                return  # Already registered
        acct_id = _unique_id("google", data["accounts"])
        account = {
            "id": acct_id,
            "type": "google",
            "label": "",
            "token_file": "google_token.json",
            "calendar_id": "primary",
        }
        add_account(account)
    except Exception as e:
        logger.warning(
            f"Failed to save Google calendar account to registry: {e}",
        )


# ── Google Workspace wizard ──────────────────────────────────────────


async def wizard_google(
    host: WizardHost,
    recipient_id: str,
    args: list,
):
    m = host.format_mode
    sub = args[0].lower() if args else ""
    data_dir = Path.home() / ".familiar" / "data"
    creds_file = data_dir / "google_credentials.json"

    if sub == "check":
        await wizard_google_check_status(host, recipient_id)
        return
    if sub == "install":
        await wizard_google_install_libs(host, recipient_id)
        return
    if sub == "auth":
        svc = args[1].lower() if len(args) > 1 else "calendar"
        await wizard_google_auth(host, recipient_id, svc)
        return
    if sub == "code":
        auth_code = " ".join(args[1:]) if len(args) > 1 else ""
        if not auth_code:
            await host.wizard_send(
                recipient_id,
                f"Usage: {fmt_code('/connect google code YOUR_AUTH_CODE', m)}",
            )
            return
        await wizard_google_handle_code(host, recipient_id, auth_code)
        return

    # Default: show guided setup
    has_libs = _check_google_libs()
    has_creds = creds_file.exists()

    token_files = {
        "Calendar": data_dir / "google_token.json",
        "Drive": data_dir / "google_token_drive.json",
        "Contacts": data_dir / "google_token_contacts.json",
        "Gmail": data_dir / "google_token_gmail.json",
    }

    ck, cr = "\u2705", "\u274c"
    step_lines: list[str] = []
    next_action = None

    if not has_libs:
        step_lines.append(
            f"{cr} {fmt_bold('Step 1:', m)} Install Google libraries",
        )
        next_action = (
            f"\U0001f449 {fmt_bold('Next:', m)} Run:\n  {fmt_code('/connect google install', m)}"
        )
    else:
        step_lines.append(
            f"{ck} {fmt_bold('Step 1:', m)} Google libraries installed",
        )

    if not has_creds:
        step_lines.append(
            f"{cr} {fmt_bold('Step 2:', m)} OAuth credentials.json",
        )
        if not next_action:
            actual_path = str(
                data_dir / "google_credentials.json",
            )
            console_link = fmt_link(
                "Google Cloud Console",
                "https://console.cloud.google.com/apis/credentials",
                m,
            )
            next_action = (
                f"\U0001f449 {fmt_bold('Next:', m)} "
                f"Create a Google Cloud project:\n\n"
                f"1. Go to {console_link}\n"
                f'2. Create project: "Familiar AI Assistant"\n'
                f"3. Enable APIs: Calendar, Drive, People, Gmail\n"
                f"4. Create OAuth consent screen "
                f"(External, test mode)\n"
                f"5. Create OAuth Client ID"
                f" \u2192 {fmt_bold('Desktop app', m)}\n"
                f"6. Download the JSON file\n\n"
                f"{fmt_bold('Then place it here:', m)}\n"
                f"  {fmt_code(actual_path, m)}"
            )
    else:
        step_lines.append(
            f"{ck} {fmt_bold('Step 2:', m)} credentials.json present",
        )

    authorized, not_authorized = [], []
    for name, path in token_files.items():
        (authorized if path.exists() else not_authorized).append(name)

    if authorized:
        step_lines.append(
            f"{ck} {fmt_bold('Step 3:', m)} Authorized: {', '.join(authorized)}",
        )
    if not_authorized:
        prefix = "\u2b1c" if authorized else cr
        step_lines.append(
            f"{prefix} {fmt_bold('Step 3:', m)} Not yet authorized: {', '.join(not_authorized)}",
        )
        if not next_action and has_libs and has_creds:
            svc = not_authorized[0].lower()
            next_action = (
                f"\U0001f449 {fmt_bold('Next:', m)} "
                f"Authorize {not_authorized[0]}:\n"
                f"  {fmt_code(f'/connect google auth {svc}', m)}"
            )

    if has_libs and has_creds and not not_authorized:
        next_action = (
            f"\U0001f389 {fmt_bold('All Google services connected!', m)}"
            f' Try: "What\'s on my calendar?"'
        )

    lines = [
        f"\U0001f4c5 {fmt_bold('Google Workspace Setup', m)}\n",
        *step_lines,
        "",
        next_action or "",
    ]

    options: list[tuple[str, str]] = []
    if not has_libs:
        options.append(("google_install", "Install Libraries"))
    if has_libs and has_creds and not_authorized:
        for svc in not_authorized[:3]:
            options.append(
                (f"google_auth_{svc.lower()}", f"Authorize {svc}"),
            )
    if has_creds:
        options.append(("google_check", "Check Status"))

    if options:
        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            options,
        )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


async def wizard_google_install_libs(
    host: WizardHost,
    recipient_id: str,
):
    if _check_google_libs():
        await host.wizard_send(
            recipient_id,
            "\u2705 Google API libraries already installed!",
        )
        return

    await host.wizard_send(
        recipient_id,
        "\U0001f4e6 Installing Google API libraries...",
    )
    try:
        import subprocess

        result = subprocess.run(
            [
                "pip",
                "install",
                "google-auth-oauthlib",
                "google-auth-httplib2",
                "google-api-python-client",
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=120,
        )
        if result.returncode == 0 and _check_google_libs():
            m = host.format_mode
            await host.wizard_send(
                recipient_id,
                f"\u2705 {fmt_bold('Google libraries installed!', m)}"
                f"\n\n"
                f"Next: Place your credentials.json file at:\n"
                f"  {fmt_code('~/.familiar/data/google_credentials.json', m)}"
                f"\n\n"
                f"Then run {fmt_code('/connect google', m)} to continue.",
            )
        else:
            m = host.format_mode
            error = result.stderr[-300:] if result.stderr else "Unknown error"
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f Google library installation failed.\n\n"
                f"Error: {fmt_code(error, m)}\n\n"
                f"Try again with "
                f"{fmt_code('/connect google install', m)}",
            )
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Installation failed: {e}",
        )


async def wizard_google_check_status(
    host: WizardHost,
    recipient_id: str,
):
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"
    token_files = {
        "Calendar": data_dir / "google_token.json",
        "Drive": data_dir / "google_token_drive.json",
        "Contacts": data_dir / "google_token_contacts.json",
        "Gmail": data_dir / "google_token_gmail.json",
    }
    lines = [f"\U0001f4c5 {fmt_bold('Google Token Status', m)}\n"]
    for name, path in token_files.items():
        _icon = "\u2705" if path.exists() else "\u274c"
        lines.append(f"  {_icon} {name}")
    await host.wizard_send(recipient_id, "\n".join(lines))


async def wizard_google_auth(
    host: WizardHost,
    recipient_id: str,
    service: str,
):
    """Start headless Google OAuth flow for a service."""
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"
    creds_file = data_dir / "google_credentials.json"

    if not _check_google_libs():
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Google libraries not installed.\n"
            f"Run: {fmt_code('/connect google install', m)}",
        )
        return

    if not creds_file.exists():
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f credentials.json not found.\n"
            f"Place it at: "
            f"{fmt_code('~/.familiar/data/google_credentials.json', m)}",
        )
        return

    # Validate credentials type
    try:
        import json as _json

        creds_data = _json.loads(creds_file.read_text())
        if "web" in creds_data and "installed" not in creds_data:
            await host.wizard_send(
                recipient_id,
                f"\u274c {fmt_bold('Wrong credentials type', m)}\n\n"
                f"Your {fmt_code('credentials.json', m)} is a "
                f"{fmt_bold('Web app', m)} client. "
                f"Familiar needs a "
                f"{fmt_bold('Desktop app', m)} client.\n\n"
                f"Go to "
                f"https://console.cloud.google.com/apis/credentials\n"
                f"Create a new OAuth client ID"
                f" \u2192 {fmt_bold('Desktop app', m)}\n"
                f"Download and replace "
                f"{fmt_code(str(creds_file), m)}",
            )
            return
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    scopes_map = {
        "calendar": ["https://www.googleapis.com/auth/calendar"],
        "drive": [
            "https://www.googleapis.com/auth/drive.readonly",
            "https://www.googleapis.com/auth/drive.file",
        ],
        "contacts": [
            "https://www.googleapis.com/auth/contacts.readonly",
        ],
        "gmail": [
            "https://www.googleapis.com/auth/gmail.readonly",
            "https://www.googleapis.com/auth/gmail.send",
        ],
    }
    token_map = {
        "calendar": data_dir / "google_token.json",
        "drive": data_dir / "google_token_drive.json",
        "contacts": data_dir / "google_token_contacts.json",
        "gmail": data_dir / "google_token_gmail.json",
    }

    if service not in scopes_map:
        await host.wizard_send(
            recipient_id,
            f"Unknown service: {service}\nAvailable: {', '.join(scopes_map.keys())}",
        )
        return

    token_file = token_map[service]
    if token_file.exists():
        # Validate the token — it may have been revoked
        token_valid = False
        try:
            from google.auth.transport.requests import Request
            from google.oauth2.credentials import Credentials

            creds = Credentials.from_authorized_user_file(
                str(token_file), scopes_map[service]
            )
            if creds.valid:
                token_valid = True
            elif creds.expired and creds.refresh_token:
                creds.refresh(Request())
                token_file.write_text(creds.to_json())
                token_valid = True
        except Exception as e:
            logger.warning("Token validation failed for %s: %s", service, e)
            token_file.unlink(missing_ok=True)

        if token_valid:
            await host.wizard_send(
                recipient_id,
                f"\u2705 {service.title()} is already authorized!\n\n"
                f"To re-authorize, delete the token:\n"
                f"  {fmt_code(f'rm {token_file}', m)}\n"
                f"Then run this command again.",
            )
            return
        else:
            await host.wizard_send(
                recipient_id,
                f"\u26a0\ufe0f {service.title()} token expired or revoked. Re-authorizing...",
            )

    try:
        import http.server
        import socket
        import threading
        import urllib.parse

        from google_auth_oauthlib.flow import InstalledAppFlow

        with socket.socket() as sock:
            sock.bind(("127.0.0.1", 0))
            port = sock.getsockname()[1]

        redirect_uri = f"http://localhost:{port}"
        flow = InstalledAppFlow.from_client_secrets_file(
            str(creds_file),
            scopes_map[service],
            redirect_uri=redirect_uri,
        )
        auth_url, _ = flow.authorization_url(prompt="consent")

        _auth_code_holder: dict = {}

        class _Handler(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                params = urllib.parse.parse_qs(
                    urllib.parse.urlparse(self.path).query,
                )
                _auth_code_holder["code"] = params.get(
                    "code",
                    [None],
                )[0]
                body = (
                    b"<html><body><h2>Authorization complete."
                    b" You may close this tab.</h2></body></html>"
                )
                self.send_response(200)
                self.send_header("Content-Type", "text/html")
                self.send_header(
                    "Content-Length",
                    str(len(body)),
                )
                self.end_headers()
                self.wfile.write(body)

            def log_message(self, *args):
                pass

        server = http.server.HTTPServer(
            ("127.0.0.1", port),
            _Handler,
        )
        server.timeout = 300
        threading.Thread(
            target=server.handle_request,
            daemon=True,
        ).start()

        if not hasattr(host, "_pending_google_flows"):
            host._pending_google_flows = {}

        host._pending_google_flows[recipient_id] = {
            "flow": flow,
            "token_file": token_file,
            "service_name": service.title(),
            "server": server,
        }

        await host.wizard_send(
            recipient_id,
            f"\U0001f510 {fmt_bold(f'Authorize {service.title()}', m)}"
            f"\n\n"
            f"1. Open this link in a browser on the "
            f"{fmt_bold('same machine', m)} as Familiar:\n\n"
            f"{auth_url}\n\n"
            f"2. Sign in with your Google account and "
            f"approve permissions\n"
            f"3. You'll be redirected to localhost:{port} "
            f"automatically \u2705\n\n"
            f"{fmt_italic('Remote server? Copy the URL to any browser, approve, then paste the full redirect URL here:', m)}\n"
            f"   {fmt_code('/connect google code PASTE_REDIRECT_URL', m)}",
        )
    except ImportError:
        await host.wizard_send(
            recipient_id,
            "\u274c Google libraries not installed. Run /connect google install first.",
        )
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Auth error: {e}",
        )


async def wizard_google_handle_code(
    host: WizardHost,
    recipient_id: str,
    auth_code: str,
):
    m = host.format_mode
    if not hasattr(host, "_pending_google_flows") or recipient_id not in host._pending_google_flows:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f No pending Google authorization.\n"
            f"Start with: "
            f"{fmt_code('/connect google auth calendar', m)}",
        )
        return

    pending = host._pending_google_flows.pop(recipient_id)
    flow = pending["flow"]
    token_file = pending["token_file"]
    service_name = pending["service_name"]

    try:
        import urllib.parse

        raw = auth_code.strip()
        if raw.startswith("http"):
            params = urllib.parse.parse_qs(
                urllib.parse.urlparse(raw).query,
            )
            code_value = params.get("code", [raw])[0]
        else:
            code_value = raw

        flow.fetch_token(code=code_value)
        creds = flow.credentials
        token_file.write_text(creds.to_json())

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold(f'{service_name} authorized!', m)}\n\n"
            f"Token saved to:\n"
            f"  {fmt_code(str(token_file), m)}\n\n"
            f'Try it: "What\'s on my calendar today?"',
        )
    except Exception as e:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f Authorization failed: {e}\n\n"
            f"The code may have expired. Try again:\n"
            f"  {fmt_code(f'/connect google auth {service_name.lower()}', m)}",
        )


# ── Proton Bridge wizard ─────────────────────────────────────────────


async def wizard_proton_bridge(host: WizardHost, recipient_id: str):
    """Start automated Proton Mail Bridge setup."""
    import shutil

    m = host.format_mode

    # If Bridge is already listening and credentials are configured, test instead
    imap_server = os.environ.get("EMAIL_IMAP_SERVER", "")
    imap_port = int(os.environ.get("EMAIL_IMAP_PORT", "0") or "0")
    email_addr = os.environ.get("EMAIL_ADDRESS", "")
    email_pass = os.environ.get("EMAIL_PASSWORD", "")
    if (
        imap_server in ("127.0.0.1", "localhost")
        and imap_port > 0
        and email_addr
        and email_pass
        and _check_tcp_port(imap_server, imap_port)
    ):
        # Bridge is running and credentials exist — test the connection
        await host.wizard_send(
            recipient_id,
            f"\U0001f4e7 {fmt_bold('Proton Mail Bridge', m)}\n\n"
            f"\u2705 Bridge is running on {imap_server}:{imap_port}\n"
            f"\u2705 Credentials configured for {fmt_code(email_addr, m)}\n\n"
            f"Testing IMAP connection...",
        )
        env_vars = {
            "EMAIL_ADDRESS": email_addr,
            "EMAIL_PASSWORD": email_pass,
            "EMAIL_IMAP_SERVER": imap_server,
            "EMAIL_SMTP_SERVER": os.environ.get("EMAIL_SMTP_SERVER", imap_server),
            "EMAIL_IMAP_PORT": str(imap_port),
            "EMAIL_SMTP_PORT": os.environ.get("EMAIL_SMTP_PORT", "1025"),
        }
        success, result = _test_imap_connection(env_vars)
        if success:
            await host.wizard_send(
                recipient_id,
                f"\u2705 {fmt_bold('Proton Mail connected!', m)}\n\n"
                f"  {result}\n\n"
                f"Email sending and receiving is ready.",
            )
        else:
            await host.wizard_send(
                recipient_id,
                f"\u274c {fmt_bold('Connection test failed', m)}\n\n"
                f"  {result}\n\n"
                f"Bridge is listening but IMAP login failed.\n"
                f"Check your Bridge password in the Bridge GUI and update with:\n"
                f"  {fmt_code('/connect email set EMAIL_PASSWORD <bridge_password>', m)}",
            )
        return

    await host.wizard_send(
        recipient_id,
        f"\U0001f4e7 {fmt_bold('Proton Mail Bridge Setup', m)}\n\n"
        f"Checking for Proton Mail Bridge...",
    )

    bridge_bin = proton_find_bridge()

    if not bridge_bin:
        has_flatpak = shutil.which("flatpak")
        if not has_flatpak:
            await proton_fallback_manual(
                host,
                recipient_id,
                "Flatpak not available for automatic Bridge install.",
            )
            return

        await host.wizard_send(
            recipient_id,
            "\U0001f4e6 Bridge not found. Installing via Flatpak...",
        )
        success = await proton_install_bridge(host, recipient_id)
        if not success:
            await proton_fallback_manual(
                host,
                recipient_id,
                "Flatpak install failed.",
            )
            return

        bridge_bin = proton_find_bridge()
        if not bridge_bin:
            await proton_fallback_manual(
                host,
                recipient_id,
                "Bridge binary not found after install.",
            )
            return

    await host.wizard_send(
        recipient_id,
        f"\u2705 Bridge found: {fmt_code(bridge_bin, m)}",
    )

    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {
        "type": "proton",
        "step": "email",
        "bridge_bin": bridge_bin,
    }

    await host.wizard_send(
        recipient_id,
        f"\U0001f4e7 Enter your {fmt_bold('Proton email address', m)}:",
    )


def proton_find_bridge():
    import shutil
    import subprocess

    native = shutil.which("protonmail-bridge")
    if native:
        return native
    try:
        result = subprocess.run(
            ["flatpak", "info", "ch.protonmail.protonmail-bridge"],
            capture_output=True,
            timeout=10,
        )
        if result.returncode == 0:
            return "flatpak run ch.protonmail.protonmail-bridge"
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    return None


async def proton_install_bridge(
    host: WizardHost,
    recipient_id: str,
) -> bool:
    # Ensure flathub remote is configured for --user installs
    try:
        await asyncio.create_subprocess_exec(
            "flatpak",
            "remote-add",
            "--user",
            "--if-not-exists",
            "flathub",
            "https://flathub.org/repo/flathub.flatpakrepo",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    try:
        proc = await asyncio.create_subprocess_exec(
            "flatpak",
            "install",
            "--user",
            "-y",
            "flathub",
            "ch.protonmail.protonmail-bridge",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(
            proc.communicate(),
            timeout=300,
        )
        if proc.returncode == 0:
            await host.wizard_send(
                recipient_id,
                "\u2705 Proton Mail Bridge installed via Flatpak.",
            )
            return True
        else:
            logger.error(
                f"Flatpak install failed: {stderr.decode(errors='replace')[:500]}",
            )
            return False
    except asyncio.TimeoutError:
        await host.wizard_send(
            recipient_id,
            "\u26a0\ufe0f Bridge installation timed out.",
        )
        return False
    except Exception as e:
        logger.error(f"Bridge install error: {e}")
        return False


async def proton_handle_step(
    host: WizardHost,
    recipient_id: str,
    text: str,
    message_ref,
):
    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 == "email":
        if "@" not in text or "." not in text:
            await host.wizard_send(
                recipient_id,
                "That doesn't look like an email address. Please try again:",
            )
            return
        state["email"] = text
        state["step"] = "password"
        await host.wizard_send(
            recipient_id,
            f"\U0001f510 Enter your "
            f"{fmt_bold('Proton password', m)}:"
            + (
                "\n\n"
                + fmt_italic(
                    "Your message will be deleted immediately.",
                    m,
                )
                if host.supports_message_deletion
                else ""
            ),
        )

    elif step == "password":
        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["password"] = text
        state["step"] = "logging_in"
        await host.wizard_send(
            recipient_id,
            "\U0001f504 Logging into Proton Mail Bridge...",
        )
        await proton_do_login(
            host,
            recipient_id,
            state["email"],
            text,
        )

    elif step == "2fa":
        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["step"] = "completing_2fa"
        await host.wizard_send(
            recipient_id,
            "\U0001f504 Submitting 2FA code...",
        )
        await proton_do_2fa(host, recipient_id, text)


async def proton_do_login(
    host: WizardHost,
    recipient_id: str,
    email: str,
    password: str,
):
    host._ensure_wizard_state()
    m = host.format_mode
    state = host._wizard_state.get(recipient_id)
    if not state:
        return
    bridge_bin = state["bridge_bin"]

    try:
        if bridge_bin.startswith("flatpak run"):
            cmd = bridge_bin.split() + ["--cli"]
        else:
            cmd = [bridge_bin, "--cli"]

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        proc.stdin.write(b"login\n")
        await proc.stdin.drain()
        await asyncio.sleep(1)
        proc.stdin.write(email.encode() + b"\n")
        await proc.stdin.drain()
        await asyncio.sleep(1)
        proc.stdin.write(password.encode() + b"\n")
        await proc.stdin.drain()

        try:
            output = await asyncio.wait_for(
                proton_read_until_prompt(proc.stdout),
                timeout=30,
            )
        except asyncio.TimeoutError:
            output = ""

        output_lower = output.lower()

        if "two factor" in output_lower or "2fa" in output_lower or "totp" in output_lower:
            if not hasattr(host, "_proton_bridge_proc"):
                host._proton_bridge_proc = {}
            host._proton_bridge_proc[recipient_id] = proc
            state["step"] = "2fa"
            await host.wizard_send(
                recipient_id,
                f"\U0001f510 {fmt_bold('2FA required', m)}\n\n"
                f"Enter your 2FA code:"
                + (
                    "\n\n"
                    + fmt_italic(
                        "Your message will be deleted immediately.",
                        m,
                    )
                    if host.supports_message_deletion
                    else ""
                ),
            )
            return

        if "error" in output_lower or "invalid" in output_lower or "failed" in output_lower:
            proc.stdin.write(b"exit\n")
            await proc.stdin.drain()
            try:
                await asyncio.wait_for(proc.wait(), timeout=5)
            except asyncio.TimeoutError:
                proc.kill()
            host._wizard_state.pop(recipient_id, None)
            await proton_fallback_manual(
                host,
                recipient_id,
                f"Bridge login failed. CLI output:\n{fmt_code(output[:300], m)}",
            )
            return

        proc.stdin.write(b"exit\n")
        await proc.stdin.drain()
        try:
            await asyncio.wait_for(proc.wait(), timeout=5)
        except asyncio.TimeoutError:
            proc.kill()

        await proton_finish_setup(host, recipient_id)

    except Exception as e:
        logger.error(f"Proton bridge login error: {e}")
        host._wizard_state.pop(recipient_id, None)
        await proton_fallback_manual(
            host,
            recipient_id,
            f"Bridge CLI automation failed: {e}",
        )


async def proton_do_2fa(
    host: WizardHost,
    recipient_id: str,
    code: str,
):
    procs = getattr(host, "_proton_bridge_proc", {})
    proc = procs.get(recipient_id)
    if not proc or proc.returncode is not None:
        host._wizard_state.pop(recipient_id, None)
        procs.pop(recipient_id, None)
        await proton_fallback_manual(
            host,
            recipient_id,
            "Bridge process ended unexpectedly.",
        )
        return

    try:
        proc.stdin.write(code.encode() + b"\n")
        await proc.stdin.drain()

        try:
            output = await asyncio.wait_for(
                proton_read_until_prompt(proc.stdout),
                timeout=15,
            )
        except asyncio.TimeoutError:
            output = ""

        output_lower = output.lower()
        if "error" in output_lower or "invalid" in output_lower or "failed" in output_lower:
            proc.stdin.write(b"exit\n")
            await proc.stdin.drain()
            try:
                await asyncio.wait_for(proc.wait(), timeout=5)
            except asyncio.TimeoutError:
                proc.kill()
            host._wizard_state.pop(recipient_id, None)
            procs.pop(recipient_id, None)
            await proton_fallback_manual(
                host,
                recipient_id,
                "2FA verification failed.",
            )
            return

        proc.stdin.write(b"exit\n")
        await proc.stdin.drain()
        try:
            await asyncio.wait_for(proc.wait(), timeout=5)
        except asyncio.TimeoutError:
            proc.kill()

        procs.pop(recipient_id, None)
        await proton_finish_setup(host, recipient_id)

    except Exception as e:
        logger.error(f"Proton 2FA error: {e}")
        host._wizard_state.pop(recipient_id, None)
        procs.pop(recipient_id, None)
        await proton_fallback_manual(
            host,
            recipient_id,
            f"2FA automation failed: {e}",
        )


async def proton_finish_setup(host: WizardHost, recipient_id: str):
    host._ensure_wizard_state()
    m = host.format_mode
    state = host._wizard_state.get(recipient_id)
    if not state:
        return
    email = state["email"]
    bridge_bin = state["bridge_bin"]

    await host.wizard_send(
        recipient_id,
        "\U0001f511 Extracting Bridge credentials...",
    )
    bridge_password = await proton_extract_bridge_password(
        bridge_bin,
        email,
    )

    if not bridge_password:
        host._wizard_state.pop(recipient_id, None)
        await proton_fallback_manual(
            host,
            recipient_id,
            "Could not extract Bridge password automatically.\n"
            "Open Bridge \u2192 click your account"
            " \u2192 copy the Bridge password.",
        )
        return

    await host.wizard_send(
        recipient_id,
        "\U0001f680 Starting Bridge in background...",
    )
    await proton_start_background(bridge_bin)
    await asyncio.sleep(3)

    preset = EMAIL_PRESETS["proton"]
    env_vars = {
        "EMAIL_ADDRESS": email,
        "EMAIL_PASSWORD": bridge_password,
        "EMAIL_IMAP_SERVER": preset["imap_server"],
        "EMAIL_SMTP_SERVER": preset["smtp_server"],
        "EMAIL_IMAP_PORT": str(preset["imap_port"]),
        "EMAIL_SMTP_PORT": str(preset["smtp_port"]),
    }
    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:
        logger.error(f"Failed to save .env: {e}")

    # Save to multi-account registry
    try:
        from familiar.skills.email.accounts import (
            _load_accounts_file,
            _unique_id,
            add_account,
        )

        data = _load_accounts_file()
        acct_id = _unique_id("proton", data["accounts"])
        account = {
            "id": acct_id,
            "address": email,
            "password": bridge_password,
            "imap_server": preset["imap_server"],
            "smtp_server": preset["smtp_server"],
            "imap_port": preset["imap_port"],
            "smtp_port": preset["smtp_port"],
            "provider": "proton",
        }
        add_account(account)
    except Exception as e:
        logger.warning(f"Failed to save to account registry: {e}")

    await host.wizard_send(
        recipient_id,
        "\U0001f50d Testing IMAP connection...",
    )
    success, result = _test_imap_connection(env_vars)
    host._wizard_state.pop(recipient_id, None)

    if success:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Proton Mail connected!', m)}\n\n"
            f"  Address: {fmt_code(email, m)}\n"
            f"  IMAP: {fmt_code('127.0.0.1:1143', m)} \u2705\n"
            f"  {result}\n\n"
            f"Bridge is running in the background.\n\n"
            f'Try: "Check my email" or '
            f'"Search for messages from..."\n\n'
            f"{fmt_bold('Add another account?', m)}\n"
            f"  {fmt_code('/connect email', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u26a0\ufe0f "
            f"{fmt_bold('Credentials saved but connection failed', m)}"
            f"\n\n"
            f"  Error: {result}\n\n"
            f"Make sure Bridge is running, then try:\n"
            f"  {fmt_code('/connect email test', m)}",
        )


async def proton_extract_bridge_password(
    bridge_bin: str,
    email: str,
):
    import re

    try:
        if bridge_bin.startswith("flatpak run"):
            cmd = bridge_bin.split() + ["--cli"]
        else:
            cmd = [bridge_bin, "--cli"]

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        proc.stdin.write(b"info\n")
        await proc.stdin.drain()
        await asyncio.sleep(2)
        proc.stdin.write(b"exit\n")
        await proc.stdin.drain()

        try:
            stdout, _ = await asyncio.wait_for(
                proc.communicate(),
                timeout=15,
            )
        except asyncio.TimeoutError:
            proc.kill()
            return None

        output = stdout.decode(errors="replace")
        match = re.search(r"[Pp]assword:\s*(\S+)", output)
        if match:
            pwd = match.group(1)
            if len(pwd) >= 8 and pwd.lower() not in ("password:", "password"):
                return pwd
        return None
    except Exception as e:
        logger.error(f"Bridge password extraction error: {e}")
        return None


async def proton_start_background(bridge_bin: str):
    import socket

    try:
        if bridge_bin.startswith("flatpak run"):
            cmd = bridge_bin.split() + ["--noninteractive"]
        else:
            cmd = [bridge_bin, "--noninteractive"]

        try:
            with socket.create_connection(
                ("127.0.0.1", 1143),
                timeout=2,
            ):
                logger.info(
                    "Bridge already running (port 1143 open)",
                )
                return
        except (OSError, ConnectionRefusedError, TimeoutError) as e:
            logger.debug("I/O error suppressed: %s", e)
        await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
            start_new_session=True,
        )
        logger.info("Started Proton Mail Bridge in background")
    except Exception as e:
        logger.warning(f"Could not start Bridge background: {e}")


async def proton_read_until_prompt(stream, max_bytes=8192):
    chunks = []
    while True:
        try:
            chunk = await asyncio.wait_for(
                stream.read(max_bytes),
                timeout=3,
            )
            if not chunk:
                break
            chunks.append(chunk)
        except asyncio.TimeoutError:
            break
    return b"".join(chunks).decode(errors="replace")


async def proton_fallback_manual(
    host: WizardHost,
    recipient_id: str,
    reason: str,
):
    m = host.format_mode
    host._ensure_wizard_state()
    host._wizard_state.pop(recipient_id, None)
    await host.wizard_send(
        recipient_id,
        f"\u26a0\ufe0f "
        f"{fmt_bold('Automated setup could not complete', m)}\n\n"
        f"{reason}\n\n"
        f"{fmt_bold('Manual setup:', m)}\n"
        f"{_password_help('proton', m)}\n"
        f"Then send:\n"
        f"  {fmt_code('/connect email proton your@email.com BRIDGE_PASSWORD', m)}",
    )


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


async def handle_menu_selection(
    host: WizardHost,
    recipient_id: str,
    key: str,
) -> bool:
    """Handle email/calendar/google-related menu callbacks.

    Returns ``True`` if *key* was recognised and handled, ``False``
    otherwise.
    """
    # Email sub-menu
    if key == "email_menu":
        await wizard_email(host, recipient_id, [])
        return True
    if key == "email_custom":
        await wizard_email_custom(host, recipient_id)
        return True
    if key == "email_proton":
        await wizard_proton_bridge(host, recipient_id)
        return True
    if key.startswith("email_") and key.replace("email_", "") in EMAIL_PRESETS:
        provider = key.replace("email_", "")
        await start_email_wizard(host, recipient_id, provider)
        return True

    # Calendar sub-menu
    if key == "calendar_menu":
        await wizard_calendar(host, recipient_id, [])
        return True
    if key == "calendar_google":
        await start_google_calendar_wizard(host, recipient_id)
        return True
    if key == "calendar_caldav":
        await start_caldav_wizard(host, recipient_id)
        return True
    if key == "calendar_test":
        await wizard_calendar_test(host, recipient_id)
        return True

    # Google sub-menu
    if key == "google_menu":
        await wizard_google(host, recipient_id, [])
        return True
    if key == "google_install":
        await wizard_google_install_libs(host, recipient_id)
        return True
    if key == "google_check":
        await wizard_google_check_status(host, recipient_id)
        return True
    if key.startswith("google_auth_"):
        svc = key.replace("google_auth_", "")
        await wizard_google_auth(host, recipient_id, svc)
        return True

    return False
