// ChainSettingsDialog — modal for changing the hub endpoint.
// Reads current values from HubService, persists on save via
// HubService.reconnect.

import 'package:chain_client_sdk/chain_client_sdk.dart';
import 'package:flutter/material.dart';

import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../data/hub_auth_token.dart';
import '../data/registry_token.dart';
import '../data/system_actions.dart';
import '../l10n/app_localizations.dart';
import '../pages/welcome.dart' show showFaiDoc;
import '../theme/theme.dart';
import '../theme/tokens.dart';
import 'chain_error_box.dart';
import 'chain_pill.dart';
import 'chain_system_ai_editor.dart';
import 'theme_picker_grid.dart';

class ChainSettingsDialog extends StatefulWidget {
  const ChainSettingsDialog({super.key});

  /// Convenience launcher used from the sidebar gear icon.
  static Future<bool> show(BuildContext context) async {
    final ok = await showDialog<bool>(
      context: context,
      builder: (_) => const ChainSettingsDialog(),
    );
    return ok ?? false;
  }

  @override
  State<ChainSettingsDialog> createState() => _FaiSettingsDialogState();
}

/// Sidebar categories — every Settings panel belongs to exactly
/// one. Order here drives the sidebar order and the
/// first-opened category.
enum _Category { general, appearance, ai, integrations, security, maintenance }

