import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';

import '../data/hub.dart';
import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';
import '../widgets/widgets.dart';
import 'welcome.dart' show showFaiDoc;

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

  @override
  State<ApprovalsPage> createState() => _ApprovalsPageState();
}

class _ApprovalsPageState extends State<ApprovalsPage>
    with SingleTickerProviderStateMixin {
  late final TabController _tab;
  late Future<List<ApprovalRecord>> _pendingFuture;
  late Future<List<ApprovalRecord>> _historyFuture;

  /// IDs the operator currently has multi-selected for a
  /// batch operation. Empty set hides the batch action bar
  /// and falls back to per-row Approve / Reject buttons.
  final Set<String> _selectedIds = <String>{};
  bool _batchInFlight = false;
  // Reviewer identity recorded in the audit log. Defaults to
  // the OS user (closest stable identity Studio has without an
  // auth backend); operators can override it per session.
  late final String _reviewer = _defaultReviewer();

  static String _defaultReviewer() {
    final user =
        Platform.environment['USER'] ??
        Platform.environment['USERNAME'] ??
        'studio';
    return '$user@studio';
  }

  @override
  void initState() {
    super.initState();
    _tab = TabController(length: 2, vsync: this);
    _refresh();
  }

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

  void _refresh() {
    setState(() {
      _pendingFuture = HubService.instance.listApprovalsRecords(
        statuses: const ['pending'],
      );
      _historyFuture = HubService.instance.listApprovalsRecords(
        statuses: const ['approved', 'rejected', 'expired'],
        limit: 200,
      );
    });
  }

  Future<void> _approve(ApprovalRecord a) async {
    final l = AppLocalizations.of(context)!;
    try {
      await HubService.instance.approve(a.id, _reviewer);
      _toast(l.approvalsApprovedToast(a.flowName, a.stepId));
      _refresh();
    } catch (e) {
      _toast(l.approvalsApproveFailed(e.toString()));
    }
  }

  Future<void> _reject(ApprovalRecord a) async {
    final l = AppLocalizations.of(context)!;
    final reason = await _promptReason(context);
    if (reason == null || reason.isEmpty) return;
    try {
      await HubService.instance.reject(a.id, _reviewer, reason);
      _toast(l.approvalsRejectedToast(a.flowName, a.stepId));
      _refresh();
    } catch (e) {
      _toast(l.approvalsRejectFailed(e.toString()));
    }
  }

  void _toggleSelection(String id) {
    setState(() {
      if (!_selectedIds.add(id)) _selectedIds.remove(id);
    });
  }

  void _selectAll(Iterable<ApprovalRecord> all) {
    setState(() {
      _selectedIds
        ..clear()
        ..addAll(all.map((a) => a.id));
    });
  }

  void _clearSelection() => setState(() => _selectedIds.clear());

  /// Loops the picked records sequentially through the
  /// per-record approve / reject calls so a partial failure
  /// surfaces with a clear "X done, Y failed" toast rather
  /// than a confusing all-or-nothing rollback. Selection
  /// drains as items succeed.
  Future<void> _batchApprove(List<ApprovalRecord> picked) async {
    final l = AppLocalizations.of(context)!;
    setState(() => _batchInFlight = true);
    var ok = 0;
    var failed = 0;
    for (final a in picked) {
      try {
        await HubService.instance.approve(a.id, _reviewer);
        ok += 1;
        _selectedIds.remove(a.id);
      } catch (_) {
        failed += 1;
      }
    }
    if (!mounted) return;
    setState(() => _batchInFlight = false);
    _toast(
      failed == 0
          ? l.approvalsBatchApproveDoneToast(ok)
          : l.approvalsBatchPartialFailure(ok, failed),
    );
    _refresh();
  }

  Future<void> _batchReject(List<ApprovalRecord> picked) async {
    final l = AppLocalizations.of(context)!;
    final reason = await _promptReason(context);
    if (reason == null || reason.isEmpty) return;
    setState(() => _batchInFlight = true);
    var ok = 0;
    var failed = 0;
    for (final a in picked) {
      try {
        await HubService.instance.reject(a.id, _reviewer, reason);
        ok += 1;
        _selectedIds.remove(a.id);
      } catch (_) {
        failed += 1;
      }
    }
    if (!mounted) return;
    setState(() => _batchInFlight = false);
    _toast(
      failed == 0
          ? l.approvalsBatchRejectDoneToast(ok)
          : l.approvalsBatchPartialFailure(ok, failed),
    );
    _refresh();
  }

  void _toast(String msg) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
  }

  Future<String?> _promptReason(BuildContext context) async {
    final controller = TextEditingController();
    final l = AppLocalizations.of(context)!;
    return showDialog<String>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(l.approvalsRejectDialogTitle),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: InputDecoration(
            labelText: l.approvalsRejectReasonLabel,
            border: const OutlineInputBorder(),
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, null),
            child: Text(l.buttonCancel),
          ),
          FilledButton(
            onPressed: () => Navigator.pop(ctx, controller.text),
            style: FilledButton.styleFrom(
              backgroundColor: Theme.of(ctx).colorScheme.error,
              foregroundColor: Theme.of(ctx).colorScheme.onError,
            ),
            child: Text(l.approvalsRejectButton),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      backgroundColor: theme.scaffoldBackgroundColor,
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)!.approvalsTitle),
        bottom: TabBar(
          controller: _tab,
          tabs: [
            Tab(text: AppLocalizations.of(context)!.approvalsTabPending),
            Tab(text: AppLocalizations.of(context)!.approvalsTabHistory),
          ],
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.help_outline, size: 18),
            tooltip: AppLocalizations.of(context)!.helpTooltip,
            onPressed: () => showFaiDoc(context, 'approvals'),
          ),
          IconButton(
            icon: const Icon(Icons.refresh, size: 18),
            tooltip: AppLocalizations.of(context)!.approvalsReloadTooltip,
            onPressed: _refresh,
          ),
          const SizedBox(width: ChainSpace.sm),
        ],
      ),
      body: TabBarView(
        controller: _tab,
        children: [
          _PendingList(
            future: _pendingFuture,
            selectedIds: _selectedIds,
            batchInFlight: _batchInFlight,
            onApprove: _approve,
            onReject: _reject,
            onToggle: _toggleSelection,
            onSelectAll: _selectAll,
            onClearSelection: _clearSelection,
            onBatchApprove: _batchApprove,
            onBatchReject: _batchReject,
            onRetry: _refresh,
          ),
          _HistoryList(future: _historyFuture, onRetry: _refresh),
        ],
      ),
    );
  }
}

