import 'dart:async';

import 'package:flutter/material.dart';

import '../../routing/app_routes.dart';
import '../../utils/downloads/download_job.dart';
import '../../utils/downloads/download_manager.dart';
import '../../utils/haptics.dart';
import 'downloads_sheet.dart';
import 'stale_download_guard.dart';

/// Decides whether a just-finished download should open straight in the viewer.
///
/// Auto-open only the lone, foreground case; any parallelism (past or present)
/// or a backgrounded app parks the download in the tray instead so it never
/// steals focus. Pure so it can be unit-tested.
bool shouldAutoOpenCompletion({
  required bool foreground,
  required bool suppressAutoOpen,
  required bool completedIsSoleVisibleJob,
  required bool onOriginScreen,
}) =>
    foreground &&
    !suppressAutoOpen &&
    completedIsSoleVisibleJob &&
    onOriginScreen;

/// Whether the downloads chip should be visible. Hidden while the overview sheet
/// is open (so it can't stack sheets) and for a single in-progress download the
/// user is still watching inline on its originating screen. Shown for parallel
/// downloads, or a lone one that is finished/failed or whose screen was left.
/// Pure so it can be unit-tested.
bool shouldShowDownloadChip({
  required bool sheetOpen,
  required int jobCount,
  required bool anySurfaced,
}) {
  if (sheetOpen || jobCount == 0) return false;
  if (jobCount >= 2) return true;
  return anySurfaced;
}

/// Height of a screen-specific bottom bar the chip must float above (e.g. the
/// chat message input). Screens publish their bar height here while active and
/// reset it to 0 on dispose; 0 means "nothing extra to clear".
final ValueNotifier<double> downloadChipBottomObstruction = ValueNotifier(0);

/// Bumps [epoch] on every full-page navigation (ignoring dialogs/sheets/popups)
/// so the downloads tray can tell whether the user has left the screen a
/// download was started on.
class DownloadRouteObserver extends NavigatorObserver {
  static final ValueNotifier<int> epoch = ValueNotifier(0);

  static void _bump(Route<dynamic>? route) {
    if (route is PageRoute) epoch.value++;
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) =>
      _bump(route);

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) =>
      _bump(route);

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
      _bump(newRoute);

  @override
  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
      _bump(route);
}

/// Wraps the app's navigator and floats the downloads tray above every route.
///
/// Mounted in `MaterialApp.builder`, ABOVE the navigator, so the chip is never
/// covered by a full-page push (folder views, chat, viewer). Navigation uses
/// [AppRoutes.overlayContext] (a descendant of the root navigator) rather than
/// this widget's own context, which sits above the navigator.
///
/// Also the completion coordinator: a lone foreground download opens straight in
/// the viewer; parallel downloads park in the tray so nothing steals focus.
class DownloadTrayHost extends StatefulWidget {
  const DownloadTrayHost({required this.child, super.key});

  final Widget child;

  @override
  State<DownloadTrayHost> createState() => _DownloadTrayHostState();
}