class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
  late final TextEditingController _host;
  late final TextEditingController _port;
  bool _secure = false;
  bool _saving = false;
  String? _error;
  String? _channelToast;
  ChannelStatusSnapshot? _channels;
  SystemAiStatus? _aiStatus;
  List<McpClientInfo>? _mcpClients;
  List<N8nEndpointInfo>? _n8nEndpoints;
  _Category _category = _Category.general;

  /// Length of the configured registry token, or null when the
  /// `~/.chain/registry-token` file is missing / empty. Reloaded
  /// after save / clear.
  int? _registryTokenChars;

  /// Length of the configured hub gRPC bearer token, or null
  /// when `~/.chain/hub-auth-token` is missing / empty. Reloaded
  /// after save / clear; UI only ever sees the trimmed length.
  int? _hubAuthTokenChars;

  @override
  void initState() {
    super.initState();
    final ep = HubService.instance.currentEndpoint;
    _host = TextEditingController(text: ep.host);
    _port = TextEditingController(text: ep.port.toString());
    _secure = ep.secure;
    _loadChannels();
    _loadAiStatus();
    _loadMcpClients();
    _loadN8nEndpoints();
    _loadRegistryToken();
    _loadHubAuthToken();
  }

  Future<void> _loadHubAuthToken() async {
    try {
      final n = await HubAuthToken.charCount();
      if (!mounted) return;
      setState(() => _hubAuthTokenChars = n);
    } catch (_) {
      /* fail-quiet */
    }
  }

  Future<void> _saveHubAuthToken(String token) async {
    try {
      await HubAuthToken.write(token);
      await HubService.instance.reloadAuthToken();
      await _loadHubAuthToken();
      if (!mounted) return;
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.hubAuthTokenSavedToast)));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'auth.hub-token.save', e);
    }
  }

  Future<void> _clearHubAuthToken() async {
    try {
      await HubAuthToken.clear();
      await HubService.instance.reloadAuthToken();
      await _loadHubAuthToken();
      if (!mounted) return;
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.hubAuthTokenClearedToast)));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'auth.hub-token.clear', e);
    }
  }

  Future<void> _loadRegistryToken() async {
    try {
      final n = await RegistryToken.charCount();
      if (!mounted) return;
      setState(() => _registryTokenChars = n);
    } catch (_) {
      /* fail-quiet */
    }
  }

  Future<void> _saveRegistryToken(String token) async {
    try {
      await RegistryToken.write(token);
      await _loadRegistryToken();
      if (!mounted) return;
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.registryTokenSavedToast)));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'auth.registry-token.save', e);
    }
  }

  Future<void> _clearRegistryToken() async {
    try {
      await RegistryToken.delete();
      await _loadRegistryToken();
      if (!mounted) return;
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.registryTokenClearedToast)));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'auth.registry-token.clear', e);
    }
  }

  Future<void> _loadN8nEndpoints() async {
    try {
      final list = await HubService.instance.listN8nEndpoints();
      if (!mounted) return;
      setState(() => _n8nEndpoints = list);
    } catch (_) {
      /* fail-quiet */
    }
  }

  Future<void> _addN8nEndpoint(N8nEndpointDraft draft) async {
    try {
      final list = await HubService.instance.addN8nEndpoint(
        name: draft.name,
        baseUrl: draft.baseUrl,
        apiKeyEnv: draft.apiKeyEnv,
        description: draft.description,
      );
      if (!mounted) return;
      setState(() => _n8nEndpoints = list);
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.addedN8nToast(draft.name))));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.n8n.add', e);
    }
  }

  Future<void> _removeN8nEndpoint(String name) async {
    try {
      final list = await HubService.instance.removeN8nEndpoint(name);
      if (!mounted) return;
      setState(() => _n8nEndpoints = list);
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.n8n.remove', e);
    }
  }

  Future<void> _refreshN8nEndpoints() async {
    try {
      final list = await HubService.instance.refreshN8nEndpoints();
      if (!mounted) return;
      setState(() => _n8nEndpoints = list);
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.n8n.refresh', e);
    }
  }

  Future<void> _loadMcpClients() async {
    try {
      final list = await HubService.instance.listMcpClients();
      if (!mounted) return;
      setState(() => _mcpClients = list);
    } catch (_) {
      // Same fail-quiet rule as channels.
    }
  }

  Future<void> _addMcpClient(McpClientDraft draft) async {
    try {
      final list = await HubService.instance.addMcpClient(
        name: draft.name,
        endpoint: draft.endpoint,
        apiKeyEnv: draft.apiKeyEnv,
        description: draft.description,
      );
      if (!mounted) return;
      setState(() => _mcpClients = list);
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.addedMcpToast(draft.name))));
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.mcp.add', e);
    }
  }

  Future<void> _removeMcpClient(String name) async {
    try {
      final list = await HubService.instance.removeMcpClient(name);
      if (!mounted) return;
      setState(() => _mcpClients = list);
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.mcp.remove', e);
    }
  }

  Future<void> _refreshMcpClients() async {
    try {
      final list = await HubService.instance.refreshMcpClients();
      if (!mounted) return;
      setState(() => _mcpClients = list);
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'settings.mcp.refresh', e);
    }
  }

  Future<void> _loadChannels() async {
    try {
      final snap = await HubService.instance.channelStatus();
      if (!mounted) return;
      setState(() => _channels = snap);
    } catch (_) {
      // Silent: channels block stays hidden when the hub is
      // unreachable (the endpoint section is the operator's
      // path back to a working connection).
    }
  }

  Future<void> _loadAiStatus() async {
    try {
      final s = await HubService.instance.systemAiStatus();
      if (!mounted) return;
      setState(() => _aiStatus = s);
    } catch (_) {
      // Same fail-quiet rule as channels.
    }
  }

  Future<void> _switchChannel(String name) async {
    setState(() {
      _saving = true;
      _channelToast = null;
    });
    final r = await SystemActions.chainChannelSwitch(name);
    if (!mounted) return;
    final l = AppLocalizations.of(context)!;
    setState(() {
      _saving = false;
      _channelToast = r.ok
          ? '${l.channelSwitchOk(name)}\n${r.stdout.trim()}'
          : l.channelSwitchFailed(r.stderr.isEmpty ? r.stdout : r.stderr);
    });
    if (r.ok) await _loadChannels();
  }

  Future<void> _runDaemon(
    String label,
    Future<({bool ok, String stdout, String stderr})> Function() action,
  ) async {
    setState(() {
      _saving = true;
      _channelToast = null;
    });
    final r = await action();
    if (!mounted) return;
    final l = AppLocalizations.of(context)!;
    setState(() {
      _saving = false;
      _channelToast = r.ok
          ? '${l.daemonActionResultOk(label)}\n${r.stdout.trim()}'
          : '${l.daemonActionResultFailed(label)}\n${(r.stderr.isEmpty ? r.stdout : r.stderr).trim()}';
    });
  }

  Future<void> _connectToChannel(ChannelInfo ch) async {
    setState(() {
      _host.text = '127.0.0.1';
      _port.text = ch.port.toString();
      _secure = false;
    });
    await _save();
  }

  @override
  void dispose() {
    _host.dispose();
    _port.dispose();
    super.dispose();
  }

  Future<void> _save() async {
    setState(() {
      _saving = true;
      _error = null;
    });
    final port = int.tryParse(_port.text.trim());
    if (port == null || port <= 0 || port > 65535) {
      final l = AppLocalizations.of(context)!;
      setState(() {
        _saving = false;
        _error = l.settingsPortError;
      });
      return;
    }
    final endpoint = HubEndpoint(
      host: _host.text.trim().isEmpty ? '127.0.0.1' : _host.text.trim(),
      port: port,
      secure: _secure,
    );
    try {
      await HubService.instance.reconnect(endpoint);
      if (!mounted) return;
      Navigator.pop(context, true);
    } catch (e) {
      setState(() {
        _saving = false;
        _error = e.toString();
      });
    }
  }

  Widget _buildCategoryPanel(
    BuildContext context,
    AppLocalizations l,
    ThemeData theme,
  ) {
    final children = switch (_category) {
      _Category.general => _generalPanel(l, theme),
      _Category.appearance => _appearancePanel(l, theme),
      _Category.ai => _aiPanel(l, theme),
      _Category.integrations => _integrationsPanel(l, theme),
      _Category.security => _securityPanel(l, theme),
      _Category.maintenance => _maintenancePanel(l, theme),
    };
    return SingleChildScrollView(
      padding: const EdgeInsets.fromLTRB(
        ChainSpace.xl,
        ChainSpace.xl,
        ChainSpace.xl,
        ChainSpace.lg,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: children,
      ),
    );
  }

  Widget _panelTitle(
    String title,
    String subtitle,
    ThemeData theme, {
    String? docSlug,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: ChainSpace.lg),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Expanded(
                child: Text(
                  title,
                  style: theme.textTheme.titleLarge?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
              if (docSlug != null)
                IconButton(
                  icon: const Icon(Icons.help_outline, size: 18),
                  tooltip: AppLocalizations.of(context)!.helpTooltip,
                  onPressed: () => showFaiDoc(context, docSlug),
                  visualDensity: VisualDensity.compact,
                ),
            ],
          ),
          const SizedBox(height: 4),
          Text(
            subtitle,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }

  List<Widget> _generalPanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.settingsTitle,
        l.settingsHubEndpointHint,
        theme,
        docSlug: 'architecture',
      ),
      TextField(
        controller: _host,
        decoration: InputDecoration(
          labelText: l.settingsHost,
          border: const OutlineInputBorder(),
          isDense: true,
        ),
        autofocus: true,
      ),
      const SizedBox(height: ChainSpace.md),
      Row(
        children: [
          Expanded(
            flex: 2,
            child: TextField(
              controller: _port,
              decoration: InputDecoration(
                labelText: l.settingsPort,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
              keyboardType: TextInputType.number,
            ),
          ),
          const SizedBox(width: ChainSpace.md),
          Expanded(
            flex: 3,
            child: SwitchListTile(
              contentPadding: EdgeInsets.zero,
              dense: true,
              title: Text(l.settingsTls),
              subtitle: Text(
                l.settingsTlsSubtitle,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
              value: _secure,
              onChanged: (v) => setState(() => _secure = v),
            ),
          ),
        ],
      ),
      if (_error != null) ...[
        const SizedBox(height: ChainSpace.md),
        // Copyable — errors must be selectable + one-tap copyable.
        ChainErrorBox(text: _error!, isError: true, maxHeight: 200),
      ],
      const SizedBox(height: ChainSpace.md),
      Container(
        padding: const EdgeInsets.all(ChainSpace.sm),
        decoration: BoxDecoration(
          color: theme.colorScheme.surfaceContainerHigh,
          borderRadius: BorderRadius.circular(ChainRadius.sm),
        ),
        child: Row(
          children: [
            Icon(
              Icons.link,
              size: 14,
              color: theme.colorScheme.onSurfaceVariant,
            ),
            const SizedBox(width: ChainSpace.sm),
            Text(
              _previewUrl(),
              style: ChainTheme.mono(
                size: 11,
                color: theme.colorScheme.onSurface,
              ),
            ),
          ],
        ),
      ),
      if (_channels != null) ...[
        const SizedBox(height: ChainSpace.xl),
        Text(
          l.channelsHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          l.channelsBlurb,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        for (final ch in _channels!.channels)
          _ChannelRow(
            channel: ch,
            active: ch.name == _channels!.active,
            onConnect: _saving ? null : () => _connectToChannel(ch),
            onSwitch: _saving ? null : () => _switchChannel(ch.name),
            onEnableAutostart: _saving
                ? null
                : () => _runDaemon(
                    l.daemonActionEnableAutostart,
                    () => SystemActions.chainDaemonEnable(ch.name),
                  ),
            onDisableAutostart: _saving
                ? null
                : () => _runDaemon(
                    l.daemonActionDisableAutostart,
                    () => SystemActions.chainDaemonDisable(ch.name),
                  ),
          ),
        if (_channelToast != null) ...[
          const SizedBox(height: ChainSpace.sm),
          ChainErrorBox(text: _channelToast!, maxHeight: 200),
        ],
      ],
      const SizedBox(height: ChainSpace.xl),
      const _DefaultScopePanel(),
    ];
  }

  List<Widget> _appearancePanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.themePluginHeader,
        l.themePluginHint,
        theme,
      ),
      const _ThemePluginPanel(),
    ];
  }

  List<Widget> _aiPanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.settingsAiPanelTitle,
        l.settingsAiPanelBody,
        theme,
      ),
      if (_aiStatus == null)
        const Center(child: CircularProgressIndicator(strokeWidth: 2))
      else
        _SystemAiPanel(
          status: _aiStatus!,
          onEdit: () async {
            final updated = await ChainSystemAiEditor.show(context, _aiStatus!);
            if (updated != null && mounted) {
              setState(() => _aiStatus = updated);
            }
          },
        ),
    ];
  }

  List<Widget> _integrationsPanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.settingsIntegrationsPanelTitle,
        l.settingsIntegrationsPanelBody,
        theme,
      ),
      if (_mcpClients == null)
        const Padding(
          padding: EdgeInsets.symmetric(vertical: ChainSpace.md),
          child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
        )
      else
        _McpClientsPanel(
          clients: _mcpClients!,
          onAdd: _addMcpClient,
          onRemove: _removeMcpClient,
          onRefresh: _refreshMcpClients,
        ),
      const SizedBox(height: ChainSpace.xl),
      if (_n8nEndpoints != null)
        _N8nEndpointsPanel(
          endpoints: _n8nEndpoints!,
          onAdd: _addN8nEndpoint,
          onRemove: _removeN8nEndpoint,
          onRefresh: _refreshN8nEndpoints,
        ),
    ];
  }

  List<Widget> _securityPanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.settingsSecurityPanelTitle,
        l.settingsSecurityPanelBody,
        theme,
        docSlug: 'security',
      ),
      _RegistryCredentialsPanel(
        configuredChars: _registryTokenChars,
        onSave: _saveRegistryToken,
        onClear: _clearRegistryToken,
      ),
      const SizedBox(height: ChainSpace.xl),
      _HubAuthTokenPanel(
        configuredChars: _hubAuthTokenChars,
        onSave: _saveHubAuthToken,
        onClear: _clearHubAuthToken,
      ),
    ];
  }

  List<Widget> _maintenancePanel(AppLocalizations l, ThemeData theme) {
    return [
      _panelTitle(
        l.settingsMaintenancePanelTitle,
        l.settingsMaintenancePanelBody,
        theme,
        docSlug: 'audit',
      ),
      _MaintenancePanel(
        onResetDone: () async {
          await HubService.instance.reconnect(
            HubService.instance.currentEndpoint,
          );
          if (!mounted) return;
          await _loadChannels();
          await _loadAiStatus();
          await _loadMcpClients();
          await _loadN8nEndpoints();
          await _loadRegistryToken();
        },
      ),
    ];
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return AlertDialog(
      title: Text(l.settingsTitle),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(ChainRadius.md),
      ),
      contentPadding: EdgeInsets.zero,
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 820, maxHeight: 620),
        child: SizedBox(
          width: 820,
          height: 620,
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Sidebar(
                current: _category,
                onSelect: (c) => setState(() => _category = c),
              ),
              VerticalDivider(
                width: 1,
                color: theme.colorScheme.outlineVariant,
              ),
              Expanded(child: _buildCategoryPanel(context, l, theme)),
            ],
          ),
        ),
      ),
      actions: [
        TextButton(
          onPressed: _saving ? null : () => Navigator.pop(context, false),
          child: Text(l.buttonCancel),
        ),
        FilledButton(
          onPressed: _saving ? null : _save,
          child: _saving
              ? const SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : Text(l.settingsSaveAndConnect),
        ),
      ],
    );
  }

  String _previewUrl() {
    final scheme = _secure ? 'https' : 'http';
    final host = _host.text.trim().isEmpty ? '127.0.0.1' : _host.text.trim();
    final port = _port.text.trim().isEmpty ? '50051' : _port.text.trim();
    return '$scheme://$host:$port';
  }
}

