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

import 'package:flutter/material.dart';

import '../data/error_presentation.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 AuditPage extends StatefulWidget {
  const AuditPage({super.key});

  @override
  State<AuditPage> createState() => _AuditPageState();
}

class _AuditPageState extends State<AuditPage> {
  String _typeFilter = 'all';
  List<AuditEvent> _events = const [];
  String? _error;
  bool _initialLoaded = false;
  Timer? _poller;

  /// Live event feed. New events nudge an immediate refresh so the
  /// page updates instantly instead of waiting up to 2 s. The poll
  /// stays as the safety net: if the stream errors or the hub drops,
  /// the page is still fresh within the poll interval. Reusing the
  /// proven `_refresh()` (re-fetch latest 100) keeps list management,
  /// filtering, and the hash-chain ordering exactly as before.
  StreamSubscription<AuditEvent>? _eventSub;
  Timer? _nudgeDebounce;

  @override
  void initState() {
    super.initState();
    _refresh();
    _poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh());
    _subscribeLive();
  }

  @override
  void dispose() {
    _poller?.cancel();
    _nudgeDebounce?.cancel();
    _eventSub?.cancel();
    super.dispose();
  }

  void _subscribeLive() {
    _eventSub?.cancel();
    _eventSub = HubService.instance.streamEvents(backfill: 0).listen(
      (_) => _nudge(),
      // On a persistent error the 2 s poll keeps the page fresh, so we
      // do nothing. On a clean close (hub restart / stream end) try to
      // re-establish the live feed once after a short delay.
      onError: (_) {},
      onDone: () {
        _eventSub = null;
        if (mounted) {
          Timer(const Duration(seconds: 3), () {
            if (mounted && _eventSub == null) _subscribeLive();
          });
        }
      },
      cancelOnError: true,
    );
  }

  /// Coalesce a burst of events into a single refresh.
  void _nudge() {
    if (_nudgeDebounce?.isActive ?? false) return;
    _nudgeDebounce = Timer(const Duration(milliseconds: 300), () {
      if (mounted) _refresh();
    });
  }

  Future<void> _onClearPressed() async {
    final l = AppLocalizations.of(context)!;
    final outcome = await _ClearAuditDialog.show(context);
    if (outcome == null || !mounted) return;
    try {
      final r = await HubService.instance.clearEventLog(
        reviewer: outcome.reviewer,
        reason: outcome.reason,
      );
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(l.auditClearedToast(r.purged, r.channel))),
      );
      _refresh();
    } catch (e) {
      if (!mounted) return;
      showFaiErrorSnack(context, 'audit.clear', e);
    }
  }

  // _friendly was an inline shadow of friendlyError() in
  // data/friendly_error.dart — kept only because the original
  // call site predated the central mapper. Removed in favour
  // of showFaiErrorSnack, which uses friendlyError under the
  // hood (with proper gRPC-code mapping + selectable
  // copy-affordance).

  Future<void> _refresh() async {
    try {
      final events = await HubService.instance.recentEvents(limit: 100);
      if (!mounted) return;
      setState(() {
        _events = events;
        _error = null;
        _initialLoaded = true;
      });
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _error = e.toString();
        _initialLoaded = true;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final filtered = _typeFilter == 'all'
        ? _events
        : _events.where((e) => e.type.startsWith(_typeFilter)).toList();

    return Scaffold(
      backgroundColor: theme.scaffoldBackgroundColor,
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)!.auditTitle),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: ChainSpace.lg),
            child: _FilterChips(
              value: _typeFilter,
              onChanged: (v) => setState(() => _typeFilter = v),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.help_outline, size: 18),
            tooltip: AppLocalizations.of(context)!.helpTooltip,
            onPressed: () => showFaiDoc(context, 'audit'),
          ),
          IconButton(
            icon: const Icon(Icons.delete_sweep_outlined, size: 18),
            tooltip: AppLocalizations.of(context)!.auditClearLogTooltip,
            onPressed: _onClearPressed,
          ),
          const SizedBox(width: ChainSpace.sm),
        ],
      ),
      body: Column(
        children: [
          _LiveStatusBar(eventCount: filtered.length, error: _error),
          Expanded(
            child: !_initialLoaded
                ? const Center(child: CircularProgressIndicator())
                : _error != null && _events.isEmpty
                ? ChainEmptyState(
                    icon: Icons.cloud_off_outlined,
                    iconColor: theme.colorScheme.error,
                    title: AppLocalizations.of(context)!.hubUnreachable,
                    hint: AppLocalizations.of(context)!.hubUnreachableHint,
                  )
                : filtered.isEmpty
                ? ChainEmptyState(
                    icon: Icons.timeline_outlined,
                    title: AppLocalizations.of(context)!.auditNoEvents,
                    hint: AppLocalizations.of(context)!.auditNoEventsHint,
                  )
                : _GroupedEventList(
                    events: filtered,
                    allEvents: _events,
                    toneFor: (t) => _toneFor(t, theme),
                    formatTime: _formatTime,
                    contextLine: _contextLine,
                  ),
          ),
        ],
      ),
    );
  }

  /// Render the event timestamp so the operator never has to
  /// guess what day "21:25" was. Same-day events keep the
  /// compact `HH:mm:ss` form; older events get a `MM-dd` prefix
  /// or a full `YYYY-MM-dd` for last year's tail of the log.
  /// Always rendered in the operator's local time zone — the
  /// dialog still shows the full ISO timestamp for unambiguous
  /// cross-checking.
  String _formatTime(DateTime ts) {
    final local = ts.toLocal();
    final now = DateTime.now();
    final hh = local.hour.toString().padLeft(2, '0');
    final mm = local.minute.toString().padLeft(2, '0');
    final ss = local.second.toString().padLeft(2, '0');
    final time = '$hh:$mm:$ss';
    final sameDay =
        local.year == now.year &&
        local.month == now.month &&
        local.day == now.day;
    if (sameDay) return time;
    // Older events always carry the full ISO date. `MM-dd`
    // alone is locale-ambiguous (US reads it as May-04, EU as
    // 5 April) — operators should not have to guess.
    final mo = local.month.toString().padLeft(2, '0');
    final dd = local.day.toString().padLeft(2, '0');
    return '${local.year}-$mo-$dd $time';
  }

  String _contextLine(AuditEvent e) {
    final parts = <String>[];
    if (e.flowName != null) parts.add(e.flowName!);
    if (e.stepId != null) parts.add(' › ${e.stepId}');
    if (e.moduleName != null) parts.add('  via ${e.moduleName}');
    if (e.error != null) parts.add('  [error: ${e.error}]');
    return parts.join('');
  }

  Color _toneFor(String type, ThemeData theme) {
    if (type.endsWith('.failed')) return theme.colorScheme.error;
    if (type.endsWith('.completed')) return theme.colorScheme.primary;
    if (type.endsWith('.started')) return ChainColors.warning;
    return theme.colorScheme.outline;
  }
}

