// Welcome — first surface a new operator sees, default sidebar
// destination on launch. Pure introduction: what Ch∆In is, the
// three concepts, the trust posture. No live data, no
// federation chrome — that's the Store's role.
//
// See docs/landing-page-design.md for the multi-phase plan;
// this file is Phase A (scaffolding + static content). Phase B
// adds an embedded markdown reader; Phase C adds a live
// getting-started checklist.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../data/hub.dart';
import '../data/system_actions.dart';
import '../l10n/app_localizations.dart';
import '../main.dart' show StudioShellState;
import '../theme/theme.dart';
import '../theme/tokens.dart';
import '../widgets/widgets.dart';

class WelcomePage extends StatelessWidget {
  const WelcomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    // The shell polls hub health on a 5 s tick. When the hub is
    // down a brand-new operator otherwise sees a dead, all-
    // unchecked onboarding checklist with no obvious recovery —
    // so we lead with a "start the hub" hero instead.
    final hubDown = StudioShellState.of(context)?.connected == false;
    return Scaffold(
      backgroundColor: theme.scaffoldBackgroundColor,
      appBar: AppBar(titleSpacing: ChainSpace.xl, title: Text(l.navWelcome)),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.fromLTRB(
            ChainSpace.xl,
            ChainSpace.lg,
            ChainSpace.xl,
            ChainSpace.xxl,
          ),
          // Center the 960-px content column on wide windows
          // so the right margin matches the left rather than
          // dumping all the slack onto one side. 960 px stays
          // an opinionated reading width — long-form prose
          // gets uncomfortable past ~1100 px — but on Stefan's
          // 1440-px default it now sits with ~190 px of
          // breathing room on both sides instead of 380 px
          // on the right and 20 px on the left.
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 960),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const _Hero(),
                  const SizedBox(height: ChainSpace.xxl),
                  // When the hub is down the onboarding checklist
                  // would render dead (all-unchecked, nothing to
                  // probe). Lead with a "start the hub" card
                  // instead; restore the checklist once connected.
                  if (hubDown) ...[
                    const _HubDownHero(),
                    const SizedBox(height: ChainSpace.xxl),
                  ] else
                    const _OnboardingChecklist(),
                  const _PillarRow(),
                  const SizedBox(height: ChainSpace.xxl),
                  const _SectionLabel(textKey: _SectionLabelKey.trust),
                  const SizedBox(height: ChainSpace.md),
                  const _TrustPosture(),
                  const SizedBox(height: ChainSpace.xxl),
                  const _SectionLabel(textKey: _SectionLabelKey.docs),
                  const SizedBox(height: ChainSpace.md),
                  const _DocsRow(),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Top hero — gradient backdrop matching the Today-Hero in the
/// store so the visual language carries between pages. Title +
/// subtitle, no CTAs.
class _Hero extends StatelessWidget {
  const _Hero();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(ChainSpace.xxl),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(ChainRadius.md),
        gradient: LinearGradient(
          colors: [
            theme.colorScheme.primary.withValues(alpha: 0.18),
            theme.colorScheme.primary.withValues(alpha: 0.04),
            theme.colorScheme.surfaceContainer.withValues(alpha: 0.0),
          ],
          stops: const [0.0, 0.55, 1.0],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        border: Border.all(
          color: theme.colorScheme.primary.withValues(alpha: 0.25),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(ChainSpace.md),
                decoration: BoxDecoration(
                  color: theme.colorScheme.primary.withValues(alpha: 0.14),
                  shape: BoxShape.circle,
                ),
                child: Icon(
                  Icons.waving_hand_outlined,
                  size: 28,
                  color: theme.colorScheme.primary,
                ),
              ),
              const SizedBox(width: ChainSpace.lg),
              Expanded(
                child: Text(
                  l.welcomeHeroTitle,
                  style: theme.textTheme.displaySmall?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: ChainSpace.lg),
          Text(
            l.welcomeHeroSubtitle,
            style: theme.textTheme.titleMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
              height: 1.45,
            ),
          ),
        ],
      ),
    );
  }
}

/// Prominent recovery hero shown on the Welcome page when the
/// hub is not reachable. Headline + one-line explanation + a big
/// "Start hub" CTA wired to the shell's daemon-start path, with
/// a secondary link to the install guide for the case where no
/// `fai` binary can be located.
class _HubDownHero extends StatefulWidget {
  const _HubDownHero();

