# 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

"""Security wizards — Vaultwarden, Encryption, PHI Detection, RBAC, Users."""

from __future__ import annotations

import json
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__)


# ── Vaultwarden ─────────────────────────────────────────────────────


def _vaultwarden_is_provisioned() -> bool:
    """Check if Vaultwarden is running via Familiar's service manager."""
    try:
        from familiar.services.manager import get_service_manager

        mgr = get_service_manager()
        status = mgr.get_status("vaultwarden")
        return status and status.get("running", False)
    except Exception:
        return False


def _vaultwarden_provision_url() -> str:
    """Return the local URL if Vaultwarden is provisioned."""
    try:
        from familiar.services.specs import SERVICE_SPECS

        spec = SERVICE_SPECS.get("vaultwarden")
        if spec:
            host_port = list(spec.ports.keys())[0]
            return f"http://localhost:{host_port}"
    except Exception:
        pass
    return "http://localhost:8060"


def _vaultwarden_get_admin_token() -> str:
    """Read the generated admin token from the service secrets store."""
    try:
        secrets_file = Path.home() / ".familiar" / "services" / "secrets.json"
        if secrets_file.exists():
            import json as _json

            data = _json.loads(secrets_file.read_text(encoding="utf-8"))
            vw = data.get("vaultwarden", {})
            return vw.get("ADMIN_TOKEN", "")
    except Exception:
        pass
    return ""


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

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

    # /connect vaultwarden <URL> <TOKEN>
    if sub.startswith("http") and len(args) >= 2:
        url = args[0]
        token = args[1]
        await wizard_vaultwarden_save(host, recipient_id, url, token)
        return

    has_url = os.environ.get("VAULTWARDEN_URL", "")
    has_token = bool(os.environ.get("VAULTWARDEN_TOKEN"))

    if has_url and has_token:
        await host.wizard_send(
            recipient_id,
            f"\U0001f511 {fmt_bold('Vaultwarden Configuration', m)}\n\n"
            f"  \u2705 URL: {fmt_code(has_url, m)}\n"
            f"  \u2705 Token: \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\n\n"
            f"{fmt_bold('Commands:', m)}\n"
            f"  {fmt_code('/connect vaultwarden test', m)}"
            f" \u2014 test connection\n",
        )
        return

    # ── Novice-friendly: detect if Vaultwarden can be auto-provisioned ──
    is_provisioned = _vaultwarden_is_provisioned()

    if host.supports_buttons:
        await start_vaultwarden_wizard(host, recipient_id, is_provisioned)
    else:
        if is_provisioned:
            # Already running — auto-fill the URL and just need the token
            local_url = _vaultwarden_provision_url()
            admin_token = _vaultwarden_get_admin_token()
            if admin_token:
                # Fully auto-configure: provisioned + admin token known
                await wizard_vaultwarden_save(host, recipient_id, local_url, admin_token)
                return
            await host.wizard_send(
                recipient_id,
                f"\U0001f511 {fmt_bold('Connect Vaultwarden', m)}\n\n"
                f"Vaultwarden is already running at "
                f"{fmt_code(local_url, m)}.\n\n"
                f"Enter your {fmt_bold('admin API token', m)}:\n"
                f"Find it at: {fmt_code(local_url + '/admin', m)}"
                f" \u2192 General Settings\n\n"
                f"{fmt_bold('One-shot:', m)} "
                f"{fmt_code(f'/connect vaultwarden {local_url} <TOKEN>', m)}",
            )
        else:
            blurb = _service_doc_blurb("vaultwarden", m)
            await host.wizard_send(
                recipient_id,
                f"\U0001f511 {fmt_bold('Connect Vaultwarden', m)}\n"
                f"{blurb}\n"
                f"Vaultwarden is a self-hosted password manager "
                f"(Bitwarden-compatible).\n\n"
                f"{fmt_bold('Option 1 — Auto-install (recommended):', m)}\n"
                f"  Go to the Server tab in the dashboard and click "
                f'"Provision" on Vaultwarden. It will set up '
                f"automatically and connect itself.\n\n"
                f"{fmt_bold('Option 2 — Existing instance:', m)}\n"
                f"  {fmt_code('/connect vaultwarden <URL> <TOKEN>', m)}\n\n"
                f"{fmt_italic('Credentials saved to ~/.familiar/.env (chmod 600)', m)}",
            )