class _PendingList extends StatelessWidget {
  final Future<List<ApprovalRecord>> future;
  final Set<String> selectedIds;
  final bool batchInFlight;
  final void Function(ApprovalRecord) onApprove;
  final void Function(ApprovalRecord) onReject;
  final void Function(String id) onToggle;
  final void Function(Iterable<ApprovalRecord>) onSelectAll;
  final VoidCallback onClearSelection;
  final Future<void> Function(List<ApprovalRecord>) onBatchApprove;
  final Future<void> Function(List<ApprovalRecord>) onBatchReject;
  final VoidCallback onRetry;

  const _PendingList({
    required this.future,
    required this.selectedIds,
    required this.batchInFlight,
    required this.onApprove,
    required this.onReject,
    required this.onToggle,
    required this.onSelectAll,
    required this.onClearSelection,
    required this.onBatchApprove,
    required this.onBatchReject,
    required this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return FutureBuilder<List<ApprovalRecord>>(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return ChainEmptyState(
            icon: Icons.cloud_off_outlined,
            iconColor: theme.colorScheme.error,
            title: l.hubUnreachable,
            hint: l.hubUnreachableHint,
            action: FilledButton.tonal(
              onPressed: onRetry,
              child: Text(l.buttonRetry),
            ),
          );
        }
        final pending = snapshot.data ?? [];
        if (pending.isEmpty) {
          return ChainEmptyState(
            icon: Icons.task_alt_outlined,
            title: l.approvalsInboxZero,
            hint: l.approvalsInboxHint,
          );
        }
        final selected = pending
            .where((a) => selectedIds.contains(a.id))
            .toList();
        return Stack(
          children: [
            ListView.separated(
              padding: EdgeInsets.fromLTRB(
                ChainSpace.xl,
                ChainSpace.xl,
                ChainSpace.xl,
                // Keep the last card clear of the floating
                // batch action bar.
                selected.isEmpty ? ChainSpace.xl : 96.0,
              ),
              itemCount: pending.length,
              separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.md),
              itemBuilder: (context, i) {
                final a = pending[i];
                final isSelected = selectedIds.contains(a.id);
                return _ApprovalCard(
                  approval: a,
                  selected: isSelected,
                  onToggleSelected: () => onToggle(a.id),
                  onApprove: () => onApprove(a),
                  onReject: () => onReject(a),
                );
              },
            ),
            if (selected.isNotEmpty)
              Positioned(
                left: ChainSpace.xl,
                right: ChainSpace.xl,
                bottom: ChainSpace.lg,
                child: _BatchActionBar(
                  selectedCount: selected.length,
                  totalCount: pending.length,
                  inFlight: batchInFlight,
                  onSelectAll: () => onSelectAll(pending),
                  onClear: onClearSelection,
                  onApprove: () => onBatchApprove(selected),
                  onReject: () => onBatchReject(selected),
                ),
              ),
          ],
        );
      },
    );
  }
}