/// Audit list with time-bucket section headers. Walks the
/// already-filtered events once and inserts a tiny header row
/// whenever the relative-day bucket changes (Today / Yesterday
/// / Earlier this week / Older). Reads the same events twice
/// — one filtered slice for the visible rows, the full set
/// for the per-flow-execution detail view triggered from the
/// event-detail dialog.
class _GroupedEventList extends StatelessWidget {
  final List<AuditEvent> events;
  final List<AuditEvent> allEvents;
  final Color Function(String type) toneFor;
  final String Function(DateTime ts) formatTime;
  final String Function(AuditEvent e) contextLine;

  const _GroupedEventList({
    required this.events,
    required this.allEvents,
    required this.toneFor,
    required this.formatTime,
    required this.contextLine,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final items = _itemsWithHeaders(events, l);
    return ListView.builder(
      padding: const EdgeInsets.all(ChainSpace.xl),
      itemCount: items.length,
      itemBuilder: (context, i) {
        final item = items[i];
        if (item is _GroupHeader) {
          return Padding(
            padding: EdgeInsets.only(
              top: i == 0 ? 0 : ChainSpace.lg,
              bottom: ChainSpace.sm,
            ),
            child: Text(
              item.label,
              style: theme.textTheme.labelSmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                letterSpacing: 0.6,
                fontSize: 10,
              ),
            ),
          );
        }
        final e = (item as _EventItem).event;
        return Padding(
          padding: const EdgeInsets.only(bottom: ChainSpace.xs),
          child: ChainDataRow(
            accent: toneFor(e.type),
            leading: formatTime(e.timestamp),
            title: e.type,
            subtitle: contextLine(e),
            trailing: e.durationMs != null ? '${e.durationMs}ms' : null,
            onTap: () => showDialog<void>(
              context: context,
              builder: (_) =>
                  _EventDetailDialog(event: e, allEvents: allEvents),
            ),
          ),
        );
      },
    );
  }