class _DownloadTrayHostState extends State<DownloadTrayHost>
    with WidgetsBindingObserver {
  DownloadManager get _manager => DownloadManager.instance;

  StreamSubscription<DownloadJob>? _completionSub;

  /// Once any parallelism is seen, auto-open is suppressed until the tray
  /// drains — so a burst of downloads never surprises the user by opening one.
  bool _suppressAutoOpen = false;
  bool _isForeground = true;

  /// True while the downloads overview sheet is open — the chip hides so it
  /// can't be tapped again (which would stack sheets).
  bool _sheetOpen = false;

  /// Route epoch at which each visible job was started, so the chip only
  /// surfaces a lone download once the user has navigated away from it.
  final Map<DownloadJob, int> _originEpoch = {};

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _isForeground =
        WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed;
    unawaited(_manager.initialize());
    _completionSub = _manager.completions.listen(_onCompleted);
    _manager.visibleJobs.addListener(_onVisibleChanged);
    _manager.pendingOpen.addListener(_onPendingOpen);
    _onVisibleChanged();
    // A completion notification tapped during cold start may already be queued.
    WidgetsBinding.instance.addPostFrameCallback((_) => _onPendingOpen());
  }

  @override
  void dispose() {
    _completionSub?.cancel();
    _manager.visibleJobs.removeListener(_onVisibleChanged);
    _manager.pendingOpen.removeListener(_onPendingOpen);
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    _isForeground = state == AppLifecycleState.resumed;
  }

  void _onVisibleChanged() {
    final jobs = _manager.visibleJobs.value;
    if (jobs.length >= 2) {
      _suppressAutoOpen = true;
    } else if (jobs.isEmpty) {
      _suppressAutoOpen = false;
    }
    // Stamp new jobs with the current route epoch; forget vanished ones.
    for (final job in jobs) {
      _originEpoch.putIfAbsent(job, () => DownloadRouteObserver.epoch.value);
    }
    _originEpoch.removeWhere((job, _) => !jobs.contains(job));
  }

  /// The chip only shows when there's something the inline UI can't already
  /// convey: multiple downloads, a finished/failed one (no inline progress
  /// left), or a lone download whose originating screen the user has left.
  bool _shouldShowChip(List<DownloadJob> jobs, int epoch) =>
      shouldShowDownloadChip(
        sheetOpen: _sheetOpen,
        jobCount: jobs.length,
        anySurfaced: jobs.any(
          (j) => j.isDone || j.isFailed || (_originEpoch[j] ?? epoch) != epoch,
        ),
      );

  Future<void> _openSheet() async {
    if (_sheetOpen) return;
    final ctx = AppRoutes.overlayContext;
    if (ctx == null) return;
    setState(() => _sheetOpen = true);
    await showDownloadsSheet(ctx);
    if (mounted) setState(() => _sheetOpen = false);
  }

  void _onCompleted(DownloadJob job) {
    final visible = _manager.visibleJobs.value;
    final sole = visible.length == 1 && identical(visible.first, job);
    final epoch = DownloadRouteObserver.epoch.value;
    final onOriginScreen = (_originEpoch[job] ?? epoch) == epoch;
    if (shouldAutoOpenCompletion(
      foreground: _isForeground,
      suppressAutoOpen: _suppressAutoOpen,
      completedIsSoleVisibleJob: sole,
      onOriginScreen: onOriginScreen,
    )) {
      _openJob(job);
    }
    // Otherwise it stays in the tray; the chip + its own notification let the
    // user open it whenever they like.
  }

  void _onPendingOpen() {
    final job = _manager.pendingOpen.value;
    if (job == null) return;
    _manager.pendingOpen.value = null;
    _openJob(job);
  }

  void _openJob(DownloadJob job) {
    final path = job.localPath;
    final ctx = AppRoutes.overlayContext;
    if (path == null || ctx == null) {
      _manager.markOpened(job);
      return;
    }
    if (!ensureDownloadStillExists(ctx, job)) return;
    _manager.markOpened(job);
    Haptics.success();
    AppRoutes.openFileViewer(ctx, path, remoteFile: job.remoteFile);
  }

  /// Distance from the bottom to float the chip. Anchored bottom-LEFT (FABs are
  /// always bottom-right, so no collision), it clears the bottom nav bar (on
  /// tab-root screens) plus any screen-specific bottom bar such as the chat
  /// message input (via [downloadChipBottomObstruction]).
  double _bottomInset(BuildContext context) {
    final atRoot =
        !(AppRoutes.rootNavigatorKey.currentState?.canPop() ?? false);
    return MediaQuery.paddingOf(context).bottom +
        (atRoot ? 72 : 16) +
        downloadChipBottomObstruction.value;
  }

  @override
  Widget build(BuildContext context) => Stack(
    children: [
      Positioned.fill(child: widget.child),
      // Rebuilds on navigation and when a screen's bottom bar height changes so
      // the height re-adapts; AnimatedPositioned glides the chip between the
      // heights instead of jumping.
      AnimatedBuilder(
        animation: Listenable.merge([
          DownloadRouteObserver.epoch,
          downloadChipBottomObstruction,
        ]),
        builder: (context, _) => AnimatedPositioned(
          duration: const Duration(milliseconds: 220),
          curve: Curves.easeOutCubic,
          left: 16,
          right: 16,
          bottom: _bottomInset(context),
          child: Align(
            alignment: Alignment.bottomLeft,
            child: ValueListenableBuilder<List<DownloadJob>>(
              valueListenable: _manager.visibleJobs,
              builder: (context, jobs, _) {
                if (!_shouldShowChip(jobs, DownloadRouteObserver.epoch.value)) {
                  return const SizedBox.shrink();
                }
                // Swipe left to clear the whole tray away quickly.
                return Dismissible(
                  key: const ValueKey('download-tray-chip'),
                  direction: DismissDirection.endToStart,
                  resizeDuration: null,
                  onDismissed: (_) => _manager.clearAll(),
                  child: _TrayChip(jobs: jobs, onTap: _openSheet),
                );
              },
            ),
          ),
        ),
      ),
    ],
  );
}