  @override
  State<_HubDownHero> createState() => _HubDownHeroState();
}

class _HubDownHeroState extends State<_HubDownHero> {
  bool _starting = false;

  Future<void> _start() async {
    final shell = StudioShellState.of(context);
    if (shell == null) return;
    setState(() => _starting = true);
    await shell.startDaemon();
    if (!mounted) return;
    setState(() => _starting = false);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    // No `fai` binary → "Start hub" cannot work; lead the
    // operator to the install guide instead.
    final canStart = SystemActions.chainBinaryExists();
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(ChainSpace.xl),
      decoration: BoxDecoration(
        color: theme.colorScheme.errorContainer.withValues(alpha: 0.35),
        borderRadius: BorderRadius.circular(ChainRadius.md),
        border: Border.all(
          color: theme.colorScheme.error.withValues(alpha: 0.4),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(
                Icons.cloud_off_outlined,
                size: 24,
                color: theme.colorScheme.error,
              ),
              const SizedBox(width: ChainSpace.md),
              Expanded(
                child: Text(
                  l.welcomeHubDownTitle,
                  style: theme.textTheme.titleLarge?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: ChainSpace.md),
          Text(
            l.welcomeHubDownBody,
            style: theme.textTheme.bodyMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
              height: 1.45,
            ),
          ),
          const SizedBox(height: ChainSpace.xs),
          Text(
            l.welcomeHubDownEndpoint(HubService.instance.endpointLabel),
            style: ChainTheme.mono(
              size: 11,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          const SizedBox(height: ChainSpace.lg),
          if (canStart)
            FilledButton.icon(
              onPressed: _starting ? null : _start,
              icon: _starting
                  ? const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.play_arrow),
              label: Text(
                _starting ? l.welcomeHubDownStarting : l.welcomeHubDownStart,
              ),
            )
          else
            const ChainBinaryRecovery(),
          const SizedBox(height: ChainSpace.sm),
          TextButton.icon(
            icon: const Icon(Icons.open_in_new, size: 16),
            label: Text(l.welcomeHubDownDocsLink),
            onPressed: () => SystemActions.openInOs(kFaiInstallDocsUrl),
          ),
        ],
      ),
    );
  }
}

enum _SectionLabelKey { trust, docs }

class _SectionLabel extends StatelessWidget {
  final _SectionLabelKey textKey;
  const _SectionLabel({required this.textKey});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final text = switch (textKey) {
      _SectionLabelKey.trust => l.welcomeTrustHeader,
      _SectionLabelKey.docs => l.welcomeDocsHeader,
    };
    return Text(
      text,
      style: theme.textTheme.labelSmall?.copyWith(
        color: theme.colorScheme.onSurfaceVariant,
        letterSpacing: 0.6,
        fontSize: 10,
      ),
    );
  }
}

/// "Hub. Module. Flow." three-card row. Stacks on narrow
/// windows so card text never gets cropped.
class _PillarRow extends StatelessWidget {
  const _PillarRow();

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    final pillars = <(IconData, String, String)>[
      (Icons.hub_outlined, l.welcomePillarHubTitle, l.welcomePillarHubBody),
      (
        Icons.extension_outlined,
        l.welcomePillarModuleTitle,
        l.welcomePillarModuleBody,
      ),
      (
        Icons.account_tree_outlined,
        l.welcomePillarFlowTitle,
        l.welcomePillarFlowBody,
      ),
    ];
    return LayoutBuilder(
      builder: (context, constraints) {
        // ~640 dp is the comfortable threshold for three cards
        // side by side; below that we stack so each card
        // gets its full text width.
        final stacked = constraints.maxWidth < 640;
        if (stacked) {
          return Column(
            children: [
              for (final p in pillars) ...[
                _Pillar(icon: p.$1, title: p.$2, body: p.$3),
                const SizedBox(height: ChainSpace.md),
              ],
            ],
          );
        }
        // IntrinsicHeight bounds the Row's height so
        // `crossAxisAlignment: stretch` has something to
        // stretch against. Without it the Row inherits the
        // unbounded height of the surrounding
        // `SingleChildScrollView` and Flutter throws
        // "BoxConstraints forces an infinite height".
        return IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              for (var i = 0; i < pillars.length; i++) ...[
                if (i > 0) const SizedBox(width: ChainSpace.md),
                Expanded(
                  child: _Pillar(
                    icon: pillars[i].$1,
                    title: pillars[i].$2,
                    body: pillars[i].$3,
                  ),
                ),
              ],
            ],
          ),
        );
      },
    );
  }
}