class _ChannelRow extends StatelessWidget {
  final ChannelInfo channel;
  final bool active;
  final VoidCallback? onConnect;

  /// Switches the active channel pointer (`fai channel switch`).
  /// Distinct from [onConnect], which only re-points Studio's
  /// gRPC wire at a different running daemon.
  final VoidCallback? onSwitch;
  final VoidCallback? onEnableAutostart;
  final VoidCallback? onDisableAutostart;

  const _ChannelRow({
    required this.channel,
    required this.active,
    required this.onConnect,
    required this.onSwitch,
    required this.onEnableAutostart,
    required this.onDisableAutostart,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Icon(
            channel.running ? Icons.circle : Icons.circle_outlined,
            size: 10,
            color: channel.running
                ? ChainColors.success
                : theme.colorScheme.outline,
          ),
          const SizedBox(width: ChainSpace.sm),
          SizedBox(
            width: 88,
            child: Text(
              channel.name,
              style: ChainTheme.mono(
                size: 11,
                weight: active ? FontWeight.w600 : FontWeight.w400,
                color: theme.colorScheme.onSurface,
              ),
            ),
          ),
          SizedBox(
            width: 64,
            child: Text(
              ':${channel.port}',
              style: ChainTheme.mono(
                size: 11,
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
          Expanded(
            child: Text(
              channel.running ? l.channelsRunning : l.channelsStopped,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
          if (active)
            Padding(
              padding: const EdgeInsets.only(right: ChainSpace.sm),
              child: ChainPill(
                label: l.channelsActive,
                tone: ChainPillTone.success,
              ),
            ),
          PopupMenuButton<String>(
            tooltip: l.channelsActionsTooltip,
            icon: const Icon(Icons.more_vert, size: 18),
            onSelected: (v) {
              switch (v) {
                case 'connect':
                  onConnect?.call();
                  break;
                case 'switch':
                  onSwitch?.call();
                  break;
                case 'enable':
                  onEnableAutostart?.call();
                  break;
                case 'disable':
                  onDisableAutostart?.call();
                  break;
              }
            },
            itemBuilder: (_) => [
              PopupMenuItem(
                value: 'connect',
                enabled: channel.running && onConnect != null,
                child: _MenuRow(icon: Icons.link, text: l.channelsConnect),
              ),
              PopupMenuItem(
                value: 'switch',
                enabled: !active && onSwitch != null,
                child: _MenuRow(icon: Icons.swap_horiz, text: l.channelsSwitch),
              ),
              const PopupMenuDivider(),
              PopupMenuItem(
                value: 'enable',
                enabled: onEnableAutostart != null,
                child: _MenuRow(
                  icon: Icons.play_circle_outline,
                  text: l.channelsEnableAutostart,
                ),
              ),
              PopupMenuItem(
                value: 'disable',
                enabled: onDisableAutostart != null,
                child: _MenuRow(
                  icon: Icons.pause_circle_outline,
                  text: l.channelsDisableAutostart,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _MenuRow extends StatelessWidget {
  final IconData icon;
  final String text;
  const _MenuRow({required this.icon, required this.text});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(icon, size: 14),
        const SizedBox(width: ChainSpace.sm),
        Text(text),
      ],
    );
  }
}

class _SystemAiPanel extends StatelessWidget {
  final SystemAiStatus status;
  final VoidCallback onEdit;

  const _SystemAiPanel({required this.status, required this.onEdit});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              l.systemAiHeader,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
                fontSize: 10,
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            ChainPill(
              label: status.enabled ? l.systemAiEnabled : l.systemAiOff,
              tone: status.enabled ? ChainPillTone.success : ChainPillTone.neutral,
            ),
            if (status.enabled) ...[
              const SizedBox(width: ChainSpace.xs),
              ChainPill(
                label: status.privacyMode,
                tone: status.privacyMode == 'full'
                    ? ChainPillTone.warning
                    : ChainPillTone.neutral,
              ),
            ],
            const Spacer(),
            TextButton.icon(
              onPressed: onEdit,
              icon: const Icon(Icons.edit_outlined, size: 14),
              label: Text(
                status.enabled ? l.systemAiEdit : l.systemAiConfigure,
              ),
            ),
          ],
        ),
        const SizedBox(height: 4),
        if (status.enabled) ...[
          _StatRow(label: 'provider', value: status.provider),
          _StatRow(label: 'endpoint', value: status.endpoint, mono: true),
          _StatRow(label: 'model', value: status.model, mono: true),
          if (status.apiKeyEnv.isNotEmpty)
            _StatRow(
              label: 'api_key_env',
              value: '\$${status.apiKeyEnv}',
              mono: true,
            ),
        ] else ...[
          Padding(
            padding: const EdgeInsets.only(top: 2),
            child: Text(
              l.systemAiOffBlurb,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ],
    );
  }
}

class _StatRow extends StatelessWidget {
  final String label;
  final String value;
  final bool mono;

  const _StatRow({required this.label, required this.value, this.mono = false});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 1),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 96,
            child: Text(
              label,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value.isEmpty ? '—' : value,
              style: mono
                  ? ChainTheme.mono(size: 11, color: theme.colorScheme.onSurface)
                  : theme.textTheme.bodySmall,
            ),
          ),
        ],
      ),
    );
  }
}

/// Draft passed back from the add-server dialog. Kept tiny so
/// the panel can stay stateless.
class McpClientDraft {
  final String name;
  final String endpoint;
  final String apiKeyEnv;
  final String description;
  const McpClientDraft({
    required this.name,
    required this.endpoint,
    required this.apiKeyEnv,
    required this.description,
  });
}

/// MCP-clients panel — shows every configured server with its
/// last-discovery health, plus Add / Remove / Refresh actions.
/// Configure a server here, the store gains synthetic
/// capabilities for every advertised tool. Mirrors `chain mcp …`
/// on the CLI.
class _McpClientsPanel extends StatelessWidget {
  final List<McpClientInfo> clients;
  final ValueChanged<McpClientDraft> onAdd;
  final ValueChanged<String> onRemove;
  final VoidCallback onRefresh;