  List<_ListItem> _itemsWithHeaders(
    List<AuditEvent> events,
    AppLocalizations l,
  ) {
    final out = <_ListItem>[];
    String? lastBucket;
    final now = DateTime.now();
    for (final e in events) {
      final bucket = _bucketLabel(e.timestamp, now, l);
      if (bucket != lastBucket) {
        out.add(_GroupHeader(bucket));
        lastBucket = bucket;
      }
      out.add(_EventItem(e));
    }
    return out;
  }

  /// Day-bucket label. Comparing in the operator's local
  /// timezone so an event at 23:55 yesterday in Berlin doesn't
  /// land in "today" because UTC happened to spill into a new
  /// day.
  static String _bucketLabel(DateTime ts, DateTime now, AppLocalizations l) {
    final local = ts.toLocal();
    final localNow = now.toLocal();
    final today = DateTime(localNow.year, localNow.month, localNow.day);
    final eventDay = DateTime(local.year, local.month, local.day);
    final daysAgo = today.difference(eventDay).inDays;
    if (daysAgo <= 0) return l.auditGroupToday;
    if (daysAgo == 1) return l.auditGroupYesterday;
    if (daysAgo <= 6) return l.auditGroupThisWeek;
    return l.auditGroupOlder;
  }
}

sealed class _ListItem {
  const _ListItem();
}

class _GroupHeader extends _ListItem {
  final String label;
  const _GroupHeader(this.label);
}

class _EventItem extends _ListItem {
  final AuditEvent event;
  const _EventItem(this.event);
}

class _FilterChips extends StatelessWidget {
  final String value;
  final ValueChanged<String> onChanged;

  const _FilterChips({required this.value, required this.onChanged});

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context)!;
    final items = [
      ('all', l.auditFilterAll),
      ('flow.', l.auditFilterFlow),
      ('step.', l.auditFilterStep),
      ('module.', l.auditFilterModule),
    ];
    return Row(
      children: [
        for (final (v, label) in items)
          Padding(
            padding: const EdgeInsets.only(left: ChainSpace.xs),
            child: _ChipButton(
              label: label,
              selected: value == v,
              onTap: () => onChanged(v),
            ),
          ),
      ],
    );
  }
}

class _ChipButton extends StatefulWidget {
  final String label;
  final bool selected;
  final VoidCallback onTap;

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

  @override
  State<_ChipButton> createState() => _ChipButtonState();
}

class _ChipButtonState extends State<_ChipButton> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final fg = widget.selected
        ? theme.colorScheme.primary
        : _hovered
        ? theme.colorScheme.onSurface
        : theme.colorScheme.onSurfaceVariant;
    final bg = widget.selected
        ? theme.colorScheme.primary.withValues(alpha: 0.12)
        : _hovered
        ? theme.colorScheme.surfaceContainerHigh
        : Colors.transparent;
    return MouseRegion(
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      cursor: SystemMouseCursors.click,
      child: GestureDetector(
        onTap: widget.onTap,
        child: AnimatedContainer(
          duration: ChainMotion.fast,
          padding: const EdgeInsets.symmetric(
            horizontal: ChainSpace.md,
            vertical: 6,
          ),
          decoration: BoxDecoration(
            color: bg,
            borderRadius: BorderRadius.circular(ChainRadius.sm),
            border: Border.all(
              color: widget.selected
                  ? theme.colorScheme.primary.withValues(alpha: 0.3)
                  : Colors.transparent,
            ),
          ),
          child: Text(
            widget.label,
            style: ChainTheme.mono(
              size: 11,
              weight: widget.selected ? FontWeight.w500 : FontWeight.w400,
              color: fg,
            ),
          ),
        ),
      ),
    );
  }
}