class _Pillar extends StatelessWidget {
  final IconData icon;
  final String title;
  final String body;

  const _Pillar({required this.icon, required this.title, required this.body});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Container(
      padding: const EdgeInsets.all(ChainSpace.lg),
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainer,
        borderRadius: BorderRadius.circular(ChainRadius.md),
        border: Border.all(color: theme.colorScheme.outlineVariant),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 28, color: theme.colorScheme.primary),
          const SizedBox(height: ChainSpace.md),
          Text(
            title,
            style: theme.textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.w700,
            ),
          ),
          const SizedBox(height: ChainSpace.sm),
          Text(
            body,
            style: theme.textTheme.bodyMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
              height: 1.45,
            ),
          ),
        ],
      ),
    );
  }
}

/// Three trust-posture rows: Sandbox / Audit / Air-gap. Used to
/// live as carousel slides in the store; reading them here as
/// a single deck reads better than rotating through them.
class _TrustPosture extends StatelessWidget {
  const _TrustPosture();

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    final rows = <(IconData, String, String)>[
      (
        Icons.shield_outlined,
        l.welcomeTrustSandboxTitle,
        l.welcomeTrustSandboxBody,
      ),
      (
        Icons.verified_outlined,
        l.welcomeTrustAuditTitle,
        l.welcomeTrustAuditBody,
      ),
      (
        Icons.rocket_launch_outlined,
        l.welcomeTrustAirgapTitle,
        l.welcomeTrustAirgapBody,
      ),
    ];
    return Column(
      children: [
        for (var i = 0; i < rows.length; i++) ...[
          if (i > 0) const SizedBox(height: ChainSpace.sm),
          _TrustRow(icon: rows[i].$1, title: rows[i].$2, body: rows[i].$3),
        ],
      ],
    );
  }
}

class _TrustRow extends StatelessWidget {
  final IconData icon;
  final String title;
  final String body;