async def start_vaultwarden_wizard(
    host: WizardHost, recipient_id: str, is_provisioned: bool = False
) -> None:
    host._ensure_wizard_state()
    m = host.format_mode

    # If provisioned and we have the admin token, just auto-configure
    if is_provisioned:
        local_url = _vaultwarden_provision_url()
        admin_token = _vaultwarden_get_admin_token()
        if admin_token:
            await wizard_vaultwarden_save(host, recipient_id, local_url, admin_token)
            return
        # Provisioned but no stored token — just need the token
        host._wizard_state[recipient_id] = {
            "type": "vaultwarden",
            "step": "token",
            "url": local_url,
        }
        await host.wizard_send(
            recipient_id,
            f"\U0001f511 {fmt_bold('Vaultwarden Setup', m)}\n\n"
            f"Vaultwarden is running at {fmt_code(local_url, m)}.\n\n"
            f"Enter your {fmt_bold('admin API token', m)}:\n"
            f"Find it at: {fmt_code(local_url + '/admin', m)}"
            f" \u2192 General Settings",
        )
        return

    # Not provisioned — offer to auto-install or enter existing URL
    host._wizard_state[recipient_id] = {
        "type": "vaultwarden",
        "step": "choose",
    }
    await host.wizard_send(
        recipient_id,
        f"\U0001f511 {fmt_bold('Vaultwarden Setup', m)}\n\n"
        f"Vaultwarden is a self-hosted password manager "
        f"(Bitwarden-compatible).\n\n"
        f"{fmt_bold('Choose an option:', m)}\n"
        f"  {fmt_bold('1', m)} \u2014 Auto-install (sets up Vaultwarden for you)\n"
        f"  {fmt_bold('2', m)} \u2014 Connect an existing Vaultwarden instance\n\n"
        f"Type {fmt_code('1', m)} or {fmt_code('2', m)}:",
    )


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

    if step == "choose":
        if text == "1":
            # Auto-provision Vaultwarden — state cleared on success inside
            await _vaultwarden_auto_provision(host, recipient_id)
        elif text == "2":
            # Manual: ask for URL
            state["step"] = "url"
            await host.wizard_send(
                recipient_id,
                f"Enter your {fmt_bold('Vaultwarden instance URL', m)}:\n\n"
                f"{fmt_italic('Example: https://vault.example.org', m)}",
            )
        else:
            await host.wizard_send(
                recipient_id,
                f"Please type {fmt_code('1', m)} for auto-install "
                f"or {fmt_code('2', m)} for existing instance:",
            )

    elif 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.rstrip("/")
        state["step"] = "token"

        warning = ""
        if not host.supports_message_deletion:
            pw_warning = (
                "This channel cannot delete messages. "
                "Your token will remain visible in chat history. "
                "Consider using one-shot: "
                "/connect vaultwarden <URL> <TOKEN>"
            )
            warning = f"\n\n\u26a0\ufe0f {fmt_italic(pw_warning, m)}"

        await host.wizard_send(
            recipient_id,
            f"\U0001f510 Enter your "
            f"{fmt_bold('Vaultwarden API token', m)}:\n\n"
            f"Find it at: Admin panel \u2192 General Settings"
            f"{warning}"
            + (
                "\n\n"
                + fmt_italic(
                    "Your message will be deleted immediately for safety.",
                    m,
                )
                if host.supports_message_deletion
                else ""
            ),
        )

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


async def _vaultwarden_auto_provision(host: WizardHost, recipient_id: str) -> None:
    """Provision Vaultwarden via the service manager and auto-configure."""
    import asyncio

    m = host.format_mode
    await host.wizard_send(
        recipient_id,
        f"\U0001f504 {fmt_bold('Installing Vaultwarden...', m)}\n"
        f"This may take a minute while the container image downloads.",
    )
    try:
        from familiar.services.manager import get_service_manager

        mgr = get_service_manager()
        result = mgr.provision("vaultwarden")
        if not result.get("success", False):
            error = result.get("error", "Unknown error")
            # Reset to choose step so user can try again or pick option 2
            host._ensure_wizard_state()
            host._wizard_state[recipient_id] = {"type": "vaultwarden", "step": "choose"}
            await host.wizard_send(
                recipient_id,
                f"\u274c {fmt_bold('Provisioning failed', m)}\n\n"
                f"  {error}\n\n"
                f"Make sure Podman or Docker is installed, then try again.\n"
                f"Type {fmt_code('1', m)} to retry or {fmt_code('2', m)} "
                f"to connect an existing instance.",
            )
            return

        # Start the service
        start_result = mgr.start("vaultwarden")
        if not start_result.get("success", False):
            host._ensure_wizard_state()
            host._wizard_state[recipient_id] = {"type": "vaultwarden", "step": "choose"}
            await host.wizard_send(
                recipient_id,
                f"\u274c {fmt_bold('Container created but failed to start', m)}\n\n"
                f"  {start_result.get('error', 'Unknown error')}\n\n"
                f"Type {fmt_code('1', m)} to retry or {fmt_code('2', m)} "
                f"to connect an existing instance.",
            )
            return

        # Wait briefly for it to come up (async-safe)
        for _ in range(10):
            await asyncio.sleep(1)
            status = mgr.get_status("vaultwarden")
            if status and status.get("running"):
                break

        local_url = _vaultwarden_provision_url()
        admin_token = _vaultwarden_get_admin_token()

        if admin_token:
            # Auto-configure credentials — clear wizard state (success)
            host._wizard_state.pop(recipient_id, None)
            await wizard_vaultwarden_save(host, recipient_id, local_url, admin_token)
            await host.wizard_send(
                recipient_id,
                f"\n\U0001f389 {fmt_bold('Vaultwarden is ready!', m)}\n\n"
                f"  Web UI: {fmt_code(local_url, m)}\n"
                f"  Admin panel: {fmt_code(local_url + '/admin', m)}\n\n"
                f"{fmt_bold('Next steps:', m)}\n"
                f"1. Open {fmt_code(local_url, m)} in your browser\n"
                f"2. Create your first user account\n"
                f"3. Install the Bitwarden browser extension\n"
                f"4. Set the server URL to {fmt_code(local_url, m)}",
            )
        else:
            # Set wizard state so user can enter the token
            host._ensure_wizard_state()
            host._wizard_state[recipient_id] = {
                "type": "vaultwarden",
                "step": "token",
                "url": local_url,
            }
            await host.wizard_send(
                recipient_id,
                f"\u2705 {fmt_bold('Vaultwarden installed!', m)}\n\n"
                f"  Running at: {fmt_code(local_url, m)}\n\n"
                f"Enter the {fmt_bold('admin token', m)} from:\n"
                f"  {fmt_code(local_url + '/admin', m)}",
            )

    except ImportError:
        await host.wizard_send(
            recipient_id,
            f"\u274c Auto-install requires the server provisioning module.\n"
            f"Use {fmt_code('/connect vaultwarden <URL> <TOKEN>', m)} "
            f"to connect an existing instance instead.",
        )
    except Exception as e:
        logger.exception("Vaultwarden auto-provision failed")
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Installation failed', m)}: {e}\n\n"
            f"Try provisioning from the Server tab in the dashboard, "
            f"or connect an existing instance with:\n"
            f"  {fmt_code('/connect vaultwarden <URL> <TOKEN>', m)}",
        )