  const _McpClientsPanel({
    required this.clients,
    required this.onAdd,
    required this.onRemove,
    required this.onRefresh,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              l.mcpHeader,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
                fontSize: 10,
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            ChainPill(
              label: l.mcpServersCount(clients.length),
              tone: ChainPillTone.neutral,
            ),
            const Spacer(),
            IconButton(
              icon: const Icon(Icons.refresh, size: 16),
              tooltip: l.mcpRediscoverTooltip,
              visualDensity: VisualDensity.compact,
              onPressed: onRefresh,
            ),
            IconButton(
              icon: const Icon(Icons.add, size: 16),
              tooltip: l.mcpAddTooltip,
              visualDensity: VisualDensity.compact,
              onPressed: () async {
                final draft = await _AddMcpClientDialog.show(context);
                if (draft != null) onAdd(draft);
              },
            ),
          ],
        ),
        const SizedBox(height: 2),
        Text(
          l.mcpHint,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        if (clients.isEmpty)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
            child: Text(
              l.mcpEmpty,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          )
        else
          for (final c in clients)
            _McpClientRow(client: c, onRemove: () => onRemove(c.name)),
      ],
    );
  }
}

class _McpClientRow extends StatelessWidget {
  final McpClientInfo client;
  final VoidCallback onRemove;
  const _McpClientRow({required this.client, required this.onRemove});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final ok = client.healthy;
    final dotColor = client.errorKind.isEmpty
        ? (ok ? ChainColors.success : ChainColors.muted)
        : theme.colorScheme.error;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 6),
            child: Icon(Icons.circle, size: 8, color: dotColor),
          ),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Text(
                      client.name,
                      style: ChainTheme.mono(
                        size: 12,
                        weight: FontWeight.w600,
                        color: theme.colorScheme.onSurface,
                      ),
                    ),
                    const SizedBox(width: ChainSpace.sm),
                    if (client.toolCount > 0)
                      ChainPill(
                        label: l.mcpToolsCount(client.toolCount),
                        tone: ChainPillTone.success,
                      )
                    else if (client.errorKind.isNotEmpty)
                      ChainPill(
                        label: client.errorKind,
                        tone: ChainPillTone.danger,
                      ),
                  ],
                ),
                Text(
                  client.endpoint,
                  style: ChainTheme.mono(
                    size: 10,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
                if (client.message.isNotEmpty)
                  Text(
                    client.message,
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: client.errorKind.isEmpty
                          ? theme.colorScheme.onSurfaceVariant
                          : theme.colorScheme.error,
                    ),
                  ),
              ],
            ),
          ),
          IconButton(
            icon: const Icon(Icons.delete_outline, size: 14),
            tooltip: l.mcpRemoveTooltip,
            visualDensity: VisualDensity.compact,
            onPressed: onRemove,
          ),
        ],
      ),
    );
  }
}

class _AddMcpClientDialog extends StatefulWidget {
  const _AddMcpClientDialog();

  static Future<McpClientDraft?> show(BuildContext context) {
    return showDialog<McpClientDraft>(
      context: context,
      builder: (_) => const _AddMcpClientDialog(),
    );
  }

  @override
  State<_AddMcpClientDialog> createState() => _AddMcpClientDialogState();
}

class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
  final _name = TextEditingController();
  final _endpoint = TextEditingController();
  final _apiKey = TextEditingController();
  final _desc = TextEditingController();

  /// Pick a suggestion → fill the form. Operators still hit
  /// "Add + discover" so nothing happens unprompted.
  void _applySuggestion(_McpSuggestion s) {
    final l = AppLocalizations.of(context)!;
    setState(() {
      _name.text = s.name;
      _endpoint.text = s.endpoint;
      _apiKey.text = s.apiKeyEnv;
      _desc.text = s.resolveDescription(l);
    });
  }

  @override
  void dispose() {
    _name.dispose();
    _endpoint.dispose();
    _apiKey.dispose();
    _desc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return AlertDialog(
      title: Text(l.addMcpTitle),
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 520),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l.addMcpIntro,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: ChainSpace.md),
            // Curated suggestions from the official Anthropic
            // MCP servers + a few community staples. Click a
            // chip to pre-fill the form. The operator still
            // confirms via "Add + discover" — no unprompted
            // subprocess spawn.
            Text(
              l.addMcpSuggestionsLabel,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.5,
              ),
            ),
            const SizedBox(height: 6),
            Wrap(
              spacing: ChainSpace.xs,
              runSpacing: ChainSpace.xs,
              children: [
                for (final s in _kMcpSuggestions)
                  ActionChip(
                    avatar: Icon(s.icon, size: 14),
                    label: Text(s.name),
                    onPressed: () => _applySuggestion(s),
                  ),
              ],
            ),
            const Divider(height: ChainSpace.xl),
            TextField(
              controller: _name,
              autofocus: true,
              decoration: InputDecoration(
                labelText: l.addMcpNameLabel,
                hintText: l.addMcpNameHint,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _endpoint,
              decoration: InputDecoration(
                labelText: l.addMcpEndpointLabel,
                hintText: l.addMcpEndpointHint,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _apiKey,
              decoration: InputDecoration(
                labelText: l.addMcpApiKeyLabel,
                hintText: l.addMcpApiKeyHint,
                prefixText: '\$',
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _desc,
              decoration: InputDecoration(
                labelText: l.addMcpNotesLabel,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, null),
          child: Text(l.buttonCancel),
        ),
        FilledButton(
          onPressed: () {
            if (_name.text.trim().isEmpty || _endpoint.text.trim().isEmpty) {
              return;
            }
            Navigator.pop(
              context,
              McpClientDraft(
                name: _name.text.trim(),
                endpoint: _endpoint.text.trim(),
                apiKeyEnv: _apiKey.text.trim(),
                description: _desc.text.trim(),
              ),
            );
          },
          child: Text(l.addMcpAddButton),
        ),
      ],
    );
  }
}

class N8nEndpointDraft {
  final String name;
  final String baseUrl;
  final String apiKeyEnv;
  final String description;
  const N8nEndpointDraft({
    required this.name,
    required this.baseUrl,
    required this.apiKeyEnv,
    required this.description,
  });
}

/// n8n-endpoints panel — sister of `_McpClientsPanel`. Shows
/// every configured n8n install with last-discovery health
/// (workflow count or error_kind), plus Add / Remove / Refresh.
class _N8nEndpointsPanel extends StatelessWidget {
  final List<N8nEndpointInfo> endpoints;
  final ValueChanged<N8nEndpointDraft> onAdd;
  final ValueChanged<String> onRemove;
  final VoidCallback onRefresh;