  const _TrustRow({
    required this.icon,
    required this.title,
    required this.body,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Container(
      padding: const EdgeInsets.all(ChainSpace.lg),
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainer,
        borderRadius: BorderRadius.circular(ChainRadius.md),
        border: Border.all(color: theme.colorScheme.outlineVariant),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 22, color: theme.colorScheme.primary),
          const SizedBox(width: ChainSpace.lg),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: theme.textTheme.titleSmall?.copyWith(
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  body,
                  style: theme.textTheme.bodyMedium?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                    height: 1.45,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// SharedPreferences key the checklist persists "operator
/// dismissed me" under. Once true, the checklist is gone for
/// good — this is intentional; we don't want to nag operators
/// who chose a non-default path (e.g. CLI-only, no Studio
/// onboarding needed).
const String _kChecklistDismissedKey = 'welcome.checklist.dismissed';

/// Live getting-started checklist. Probes the running hub for
/// four signals that the operator has the basic loop wired up
/// (System AI configured, MCP source added, text module
/// installed, any flow run). Each signal is fetched in
/// parallel; failures stay false and the checklist still
/// renders (with the "Refresh" button for retry).
class _OnboardingChecklist extends StatefulWidget {
  const _OnboardingChecklist();

  @override
  State<_OnboardingChecklist> createState() => _OnboardingChecklistState();
}

class _OnboardingChecklistState extends State<_OnboardingChecklist> {
  bool _dismissed = false;
  bool _initialised = false;
  bool _refreshing = false;
  bool _aiOk = false;
  bool _mcpOk = false;
  bool _moduleOk = false;
  bool _flowOk = false;

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

  Future<void> _bootstrap() async {
    final prefs = await SharedPreferences.getInstance();
    if (!mounted) return;
    setState(() {
      _dismissed = prefs.getBool(_kChecklistDismissedKey) ?? false;
      _initialised = true;
    });
    if (!_dismissed) {
      await _refresh();
    }
  }

  Future<void> _refresh() async {
    setState(() => _refreshing = true);
    final hub = HubService.instance;
    // Probe each signal independently. A failure in one (e.g.
    // hub temporarily unreachable) leaves the others
    // computable; the checklist degrades gracefully.
    final ai = hub
        .systemAiStatus()
        .then((s) => s.enabled)
        .catchError((_) => false);
    final mcp = hub
        .listMcpClients()
        .then((c) => c.isNotEmpty)
        .catchError((_) => false);
    final module = hub
        .listModules()
        // "A module is installed" = any installed module providing a
        // non-built-in capability. We match on the capability rather
        // than the module name (which is hyphenated, e.g.
        // `text-extract`), and accept ANY provider — so the doc'd
        // first install of `debug.echo` ticks the box, not just
        // `text.*`. `system.*` are built-ins, not installs.
        .then(
          (m) => m.any(
            (x) => x.capabilities.any((c) => !c.startsWith('system.')),
          ),
        )
        .catchError((_) => false);
    final flow = hub
        .recentEvents(limit: 100)
        .then((events) => events.any((e) => e.type == 'flow.completed'))
        .catchError((_) => false);
    final results = await Future.wait([ai, mcp, module, flow]);
    if (!mounted) return;
    setState(() {
      _refreshing = false;
      _aiOk = results[0];
      _mcpOk = results[1];
      _moduleOk = results[2];
      _flowOk = results[3];
    });
  }

  Future<void> _dismiss() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_kChecklistDismissedKey, true);
    if (!mounted) return;
    setState(() => _dismissed = true);
  }

  @override
  Widget build(BuildContext context) {
    if (!_initialised || _dismissed) return const SizedBox.shrink();
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final allDone = _aiOk && _mcpOk && _moduleOk && _flowOk;
    // Each row gets an `onTap` that takes the operator to
    // the page where they can actually complete the item.
    // Replaces the old "read the hint then guess where the
    // feature lives" behaviour with a one-click hop.
    final shell = StudioShellState.of(context);
    final items = <(bool, String, String, VoidCallback?)>[
      (
        _aiOk,
        l.welcomeChecklistAi,
        l.welcomeChecklistAiHint,
        () => ChainSettingsDialog.show(context),
      ),
      (
        _mcpOk,
        l.welcomeChecklistMcp,
        l.welcomeChecklistMcpHint,
        () => ChainSettingsDialog.show(context),
      ),
      (
        _moduleOk,
        l.welcomeChecklistModule,
        l.welcomeChecklistModuleHint,
        shell == null ? null : () => shell.navigateTo('store'),
      ),
      (
        _flowOk,
        l.welcomeChecklistFlow,
        l.welcomeChecklistFlowHint,
        shell == null ? null : () => shell.navigateTo('flows'),
      ),
    ];
    return Padding(
      padding: const EdgeInsets.only(bottom: ChainSpace.xxl),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Text(
                l.welcomeChecklistHeader,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                  letterSpacing: 0.6,
                  fontSize: 10,
                ),
              ),
              const Spacer(),
              IconButton(
                icon: _refreshing
                    ? const SizedBox(
                        width: 14,
                        height: 14,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.refresh, size: 16),
                visualDensity: VisualDensity.compact,
                tooltip: _refreshing
                    ? l.welcomeChecklistRefreshing
                    : l.welcomeChecklistRefresh,
                onPressed: _refreshing ? null : _refresh,
              ),
            ],
          ),
          const SizedBox(height: 4),
          Text(
            l.welcomeChecklistBody,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          const SizedBox(height: ChainSpace.md),
          Container(
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainer,
              borderRadius: BorderRadius.circular(ChainRadius.md),
              border: Border.all(color: theme.colorScheme.outlineVariant),
            ),
            child: Column(
              children: [
                for (var i = 0; i < items.length; i++) ...[
                  if (i > 0)
                    Divider(height: 1, color: theme.colorScheme.outlineVariant),
                  _ChecklistRow(
                    done: items[i].$1,
                    title: items[i].$2,
                    hint: items[i].$3,
                    onTap: items[i].$4,
                  ),
                ],
              ],
            ),
          ),
          if (allDone) ...[
            const SizedBox(height: ChainSpace.md),
            _AllDoneCelebration(onDismiss: _dismiss),
          ],
        ],
      ),
    );
  }
}

