/// A task actively being run. This is what the full-screen runner and the
/// ongoing notification are both driven from.
class LiveSession {
  String commitmentId;

  String title;

  String goalTitle;

  /// Wall-clock start of the run.
  DateTime startedAt;

  /// Seconds of *foreground* work accumulated. Backgrounding the app stops
  /// this accruing — that is the whole point of Timer proof.
  int accumulatedSeconds;

  /// When the current active stretch began; null while paused.
  DateTime? resumedAt;

  /// Seconds required before the run counts as proof.
  int requiredSeconds;

  /// How many times the user left the app mid-run. Surfaced afterwards rather
  /// than hidden, because leaving repeatedly is the behaviour worth seeing.
  int backgroundedCount;

  LiveSession({
    this.commitmentId = "",
    this.title = "",
    this.goalTitle = "",
    required this.startedAt,
    this.accumulatedSeconds = 0,
    this.resumedAt,
    this.requiredSeconds = 0,
    this.backgroundedCount = 0,
  });

  factory LiveSession.fromJson(Map<String, dynamic> json) {
    return LiveSession(
      commitmentId: json['commitmentId'] ?? "",
      title: json['title'] ?? "",
      goalTitle: json['goalTitle'] ?? "",
      startedAt: DateTime.tryParse(json['startedAt'] ?? "") ?? DateTime.now(),
      accumulatedSeconds: json['accumulatedSeconds'] ?? 0,
      resumedAt: DateTime.tryParse(json['resumedAt'] ?? ""),
      requiredSeconds: json['requiredSeconds'] ?? 0,
      backgroundedCount: json['backgroundedCount'] ?? 0,
    );
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = <String, dynamic>{};
    data['commitmentId'] = commitmentId;
    data['title'] = title;
    data['goalTitle'] = goalTitle;
    data['startedAt'] = startedAt.toIso8601String();
    data['accumulatedSeconds'] = accumulatedSeconds;
    data['resumedAt'] = resumedAt?.toIso8601String();
    data['requiredSeconds'] = requiredSeconds;
    data['backgroundedCount'] = backgroundedCount;
    return data;
  }

  bool get running {
    return resumedAt != null;
  }

  /// Foreground seconds as of now, including the stretch in progress. This is
  /// derived from wall-clock rather than counted by the ticker, so the value
  /// stays correct across a screen-off period where timers are throttled.
  int elapsedSeconds({DateTime? now}) {
    if (resumedAt == null) {
      return accumulatedSeconds;
    }
    final DateTime moment = now ?? DateTime.now();
    return accumulatedSeconds + moment.difference(resumedAt!).inSeconds;
  }

  int remainingSeconds({DateTime? now}) {
    final int remaining = requiredSeconds - elapsedSeconds(now: now);
    return remaining > 0 ? remaining : 0;
  }

  /// 0..1 toward the requirement.
  double progress({DateTime? now}) {
    if (requiredSeconds <= 0) {
      return 0;
    }
    final double value = elapsedSeconds(now: now) / requiredSeconds;
    return value > 1 ? 1 : value;
  }

  /// The requirement has been met — completion is now allowed.
  bool satisfied({DateTime? now}) {
    if (requiredSeconds <= 0) {
      return true;
    }
    return elapsedSeconds(now: now) >= requiredSeconds;
  }

  /// Pause and bank the stretch that just ended.
  void pause({DateTime? now}) {
    if (resumedAt == null) {
      return;
    }
    final DateTime moment = now ?? DateTime.now();
    accumulatedSeconds =
        accumulatedSeconds + moment.difference(resumedAt!).inSeconds;
    resumedAt = null;
  }

  void resume({DateTime? now}) {
    if (resumedAt != null) {
      return;
    }
    resumedAt = now ?? DateTime.now();
  }
}