  const _N8nEndpointsPanel({
    required this.endpoints,
    required this.onAdd,
    required this.onRemove,
    required this.onRefresh,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              l.n8nHeader,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
                fontSize: 10,
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            ChainPill(
              label: l.n8nEndpointsCount(endpoints.length),
              tone: ChainPillTone.neutral,
            ),
            const Spacer(),
            IconButton(
              icon: const Icon(Icons.refresh, size: 16),
              tooltip: l.n8nRediscoverTooltip,
              visualDensity: VisualDensity.compact,
              onPressed: onRefresh,
            ),
            IconButton(
              icon: const Icon(Icons.add, size: 16),
              tooltip: l.n8nAddTooltip,
              visualDensity: VisualDensity.compact,
              onPressed: () async {
                final draft = await _AddN8nEndpointDialog.show(context);
                if (draft != null) onAdd(draft);
              },
            ),
          ],
        ),
        const SizedBox(height: 2),
        Text(
          l.n8nHint,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        if (endpoints.isEmpty)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
            child: Text(
              l.n8nEmpty,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          )
        else
          for (final e in endpoints)
            _N8nEndpointRow(endpoint: e, onRemove: () => onRemove(e.name)),
      ],
    );
  }
}

class _N8nEndpointRow extends StatelessWidget {
  final N8nEndpointInfo endpoint;
  final VoidCallback onRemove;
  const _N8nEndpointRow({required this.endpoint, required this.onRemove});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final ok = endpoint.healthy;
    final dotColor = endpoint.errorKind.isEmpty
        ? (ok ? ChainColors.success : ChainColors.muted)
        : theme.colorScheme.error;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 6),
            child: Icon(Icons.circle, size: 8, color: dotColor),
          ),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Text(
                      endpoint.name,
                      style: ChainTheme.mono(
                        size: 12,
                        weight: FontWeight.w600,
                        color: theme.colorScheme.onSurface,
                      ),
                    ),
                    const SizedBox(width: ChainSpace.sm),
                    if (endpoint.workflowCount > 0)
                      ChainPill(
                        label: l.n8nWorkflowsCount(endpoint.workflowCount),
                        tone: ChainPillTone.success,
                      )
                    else if (endpoint.errorKind.isNotEmpty)
                      ChainPill(
                        label: endpoint.errorKind,
                        tone: ChainPillTone.danger,
                      ),
                  ],
                ),
                Text(
                  endpoint.baseUrl,
                  style: ChainTheme.mono(
                    size: 10,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
                if (endpoint.message.isNotEmpty)
                  Text(
                    endpoint.message,
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: endpoint.errorKind.isEmpty
                          ? theme.colorScheme.onSurfaceVariant
                          : theme.colorScheme.error,
                    ),
                  ),
              ],
            ),
          ),
          IconButton(
            icon: const Icon(Icons.delete_outline, size: 14),
            tooltip: l.n8nRemoveTooltip,
            visualDensity: VisualDensity.compact,
            onPressed: onRemove,
          ),
        ],
      ),
    );
  }
}

class _AddN8nEndpointDialog extends StatefulWidget {
  const _AddN8nEndpointDialog();

  static Future<N8nEndpointDraft?> show(BuildContext context) {
    return showDialog<N8nEndpointDraft>(
      context: context,
      builder: (_) => const _AddN8nEndpointDialog(),
    );
  }

  @override
  State<_AddN8nEndpointDialog> createState() => _AddN8nEndpointDialogState();
}

class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
  final _name = TextEditingController();
  final _baseUrl = TextEditingController();
  final _apiKey = TextEditingController();
  final _desc = TextEditingController();

  @override
  void dispose() {
    _name.dispose();
    _baseUrl.dispose();
    _apiKey.dispose();
    _desc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return AlertDialog(
      title: Text(l.addN8nTitle),
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 480),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l.addN8nIntro,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: ChainSpace.md),
            TextField(
              controller: _name,
              autofocus: true,
              decoration: InputDecoration(
                labelText: l.addN8nNameLabel,
                hintText: l.addN8nNameHint,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _baseUrl,
              decoration: InputDecoration(
                labelText: l.addN8nBaseUrlLabel,
                hintText: l.addN8nBaseUrlHint,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _apiKey,
              decoration: InputDecoration(
                labelText: l.addN8nApiKeyLabel,
                hintText: l.addN8nApiKeyHint,
                prefixText: '\$',
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.sm),
            TextField(
              controller: _desc,
              decoration: InputDecoration(
                labelText: l.addN8nNotesLabel,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, null),
          child: Text(l.buttonCancel),
        ),
        FilledButton(
          onPressed: () {
            if (_name.text.trim().isEmpty || _baseUrl.text.trim().isEmpty) {
              return;
            }
            Navigator.pop(
              context,
              N8nEndpointDraft(
                name: _name.text.trim(),
                baseUrl: _baseUrl.text.trim(),
                apiKeyEnv: _apiKey.text.trim(),
                description: _desc.text.trim(),
              ),
            );
          },
          child: Text(l.addN8nAddButton),
        ),
      ],
    );
  }
}

/// One curated MCP-server suggestion — the operator clicks
/// the chip to pre-fill the add-dialog. Strictly informational
/// metadata; nothing is auto-spawned. Trust model:
/// referencing public projects with their canonical install
/// command is the same legal / security shape as a package
/// Theme-plugin picker. Lists installed `studio.theme.*`
/// capabilities (via list_capabilities) and lets the operator
/// pick one or fall back to Studio's built-in palette. The
/// choice is persisted in SharedPreferences and applied at
/// the MaterialApp level via `StudioApp.of(context).setThemePlugin`.
///
/// When no theme plugin is installed the panel renders a
/// short hint pointing at the Store rather than a useless
/// empty dropdown.
class _ThemePluginPanel extends StatelessWidget {
  const _ThemePluginPanel();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.themePluginHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 8),
        // Rich grid of theme tiles. Each shows live swatches
        // from the plugin's own palette so the operator can
        // preview without applying. The "Custom" tile opens
        // a seed-colour dialog.
        const ThemePickerGrid(),
      ],
    );
  }
}

/// Hub maintenance panel — currently one action: reset operator
/// state via `chain reset --yes`. Lives at the bottom of Settings
/// so the everyday config controls aren't crowded by a destructive
/// button. The reset itself is gated by an interactive confirm
/// dialog before the CLI is invoked.
class _MaintenancePanel extends StatefulWidget {
  /// Called after a successful reset so the parent Settings dialog
  /// can bounce its gRPC channel and reload its panels.
  final Future<void> Function() onResetDone;

  const _MaintenancePanel({required this.onResetDone});

  @override
  State<_MaintenancePanel> createState() => _MaintenancePanelState();
}

class _MaintenancePanelState extends State<_MaintenancePanel> {
  bool _keepModules = false;
  bool _keepData = false;
  bool _resetting = false;
  String? _resultText;
  bool _resultOk = false;