async def wizard_vaultwarden_save(
    host: WizardHost, recipient_id: str, url: str, token: str
) -> None:
    url = url.rstrip("/")
    env_vars = {"VAULTWARDEN_URL": url, "VAULTWARDEN_TOKEN": token}
    for k, v in env_vars.items():
        os.environ[k] = v
    env_path = Path.home() / ".familiar" / ".env"
    try:
        _update_env_file(env_path, env_vars, service_key="vaultwarden")
    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 Vaultwarden connection...")
    await wizard_vaultwarden_test(host, recipient_id)


async def wizard_vaultwarden_test(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    url = os.environ.get("VAULTWARDEN_URL", "")
    token = os.environ.get("VAULTWARDEN_TOKEN", "")
    if not url or not token:
        await host.wizard_send(
            recipient_id,
            f"\u274c No Vaultwarden credentials configured.\n"
            f"Run: {fmt_code('/connect vaultwarden', m)}",
        )
        return
    ok, msg = _test_http_service(
        f"{url}/api/folders",
        headers={"Authorization": f"Bearer {token}"},
    )
    if ok:
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Vaultwarden connected!', m)}\n\n"
            f"  URL: {fmt_code(url, m)}\n"
            f"  Status: {msg}\n\n"
            f"Saved to: {fmt_code('~/.familiar/.env', m)}",
        )
    else:
        await host.wizard_send(
            recipient_id,
            f"\u274c {fmt_bold('Vaultwarden connection failed', m)}\n\n"
            f"  {msg}\n\n"
            f"Check your URL and API token, then try again.",
        )


# ── Encryption ──────────────────────────────────────────────────────


