// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

part of '../../project_statistics.dart';

/// Immutable project-level observations used for future filtering and hints.
class ProjectStatistics {
  /// Creates a `ProjectStatistics` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  ProjectStatistics({
    required String projectId,
    this.completedTaskCount = 0,
    Map<int, int> durationMinuteCounts = const <int, int>{},
    Map<ProjectCompletionTimeBucket, int> completionTimeBucketCounts =
        const <ProjectCompletionTimeBucket, int>{},
    this.totalPushesBeforeCompletion = 0,
    this.completedAfterPushCount = 0,
    Map<RewardLevel, int> rewardCounts = const <RewardLevel, int>{},
    Map<DifficultyLevel, int> difficultyCounts = const <DifficultyLevel, int>{},
    Map<ReminderProfile, int> reminderProfileCounts =
        const <ReminderProfile, int>{},
  })  : projectId = _requiredTrimmed(projectId, 'projectId'),
        durationMinuteCounts = Map<int, int>.unmodifiable(
          _positiveIntKeyCounts(durationMinuteCounts, 'durationMinuteCounts'),
        ),
        completionTimeBucketCounts =
            Map<ProjectCompletionTimeBucket, int>.unmodifiable(
          _enumCounts(
            completionTimeBucketCounts,
            'completionTimeBucketCounts',
          ),
        ),
        rewardCounts = Map<RewardLevel, int>.unmodifiable(
          _enumCounts(rewardCounts, 'rewardCounts'),
        ),
        difficultyCounts = Map<DifficultyLevel, int>.unmodifiable(
          _enumCounts(difficultyCounts, 'difficultyCounts'),
        ),
        reminderProfileCounts = Map<ReminderProfile, int>.unmodifiable(
          _enumCounts(reminderProfileCounts, 'reminderProfileCounts'),
        ) {
    _validateNonNegative(completedTaskCount, 'completedTaskCount');
    _validateNonNegative(
      totalPushesBeforeCompletion,
      'totalPushesBeforeCompletion',
    );
    _validateNonNegative(completedAfterPushCount, 'completedAfterPushCount');
  }

  /// Project these observations belong to.
  final String projectId;

  /// Completion activities applied to this project aggregate.
  final int completedTaskCount;

  /// Duration samples stored as minute-to-count buckets.
  final Map<int, int> durationMinuteCounts;

  /// Completion timestamp samples stored as coarse bucket counts.
  final Map<ProjectCompletionTimeBucket, int> completionTimeBucketCounts;

  /// Sum of push counts present when tasks were completed.
  final int totalPushesBeforeCompletion;

  /// Number of completed tasks that had at least one prior push.
  final int completedAfterPushCount;

  /// Reward observations from completed tasks.
  final Map<RewardLevel, int> rewardCounts;

  /// Difficulty observations from completed tasks.
  final Map<DifficultyLevel, int> difficultyCounts;

  /// Reminder-profile observations when explicitly known.
  final Map<ReminderProfile, int> reminderProfileCounts;

  /// Known duration sample count derived from the duration distribution.
  int get knownDurationSampleCount => _countTotal(durationMinuteCounts);

  /// Sum of known duration samples in minutes.
  int get totalKnownDurationMinutes {
    var total = 0;
    for (final entry in durationMinuteCounts.entries) {
      total += entry.key * entry.value;
    }
    return total;
  }

  /// Numerator for average pushes before completion.
  int get averagePushesBeforeCompletionNumerator {
    return totalPushesBeforeCompletion;
  }

  /// Denominator for average pushes before completion.
  int get averagePushesBeforeCompletionDenominator {
    return completedTaskCount;
  }

  /// Derived average push count, without storing floating-point aggregate state.
  double? get averagePushesBeforeCompletion {
    if (completedTaskCount == 0) {
      return null;
    }
    return totalPushesBeforeCompletion / completedTaskCount;
  }

  /// Return a copy with selected aggregate fields changed.
  ProjectStatistics copyWith({
    String? projectId,
    int? completedTaskCount,
    Map<int, int>? durationMinuteCounts,
    Map<ProjectCompletionTimeBucket, int>? completionTimeBucketCounts,
    int? totalPushesBeforeCompletion,
    int? completedAfterPushCount,
    Map<RewardLevel, int>? rewardCounts,
    Map<DifficultyLevel, int>? difficultyCounts,
    Map<ReminderProfile, int>? reminderProfileCounts,
  }) {
    return ProjectStatistics(
      projectId: projectId ?? this.projectId,
      completedTaskCount: completedTaskCount ?? this.completedTaskCount,
      durationMinuteCounts: durationMinuteCounts ?? this.durationMinuteCounts,
      completionTimeBucketCounts:
          completionTimeBucketCounts ?? this.completionTimeBucketCounts,
      totalPushesBeforeCompletion:
          totalPushesBeforeCompletion ?? this.totalPushesBeforeCompletion,
      completedAfterPushCount:
          completedAfterPushCount ?? this.completedAfterPushCount,
      rewardCounts: rewardCounts ?? this.rewardCounts,
      difficultyCounts: difficultyCounts ?? this.difficultyCounts,
      reminderProfileCounts:
          reminderProfileCounts ?? this.reminderProfileCounts,
    );
  }

  /// Record one completed task observation.
  ProjectStatistics recordCompletion({
    required DateTime completedAt,
    int? durationMinutes,
    int pushesBeforeCompletion = 0,
    RewardLevel? reward,
    DifficultyLevel? difficulty,
    ReminderProfile? reminderProfile,
  }) {
    if (durationMinutes != null && durationMinutes <= 0) {
      throw ArgumentError.value(
        durationMinutes,
        'durationMinutes',
        'Duration minutes must be positive when present.',
      );
    }
    if (pushesBeforeCompletion < 0) {
      throw ArgumentError.value(
        pushesBeforeCompletion,
        'pushesBeforeCompletion',
        'Push count cannot be negative.',
      );
    }

    return copyWith(
      completedTaskCount: completedTaskCount + 1,
      durationMinuteCounts: durationMinutes == null
          ? durationMinuteCounts
          : _incrementCount(durationMinuteCounts, durationMinutes),
      completionTimeBucketCounts: _incrementCount(
        completionTimeBucketCounts,
        bucketForCompletion(completedAt),
      ),
      totalPushesBeforeCompletion:
          totalPushesBeforeCompletion + pushesBeforeCompletion,
      completedAfterPushCount: pushesBeforeCompletion > 0
          ? completedAfterPushCount + 1
          : completedAfterPushCount,
      rewardCounts: reward == null
          ? rewardCounts
          : _incrementCount(
              rewardCounts,
              reward,
            ),
      difficultyCounts: difficulty == null
          ? difficultyCounts
          : _incrementCount(
              difficultyCounts,
              difficulty,
            ),
      reminderProfileCounts: reminderProfile == null
          ? reminderProfileCounts
          : _incrementCount(
              reminderProfileCounts,
              reminderProfile,
            ),
    );
  }

  /// Resolve the completion-time bucket for [completedAt].
  static ProjectCompletionTimeBucket bucketForCompletion(DateTime completedAt) {
    final hour = completedAt.hour;
    if (hour < 5) {
      return ProjectCompletionTimeBucket.overnight;
    }
    if (hour < 12) {
      return ProjectCompletionTimeBucket.morning;
    }
    if (hour < 17) {
      return ProjectCompletionTimeBucket.afternoon;
    }
    if (hour < 21) {
      return ProjectCompletionTimeBucket.evening;
    }
    return ProjectCompletionTimeBucket.night;
  }
}