  Future<void> _confirmAndReset() async {
    final l = AppLocalizations.of(context)!;
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(l.maintenanceResetConfirmTitle),
        content: Text(l.maintenanceResetConfirmBody),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, false),
            child: Text(l.buttonCancel),
          ),
          FilledButton(
            style: FilledButton.styleFrom(
              backgroundColor: Theme.of(ctx).colorScheme.error,
            ),
            onPressed: () => Navigator.pop(ctx, true),
            child: Text(l.maintenanceResetConfirmButton),
          ),
        ],
      ),
    );
    if (confirmed != true || !mounted) return;
    setState(() {
      _resetting = true;
      _resultText = null;
    });
    final r = await SystemActions.chainReset(
      keepModules: _keepModules,
      keepData: _keepData,
    );
    if (!mounted) return;
    setState(() {
      _resetting = false;
      _resultOk = r.ok;
      _resultText = r.ok
          ? r.stdout.trim()
          : (r.stderr.isEmpty ? r.stdout : r.stderr).trim();
    });
    if (r.ok) await widget.onResetDone();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.maintenanceHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          l.maintenanceResetBlurb,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        CheckboxListTile(
          contentPadding: EdgeInsets.zero,
          dense: true,
          controlAffinity: ListTileControlAffinity.leading,
          value: _keepModules,
          onChanged: _resetting
              ? null
              : (v) => setState(() => _keepModules = v ?? false),
          title: Text(l.maintenanceKeepModulesTitle),
          subtitle: Text(
            l.maintenanceKeepModulesSubtitle,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ),
        CheckboxListTile(
          contentPadding: EdgeInsets.zero,
          dense: true,
          controlAffinity: ListTileControlAffinity.leading,
          value: _keepData,
          onChanged: _resetting
              ? null
              : (v) => setState(() => _keepData = v ?? false),
          title: Text(l.maintenanceKeepDataTitle),
          subtitle: Text(
            l.maintenanceKeepDataSubtitle,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        Row(
          children: [
            FilledButton.icon(
              icon: _resetting
                  ? const SizedBox(
                      width: 14,
                      height: 14,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.restart_alt, size: 16),
              label: Text(
                _resetting
                    ? l.maintenanceResetInProgress
                    : l.maintenanceResetButton,
              ),
              style: FilledButton.styleFrom(
                backgroundColor: theme.colorScheme.errorContainer,
                foregroundColor: theme.colorScheme.onErrorContainer,
              ),
              onPressed: _resetting ? null : _confirmAndReset,
            ),
          ],
        ),
        if (_resultText != null) ...[
          const SizedBox(height: ChainSpace.sm),
          ChainErrorBox(text: _resultText!, isError: !_resultOk, maxHeight: 240),
        ],
      ],
    );
  }
}

/// Credentials panel for the operator's registry auth token.
/// The hub reads the same token via `~/.chain/registry-token`
/// (or the `CHAIN_REGISTRY_TOKEN` env var, which still wins)
/// when downloading `.fai` bundles from a registry behind a
/// signin wall — Forgejo with REQUIRE_SIGNIN_VIEW=true,
/// GitHub-private releases, etc.
///
/// The token itself is never round-tripped back into Studio
/// after save: `configuredChars` is the trimmed length, used
/// only as a "Configured (40 chars)" status display so the
/// operator knows something landed without seeing the secret.
class _RegistryCredentialsPanel extends StatefulWidget {
  /// Trimmed length of the persisted token, or null when none
  /// is configured.
  final int? configuredChars;
  final ValueChanged<String> onSave;
  final Future<void> Function() onClear;

  const _RegistryCredentialsPanel({
    required this.configuredChars,
    required this.onSave,
    required this.onClear,
  });

  @override
  State<_RegistryCredentialsPanel> createState() =>
      _RegistryCredentialsPanelState();
}

class _RegistryCredentialsPanelState extends State<_RegistryCredentialsPanel> {
  final _controller = TextEditingController();
  bool _obscured = true;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final isConfigured = widget.configuredChars != null;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.registryCredentialsHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          l.registryCredentialsBlurb,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        Row(
          children: [
            Icon(
              isConfigured ? Icons.check_circle : Icons.radio_button_unchecked,
              size: 14,
              color: isConfigured
                  ? theme.colorScheme.primary
                  : theme.colorScheme.onSurfaceVariant,
            ),
            const SizedBox(width: ChainSpace.xs),
            Text(
              isConfigured
                  ? l.registryTokenStatusConfigured(widget.configuredChars!)
                  : l.registryTokenStatusNotSet,
              style: theme.textTheme.bodySmall?.copyWith(
                color: isConfigured
                    ? theme.colorScheme.onSurface
                    : theme.colorScheme.onSurfaceVariant,
                fontWeight: isConfigured ? FontWeight.w600 : null,
              ),
            ),
            const Spacer(),
            if (isConfigured)
              TextButton.icon(
                icon: const Icon(Icons.delete_outline, size: 14),
                onPressed: () async {
                  await widget.onClear();
                  _controller.clear();
                },
                label: Text(l.registryTokenClearButton),
              ),
          ],
        ),
        const SizedBox(height: ChainSpace.sm),
        Row(
          children: [
            Expanded(
              child: TextField(
                controller: _controller,
                obscureText: _obscured,
                decoration: InputDecoration(
                  labelText: l.registryTokenFieldLabel,
                  hintText: l.registryTokenFieldHint,
                  border: const OutlineInputBorder(),
                  isDense: true,
                  suffixIcon: IconButton(
                    icon: Icon(
                      _obscured ? Icons.visibility : Icons.visibility_off,
                      size: 16,
                    ),
                    onPressed: () => setState(() => _obscured = !_obscured),
                  ),
                ),
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            FilledButton.icon(
              icon: const Icon(Icons.save_outlined, size: 14),
              label: Text(l.registryTokenSaveButton),
              onPressed: _controller.text.trim().isEmpty
                  ? null
                  : () {
                      widget.onSave(_controller.text);
                      _controller.clear();
                    },
            ),
          ],
        ),
        const SizedBox(height: 4),
        Text(
          l.registryTokenStorageHint,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            fontSize: 11,
          ),
        ),
      ],
    );
  }
}

/// Operator panel for the hub's gRPC bearer token. When the
/// hub has `auth.tokens:` configured, Studio must present
/// `Authorization: Bearer <token>` on every call. The token
/// is stored at `~/.chain/hub-auth-token` (mode 0600) and read
/// at startup by [HubService]; this panel just lets the
/// operator paste / clear without leaving the GUI.
///
/// Same hygiene as the registry-credentials panel: the token
/// itself never round-trips back into Studio after save;
/// `configuredChars` is just the trimmed length for the
/// status display.
class _HubAuthTokenPanel extends StatefulWidget {
  final int? configuredChars;
  final ValueChanged<String> onSave;
  final Future<void> Function() onClear;

  const _HubAuthTokenPanel({
    required this.configuredChars,
    required this.onSave,
    required this.onClear,
  });

  @override
  State<_HubAuthTokenPanel> createState() => _HubAuthTokenPanelState();
}