async def wizard_encryption(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    ck = "\u2705"
    em = "\u2b1c"
    data_dir = Path.home() / ".familiar" / "data"
    key_dir = Path.home() / ".familiar" / "keys"

    # Check cryptography library
    has_crypto = False
    try:
        import cryptography  # noqa: F401

        has_crypto = True
    except ImportError as e:
        logger.debug("Optional dependency not available: %s", e)
    # Key status
    key_files = list(key_dir.glob("*.key")) if key_dir.exists() else []
    key_count = len(key_files)

    # Encrypted stores
    encrypted_count = 0
    total_stores = 0
    if data_dir.exists():
        for f in data_dir.glob("*.json"):
            total_stores += 1
            enc_path = f.with_suffix(".json.enc")
            if enc_path.exists():
                encrypted_count += 1

    _icon = ck if has_crypto else em
    _status = "installed" if has_crypto else "not installed"
    _icon2 = ck if key_count > 0 else em
    _icon3 = ck if encrypted_count > 0 else em
    lines = [
        f"\U0001f510 {fmt_bold('Encryption (At Rest)', m)}\n",
        f"  {_icon} cryptography library: {_status}",
        f"  {_icon2} Encryption keys: {key_count} key(s) in ~/.familiar/keys/",
        f"  {_icon3} Encrypted stores: {encrypted_count}/{total_stores} JSON stores\n",
    ]

    if key_count > 0:
        _cmd = '"encryption status"'
        _cmd2 = '"encrypt all data stores"'
        _cmd3 = '"rotate encryption keys"'
        lines.append(f"{fmt_bold('Quick actions:', m)}")
        lines.append(f"  {fmt_italic(_cmd, m)} \u2014 detailed key info")
        lines.append(f"  {fmt_italic(_cmd2, m)} \u2014 encrypt unprotected files")
        lines.append(f"  {fmt_italic(_cmd3, m)} \u2014 generate new key + re-encrypt")
    else:
        lines.append(f"{fmt_bold('Getting started:', m)}")
        if not has_crypto:
            # cryptography is a core dependency — auto-install if
            # somehow missing
            from familiar.core.deps import ensure_packages

            ok, _ = ensure_packages([("cryptography", "cryptography")])
            has_crypto = ok
        if has_crypto:
            _cmd = '"generate encryption key"'
            lines.append(f"  Ask me to {fmt_italic(_cmd, m)}")
        else:
            lines.append(
                "  \u26a0\ufe0f cryptography library could not be installed — check server logs"
            )
        lines.append(f"  Keys are stored in {fmt_code('~/.familiar/keys/', m)} (chmod 600)")

    if host.supports_buttons:
        if key_count > 0:
            await host.wizard_send_menu(
                recipient_id,
                "\n".join(lines),
                [
                    (
                        "encryption_encrypt_all",
                        "\U0001f510 Encrypt all stores",
                    ),
                    (
                        "encryption_rotate",
                        "\U0001f504 Rotate keys",
                    ),
                ],
            )
        else:
            await host.wizard_send_menu(
                recipient_id,
                "\n".join(lines),
                [
                    (
                        "encryption_gen_key",
                        "\U0001f511 Generate encryption key",
                    ),
                ],
            )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


# ── PHI Detection ───────────────────────────────────────────────────


async def wizard_phi_detection(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"

    # Load PHI config
    safe_mode = False
    sensitivity = "standard"
    auto_redact = False
    log_detections = False
    whitelist_count = 0
    try:
        phi_path = data_dir / "phi_config.json"
        if phi_path.exists():
            phi = json.loads(phi_path.read_text())
            safe_mode = phi.get("safe_mode", False)
            sensitivity = phi.get("sensitivity", "standard")
            auto_redact = phi.get("auto_redact", False)
            log_detections = phi.get("log_detections", False)
            whitelist_count = len(phi.get("whitelist", []))
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    ck = "\u2705"
    em = "\u2b1c"

    _icon = ck if safe_mode else em
    _status = "enabled" if safe_mode else "disabled"
    _icon2 = ck if auto_redact else em
    _status2 = "on" if auto_redact else "off"
    _icon3 = ck if log_detections else em
    _status3 = "on" if log_detections else "off"
    _cmd = '"phi detection status"'
    _cmd2 = '"enable safe mode"'
    _cmd3 = '"set phi sensitivity to strict"'
    _cmd4 = '"scan text for PHI"'
    _cmd5 = '"phi classification report"'
    lines = [
        f"\U0001f3e5 {fmt_bold('PHI Detection', m)}\n",
        f"{fmt_bold('Current config:', m)}",
        f"  {_icon} Safe mode: {_status}",
        f"  Sensitivity: {fmt_bold(sensitivity, m)}",
        f"  {_icon2} Auto-redact: {_status2}",
        f"  {_icon3} Log detections: {_status3}",
        f"  Whitelist entries: {whitelist_count}\n",
        f"{fmt_bold('Sensitivity levels:', m)}",
        f"  {fmt_bold('minimal', m)} \u2014 SSNs, medical record numbers only",
        f"  {fmt_bold('standard', m)} \u2014 + names, dates, addresses, phone numbers",
        f"  {fmt_bold('strict', m)} \u2014 + any potential identifier, aggressive matching\n",
        f"{fmt_bold('Quick actions:', m)}",
        f"  {fmt_italic(_cmd, m)} \u2014 current settings",
        f"  {fmt_italic(_cmd2, m)} \u2014 block PHI in responses",
        f"  {fmt_italic(_cmd3, m)} \u2014 change level",
        f"  {fmt_italic(_cmd4, m)} \u2014 test detection",
        f"  {fmt_italic(_cmd5, m)} \u2014 detection summary",
    ]

    if host.supports_buttons:
        options = []
        if not safe_mode:
            options.append(("phi_enable_safe", "\U0001f6e1\ufe0f Enable safe mode"))
        else:
            options.append(("phi_disable", "\u274c Disable safe mode"))
        if sensitivity != "strict":
            options.append(("phi_set_strict", "\u26a0\ufe0f Set strict sensitivity"))
        if sensitivity != "standard" and safe_mode:
            options.append(("phi_set_standard", "\U0001f4ca Set standard sensitivity"))
        await host.wizard_send_menu(recipient_id, "\n".join(lines), options)
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


# ── RBAC ────────────────────────────────────────────────────────────


async def wizard_rbac(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"

    # Built-in roles
    builtin_roles = [
        ("admin", "Full access to all skills and tools"),
        ("operator", "Manage services, users, and configuration"),
        ("analyst", "Read-only access to data and reports"),
        (
            "healthcare",
            "HIPAA-scoped: PHI access + audit logging",
        ),
        ("developer", "Code, browser, and integration tools"),
        ("guest", "Minimal access: chat and basic queries"),
    ]

    # Load custom roles and assignments
    custom_roles: list[dict] = []
    assignments: dict = {}
    try:
        rbac_path = data_dir / "rbac.json"
        if rbac_path.exists():
            rbac = json.loads(rbac_path.read_text())
            custom_roles = rbac.get("custom_roles", [])
            assignments = rbac.get("assignments", {})
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    lines = [
        f"\U0001f6e1\ufe0f {fmt_bold('Role-Based Access Control', m)}\n",
        f"{fmt_bold('Built-in roles:', m)}",
    ]
    for role, desc in builtin_roles:
        lines.append(f"  {fmt_bold(role, m)} \u2014 {desc}")

    if custom_roles:
        lines.append(f"\n{fmt_bold('Custom roles:', m)} ({len(custom_roles)})")
        for cr in custom_roles[:5]:
            name = cr.get("name", "?")
            perms = len(cr.get("permissions", []))
            lines.append(f"  {fmt_bold(name, m)} \u2014 {perms} permission(s)")
        if len(custom_roles) > 5:
            lines.append(f"  ... and {len(custom_roles) - 5} more")

    assign_count = len(assignments)
    lines.append(f"\n{fmt_bold('Assignments:', m)} {assign_count} user(s) have role mappings")
    if assignments:
        for user_id, role in list(assignments.items())[:5]:
            lines.append(f"  {user_id} \u2192 {role}")
        if assign_count > 5:
            lines.append(f"  ... and {assign_count - 5} more")

    lines.append(
        f"\n{fmt_bold('Permission format:', m)}"
        f" {fmt_code('skill:tool', m)}"
        f" (e.g. {fmt_code('email:send_email', m)})"
    )

    _cmd = '"list roles"'
    _cmd2 = '"create role analyst-plus with email:read,calendar:read"'
    _cmd3 = '"assign role developer to user123"'
    _cmd4 = '"check permission email:send for user123"'
    lines.append(f"\n{fmt_bold('Quick actions:', m)}")
    lines.append(f"  {fmt_italic(_cmd, m)} \u2014 show all roles and permissions")
    lines.append(f"  {fmt_italic(_cmd2, m)}")
    lines.append(f"  {fmt_italic(_cmd3, m)}")
    lines.append(f"  {fmt_italic(_cmd4, m)}")

    if host.supports_buttons:
        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            [
                ("rbac_list", "\U0001f4cb List all roles"),
                ("rbac_create", "\u2795 Create custom role"),
                (
                    "rbac_assign",
                    "\U0001f464 Assign role to user",
                ),
            ],
        )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


# ── User Management ─────────────────────────────────────────────────


async def wizard_user_management(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"
    ck = "\u2705"
    cr = "\u274c"

    # Load users
    users: list[dict] = []
    pre_auths: list = []
    try:
        users_path = data_dir / "users.json"
        if users_path.exists():
            data = json.loads(users_path.read_text())
            users = data.get("users", [])
            pre_auths = data.get("pre_authorizations", [])
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    active_count = sum(1 for u in users if u.get("active", True))
    deactivated_count = len(users) - active_count

    lines = [
        f"\U0001f465 {fmt_bold('User Management', m)}\n",
        f"  Users: {len(users)} total ({active_count} active",
    ]
    if deactivated_count:
        lines[-1] += f", {deactivated_count} deactivated)"
    else:
        lines[-1] += ")"
    lines.append(f"  Pre-authorizations: {len(pre_auths)}")

    if users:
        lines.append(f"\n{fmt_bold('Users:', m)}")
        for u in users[:8]:
            status = ck if u.get("active", True) else cr
            name = u.get("name", u.get("username", "?"))
            email = u.get("email", "")
            role = u.get("role", "guest")
            line = f"  {status} {name}"
            if email:
                line += f" ({email})"
            line += f" \u2014 {role}"
            lines.append(line)
        if len(users) > 8:
            lines.append(f"  ... and {len(users) - 8} more")

    lines.append(f"\n{fmt_bold('Auth:', m)} PBKDF2-SHA256 password hashing")

    _cmd = '"list users"'
    _cmd2 = '"create user alice alice@example.com"'
    _cmd3 = '"deactivate user bob"'
    _cmd4 = '"list pre-authorizations"'
    _cmd5 = '"grant preauth for alice@example.com"'
    lines.append(f"\n{fmt_bold('Quick actions:', m)}")
    lines.append(f"  {fmt_italic(_cmd, m)} \u2014 show all accounts")
    lines.append(f"  {fmt_italic(_cmd2, m)}")
    lines.append(f"  {fmt_italic(_cmd3, m)}")
    lines.append(f"  {fmt_italic(_cmd4, m)}")
    lines.append(f"  {fmt_italic(_cmd5, m)}")

    if not users:
        _cmd6 = '"create a user account"'
        lines.append(f"\n{fmt_bold('Getting started:', m)}")
        lines.append(f"  Ask me to {fmt_italic(_cmd6, m)} to begin.")
        lines.append(f"  User data stored in {fmt_code('~/.familiar/data/users.json', m)}")

    if host.supports_buttons:
        await host.wizard_send_menu(
            recipient_id,
            "\n".join(lines),
            [
                ("users_list", "\U0001f4cb List users"),
                ("users_create", "\u2795 Create new user"),
            ],
        )
    else:
        await host.wizard_send(recipient_id, "\n".join(lines))


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


# ── Encryption one-button actions ─────────────────────────────────────


async def _encryption_generate_key(host: WizardHost, recipient_id: str) -> None:
    """Generate encryption key and enable encryption — one button."""
    m = host.format_mode
    key_dir = Path.home() / ".familiar" / "keys"
    key_dir.mkdir(parents=True, exist_ok=True)

    try:
        from familiar.core.encryption import generate_key
    except ImportError:
        # Try installing cryptography first
        try:
            from familiar.core.deps import ensure_packages

            ok, _ = ensure_packages([("cryptography", "cryptography")])
            if not ok:
                raise ImportError("Could not install cryptography")
            from familiar.core.encryption import generate_key
        except Exception:
            await host.wizard_send(
                recipient_id,
                "\u274c cryptography library could not be loaded.\n"
                f"Install manually: {fmt_code('pip install cryptography', m)}",
            )
            return

    try:
        new_key = generate_key()
        key_path = key_dir / "encryption.key"
        key_path.write_text(new_key)
        key_path.chmod(0o600)

        # Enable encryption in .env
        env_path = Path.home() / ".familiar" / ".env"
        os.environ["ENCRYPTION_ENABLED"] = "true"
        os.environ["FAMILIAR_ENCRYPTION_KEY"] = new_key
        _update_env_file(
            env_path,
            {"ENCRYPTION_ENABLED": "true", "FAMILIAR_ENCRYPTION_KEY": new_key},
            service_key="encryption",
        )

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Encryption key generated!', m)}\n\n"
            f"  Key saved to: {fmt_code('~/.familiar/keys/encryption.key', m)} (chmod 600)\n"
            f"  ENCRYPTION_ENABLED=true saved to {fmt_code('~/.familiar/.env', m)}\n\n"
            f"Your data stores will be encrypted on next write.\n"
            f"To encrypt existing stores now, use the {fmt_bold('Encrypt all stores', m)} button.",
        )
        # Re-show the encryption wizard with updated status
        await wizard_encryption(host, recipient_id)
    except Exception as e:
        await host.wizard_send(recipient_id, f"\u274c Key generation failed: {e}")


async def _encryption_encrypt_all(host: WizardHost, recipient_id: str) -> None:
    """Encrypt all unencrypted JSON stores."""
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"

    try:
        from familiar.core.encryption import EncryptedStorage
    except ImportError:
        await host.wizard_send(recipient_id, "\u274c cryptography library not available.")
        return

    key = os.environ.get("FAMILIAR_ENCRYPTION_KEY", "")
    if not key:
        key_path = Path.home() / ".familiar" / "keys" / "encryption.key"
        if key_path.exists():
            key = key_path.read_text().strip()
    if not key:
        await host.wizard_send(
            recipient_id,
            "\u274c No encryption key found. Generate one first.",
        )
        return

    try:
        storage = EncryptedStorage(key)
        encrypted = 0
        skipped = 0
        for json_file in data_dir.glob("*.json"):
            enc_path = json_file.with_suffix(".json.enc")
            if enc_path.exists():
                skipped += 1
                continue
            try:
                data = json_file.read_text()
                storage.encrypt_file(json_file)
                encrypted += 1
            except Exception:
                skipped += 1

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Encryption complete', m)}\n\n"
            f"  Encrypted: {encrypted} stores\n"
            f"  Already encrypted: {skipped} stores",
        )
    except Exception as e:
        await host.wizard_send(recipient_id, f"\u274c Encryption failed: {e}")


async def _encryption_rotate_keys(host: WizardHost, recipient_id: str) -> None:
    """Generate new key and re-encrypt all stores."""
    m = host.format_mode
    try:
        from familiar.core.encryption import EncryptedStorage, generate_key, rotate_key
    except ImportError:
        await host.wizard_send(recipient_id, "\u274c cryptography library not available.")
        return

    old_key = os.environ.get("FAMILIAR_ENCRYPTION_KEY", "")
    if not old_key:
        key_path = Path.home() / ".familiar" / "keys" / "encryption.key"
        if key_path.exists():
            old_key = key_path.read_text().strip()
    if not old_key:
        await host.wizard_send(recipient_id, "\u274c No existing key to rotate from.")
        return

    try:
        new_key = generate_key()
        old_storage = EncryptedStorage(old_key)
        new_storage = EncryptedStorage(new_key)
        data_dir = Path.home() / ".familiar" / "data"
        results = rotate_key(old_storage, new_storage, data_dir)

        # Save new key
        key_path = Path.home() / ".familiar" / "keys" / "encryption.key"
        key_path.write_text(new_key)
        key_path.chmod(0o600)
        os.environ["FAMILIAR_ENCRYPTION_KEY"] = new_key
        env_path = Path.home() / ".familiar" / ".env"
        _update_env_file(env_path, {"FAMILIAR_ENCRYPTION_KEY": new_key}, service_key="encryption")

        s = len(results.get("success", []))
        f = len(results.get("failed", []))
        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('Key rotation complete', m)}\n\n"
            f"  Re-encrypted: {s} stores\n"
            f"  Failed: {f} stores\n"
            f"  New key saved to: {fmt_code('~/.familiar/keys/encryption.key', m)}",
        )
    except Exception as e:
        await host.wizard_send(recipient_id, f"\u274c Key rotation failed: {e}")