class _LiveStatusBar extends StatelessWidget {
  final int eventCount;
  final String? error;

  const _LiveStatusBar({required this.eventCount, required this.error});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final live = error == null;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(
        horizontal: ChainSpace.xl,
        vertical: ChainSpace.sm,
      ),
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainerHigh,
        border: Border(
          bottom: BorderSide(color: theme.colorScheme.outlineVariant),
        ),
      ),
      child: Row(
        children: [
          ChainStatusDot(
            color: live ? ChainColors.success : ChainColors.danger,
            pulsing: live,
          ),
          const SizedBox(width: ChainSpace.sm),
          // Disconnected → SelectableText so the raw error is copyable
          // (it's the only place the underlying failure is surfaced;
          // the empty-state below shows only a generic hint).
          if (live)
            Text(
              l.auditLiveStatus(eventCount),
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            )
          else
            Flexible(
              child: SelectableText(
                l.auditDisconnected(error!),
                maxLines: 2,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ),
          const Spacer(),
          if (live)
            Row(
              children: [
                Icon(
                  Icons.shield_outlined,
                  size: 12,
                  color: theme.colorScheme.primary,
                ),
                const SizedBox(width: 4),
                Text(
                  l.auditHashChainVerified,
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.primary,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ],
            ),
        ],
      ),
    );
  }
}

class _EventDetailDialog extends StatefulWidget {
  final AuditEvent event;

  /// Full event window the audit page already fetched. Lets
  /// the dialog surface every event sharing this event's
  /// `flow_execution` without a fresh round-trip — flow runs
  /// fit comfortably inside the 100-event window the audit
  /// page polls on.
  final List<AuditEvent> allEvents;

  const _EventDetailDialog({required this.event, required this.allEvents});

  @override
  State<_EventDetailDialog> createState() => _EventDetailDialogState();
}

class _EventDetailDialogState extends State<_EventDetailDialog> {
  AskAiResult? _explanation;
  bool _explaining = false;
  SystemAiStatus? _aiStatus;

  AuditEvent get event => widget.event;

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

  Future<void> _loadAiStatus() async {
    try {
      final s = await HubService.instance.systemAiStatus();
      if (!mounted) return;
      setState(() => _aiStatus = s);
    } catch (_) {
      // Stay null → "Explain" stays hidden.
    }
  }

  Future<void> _explain({bool forceFresh = false}) async {
    setState(() {
      _explaining = true;
      _explanation = null;
    });
    final prompt = _buildPrompt(event, _aiStatus?.privacyMode ?? 'off');
    final result = await HubService.instance.askAi(
      prompt,
      forceFresh: forceFresh,
    );
    if (!mounted) return;
    setState(() {
      _explaining = false;
      _explanation = result;
    });
  }