class _HubAuthTokenPanelState extends State<_HubAuthTokenPanel> {
  final _controller = TextEditingController();
  bool _obscured = true;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final isConfigured = widget.configuredChars != null;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.hubAuthTokenHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          l.hubAuthTokenBlurb,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        Row(
          children: [
            Icon(
              isConfigured ? Icons.check_circle : Icons.radio_button_unchecked,
              size: 14,
              color: isConfigured
                  ? theme.colorScheme.primary
                  : theme.colorScheme.onSurfaceVariant,
            ),
            const SizedBox(width: ChainSpace.xs),
            Text(
              isConfigured
                  ? l.hubAuthTokenStatusConfigured(widget.configuredChars!)
                  : l.hubAuthTokenStatusNotSet,
              style: theme.textTheme.bodySmall?.copyWith(
                color: isConfigured
                    ? theme.colorScheme.onSurface
                    : theme.colorScheme.onSurfaceVariant,
                fontWeight: isConfigured ? FontWeight.w600 : null,
              ),
            ),
            const Spacer(),
            if (isConfigured)
              TextButton.icon(
                icon: const Icon(Icons.delete_outline, size: 14),
                onPressed: () async {
                  await widget.onClear();
                  _controller.clear();
                },
                label: Text(l.hubAuthTokenClearButton),
              ),
          ],
        ),
        const SizedBox(height: ChainSpace.sm),
        Row(
          children: [
            Expanded(
              child: TextField(
                controller: _controller,
                obscureText: _obscured,
                decoration: InputDecoration(
                  labelText: l.hubAuthTokenFieldLabel,
                  hintText: l.hubAuthTokenFieldHint,
                  border: const OutlineInputBorder(),
                  isDense: true,
                  suffixIcon: IconButton(
                    icon: Icon(
                      _obscured ? Icons.visibility : Icons.visibility_off,
                      size: 16,
                    ),
                    onPressed: () => setState(() => _obscured = !_obscured),
                  ),
                ),
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            FilledButton.icon(
              icon: const Icon(Icons.save_outlined, size: 14),
              label: Text(l.hubAuthTokenSaveButton),
              onPressed: _controller.text.trim().isEmpty
                  ? null
                  : () {
                      widget.onSave(_controller.text);
                      _controller.clear();
                    },
            ),
          ],
        ),
        const SizedBox(height: 4),
        Text(
          l.hubAuthTokenStorageHint,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            fontSize: 11,
          ),
        ),
      ],
    );
  }
}

class _McpSuggestion {
  final String name;
  final IconData icon;
  final String endpoint;
  final String apiKeyEnv;

  /// Localizable description. Use [resolveDescription] to fetch
  /// the actual text in the active locale — the const list
  /// can't hold a closure that takes [AppLocalizations], and
  /// hardcoding English here is what got us into the
  /// untranslated-helper-text bug.
  String resolveDescription(AppLocalizations l) =>
      _suggestionDescription(l, name);
  const _McpSuggestion({
    required this.name,
    required this.icon,
    required this.endpoint,
    required this.apiKeyEnv,
  });
}

/// Localizable lookup of an MCP-suggestion description. Keyed
/// on the same suggestion `name` field as the list below. New
/// suggestions need a matching .arb entry; the default-case
/// returns an empty string so an unconfigured suggestion fails
/// silently instead of leaking a key into the UI.
String _suggestionDescription(AppLocalizations l, String name) {
  switch (name) {
    case 'deepwiki':
      return l.mcpSuggestionDeepwikiDesc;
    case 'semgrep':
      return l.mcpSuggestionSemgrepDesc;
    case 'filesystem':
      return l.mcpSuggestionFilesystemDesc;
    case 'fetch':
      return l.mcpSuggestionFetchDesc;
    case 'github':
      return l.mcpSuggestionGithubDesc;
    case 'puppeteer':
      return l.mcpSuggestionPuppeteerDesc;
    case 'postgres':
      return l.mcpSuggestionPostgresDesc;
    case 'sqlite':
      return l.mcpSuggestionSqliteDesc;
    case 'brave-search':
      return l.mcpSuggestionBraveSearchDesc;
    case 'memory':
      return l.mcpSuggestionMemoryDesc;
    case 'time':
      return l.mcpSuggestionTimeDesc;
    default:
      return '';
  }
}

const _kMcpSuggestions = <_McpSuggestion>[
  // ── HTTPS / streamable-HTTP servers (no Node, no API key) ──
  // Descriptions are resolved via [_McpSuggestion.resolveDescription]
  // against the active locale — see `mcpSuggestion*Desc` keys in
  // app_en.arb / app_de.arb. Adding a suggestion here means
  // adding a matching key in both .arb files plus a case to
  // `_suggestionDescription` above.
  _McpSuggestion(
    name: 'deepwiki',
    icon: Icons.menu_book_outlined,
    endpoint: 'https://mcp.deepwiki.com/mcp',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'semgrep',
    icon: Icons.security,
    endpoint: 'https://mcp.semgrep.ai/mcp',
    apiKeyEnv: '',
  ),
  // ── stdio servers (require Node + npx) ───────────────────
  _McpSuggestion(
    name: 'filesystem',
    icon: Icons.folder_outlined,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-filesystem /tmp',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'fetch',
    icon: Icons.cloud_download_outlined,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-fetch',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'github',
    icon: Icons.code,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-github',
    apiKeyEnv: 'GITHUB_PERSONAL_ACCESS_TOKEN',
  ),
  _McpSuggestion(
    name: 'puppeteer',
    icon: Icons.web,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-puppeteer',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'postgres',
    icon: Icons.storage_outlined,
    endpoint:
        'stdio://npx -y @modelcontextprotocol/server-postgres postgresql://user:pw@host/db',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'sqlite',
    icon: Icons.dataset_outlined,
    endpoint:
        'stdio://npx -y @modelcontextprotocol/server-sqlite --db-path /tmp/db.sqlite',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'brave-search',
    icon: Icons.search,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-brave-search',
    apiKeyEnv: 'BRAVE_API_KEY',
  ),
  _McpSuggestion(
    name: 'memory',
    icon: Icons.psychology_outlined,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-memory',
    apiKeyEnv: '',
  ),
  _McpSuggestion(
    name: 'time',
    icon: Icons.schedule,
    endpoint: 'stdio://npx -y @modelcontextprotocol/server-time',
    apiKeyEnv: '',
  ),
];

/// Operator panel for `default_scope:` — the ordered list of
/// publisher segments the bare-form capability resolver consults
/// when a flow names a capability without an explicit
/// `<provider>/` prefix (e.g. `text.extract@^1` resolves to
/// `fai/text.extract@^1` when `default_scope: [fai]`).
///
/// The hub returns the live list plus a catalog-known shortlist
/// (`knownProviders`) — the panel renders the shortlist as
/// suggestion chips and lets operators paste arbitrary entries
/// for private publisher segments.
///
/// The order matters: when more than one segment matches a bare
/// capability name, the first match wins. The panel surfaces the
/// order with simple up/down buttons so operators can reorder
/// without dragging.
class _DefaultScopePanel extends StatefulWidget {
  const _DefaultScopePanel();

  @override
  State<_DefaultScopePanel> createState() => _DefaultScopePanelState();
}