/// Shown beneath the four-step checklist once every signal
/// flips to done. Three concrete next-threads the operator
/// can pull on, plus the dismiss button. Each suggestion
/// links somewhere meaningful inside Studio (Audit page,
/// Today doc, Flows doc) — no external browser calls.
class _AllDoneCelebration extends StatelessWidget {
  final VoidCallback onDismiss;
  const _AllDoneCelebration({required this.onDismiss});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Container(
      padding: const EdgeInsets.all(ChainSpace.lg),
      decoration: BoxDecoration(
        color: ChainColors.success.withValues(alpha: 0.08),
        borderRadius: BorderRadius.circular(ChainRadius.md),
        border: Border.all(color: ChainColors.success.withValues(alpha: 0.35)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.celebration_outlined, color: ChainColors.success),
              const SizedBox(width: ChainSpace.sm),
              Expanded(
                child: Text(
                  l.welcomeChecklistAllSetTitle,
                  style: theme.textTheme.titleSmall?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
              TextButton(
                onPressed: onDismiss,
                child: Text(l.welcomeChecklistDismiss),
              ),
            ],
          ),
          const SizedBox(height: 4),
          Text(
            l.welcomeChecklistAllSetBody,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          const SizedBox(height: ChainSpace.md),
          _NextStepRow(
            icon: Icons.timeline_outlined,
            title: l.welcomeChecklistNextAuditTitle,
            body: l.welcomeChecklistNextAuditBody,
            buttonLabel: l.welcomeChecklistNextAuditButton,
            doc: null,
            onOpenDoc: null,
          ),
          _NextStepRow(
            icon: Icons.auto_awesome,
            title: l.welcomeChecklistNextTodayTitle,
            body: l.welcomeChecklistNextTodayBody,
            buttonLabel: l.welcomeChecklistNextTodayButton,
            doc: _kDocs.firstWhere(
              (d) => d.slug == 'flows',
              orElse: () => _kDocs.first,
            ),
            onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
          ),
          _NextStepRow(
            icon: Icons.extension_outlined,
            title: l.welcomeChecklistNextModuleTitle,
            body: l.welcomeChecklistNextModuleBody,
            buttonLabel: l.welcomeChecklistNextModuleButton,
            doc: _kDocs.first,
            onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
          ),
        ],
      ),
    );
  }
}

class _NextStepRow extends StatelessWidget {
  final IconData icon;
  final String title;
  final String body;
  final String buttonLabel;
  final _DocEntry? doc;
  final void Function(_DocEntry)? onOpenDoc;

  const _NextStepRow({
    required this.icon,
    required this.title,
    required this.body,
    required this.buttonLabel,
    required this.doc,
    required this.onOpenDoc,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final canActivate = doc != null && onOpenDoc != null;
    return Padding(
      padding: const EdgeInsets.only(bottom: ChainSpace.sm),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 18, color: theme.colorScheme.primary),
          const SizedBox(width: ChainSpace.md),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: theme.textTheme.bodyMedium?.copyWith(
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 2),
                Text(
                  body,
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(width: ChainSpace.md),
          OutlinedButton(
            onPressed: canActivate ? () => onOpenDoc!(doc!) : null,
            child: Text(buttonLabel),
          ),
        ],
      ),
    );
  }
}

class _ChecklistRow extends StatelessWidget {
  final bool done;
  final String title;
  final String hint;

  /// Tap handler — takes the operator to the page where
  /// they can actually complete the item. null = no-op
  /// (e.g. AI item when System AI was already configured).
  final VoidCallback? onTap;