  String _buildPrompt(AuditEvent e, String mode) {
    // Privacy: redacted = no detail JSON, full = include it.
    final buf = StringBuffer()
      ..writeln('A Ch∆In Platform audit event was logged. Explain what')
      ..writeln('happened in plain language and suggest one concrete')
      ..writeln('fix the operator can apply now.')
      ..writeln()
      ..writeln('event_type: ${e.type}')
      ..writeln('flow: ${e.flowName ?? "(none)"}')
      ..writeln('step: ${e.stepId ?? "(none)"}')
      ..writeln(
        'module: ${e.moduleName ?? "(none)"}'
        '${e.moduleVersion != null ? "@${e.moduleVersion}" : ""}',
      )
      ..writeln('error: ${e.error ?? "(none)"}')
      ..writeln('duration: ${e.durationMs ?? 0}ms');
    if (mode == 'full' && e.detail != null) {
      buf
        ..writeln()
        ..writeln('detail:')
        ..writeln(e.detail);
    }
    return buf.toString();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    String prettyDetail() {
      final raw = event.detail;
      if (raw == null || raw.isEmpty) return '';
      // Many detail strings are JSON; try to pretty-print and
      // fall back to the raw value when parsing fails.
      try {
        final dynamic parsed = const JsonDecoder().convert(raw);
        return const JsonEncoder.withIndent('  ').convert(parsed);
      } catch (_) {
        return raw;
      }
    }

    final detail = prettyDetail();
    return AlertDialog(
      title: Text(event.type),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(ChainRadius.md),
      ),
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 640, maxHeight: 560),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              _Field(label: 'event_id', value: event.eventId, mono: true),
              _Field(
                label: 'timestamp',
                value: event.timestamp.toIso8601String(),
                mono: true,
              ),
              if (event.flowName != null)
                _Field(label: 'flow', value: event.flowName!),
              if (event.flowExecution != null)
                _Field(
                  label: 'flow_execution',
                  value: event.flowExecution!,
                  mono: true,
                ),
              if (event.invocationId != null)
                _Field(
                  label: 'invocation_id',
                  value: event.invocationId!,
                  mono: true,
                ),
              if (event.stepId != null)
                _Field(label: 'step', value: event.stepId!),
              if (event.moduleName != null)
                _Field(
                  label: 'module',
                  value: event.moduleVersion != null
                      ? '${event.moduleName} @ ${event.moduleVersion}'
                      : event.moduleName!,
                ),
              if (event.durationMs != null)
                _Field(label: 'duration', value: '${event.durationMs} ms'),
              if (event.error != null)
                _Field(
                  label: 'error',
                  value: event.error!,
                  valueColor: theme.colorScheme.error,
                ),
              if (detail.isNotEmpty) ...[
                const SizedBox(height: ChainSpace.md),
                Text(
                  l.auditDetailHeader,
                  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.md),
                  decoration: BoxDecoration(
                    color: theme.colorScheme.surfaceContainerHigh,
                    borderRadius: BorderRadius.circular(ChainRadius.sm),
                    border: Border.all(color: theme.colorScheme.outlineVariant),
                  ),
                  child: SelectableText(detail, style: ChainTheme.mono(size: 11)),
                ),
              ],
              if (_explanation != null || _explaining) ...[
                const SizedBox(height: ChainSpace.lg),
                _ExplanationPanel(
                  explaining: _explaining,
                  result: _explanation,
                  privacyMode: _aiStatus?.privacyMode ?? 'off',
                  onRegenerate: _explaining
                      ? null
                      : () => _explain(forceFresh: true),
                ),
              ],
            ],
          ),
        ),
      ),
      actions: [
        if (event.flowExecution != null)
          OutlinedButton.icon(
            onPressed: () {
              Navigator.pop(context);
              showDialog<void>(
                context: context,
                builder: (_) => _FlowRunDialog(
                  flowExecution: event.flowExecution!,
                  flowName: event.flowName ?? event.flowExecution!,
                  allEvents: widget.allEvents,
                ),
              );
            },
            icon: const Icon(Icons.account_tree_outlined, size: 16),
            label: Text(l.auditEventViewFlowRun),
          ),
        if (_aiStatus?.enabled == true && (event.error != null))
          OutlinedButton.icon(
            onPressed: _explaining ? null : _explain,
            icon: const Icon(Icons.auto_awesome, size: 16),
            label: Text(
              _explaining
                  ? l.auditAsking
                  : _explanation == null
                  ? l.auditExplain
                  : l.auditReask,
            ),
          )
        else if (_aiStatus != null &&
            !_aiStatus!.enabled &&
            (event.error != null))
          Tooltip(
            message: l.auditConfigureSystemAi,
            child: OutlinedButton.icon(
              onPressed: null,
              icon: const Icon(Icons.auto_awesome_outlined, size: 16),
              label: Text(l.auditExplain),
            ),
          ),
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text(l.buttonClose),
        ),
      ],
    );
  }
}

/// Filtered drill-down: shows every event in `allEvents`
/// whose `flowExecution` matches the picked one. Sorted by
/// timestamp ascending so the operator reads the run from
/// step.started top → flow.completed bottom. Read-only — the
/// individual event-detail dialog is not re-opened from here
/// to avoid recursive flow-run lookups; selecting a row in a
/// future iteration could surface the same Explain UI.
class _FlowRunDialog extends StatelessWidget {
  final String flowExecution;
  final String flowName;
  final List<AuditEvent> allEvents;