/// The pill itself. Listens to every visible job's status so its aggregate
/// progress updates live (the visible-list notifier only fires on add/remove).
class _TrayChip extends StatelessWidget {
  const _TrayChip({required this.jobs, required this.onTap});

  final List<DownloadJob> jobs;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
    animation: Listenable.merge([for (final j in jobs) j.status]),
    builder: (context, _) {
      final active = jobs
          .where((j) => j.status.value is DownloadInProgress)
          .toList(growable: false);
      final doneCount = jobs.where((j) => j.isDone).length;
      final failedCount = jobs.where((j) => j.isFailed).length;

      final theme = Theme.of(context);
      final colors = theme.colorScheme;

      final Widget leading;
      final String label;
      if (active.isNotEmpty) {
        final avg =
            active
                .map((j) => (j.status.value as DownloadInProgress).percent)
                .fold<double>(0, (a, b) => a + b) /
            active.length;
        final value = avg <= 0 ? null : avg / 100;
        leading = SizedBox(
          width: 18,
          height: 18,
          child: CircularProgressIndicator(strokeWidth: 2.5, value: value),
        );
        label = active.length == 1
            ? 'Download läuft… ${avg.round()}%'
            : '${active.length} Downloads… ${avg.round()}%';
      } else if (failedCount > 0 && doneCount == 0) {
        leading = Icon(Icons.error_outline, size: 20, color: colors.error);
        label = failedCount == 1
            ? 'Download fehlgeschlagen'
            : '$failedCount Downloads fehlgeschlagen';
      } else {
        leading = Icon(
          Icons.check_circle_outline,
          size: 20,
          color: colors.primary,
        );
        label = doneCount == 1 ? 'Download fertig' : '$doneCount fertig';
      }

      return Semantics(
        button: true,
        label: '$label, Downloads anzeigen',
        onTap: onTap,
        child: ExcludeSemantics(
          child: Material(
            color: colors.surfaceContainerHigh,
            elevation: 4,
            borderRadius: BorderRadius.circular(24),
            shadowColor: Colors.black45,
            child: InkWell(
              borderRadius: BorderRadius.circular(24),
              onTap: onTap,
              child: Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 10,
                ),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    leading,
                    const SizedBox(width: 12),
                    Flexible(
                      child: Text(
                        label,
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                        style: theme.textTheme.bodyMedium?.copyWith(
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                    ),
                    const SizedBox(width: 8),
                    Icon(
                      Icons.expand_less,
                      size: 20,
                      color: colors.onSurfaceVariant,
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      );
    },
  );
}