# ── PHI Detection one-button actions ─────────────────────────────────


async def _phi_set_mode(
    host: WizardHost, recipient_id: str, *, safe_mode: bool, sensitivity: str
) -> None:
    """Set PHI detection mode and sensitivity — one button."""
    m = host.format_mode
    data_dir = Path.home() / ".familiar" / "data"
    phi_path = data_dir / "phi_config.json"
    data_dir.mkdir(parents=True, exist_ok=True)

    try:
        phi = {}
        if phi_path.exists():
            phi = json.loads(phi_path.read_text())

        phi["safe_mode"] = safe_mode
        phi["sensitivity"] = sensitivity
        if safe_mode and "auto_redact" not in phi:
            phi["auto_redact"] = True
        if safe_mode and "log_detections" not in phi:
            phi["log_detections"] = True

        phi_path.write_text(json.dumps(phi, indent=2))

        if safe_mode:
            status = f"Safe mode {fmt_bold('enabled', m)} \u2014 sensitivity: {fmt_bold(sensitivity, m)}"
            if phi.get("auto_redact"):
                status += "\n  Auto-redact: on"
            if phi.get("log_detections"):
                status += "\n  Detection logging: on"
        else:
            status = f"Safe mode {fmt_bold('disabled', m)}"

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('PHI Detection updated', m)}\n\n"
            f"  {status}\n\n"
            f"Saved to: {fmt_code(str(phi_path), m)}",
        )

        # Re-show wizard with updated status
        await wizard_phi_detection(host, recipient_id)
    except Exception as e:
        await host.wizard_send(recipient_id, f"\u274c PHI config update failed: {e}")


