import 'package:meta/meta.dart';

import '../models/sessions_snapshot.dart';
import 'session_service.dart';

/// One tick of [StatusRepository.watch]: either a fresh [snapshot], or (on
/// poll failure) the last known-good snapshot marked [stale] (D6) — the UI
/// never sees a null/blank dashboard just because one poll failed.
@immutable
class SessionsPoll {
  final SessionsSnapshot? snapshot;
  final bool stale;
  final DateTime? lastOkAt;
  final String? error;

  const SessionsPoll(
      {this.snapshot, this.stale = false, this.lastOkAt, this.error});
}

/// Polls [SessionService] on an interval, applying the D6 contract: on
/// failure, keep serving the last good snapshot (marked stale) and back off
/// (3s -> 6s -> 15s) instead of hammering a broken `status.sh`. Framework
/// agnostic — `apps/mam_desktop` wires this into a Riverpod `StreamProvider`,
/// it does not reimplement the polling/backoff logic itself.
class StatusRepository {
  final SessionService sessionService;
  final Duration pollInterval;
  final List<Duration> failureBackoff;

  StatusRepository({
    required this.sessionService,
    this.pollInterval = const Duration(seconds: 4),
    this.failureBackoff = const [
      Duration(seconds: 3),
      Duration(seconds: 6),
      Duration(seconds: 15),
    ],
  }) : assert(failureBackoff.isNotEmpty, 'failureBackoff must not be empty');

  Stream<SessionsPoll> watch() async* {
    SessionsSnapshot? lastGood;
    DateTime? lastOkAt;
    var failureStreak = 0;

    while (true) {
      try {
        final snapshot = await sessionService.fetchSnapshot();
        lastGood = snapshot;
        lastOkAt = DateTime.now();
        failureStreak = 0;
        yield SessionsPoll(
            snapshot: snapshot, stale: false, lastOkAt: lastOkAt);
        await Future.delayed(pollInterval);
      } catch (e) {
        final idx = failureStreak.clamp(0, failureBackoff.length - 1);
        final delay = failureBackoff[idx];
        failureStreak++;
        yield SessionsPoll(
          snapshot: lastGood,
          stale: lastGood != null,
          lastOkAt: lastOkAt,
          error: e.toString(),
        );
        await Future.delayed(delay);
      }
    }
  }
}