  const _FlowRunDialog({
    required this.flowExecution,
    required this.flowName,
    required this.allEvents,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final related =
        allEvents.where((e) => e.flowExecution == flowExecution).toList()
          ..sort((a, b) => a.timestamp.compareTo(b.timestamp));
    final maxHeight = MediaQuery.of(context).size.height * 0.75;
    return AlertDialog(
      title: Text(l.auditFlowRunDialogTitle(flowName)),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(ChainRadius.md),
      ),
      content: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: 640, maxHeight: maxHeight),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l.auditFlowRunDialogSubtitle(related.length, flowExecution),
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: ChainSpace.md),
            Flexible(
              child: ListView.separated(
                shrinkWrap: true,
                itemCount: related.length,
                separatorBuilder: (_, _) => const SizedBox(height: ChainSpace.xs),
                itemBuilder: (context, i) {
                  final e = related[i];
                  return ChainDataRow(
                    accent: _toneFor(e.type, theme),
                    leading: _formatRelativeTime(e.timestamp),
                    title: e.type,
                    subtitle: _stepLine(e),
                    trailing: e.durationMs != null ? '${e.durationMs}ms' : null,
                  );
                },
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text(l.buttonClose),
        ),
      ],
    );
  }

  String _formatRelativeTime(DateTime ts) {
    final local = ts.toLocal();
    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 '$hh:$mm:$ss';
  }

  String _stepLine(AuditEvent e) {
    final parts = <String>[];
    if (e.stepId != null) parts.add(e.stepId!);
    if (e.moduleName != null) parts.add('via ${e.moduleName}');
    if (e.error != null) parts.add('[error: ${e.error}]');
    return parts.join(' · ');
  }

  Color _toneFor(String type, ThemeData theme) {
    if (type.endsWith('.failed')) return theme.colorScheme.error;
    if (type.endsWith('.completed')) return theme.colorScheme.primary;
    if (type.endsWith('.started')) return ChainColors.warning;
    return theme.colorScheme.outline;
  }
}

class _Field extends StatelessWidget {
  final String label;
  final String value;
  final bool mono;
  final Color? valueColor;

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

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

class _ExplanationPanel extends StatelessWidget {
  final bool explaining;
  final AskAiResult? result;
  final String privacyMode;

  /// Triggered by the "Regenerate" affordance — passes
  /// `forceFresh: true` so the next askAi skips the cache and
  /// hits the live provider. Null while [explaining] is true.
  final VoidCallback? onRegenerate;

  const _ExplanationPanel({
    required this.explaining,
    required this.result,
    required this.privacyMode,
    required this.onRegenerate,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    final ok = result?.isSuccess ?? false;
    final color = explaining
        ? theme.colorScheme.primary
        : ok
        ? theme.colorScheme.primary
        : theme.colorScheme.error;
    return 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: color.withValues(alpha: 0.3)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.auto_awesome, size: 14, color: color),
              const SizedBox(width: ChainSpace.xs),
              Text(
                l.auditSystemAi,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                  letterSpacing: 0.6,
                ),
              ),
              const SizedBox(width: ChainSpace.xs),
              ChainPill(
                label: privacyMode,
                tone: privacyMode == 'full'
                    ? ChainPillTone.warning
                    : ChainPillTone.neutral,
              ),
              if (result != null && result!.isSuccess && result!.cached) ...[
                const SizedBox(width: ChainSpace.xs),
                Tooltip(
                  message: () {
                    final hits = result!.cacheHits > 1
                        ? l.auditCachedTooltipHits(result!.cacheHits)
                        : '';
                    return result!.cachedAt.isEmpty
                        ? l.auditCachedTooltipUnknown(hits)
                        : l.auditCachedTooltipKnown(result!.cachedAt, hits);
                  }(),
                  child: ChainPill(
                    label: l.auditCachedPill,
                    tone: ChainPillTone.success,
                    icon: Icons.bolt,
                  ),
                ),
              ],
              const Spacer(),
              if (result != null && result!.isSuccess && result!.latencyMs > 0)
                Text(
                  result!.cached
                      ? l.auditOriginalLatency(result!.latencyMs)
                      : l.auditLatency(result!.latencyMs),
                  style: ChainTheme.mono(
                    size: 10,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
              if (result != null &&
                  result!.isSuccess &&
                  onRegenerate != null) ...[
                const SizedBox(width: ChainSpace.sm),
                Tooltip(
                  message: l.auditRegenerateTooltip,
                  child: IconButton(
                    icon: const Icon(Icons.refresh, size: 14),
                    visualDensity: VisualDensity.compact,
                    onPressed: onRegenerate,
                  ),
                ),
              ],
            ],
          ),
          const SizedBox(height: ChainSpace.sm),
          if (explaining)
            Row(
              children: [
                const SizedBox(
                  width: 14,
                  height: 14,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
                const SizedBox(width: ChainSpace.sm),
                Text(
                  l.auditAskingFull,
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            )
          else if (result != null) ...[
            SelectableText(
              result!.text,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: ok
                    ? theme.colorScheme.onSurface
                    : theme.colorScheme.error,
              ),
            ),
            if (result!.fixHint(l).isNotEmpty) ...[
              const SizedBox(height: ChainSpace.sm),
              Text(
                l.auditFixLabel,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                  letterSpacing: 0.6,
                ),
              ),
              const SizedBox(height: 2),
              Text(
                result!.fixHint(l),
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurface,
                ),
              ),
            ],
          ],
        ],
      ),
    );
  }
}