# ── RBAC one-button actions ───────────────────────────────────────────

_RBAC_PRESETS = {
    "admin": {"name": "admin", "description": "Full access to all skills and tools", "permissions": ["*"]},
    "operator": {"name": "operator", "description": "Manage services, users, config", "permissions": ["services:*", "users:*", "config:*"]},
    "analyst": {"name": "analyst", "description": "Read-only data and reports", "permissions": ["data:read", "reports:*", "email:read"]},
    "healthcare": {"name": "healthcare", "description": "HIPAA-scoped: PHI access + audit", "permissions": ["phi:*", "cases:*", "audit:read"]},
    "developer": {"name": "developer", "description": "Code, browser, integration tools", "permissions": ["browser:*", "github:*", "code:*"]},
    "guest": {"name": "guest", "description": "Chat and basic queries only", "permissions": ["chat:*"]},
}


def _load_rbac() -> dict:
    rbac_path = Path.home() / ".familiar" / "data" / "rbac.json"
    if rbac_path.exists():
        return json.loads(rbac_path.read_text())
    return {"custom_roles": [], "assignments": {}}


def _save_rbac(rbac: dict) -> None:
    rbac_path = Path.home() / ".familiar" / "data" / "rbac.json"
    rbac_path.parent.mkdir(parents=True, exist_ok=True)
    rbac_path.write_text(json.dumps(rbac, indent=2))