class _DefaultScopePanelState extends State<_DefaultScopePanel> {
  List<String>? _scope;
  List<String> _knownProviders = const [];
  bool _loading = true;
  bool _saving = false;
  String? _error;
  final _addController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _load();
  }

  @override
  void dispose() {
    _addController.dispose();
    super.dispose();
  }

  Future<void> _load() async {
    try {
      final r = await HubService.instance.getDefaultScope();
      if (!mounted) return;
      setState(() {
        _scope = r.scope;
        _knownProviders = r.knownProviders;
        _loading = false;
        _error = null;
      });
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _loading = false;
        _error = e.toString();
      });
    }
  }

  Future<void> _save(List<String> next) async {
    setState(() {
      _saving = true;
      _error = null;
    });
    try {
      final r = await HubService.instance.setDefaultScope(next);
      if (!mounted) return;
      setState(() {
        _scope = r.scope;
        _knownProviders = r.knownProviders;
        _saving = false;
      });
      final l = AppLocalizations.of(context)!;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(l.defaultScopeSavedToast)));
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _saving = false;
        _error = e.toString();
      });
    }
  }

  void _addEntry(String raw) {
    final entry = raw.trim();
    if (entry.isEmpty) return;
    final current = List<String>.from(_scope ?? const []);
    if (current.contains(entry)) return;
    current.add(entry);
    _addController.clear();
    _save(current);
  }

  void _removeAt(int i) {
    final current = List<String>.from(_scope ?? const []);
    if (i < 0 || i >= current.length) return;
    if (current.length == 1) {
      // Hub rejects empty; surface the constraint inline rather
      // than letting the RPC fail.
      setState(
        () => _error = AppLocalizations.of(context)!.defaultScopeNonEmptyError,
      );
      return;
    }
    current.removeAt(i);
    _save(current);
  }

  void _moveUp(int i) {
    if (i <= 0) return;
    final current = List<String>.from(_scope ?? const []);
    final item = current.removeAt(i);
    current.insert(i - 1, item);
    _save(current);
  }

  void _moveDown(int i) {
    final current = List<String>.from(_scope ?? const []);
    if (i >= current.length - 1) return;
    final item = current.removeAt(i);
    current.insert(i + 1, item);
    _save(current);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final scope = _scope;
    final unusedSuggestions = _knownProviders
        .where((p) => !(scope ?? const []).contains(p))
        .toList();

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.defaultScopeHeader,
          style: theme.textTheme.labelSmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
            letterSpacing: 0.6,
            fontSize: 10,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          l.defaultScopeBlurb,
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.sm),
        if (_loading)
          const Padding(
            padding: EdgeInsets.symmetric(vertical: ChainSpace.sm),
            child: SizedBox(
              height: 16,
              width: 16,
              child: CircularProgressIndicator(strokeWidth: 2),
            ),
          )
        else if (scope == null)
          Text(
            _error ?? l.defaultScopeLoadFailed,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.error,
            ),
          )
        else ...[
          for (var i = 0; i < scope.length; i++)
            _ScopeRow(
              index: i,
              total: scope.length,
              entry: scope[i],
              busy: _saving,
              onUp: () => _moveUp(i),
              onDown: () => _moveDown(i),
              onRemove: () => _removeAt(i),
            ),
          const SizedBox(height: ChainSpace.sm),
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _addController,
                  decoration: InputDecoration(
                    labelText: l.defaultScopeAddLabel,
                    hintText: l.defaultScopeAddHint,
                    border: const OutlineInputBorder(),
                    isDense: true,
                  ),
                  onSubmitted: _saving ? null : _addEntry,
                ),
              ),
              const SizedBox(width: ChainSpace.sm),
              FilledButton.icon(
                icon: const Icon(Icons.add, size: 14),
                label: Text(l.defaultScopeAddButton),
                onPressed: _saving
                    ? null
                    : () => _addEntry(_addController.text),
              ),
            ],
          ),
          if (unusedSuggestions.isNotEmpty) ...[
            const SizedBox(height: ChainSpace.sm),
            Text(
              l.defaultScopeSuggestions,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                fontSize: 11,
              ),
            ),
            const SizedBox(height: 4),
            Wrap(
              spacing: ChainSpace.xs,
              runSpacing: 4,
              children: [
                for (final s in unusedSuggestions)
                  ActionChip(
                    label: Text(
                      s,
                      style: const TextStyle(fontFamily: 'monospace'),
                    ),
                    onPressed: _saving ? null : () => _addEntry(s),
                  ),
              ],
            ),
          ],
          if (_error != null) ...[
            const SizedBox(height: ChainSpace.sm),
            // Copyable — errors must be selectable + one-tap copyable.
            ChainErrorBox(text: _error!, isError: true, maxHeight: 200),
          ],
        ],
      ],
    );
  }
}

class _ScopeRow extends StatelessWidget {
  final int index;
  final int total;
  final String entry;
  final bool busy;
  final VoidCallback onUp;
  final VoidCallback onDown;
  final VoidCallback onRemove;

  const _ScopeRow({
    required this.index,
    required this.total,
    required this.entry,
    required this.busy,
    required this.onUp,
    required this.onDown,
    required this.onRemove,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
            width: 24,
            child: Text(
              '${index + 1}.',
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
          Expanded(
            child: Text(entry, style: const TextStyle(fontFamily: 'monospace')),
          ),
          IconButton(
            icon: const Icon(Icons.arrow_upward, size: 16),
            tooltip: AppLocalizations.of(context)!.tooltipMoveUp,
            onPressed: busy || index == 0 ? null : onUp,
            visualDensity: VisualDensity.compact,
          ),
          IconButton(
            icon: const Icon(Icons.arrow_downward, size: 16),
            tooltip: AppLocalizations.of(context)!.tooltipMoveDown,
            onPressed: busy || index == total - 1 ? null : onDown,
            visualDensity: VisualDensity.compact,
          ),
          IconButton(
            icon: const Icon(Icons.delete_outline, size: 16),
            tooltip: AppLocalizations.of(context)!.tooltipRemove,
            onPressed: busy ? null : onRemove,
            visualDensity: VisualDensity.compact,
          ),
        ],
      ),
    );
  }
}

/// macOS-style Settings sidebar — an icon + label per category,
/// stable position, highlighted current row. Replaces the
/// previous "one long vertical scroll" with focused panels.
class _Sidebar extends StatelessWidget {
  final _Category current;
  final ValueChanged<_Category> onSelect;

  const _Sidebar({required this.current, required this.onSelect});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final items = <(_Category, IconData, String)>[
      (_Category.general, Icons.tune, l.settingsCategoryGeneral),
      (
        _Category.appearance,
        Icons.palette_outlined,
        l.settingsCategoryAppearance,
      ),
      (_Category.ai, Icons.psychology_outlined, l.settingsCategoryAi),
      (
        _Category.integrations,
        Icons.hub_outlined,
        l.settingsCategoryIntegrations,
      ),
      (_Category.security, Icons.shield_outlined, l.settingsCategorySecurity),
      (
        _Category.maintenance,
        Icons.build_outlined,
        l.settingsCategoryMaintenance,
      ),
    ];
    return SizedBox(
      width: 220,
      child: Container(
        color: theme.colorScheme.surfaceContainer,
        child: ListView(
          padding: const EdgeInsets.symmetric(vertical: ChainSpace.lg),
          children: [
            for (final (cat, icon, label) in items)
              _SidebarRow(
                icon: icon,
                label: label,
                selected: cat == current,
                onTap: () => onSelect(cat),
              ),
          ],
        ),
      ),
    );
  }
}

class _SidebarRow extends StatelessWidget {
  final IconData icon;
  final String label;
  final bool selected;
  final VoidCallback onTap;

  const _SidebarRow({
    required this.icon,
    required this.label,
    required this.selected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final fg = selected
        ? theme.colorScheme.onSurface
        : theme.colorScheme.onSurfaceVariant;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: ChainSpace.sm, vertical: 2),
      child: Material(
        color: selected
            ? theme.colorScheme.primary.withValues(alpha: 0.14)
            : Colors.transparent,
        borderRadius: BorderRadius.circular(ChainRadius.sm),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(ChainRadius.sm),
          child: Padding(
            padding: const EdgeInsets.symmetric(
              horizontal: ChainSpace.md,
              vertical: 8,
            ),
            child: Row(
              children: [
                Icon(icon, size: 16, color: fg),
                const SizedBox(width: ChainSpace.sm),
                Text(
                  label,
                  style: theme.textTheme.bodyMedium?.copyWith(
                    color: fg,
                    fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