  const _ChecklistRow({
    required this.done,
    required this.title,
    required this.hint,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final row = Padding(
      padding: const EdgeInsets.all(ChainSpace.lg),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(
            done ? Icons.check_circle : Icons.radio_button_unchecked,
            size: 20,
            color: done ? ChainColors.success : theme.colorScheme.outline,
          ),
          const SizedBox(width: ChainSpace.lg),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: theme.textTheme.titleSmall?.copyWith(
                    fontWeight: FontWeight.w600,
                    color: done
                        ? theme.colorScheme.onSurfaceVariant
                        : theme.colorScheme.onSurface,
                    decoration: done ? TextDecoration.lineThrough : null,
                  ),
                ),
                const SizedBox(height: 2),
                Text(
                  hint,
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(width: ChainSpace.md),
          ChainPill(
            label: done ? l.welcomeChecklistDone : l.welcomeChecklistTodo,
            tone: done ? ChainPillTone.success : ChainPillTone.neutral,
          ),
        ],
      ),
    );
    // Wrap in InkWell only when a tap target exists so done
    // rows (no action needed) don't shimmer hover-feedback
    // they can't act on.
    if (onTap == null) return row;
    return InkWell(onTap: onTap, child: row);
  }
}

/// Doc-card entries — each links to a bundled markdown file
/// under `assets/docs/`. The slug is the filename stem; the
/// loader picks `<slug>.md` or `<slug>_de.md` per locale.
class _DocEntry {
  final String slug;
  final IconData icon;
  final String Function(AppLocalizations) title;
  final String Function(AppLocalizations) blurb;
  const _DocEntry({
    required this.slug,
    required this.icon,
    required this.title,
    required this.blurb,
  });
}

final List<_DocEntry> _kDocs = <_DocEntry>[
  _DocEntry(
    slug: 'architecture',
    icon: Icons.account_tree_outlined,
    title: (l) => l.welcomeDocArchitectureTitle,
    blurb: (l) => l.welcomeDocArchitectureBlurb,
  ),
  _DocEntry(
    slug: 'security',
    icon: Icons.shield_outlined,
    title: (l) => l.welcomeDocSecurityTitle,
    blurb: (l) => l.welcomeDocSecurityBlurb,
  ),
  _DocEntry(
    slug: 'audit',
    icon: Icons.verified_outlined,
    title: (l) => l.welcomeDocAuditTitle,
    blurb: (l) => l.welcomeDocAuditBlurb,
  ),
  _DocEntry(
    slug: 'flows',
    icon: Icons.alt_route_outlined,
    title: (l) => l.welcomeDocFlowsTitle,
    blurb: (l) => l.welcomeDocFlowsBlurb,
  ),
  _DocEntry(
    slug: 'approvals',
    icon: Icons.gavel_outlined,
    title: (l) => l.welcomeDocApprovalsTitle,
    blurb: (l) => l.welcomeDocApprovalsBlurb,
  ),
];

/// Public entry-point for the doc-reader sheet. Pass a slug
/// ("approvals", "audit", "security", "architecture", "flows")
/// and the bottom sheet opens with the localized markdown.
/// Returns null when the slug isn't registered — caller can
/// log + show a SnackBar.
Future<void>? showFaiDoc(BuildContext context, String slug) {
  final entry = _kDocs.firstWhere(
    (d) => d.slug == slug,
    orElse: () => _kDocs.first,
  );
  return _DocReaderSheet.show(context, entry);
}

/// Two-by-two doc grid on wide windows, single column on
/// narrow. Each card opens `_DocReaderSheet` for its slug.
class _DocsRow extends StatelessWidget {
  const _DocsRow();

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          l.welcomeDocsBody,
          style: Theme.of(context).textTheme.bodySmall?.copyWith(
            color: Theme.of(context).colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: ChainSpace.md),
        LayoutBuilder(
          builder: (context, constraints) {
            final twoCols = constraints.maxWidth >= 640;
            // Narrow windows stack to a single column. Putting
            // `width: double.infinity` SizedBoxes into a Wrap
            // breaks Wrap's intrinsic-width math; a Column with
            // unconstrained child widths is the correct fit.
            if (!twoCols) {
              return Column(
                children: [
                  for (var i = 0; i < _kDocs.length; i++) ...[
                    if (i > 0) const SizedBox(height: ChainSpace.md),
                    _DocCard(entry: _kDocs[i]),
                  ],
                ],
              );
            }
            // Pair the cards into equal-height rows: IntrinsicHeight
            // + stretch makes the two cards in a row match height
            // instead of the Wrap's ragged tops (their blurbs differ
            // in length, especially across locales). An odd trailing
            // card spans the full width so it reads as intentional
            // rather than a lonely half-box with dead space beside it.
            final rows = <Widget>[];
            for (var i = 0; i < _kDocs.length; i += 2) {
              if (rows.isNotEmpty) {
                rows.add(const SizedBox(height: ChainSpace.md));
              }
              final left = _kDocs[i];
              final right = i + 1 < _kDocs.length ? _kDocs[i + 1] : null;
              if (right == null) {
                rows.add(_DocCard(entry: left));
              } else {
                rows.add(
                  IntrinsicHeight(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        Expanded(child: _DocCard(entry: left)),
                        const SizedBox(width: ChainSpace.md),
                        Expanded(child: _DocCard(entry: right)),
                      ],
                    ),
                  ),
                );
              }
            }
            return Column(children: rows);
          },
        ),
      ],
    );
  }
}