/// Outcome of the clear-audit confirmation dialog.
class _ClearOutcome {
  final String reviewer;
  final String reason;
  const _ClearOutcome({required this.reviewer, required this.reason});
}

/// Two-field confirmation dialog for "clear the audit log".
/// Reviewer defaults to the OS user (closest stable identity
/// without an auth backend); reason has no default so the
/// operator has to type *something* — the chain.reset marker
/// must carry context.
class _ClearAuditDialog extends StatefulWidget {
  const _ClearAuditDialog();

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

  @override
  State<_ClearAuditDialog> createState() => _ClearAuditDialogState();
}

class _ClearAuditDialogState extends State<_ClearAuditDialog> {
  late final TextEditingController _reviewer;
  late final TextEditingController _reason;

  @override
  void initState() {
    super.initState();
    final user =
        Platform.environment['USER'] ??
        Platform.environment['USERNAME'] ??
        'operator';
    _reviewer = TextEditingController(text: '$user@studio');
    _reason = TextEditingController();
  }

  @override
  void dispose() {
    _reviewer.dispose();
    _reason.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l = AppLocalizations.of(context)!;
    return AlertDialog(
      title: Text(l.auditClearDialogTitle),
      content: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 460),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l.auditClearDialogBody,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: ChainSpace.lg),
            TextField(
              controller: _reviewer,
              decoration: InputDecoration(
                labelText: l.auditClearReviewerLabel,
                border: const OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: ChainSpace.md),
            ValueListenableBuilder<TextEditingValue>(
              valueListenable: _reason,
              builder: (_, _, _) => TextField(
                controller: _reason,
                autofocus: true,
                decoration: InputDecoration(
                  labelText: l.auditClearReasonLabel,
                  helperText: _reason.text.trim().isEmpty
                      ? l.auditClearReasonHelper
                      : null,
                  border: const OutlineInputBorder(),
                  isDense: true,
                ),
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, null),
          child: Text(l.buttonCancel),
        ),
        ValueListenableBuilder<TextEditingValue>(
          valueListenable: _reason,
          builder: (_, _, _) => FilledButton(
            onPressed: _reason.text.trim().isEmpty
                ? null
                : () => Navigator.pop(
                    context,
                    _ClearOutcome(
                      reviewer: _reviewer.text.trim(),
                      reason: _reason.text.trim(),
                    ),
                  ),
            style: FilledButton.styleFrom(
              backgroundColor: Theme.of(context).colorScheme.error,
              foregroundColor: Theme.of(context).colorScheme.onError,
            ),
            child: Text(l.auditClearLogButton),
          ),
        ),
      ],
    );
  }
}
