// Reachability-based offline marker (#427 S1).
//
// `connectivityProvider` only knows whether a network *interface* is
// up — not whether the Minstrel server is actually reachable (captive
// portals, server down, DNS, VPN-only routes all read "online"). This
// is the single source of truth other features gate on: offline =
// the server failed its /healthz probe N times in a row; recovery on
// the first success.
//
// Deliberately NOT coupled to connectivityProvider: subscribing to
// that StreamProvider eagerly mounts its 2s checkConnectivity timeout
// and leaks a pending Timer through widget tests that never reach the
// auth state (the bug fixed for MutationReplayer). /healthz failing
// already covers interface-down — it just fails the probe.
//
// Optimistic: assume online until proven offline, so a cold launch
// behaves normally and only flips after sustained unreachability.

import 'dart:async';

import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../api/endpoints/health.dart';
import '../library/library_providers.dart' show dioProvider;

class OfflineMonitor extends Notifier<bool> {
  Timer? _initial;
  Timer? _periodic;
  int _consecutiveFails = 0;

  /// Consecutive failed probes before we declare offline. Small
  /// enough to react within ~half a minute, large enough that a
  /// single dropped request doesn't flip the whole UI.
  static const _threshold = 3;

  /// Probe cadence. Slower when believed-online (cheap heartbeat);
  /// faster when offline so recovery is noticed quickly.
  static const _onlineInterval = Duration(seconds: 30);
  static const _offlineInterval = Duration(seconds: 10);

  /// Let auth + server-url + dio providers finish async warmup
  /// before the first probe so a cold start doesn't false-positive.
  static const _initialDelay = Duration(seconds: 5);

  @override
  bool build() {
    ref.onDispose(() {
      _initial?.cancel();
      _periodic?.cancel();
    });
    _initial = Timer(_initialDelay, () {
      _check();
      _arm();
    });
    return false; // optimistic until a probe says otherwise
  }

  void _arm() {
    _periodic?.cancel();
    _periodic = Timer.periodic(
      state ? _offlineInterval : _onlineInterval,
      (_) => _check(),
    );
  }

  Future<void> _check() async {
    final Dio dio;
    try {
      // Not configured yet (no server URL) → don't flip; the app is
      // still on the connect screen and "offline" is meaningless.
      dio = await ref.read(dioProvider.future);
    } catch (_) {
      return;
    }
    try {
      // dio's own connect/receive timeouts bound this — no extra
      // Timer (which would leak in widget tests).
      await HealthApi(dio).check();
      _consecutiveFails = 0;
      _set(false);
    } catch (_) {
      _consecutiveFails++;
      if (_consecutiveFails >= _threshold) {
        _set(true);
      }
    }
  }

  void _set(bool offline) {
    if (state == offline) return;
    state = offline;
    _arm(); // cadence follows the new state
  }

  /// Force an immediate probe — e.g. a user-initiated retry. Public
  /// so S4's offline surfaces can offer a "try again" affordance.
  Future<void> recheck() => _check();
}

/// `true` when the server is unreachable. Watch this to gate
/// online-only affordances; read it once at app start to activate
/// the poller.
final offlineProvider =
    NotifierProvider<OfflineMonitor, bool>(OfflineMonitor.new);