class _DocCard extends StatefulWidget {
  final _DocEntry entry;
  const _DocCard({required this.entry});

  @override
  State<_DocCard> createState() => _DocCardState();
}

class _DocCardState extends State<_DocCard> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final entry = widget.entry;
    // Hover-lift mirrors the Store cards: a gentle 2px rise plus
    // shadow bump. Resting state stays flat-with-border so the
    // doc strip reads calm until the cursor invites a click.
    return MouseRegion(
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: AnimatedContainer(
        duration: ChainMotion.base,
        curve: ChainMotion.easing,
        transform: Matrix4.translationValues(0, _hovered ? -2 : 0, 0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(ChainRadius.md),
          boxShadow: _hovered ? ChainElevation.medium(theme.brightness) : null,
        ),
        child: Material(
          color: theme.colorScheme.surfaceContainer,
          borderRadius: BorderRadius.circular(ChainRadius.md),
          child: InkWell(
            onTap: () => _DocReaderSheet.show(context, entry),
            borderRadius: BorderRadius.circular(ChainRadius.md),
            child: Container(
              padding: const EdgeInsets.all(ChainSpace.lg),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(ChainRadius.md),
                border: Border.all(color: theme.colorScheme.outlineVariant),
              ),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Icon(entry.icon, size: 22, color: theme.colorScheme.primary),
                  const SizedBox(width: ChainSpace.lg),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          entry.title(l),
                          style: theme.textTheme.titleSmall?.copyWith(
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 4),
                        Text(
                          entry.blurb(l),
                          style: theme.textTheme.bodySmall?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                            height: 1.4,
                          ),
                        ),
                      ],
                    ),
                  ),
                  Icon(
                    Icons.chevron_right,
                    size: 18,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Structured error type for the doc reader. Allows the
/// error UI to render distinct lines for "what we tried",
/// "what to try next", and "the raw underlying error" — and
/// to surface the Forgejo browser fallback as a one-click
/// link instead of a sentence to copy.
class _DocReaderError {
  final String slug;
  final String locale;
  final List<String> attempted;
  final String underlying;
  const _DocReaderError({
    required this.slug,
    required this.locale,
    required this.attempted,
    required this.underlying,
  });

  String get forgejoUrl =>
      'https://git.flemming.ai/fai/chain/src/branch/main/docs/architecture/$slug.md';

  /// Localized, operator-facing failure body. Use this for any
  /// on-screen rendering; [toString] stays English for logs.
  String localizedBody(AppLocalizations l) => l.welcomeDocLoadFailedBody(
    slug,
    locale,
    attempted.join(', '),
    underlying,
  );

  @override
  String toString() =>
      'Could not load the bundled doc "$slug" ($locale).\n'
      'Tried: ${attempted.join(", ")}\n'
      'Underlying: $underlying\n'
      'This typically means the Studio binary on disk is from\n'
      'before the doc bundle changed. Rebuild Studio (chain studio\n'
      'or the build pipeline) or open the Forgejo copy in browser.';
}

/// Modal bottom-sheet that loads the markdown for a doc entry
/// from `assets/docs/<slug>[_de].md` and renders it inline via
/// flutter_markdown. No browser, no network — KRITIS-friendly.
class _DocReaderSheet extends StatefulWidget {
  final _DocEntry entry;

  const _DocReaderSheet({required this.entry});

  static Future<void> show(BuildContext context, _DocEntry entry) {
    return showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
      elevation: 8,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(ChainRadius.md)),
      ),
      builder: (_) => _DocReaderSheet(entry: entry),
    );
  }

  @override
  State<_DocReaderSheet> createState() => _DocReaderSheetState();
}