async def _rbac_list_roles(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    rbac = _load_rbac()
    lines = [f"\U0001f6e1\ufe0f {fmt_bold('All Roles', m)}\n", f"{fmt_bold('Built-in:', m)}"]
    for key, preset in _RBAC_PRESETS.items():
        perms = ", ".join(preset["permissions"][:3])
        lines.append(f"  {fmt_bold(key, m)} \u2014 {preset['description']}\n    Permissions: {fmt_code(perms, m)}")
    custom = rbac.get("custom_roles", [])
    if custom:
        lines.append(f"\n{fmt_bold('Custom:', m)}")
        for cr in custom:
            perms = ", ".join(cr.get("permissions", [])[:3])
            lines.append(f"  {fmt_bold(cr['name'], m)} \u2014 {cr.get('description', '')}\n    Permissions: {fmt_code(perms, m)}")
    assignments = rbac.get("assignments", {})
    if assignments:
        lines.append(f"\n{fmt_bold('Assignments:', m)}")
        for uid, role in assignments.items():
            lines.append(f"  {uid} \u2192 {role}")
    await host.wizard_send(recipient_id, "\n".join(lines))


async def _rbac_start_create(host: WizardHost, recipient_id: str) -> None:
    """Start the create-role wizard or show preset shortcuts."""
    m = host.format_mode
    host._ensure_wizard_state()
    if host.supports_buttons:
        await host.wizard_send_menu(
            recipient_id,
            f"\u2795 {fmt_bold('Create Role', m)}\n\n"
            f"Choose a preset or create custom.\n"
            f"Presets come with sensible default permissions.",
            [
                ("rbac_preset_admin", "\U0001f451 Admin (full access)"),
                ("rbac_preset_operator", "\u2699\ufe0f Operator (services/config)"),
                ("rbac_preset_analyst", "\U0001f4ca Analyst (read-only)"),
                ("rbac_preset_healthcare", "\U0001f3e5 Healthcare (HIPAA)"),
                ("rbac_preset_developer", "\U0001f4bb Developer (code/browser)"),
                ("rbac_preset_guest", "\U0001f464 Guest (chat only)"),
            ],
        )
    else:
        host._wizard_state[recipient_id] = {"type": "rbac", "step": "name"}
        await host.wizard_send(
            recipient_id,
            f"\u2795 {fmt_bold('Create Custom Role', m)}\n\n"
            f"Enter a name for the new role:",
        )


async def _rbac_create_preset(host: WizardHost, recipient_id: str, preset_key: str) -> None:
    m = host.format_mode
    preset = _RBAC_PRESETS.get(preset_key)
    if not preset:
        await host.wizard_send(recipient_id, f"\u274c Unknown preset: {preset_key}")
        return
    rbac = _load_rbac()
    custom = rbac.get("custom_roles", [])
    # Check if already exists
    existing = [r for r in custom if r["name"] == preset["name"]]
    if existing:
        await host.wizard_send(
            recipient_id,
            f"\u2705 Role {fmt_bold(preset['name'], m)} already exists.",
        )
        return
    custom.append(preset)
    rbac["custom_roles"] = custom
    _save_rbac(rbac)
    perms = ", ".join(preset["permissions"])
    await host.wizard_send(
        recipient_id,
        f"\u2705 {fmt_bold('Role created!', m)}\n\n"
        f"  Name: {fmt_bold(preset['name'], m)}\n"
        f"  Description: {preset['description']}\n"
        f"  Permissions: {fmt_code(perms, m)}\n\n"
        f"Saved to: {fmt_code('~/.familiar/data/rbac.json', m)}",
    )


async def _rbac_start_assign(host: WizardHost, recipient_id: str) -> None:
    """Start the assign-role wizard."""
    m = host.format_mode
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "rbac", "step": "assign_user"}
    rbac = _load_rbac()
    roles = list(_RBAC_PRESETS.keys()) + [r["name"] for r in rbac.get("custom_roles", [])]
    await host.wizard_send(
        recipient_id,
        f"\U0001f464 {fmt_bold('Assign Role', m)}\n\n"
        f"Available roles: {', '.join(fmt_bold(r, m) for r in roles)}\n\n"
        f"Enter: {fmt_code('username rolename', m)}\n"
        f"Example: {fmt_code('alice developer', m)}",
    )


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

    if step == "name":
        state["name"] = text.strip()
        state["step"] = "permissions"
        await host.wizard_send(
            recipient_id,
            f"Enter permissions (comma-separated):\n"
            f"Example: {fmt_code('email:read,calendar:read,data:read', m)}",
        )
        return

    if step == "permissions":
        name = state.get("name", "custom")
        perms = [p.strip() for p in text.split(",") if p.strip()]
        host._wizard_state.pop(recipient_id, None)
        rbac = _load_rbac()
        rbac.setdefault("custom_roles", []).append({
            "name": name, "description": f"Custom role: {name}", "permissions": perms,
        })
        _save_rbac(rbac)
        await host.wizard_send(
            recipient_id,
            f"\u2705 Role {fmt_bold(name, m)} created with {len(perms)} permission(s).\n"
            f"Saved to: {fmt_code('~/.familiar/data/rbac.json', m)}",
        )
        return

    if step == "assign_user":
        parts = text.strip().split(None, 1)
        if len(parts) < 2:
            await host.wizard_send(recipient_id, f"Format: {fmt_code('username rolename', m)}")
            return
        username, role = parts[0], parts[1]
        host._wizard_state.pop(recipient_id, None)
        rbac = _load_rbac()
        rbac.setdefault("assignments", {})[username] = role
        _save_rbac(rbac)
        await host.wizard_send(
            recipient_id,
            f"\u2705 Assigned {fmt_bold(role, m)} to {fmt_bold(username, m)}.\n"
            f"Saved to: {fmt_code('~/.familiar/data/rbac.json', m)}",
        )
        return


# ── User Management one-button actions ───────────────────────────────


def _load_users() -> dict:
    users_path = Path.home() / ".familiar" / "data" / "users.json"
    if users_path.exists():
        return json.loads(users_path.read_text())
    return {"users": [], "pre_authorizations": []}


def _save_users(data: dict) -> None:
    users_path = Path.home() / ".familiar" / "data" / "users.json"
    users_path.parent.mkdir(parents=True, exist_ok=True)
    users_path.write_text(json.dumps(data, indent=2))


async def _users_list(host: WizardHost, recipient_id: str) -> None:
    m = host.format_mode
    data = _load_users()
    users = data.get("users", [])
    if not users:
        await host.wizard_send(
            recipient_id,
            f"\U0001f465 No users configured yet.\n"
            f"Use the {fmt_bold('Create new user', m)} button to add one.",
        )
        return
    lines = [f"\U0001f465 {fmt_bold('All Users', m)} ({len(users)})\n"]
    for u in users:
        status = "\u2705" if u.get("active", True) else "\u274c"
        name = u.get("name", u.get("username", "?"))
        role = u.get("role", "guest")
        email = u.get("email", "")
        line = f"  {status} {fmt_bold(name, m)} \u2014 {role}"
        if email:
            line += f" ({email})"
        lines.append(line)
    await host.wizard_send(recipient_id, "\n".join(lines))