/// Floating action bar that surfaces when the operator
/// multi-selects pending approvals. Lets them approve or
/// reject the whole picked set in one round-trip per item;
/// the parent loops sequentially so a partial failure stays
/// visible per row.
class _BatchActionBar extends StatelessWidget {
  final int selectedCount;
  final int totalCount;
  final bool inFlight;
  final VoidCallback onSelectAll;
  final VoidCallback onClear;
  final VoidCallback onApprove;
  final VoidCallback onReject;

  const _BatchActionBar({
    required this.selectedCount,
    required this.totalCount,
    required this.inFlight,
    required this.onSelectAll,
    required this.onClear,
    required this.onApprove,
    required this.onReject,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return Material(
      elevation: 4,
      borderRadius: BorderRadius.circular(ChainRadius.md),
      color: theme.colorScheme.surfaceContainerHigh,
      child: Container(
        padding: const EdgeInsets.symmetric(
          horizontal: ChainSpace.lg,
          vertical: ChainSpace.md,
        ),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(ChainRadius.md),
          border: Border.all(color: theme.colorScheme.outlineVariant),
        ),
        child: Row(
          children: [
            Icon(
              Icons.check_box_outlined,
              size: 18,
              color: theme.colorScheme.primary,
            ),
            const SizedBox(width: ChainSpace.sm),
            Text(
              l.approvalsBatchSelected(selectedCount),
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(width: ChainSpace.md),
            if (selectedCount < totalCount)
              TextButton(
                onPressed: inFlight ? null : onSelectAll,
                child: Text(l.approvalsSelectAll),
              ),
            TextButton(
              onPressed: inFlight ? null : onClear,
              child: Text(l.approvalsClearSelection),
            ),
            const Spacer(),
            OutlinedButton.icon(
              onPressed: inFlight ? null : onReject,
              icon: const Icon(Icons.close, size: 14),
              label: Text(l.approvalsBatchReject),
              style: OutlinedButton.styleFrom(
                foregroundColor: theme.colorScheme.error,
                side: BorderSide(
                  color: theme.colorScheme.error.withValues(alpha: 0.5),
                ),
              ),
            ),
            const SizedBox(width: ChainSpace.sm),
            FilledButton.icon(
              onPressed: inFlight ? null : onApprove,
              icon: inFlight
                  ? const SizedBox(
                      width: 14,
                      height: 14,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.check, size: 14),
              label: Text(l.approvalsBatchApprove),
            ),
          ],
        ),
      ),
    );
  }
}

class _HistoryList extends StatelessWidget {
  final Future<List<ApprovalRecord>> future;
  final VoidCallback onRetry;