class _DocReaderSheetState extends State<_DocReaderSheet> {
  Future<String>? _content;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // `_load()` needs `Localizations.localeOf(context)`. That call
    // touches InheritedWidgets which aren't safe to read in
    // initState() — must wait until didChangeDependencies. We
    // gate on `_content == null` so it runs exactly once.
    _content ??= _load();
  }

  Future<String> _load() async {
    final locale = Localizations.localeOf(context).languageCode;
    final localised = 'assets/docs/${widget.entry.slug}_$locale.md';
    final fallback = 'assets/docs/${widget.entry.slug}.md';
    // Cache-bust attempts. If the operator is running a stale
    // build from before the flutter_markdown_plus migration,
    // the .dart_tool asset manifest can be inconsistent with
    // the compiled binary — surface "rebuild Studio" as a
    // first-class hint rather than just dumping the raw
    // PlatformException string into ChainErrorBox.
    try {
      return await rootBundle.loadString(localised);
    } catch (firstErr) {
      try {
        return await rootBundle.loadString(fallback);
      } catch (secondErr) {
        throw _DocReaderError(
          slug: widget.entry.slug,
          locale: locale,
          attempted: [localised, fallback],
          underlying: '$firstErr / $secondErr',
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final maxHeight = MediaQuery.of(context).size.height * 0.85;
    return ConstrainedBox(
      constraints: BoxConstraints(maxHeight: maxHeight),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(vertical: ChainSpace.sm),
            child: Container(
              width: 40,
              height: 4,
              decoration: BoxDecoration(
                color: theme.colorScheme.outlineVariant,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(
              ChainSpace.xxl,
              ChainSpace.sm,
              ChainSpace.lg,
              ChainSpace.sm,
            ),
            child: Row(
              children: [
                Icon(
                  widget.entry.icon,
                  size: 20,
                  color: theme.colorScheme.primary,
                ),
                const SizedBox(width: ChainSpace.sm),
                Expanded(
                  child: Text(
                    widget.entry.title(l),
                    style: theme.textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.close, size: 18),
                  visualDensity: VisualDensity.compact,
                  tooltip: l.welcomeDocClose,
                  onPressed: () => Navigator.pop(context),
                ),
              ],
            ),
          ),
          const Divider(height: 1),
          Flexible(
            child: FutureBuilder<String>(
              future: _content,
              builder: (context, snap) {
                if (snap.connectionState == ConnectionState.waiting) {
                  return const Padding(
                    padding: EdgeInsets.all(ChainSpace.xxl),
                    child: Center(child: CircularProgressIndicator()),
                  );
                }
                if (snap.hasError) {
                  final err = snap.error;
                  final structured = err is _DocReaderError ? err : null;
                  return Padding(
                    padding: const EdgeInsets.all(ChainSpace.xxl),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          l.welcomeDocFailedToLoad(''),
                          style: theme.textTheme.bodyMedium?.copyWith(
                            color: theme.colorScheme.error,
                          ),
                        ),
                        const SizedBox(height: ChainSpace.sm),
                        if (structured != null)
                          ChainErrorBox(
                            text: structured.localizedBody(l),
                            isError: true,
                            maxHeight: 200,
                          )
                        else
                          ChainErrorBox(
                            error: err,
                            isError: true,
                            maxHeight: 200,
                          ),
                        if (structured != null) ...[
                          const SizedBox(height: ChainSpace.md),
                          TextButton.icon(
                            icon: const Icon(Icons.open_in_new, size: 16),
                            label: Text(structured.forgejoUrl),
                            onPressed: () =>
                                SystemActions.openInOs(structured.forgejoUrl),
                          ),
                        ],
                      ],
                    ),
                  );
                }
                return Markdown(
                  data: snap.data ?? '',
                  padding: const EdgeInsets.all(ChainSpace.xxl),
                  selectable: true,
                  onTapLink: (text, href, title) async {
                    if (href != null && href.isNotEmpty) {
                      await SystemActions.openInOs(href);
                    }
                  },
                  styleSheet: ChainTheme.markdownStyle(theme),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