async def _users_start_create(host: WizardHost, recipient_id: str) -> None:
    """Start the create-user wizard."""
    m = host.format_mode
    host._ensure_wizard_state()
    host._wizard_state[recipient_id] = {"type": "user_management", "step": "username"}
    await host.wizard_send(
        recipient_id,
        f"\u2795 {fmt_bold('Create New User', m)}\n\n"
        f"{fmt_bold('Step 1 of 3: Username', m)}\n"
        f"Enter a username for the new user:",
    )


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

    if step == "username":
        state["username"] = text
        state["step"] = "pin"
        await host.wizard_send(
            recipient_id,
            f"{fmt_bold('Step 2 of 3: PIN', m)}\n"
            f"Enter a numeric PIN for this user (4-8 digits):\n"
            f"{fmt_italic('This PIN is used to verify identity on shared devices.', m)}",
        )
        return

    if step == "pin":
        pin = text.replace(" ", "")
        if not pin.isdigit() or len(pin) < 4 or len(pin) > 8:
            await host.wizard_send(
                recipient_id, "PIN must be 4-8 digits. Try again:",
            )
            return
        # Hash the PIN
        import hashlib
        pin_hash = hashlib.pbkdf2_hmac("sha256", pin.encode(), b"familiar-pin-salt", 100000).hex()
        state["pin_hash"] = pin_hash
        state["step"] = "role"

        # Delete PIN message
        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)

        rbac = _load_rbac()
        roles = list(_RBAC_PRESETS.keys()) + [r["name"] for r in rbac.get("custom_roles", [])]
        if host.supports_buttons:
            await host.wizard_send_menu(
                recipient_id,
                f"{fmt_bold('Step 3 of 3: Role', m)}\nPick a role for {fmt_bold(state['username'], m)}:",
                [(f"user_role_{r}", r) for r in roles],
            )
        else:
            await host.wizard_send(
                recipient_id,
                f"{fmt_bold('Step 3 of 3: Role', m)}\n"
                f"Available: {', '.join(roles)}\n\n"
                f"Enter a role name:",
            )
        return

    if step == "role":
        username = state.get("username", "user")
        pin_hash = state.get("pin_hash", "")
        role = text
        host._wizard_state.pop(recipient_id, None)

        data = _load_users()
        data.setdefault("users", []).append({
            "username": username,
            "name": username,
            "pin_hash": pin_hash,
            "role": role,
            "active": True,
        })
        _save_users(data)

        await host.wizard_send(
            recipient_id,
            f"\u2705 {fmt_bold('User created!', m)}\n\n"
            f"  Username: {fmt_bold(username, m)}\n"
            f"  Role: {fmt_bold(role, m)}\n"
            f"  PIN: set (hashed)\n\n"
            f"Saved to: {fmt_code('~/.familiar/data/users.json', m)}",
        )


# ── handle_menu_selection ────────────────────────────────────────────


async def handle_menu_selection(host: WizardHost, recipient_id: str, key: str) -> bool:
    """Handle a security-related menu button press.

    Returns ``True`` if *key* was recognised and handled.
    """
    m = host.format_mode

    # Top-level security wizard menus
    if key == "vaultwarden_menu":
        await wizard_vaultwarden(host, recipient_id, [])
        return True
    if key == "encryption_menu":
        await wizard_encryption(host, recipient_id)
        return True
    if key == "phi_detection_menu":
        await wizard_phi_detection(host, recipient_id)
        return True
    if key == "rbac_menu":
        await wizard_rbac(host, recipient_id)
        return True
    if key == "user_management_menu":
        await wizard_user_management(host, recipient_id)
        return True

    # Encryption action buttons — perform actions directly
    if key == "encryption_gen_key":
        await _encryption_generate_key(host, recipient_id)
        return True
    if key == "encryption_encrypt_all":
        await _encryption_encrypt_all(host, recipient_id)
        return True
    if key == "encryption_rotate":
        await _encryption_rotate_keys(host, recipient_id)
        return True

    # PHI Detection action buttons — perform actions directly
    if key == "phi_enable_safe":
        await _phi_set_mode(host, recipient_id, safe_mode=True, sensitivity="standard")
        return True
    if key == "phi_set_strict":
        await _phi_set_mode(host, recipient_id, safe_mode=True, sensitivity="strict")
        return True
    if key == "phi_set_standard":
        await _phi_set_mode(host, recipient_id, safe_mode=True, sensitivity="standard")
        return True
    if key == "phi_disable":
        await _phi_set_mode(host, recipient_id, safe_mode=False, sensitivity="standard")
        return True

    # RBAC action buttons — perform actions directly
    if key == "rbac_list":
        await _rbac_list_roles(host, recipient_id)
        return True
    if key == "rbac_create":
        await _rbac_start_create(host, recipient_id)
        return True
    if key == "rbac_assign":
        await _rbac_start_assign(host, recipient_id)
        return True
    # Preset role creation shortcuts
    if key.startswith("rbac_preset_"):
        preset = key[len("rbac_preset_"):]
        await _rbac_create_preset(host, recipient_id, preset)
        return True

    # User Management action buttons — perform actions directly
    if key == "users_list":
        await _users_list(host, recipient_id)
        return True
    if key == "users_create":
        await _users_start_create(host, recipient_id)
        return True
    # Dynamic role selection for user creation: user_role_<rolename>
    if key.startswith("user_role_"):
        role = key[len("user_role_"):]
        host._ensure_wizard_state()
        state = host._wizard_state.get(recipient_id)
        if state and state.get("type") == "user_management" and state.get("step") == "role":
            await user_management_handle_step(host, recipient_id, role, None)
        return True

    return False