  const _HistoryList({required this.future, required this.onRetry});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return FutureBuilder<List<ApprovalRecord>>(
      future: future,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return ChainEmptyState(
            icon: Icons.cloud_off_outlined,
            iconColor: theme.colorScheme.error,
            title: l.hubUnreachable,
            hint: l.hubUnreachableHint,
            action: FilledButton.tonal(
              onPressed: onRetry,
              child: Text(l.buttonRetry),
            ),
          );
        }
        final decided = snapshot.data ?? [];
        if (decided.isEmpty) {
          return ChainEmptyState(
            icon: Icons.history,
            title: l.approvalsHistoryEmpty,
            hint: l.approvalsHistoryEmptyHint,
          );
        }
        return ListView.separated(
          padding: const EdgeInsets.all(ChainSpace.xl),
          itemCount: decided.length,
          separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.xs),
          itemBuilder: (context, i) {
            final a = decided[i];
            return _HistoryRow(record: a);
          },
        );
      },
    );
  }
}

class _ApprovalCard extends StatelessWidget {
  final ApprovalRecord approval;
  final bool selected;
  final VoidCallback onToggleSelected;
  final VoidCallback onApprove;
  final VoidCallback onReject;

  const _ApprovalCard({
    required this.approval,
    required this.selected,
    required this.onToggleSelected,
    required this.onApprove,
    required this.onReject,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return ChainCard(
      accentTop: true,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Status row: selection + state + expiry. No flow id here —
          // the headline below leads with WHAT is being asked, not the
          // technical flow name.
          Row(
            children: [
              Checkbox(
                value: selected,
                onChanged: (_) => onToggleSelected(),
                visualDensity: VisualDensity.compact,
              ),
              const SizedBox(width: ChainSpace.xs),
              ChainPill(
                label: l.approvalsPillPending,
                tone: ChainPillTone.warning,
                icon: Icons.pending_outlined,
              ),
              const Spacer(),
              if (approval.expiresAt != null)
                ChainPill(
                  label: _expiresInLabel(context, approval.expiresAt!),
                  tone: ChainPillTone.neutral,
                  icon: Icons.schedule,
                ),
            ],
          ),
          const SizedBox(height: ChainSpace.lg),
          // Headline = the human question the flow author wrote ("what
          // am I approving"). Falls back to a clear sentence when the
          // step left the prompt empty, so the card is never reduced to
          // a cryptic flow id.
          Text(
            approval.prompt.trim().isEmpty
                ? l.approvalsRequestFallback
                : approval.prompt,
            style: theme.textTheme.titleMedium?.copyWith(
              fontWeight: FontWeight.w600,
            ),
          ),
          const SizedBox(height: 4),
          // Technical context, demoted to a small metadata line.
          Row(
            children: [
              Icon(
                Icons.account_tree_outlined,
                size: 13,
                color: theme.colorScheme.onSurfaceVariant,
              ),
              const SizedBox(width: 6),
              Expanded(
                child: Text(
                  l.approvalsFlowStepMeta(approval.flowName, approval.stepId),
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: ChainSpace.md),
          Padding(
            padding: const EdgeInsets.only(bottom: 4),
            child: Text(
              l.approvalsPayloadPreview,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
                fontSize: 10,
              ),
            ),
          ),
          if (approval.payloadPreview != null)
            Container(
              width: double.infinity,
              // Cap the height so a large payload scrolls inside the
              // card instead of stretching it off-screen; still fully
              // readable + copyable (SelectableText).
              constraints: const BoxConstraints(maxHeight: 280),
              padding: const EdgeInsets.all(ChainSpace.md),
              decoration: BoxDecoration(
                color: theme.colorScheme.surfaceContainerHigh,
                borderRadius: BorderRadius.circular(ChainRadius.sm),
                border: Border.all(color: theme.colorScheme.outlineVariant),
              ),
              child: SingleChildScrollView(
                child: SelectableText(
                  _prettyPreview(approval.payloadPreview!),
                  style: ChainTheme.mono(
                    size: 11,
                    color: theme.colorScheme.onSurface,
                  ),
                ),
              ),
            )
          else
            // No `show:` on the approval step → explain where the data
            // would come from instead of rendering nothing.
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(ChainSpace.md),
              decoration: BoxDecoration(
                color: theme.colorScheme.surfaceContainerHigh,
                borderRadius: BorderRadius.circular(ChainRadius.sm),
                border: Border.all(color: theme.colorScheme.outlineVariant),
              ),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Icon(
                    Icons.info_outline,
                    size: 14,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                  const SizedBox(width: ChainSpace.sm),
                  Expanded(
                    child: Text(
                      l.approvalsNoPayload,
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          const SizedBox(height: ChainSpace.lg),
          Row(
            children: [
              Text(
                approval.id,
                style: ChainTheme.mono(
                  size: 10,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
              const Spacer(),
              OutlinedButton.icon(
                onPressed: onReject,
                icon: const Icon(Icons.close, size: 16),
                label: Text(l.approvalsRejectButton),
                style: OutlinedButton.styleFrom(
                  foregroundColor: theme.colorScheme.error,
                  side: BorderSide(
                    color: theme.colorScheme.error.withValues(alpha: 0.5),
                  ),
                ),
              ),
              const SizedBox(width: ChainSpace.sm),
              FilledButton.icon(
                onPressed: onApprove,
                icon: const Icon(Icons.check, size: 16),
                label: Text(l.approvalsApproveButton),
              ),
            ],
          ),
        ],
      ),
    );
  }

  String _prettyPreview(String raw) {
    if (raw.isEmpty) return raw;
    try {
      final dynamic parsed = const JsonDecoder().convert(raw);
      return const JsonEncoder.withIndent('  ').convert(parsed);
    } catch (_) {
      return raw;
    }
  }

  String _expiresInLabel(BuildContext context, DateTime t) {
    final l = AppLocalizations.of(context)!;
    final remaining = t.difference(DateTime.now());
    if (remaining.isNegative) return l.approvalsPillExpired;
    final minutes = remaining.inMinutes;
    if (minutes < 60) return l.approvalsExpiresIn(minutes);
    return l.approvalsExpiresInHours(remaining.inHours);
  }
}

/// Compact one-line entry for a decided approval. Click expands
/// the prompt, decision metadata, and payload preview in a
/// dialog. Keeps the History tab scannable when there are many
/// decisions.
class _HistoryRow extends StatelessWidget {
  final ApprovalRecord record;
  const _HistoryRow({required this.record});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final (tone, icon, label) = _statusPresentation(context, record.status);
    final decided = record.decidedAt;
    final timeStr = decided == null ? '' : _formatTimestamp(decided.toLocal());
    return InkWell(
      onTap: () => showDialog<void>(
        context: context,
        builder: (_) => _HistoryDialog(record: record),
      ),
      borderRadius: BorderRadius.circular(ChainRadius.sm),
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: ChainSpace.sm,
          vertical: ChainSpace.sm,
        ),
        child: Row(
          children: [
            ChainPill(label: label, tone: tone, icon: icon),
            const SizedBox(width: ChainSpace.md),
            SizedBox(
              width: 150,
              child: Text(
                timeStr,
                style: ChainTheme.mono(
                  size: 11,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
                overflow: TextOverflow.ellipsis,
              ),
            ),
            Expanded(
              child: Text(
                '${record.flowName} › ${record.stepId}',
                style: theme.textTheme.bodyMedium?.copyWith(
                  fontWeight: FontWeight.w500,
                ),
                overflow: TextOverflow.ellipsis,
              ),
            ),
            const SizedBox(width: ChainSpace.md),
            if (record.decidedBy.isNotEmpty)
              Text(
                record.decidedBy,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
          ],
        ),
      ),
    );
  }

  static (ChainPillTone, IconData, String) _statusPresentation(
    BuildContext context,
    String status,
  ) {
    final l = AppLocalizations.of(context)!;
    switch (status) {
      case 'approved':
        return (ChainPillTone.success, Icons.check, l.approvalsPillApproved);
      case 'rejected':
        return (ChainPillTone.danger, Icons.close, l.approvalsPillRejected);
      case 'expired':
        return (
          ChainPillTone.neutral,
          Icons.timer_off_outlined,
          l.approvalsPillExpired,
        );
      default:
        return (ChainPillTone.neutral, Icons.help_outline, status);
    }
  }
}

class _HistoryDialog extends StatelessWidget {
  final ApprovalRecord record;
  const _HistoryDialog({required this.record});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final (tone, icon, label) = _HistoryRow._statusPresentation(
      context,
      record.status,
    );
    return AlertDialog(
      title: Row(
        children: [
          ChainPill(label: label, tone: tone, icon: icon),
          const SizedBox(width: ChainSpace.sm),
          Expanded(
            child: Text(
              '${record.flowName} › ${record.stepId}',
              style: theme.textTheme.titleMedium,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 540, maxHeight: 480),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              if (record.decidedAt != null)
                _kv(
                  theme,
                  AppLocalizations.of(context)!.approvalsDialogDecided,
                  '${_formatTimestamp(record.decidedAt!.toLocal())} '
                  '· ${record.decidedBy.isEmpty ? "(unknown)" : record.decidedBy}',
                ),
              _kv(
                theme,
                AppLocalizations.of(context)!.approvalsDialogCreated,
                _formatTimestamp(record.createdAt.toLocal()),
              ),
              if (record.reason.isNotEmpty)
                _kv(
                  theme,
                  AppLocalizations.of(context)!.approvalsDialogReason,
                  record.reason,
                ),
              const SizedBox(height: ChainSpace.md),
              Text(
                AppLocalizations.of(context)!.approvalsDialogPrompt,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                  letterSpacing: 0.6,
                ),
              ),
              const SizedBox(height: 4),
              SelectableText(record.prompt, style: theme.textTheme.bodyMedium),
              if (record.payloadPreview != null) ...[
                const SizedBox(height: ChainSpace.md),
                Text(
                  AppLocalizations.of(context)!.approvalsPayloadPreview,
                  style: theme.textTheme.labelSmall?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                    letterSpacing: 0.6,
                  ),
                ),
                const SizedBox(height: 4),
                Container(
                  width: double.infinity,
                  padding: const EdgeInsets.all(ChainSpace.sm),
                  decoration: BoxDecoration(
                    color: theme.colorScheme.surfaceContainerHigh,
                    borderRadius: BorderRadius.circular(ChainRadius.sm),
                    border: Border.all(color: theme.colorScheme.outlineVariant),
                  ),
                  child: SelectableText(
                    record.payloadPreview!,
                    style: ChainTheme.mono(size: 11),
                  ),
                ),
              ],
              const SizedBox(height: ChainSpace.md),
              Text(
                record.id,
                style: ChainTheme.mono(
                  size: 10,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ],
          ),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text(AppLocalizations.of(context)!.buttonClose),
        ),
      ],
    );
  }

  Widget _kv(ThemeData theme, String k, String v) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 70,
            child: Text(
              k,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
              ),
            ),
          ),
          Expanded(child: SelectableText(v, style: theme.textTheme.bodySmall)),
        ],
      ),
    );
  }
}

/// Locale-unambiguous YYYY-MM-DD HH:mm:ss formatter shared by
/// the history row + dialog. Always renders in the operator's
/// local time zone — ISO timestamps from the wire still appear
/// raw in the per-record dialog for cross-checking.
String _formatTimestamp(DateTime local) {
  final yyyy = local.year.toString().padLeft(4, '0');
  final mo = local.month.toString().padLeft(2, '0');
  final dd = local.day.toString().padLeft(2, '0');
  final hh = local.hour.toString().padLeft(2, '0');
  final mm = local.minute.toString().padLeft(2, '0');
  final ss = local.second.toString().padLeft(2, '0');
  return '$yyyy-$mo-$dd $hh:$mm:$ss';
}
