// Project-level aggregate statistics and deterministic learned suggestions.
//
// These types stay in the pure Dart core. They provide persistence-friendly
// counters and optional suggestions, but they do not overwrite configured
// project defaults or implement reports UI.

import 'models.dart';
import 'task_lifecycle.dart';

/// Coarse completion-time buckets used for learned project suggestions.
enum ProjectCompletionTimeBucket {
  overnight,
  morning,
  afternoon,
  evening,
  night,
}

/// Suggestion categories derived from project observations.
enum ProjectSuggestionType {
  durationMinutes,
  completionTimeBucket,
  reward,
  difficulty,
  reminderProfile,
}

/// How concentrated the supporting observations are for a suggestion.
enum ProjectSuggestionConfidence {
  low,
  medium,
  high,
}

/// Immutable project-level observations used for future filtering and hints.
class ProjectStatistics {
  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;
  }
}

/// Result of applying project-statistic effects from activities.
class ProjectStatisticsApplicationResult {
  ProjectStatisticsApplicationResult({
    required this.statistics,
    List<String> appliedActivityIds = const <String>[],
  }) : appliedActivityIds = List<String>.unmodifiable(appliedActivityIds);

  /// Aggregate after newly applied activity effects.
  final ProjectStatistics statistics;

  /// Completion activity ids that changed the aggregate.
  final List<String> appliedActivityIds;
}

/// Updates project statistics from canonical task activities exactly once.
class ProjectStatisticsAggregationService {
  const ProjectStatisticsAggregationService();

  /// Apply completion [activities] to [statistics].
  ///
  /// [alreadyAppliedActivityIds] is supplied by the application/persistence
  /// boundary. This service returns the new ids so Block 14 can persist task,
  /// activity, and aggregate mutations atomically.
  ProjectStatisticsApplicationResult apply({
    required ProjectStatistics statistics,
    required Iterable<TaskActivity> activities,
    Iterable<Task> tasks = const <Task>[],
    Iterable<String> alreadyAppliedActivityIds = const <String>[],
  }) {
    final taskById = {
      for (final task in tasks) task.id: task,
    };
    final appliedIds = alreadyAppliedActivityIds.toSet();
    final newlyAppliedIds = <String>[];
    var current = statistics;

    for (final activity in activities) {
      if (activity.projectId != statistics.projectId ||
          activity.code != TaskActivityCode.completed ||
          appliedIds.contains(activity.id)) {
        continue;
      }

      final task = taskById[activity.taskId];
      final completedAt = _dateTimeMetadata(activity.metadata['completedAt']) ??
          activity.occurredAt;
      final pushesBeforeCompletion =
          _intMetadata(activity.metadata['pushesBeforeCompletion']) ??
              _pushesBeforeCompletion(task);

      current = current.recordCompletion(
        completedAt: completedAt,
        durationMinutes: _durationMinutes(activity, task),
        pushesBeforeCompletion: pushesBeforeCompletion,
        reward: _rewardLevel(activity.metadata['reward']) ?? task?.reward,
        difficulty: _difficultyLevel(activity.metadata['difficulty']) ??
            task?.difficulty,
        reminderProfile:
            _reminderProfile(activity.metadata['reminderProfile']) ??
                task?.reminderOverride,
      );
      appliedIds.add(activity.id);
      newlyAppliedIds.add(activity.id);
    }

    return ProjectStatisticsApplicationResult(
      statistics: current,
      appliedActivityIds: newlyAppliedIds,
    );
  }
}

/// Minimum sample sizes for deterministic project suggestions.
class ProjectSuggestionPolicy {
  const ProjectSuggestionPolicy({
    this.minimumDurationSamples = 3,
    this.minimumCompletionTimeSamples = 3,
    this.minimumRewardSamples = 3,
    this.minimumDifficultySamples = 3,
    this.minimumReminderSamples = 3,
  });

  final int minimumDurationSamples;
  final int minimumCompletionTimeSamples;
  final int minimumRewardSamples;
  final int minimumDifficultySamples;
  final int minimumReminderSamples;
}

/// One optional learned suggestion with provenance.
class ProjectSuggestion<T extends Object> {
  const ProjectSuggestion({
    required this.type,
    required this.value,
    required this.sampleSize,
    required this.supportingSampleCount,
    required this.minimumSampleSize,
    required this.confidence,
  });

  /// Suggestion category.
  final ProjectSuggestionType type;

  /// Suggested value.
  final T value;

  /// Number of meaningful observations considered.
  final int sampleSize;

  /// Observations that support [value].
  final int supportingSampleCount;

  /// Minimum sample threshold used before producing the suggestion.
  final int minimumSampleSize;

  /// Concentration of supporting observations.
  final ProjectSuggestionConfidence confidence;
}

/// Optional suggestion set derived from one project's observations.
class ProjectSuggestionSet {
  const ProjectSuggestionSet({
    required this.statistics,
    this.durationMinutes,
    this.completionTimeBucket,
    this.reward,
    this.difficulty,
    this.reminderProfile,
  });

  final ProjectStatistics statistics;
  final ProjectSuggestion<int>? durationMinutes;
  final ProjectSuggestion<ProjectCompletionTimeBucket>? completionTimeBucket;
  final ProjectSuggestion<RewardLevel>? reward;
  final ProjectSuggestion<DifficultyLevel>? difficulty;
  final ProjectSuggestion<ReminderProfile>? reminderProfile;
}

/// Configured project defaults plus optional suggestions kept separate.
class ProjectDefaultResolution {
  const ProjectDefaultResolution({
    required this.project,
    required this.suggestions,
  });

  /// Authoritative configured project profile.
  final ProjectProfile project;

  /// Optional learned suggestions that UI can present explicitly.
  final ProjectSuggestionSet suggestions;

  int? get configuredDurationMinutes => project.defaultDurationMinutes;
  RewardLevel get configuredReward => project.defaultReward;
  DifficultyLevel get configuredDifficulty => project.defaultDifficulty;
  ReminderProfile get configuredReminderProfile =>
      project.defaultReminderProfile;
}

/// Derives deterministic project suggestions from aggregate observations.
class ProjectSuggestionService {
  const ProjectSuggestionService({
    this.policy = const ProjectSuggestionPolicy(),
  });

  final ProjectSuggestionPolicy policy;

  /// Build optional suggestions for [statistics].
  ProjectSuggestionSet suggestionsFor(ProjectStatistics statistics) {
    return ProjectSuggestionSet(
      statistics: statistics,
      durationMinutes: _durationSuggestion(statistics),
      completionTimeBucket: _modeSuggestion(
        type: ProjectSuggestionType.completionTimeBucket,
        counts: statistics.completionTimeBucketCounts,
        minimumSampleSize: policy.minimumCompletionTimeSamples,
      ),
      reward: _modeSuggestion(
        type: ProjectSuggestionType.reward,
        counts: _withoutNotSetReward(statistics.rewardCounts),
        minimumSampleSize: policy.minimumRewardSamples,
      ),
      difficulty: _modeSuggestion(
        type: ProjectSuggestionType.difficulty,
        counts: _withoutNotSetDifficulty(statistics.difficultyCounts),
        minimumSampleSize: policy.minimumDifficultySamples,
      ),
      reminderProfile: _modeSuggestion(
        type: ProjectSuggestionType.reminderProfile,
        counts: statistics.reminderProfileCounts,
        minimumSampleSize: policy.minimumReminderSamples,
      ),
    );
  }

  /// Return configured defaults and optional suggestions without merging them.
  ProjectDefaultResolution resolveDefaults({
    required ProjectProfile project,
    required ProjectStatistics statistics,
  }) {
    return ProjectDefaultResolution(
      project: project,
      suggestions: suggestionsFor(statistics),
    );
  }

  ProjectSuggestion<int>? _durationSuggestion(ProjectStatistics statistics) {
    final sampleSize = statistics.knownDurationSampleCount;
    if (sampleSize < policy.minimumDurationSamples) {
      return null;
    }

    final value = _weightedLowerMedian(statistics.durationMinuteCounts);
    if (value == null) {
      return null;
    }

    return ProjectSuggestion<int>(
      type: ProjectSuggestionType.durationMinutes,
      value: value,
      sampleSize: sampleSize,
      supportingSampleCount: statistics.durationMinuteCounts[value] ?? 0,
      minimumSampleSize: policy.minimumDurationSamples,
      confidence: _confidence(
        statistics.durationMinuteCounts[value] ?? 0,
        sampleSize,
      ),
    );
  }

  ProjectSuggestion<T>? _modeSuggestion<T extends Object>({
    required ProjectSuggestionType type,
    required Map<T, int> counts,
    required int minimumSampleSize,
  }) {
    final sampleSize = _countTotal(counts);
    if (sampleSize < minimumSampleSize) {
      return null;
    }

    final mode = _uniqueMode(counts);
    if (mode == null) {
      return null;
    }

    final support = counts[mode] ?? 0;
    return ProjectSuggestion<T>(
      type: type,
      value: mode,
      sampleSize: sampleSize,
      supportingSampleCount: support,
      minimumSampleSize: minimumSampleSize,
      confidence: _confidence(support, sampleSize),
    );
  }
}

Map<K, int> _incrementCount<K extends Object>(Map<K, int> counts, K key) {
  return {
    ...counts,
    key: (counts[key] ?? 0) + 1,
  };
}

Map<int, int> _positiveIntKeyCounts(Map<int, int> counts, String name) {
  final normalized = <int, int>{};
  for (final entry in counts.entries) {
    if (entry.key <= 0) {
      throw ArgumentError.value(entry.key, name, 'Key must be positive.');
    }
    _validateNonNegative(entry.value, name);
    if (entry.value > 0) {
      normalized[entry.key] = entry.value;
    }
  }
  return normalized;
}

Map<T, int> _enumCounts<T extends Enum>(Map<T, int> counts, String name) {
  final normalized = <T, int>{};
  for (final entry in counts.entries) {
    _validateNonNegative(entry.value, name);
    if (entry.value > 0) {
      normalized[entry.key] = entry.value;
    }
  }
  return normalized;
}

void _validateNonNegative(int value, String name) {
  if (value < 0) {
    throw ArgumentError.value(value, name, 'Count cannot be negative.');
  }
}

int _countTotal<K extends Object>(Map<K, int> counts) {
  var total = 0;
  for (final value in counts.values) {
    total += value;
  }
  return total;
}

int? _weightedLowerMedian(Map<int, int> counts) {
  if (counts.isEmpty) {
    return null;
  }
  final sampleSize = _countTotal(counts);
  final target = (sampleSize + 1) ~/ 2;
  var seen = 0;
  final sortedMinutes = counts.keys.toList()..sort();
  for (final minutes in sortedMinutes) {
    seen += counts[minutes] ?? 0;
    if (seen >= target) {
      return minutes;
    }
  }
  return null;
}

T? _uniqueMode<T extends Object>(Map<T, int> counts) {
  T? bestKey;
  var bestCount = 0;
  var tied = false;

  for (final entry in counts.entries) {
    if (entry.value > bestCount) {
      bestKey = entry.key;
      bestCount = entry.value;
      tied = false;
      continue;
    }
    if (entry.value == bestCount && bestCount > 0) {
      tied = true;
    }
  }

  return tied ? null : bestKey;
}

ProjectSuggestionConfidence _confidence(int support, int sampleSize) {
  if (sampleSize <= 0) {
    return ProjectSuggestionConfidence.low;
  }
  if (support * 3 >= sampleSize * 2) {
    return ProjectSuggestionConfidence.high;
  }
  if (support * 2 >= sampleSize) {
    return ProjectSuggestionConfidence.medium;
  }
  return ProjectSuggestionConfidence.low;
}

Map<RewardLevel, int> _withoutNotSetReward(Map<RewardLevel, int> counts) {
  return {
    for (final entry in counts.entries)
      if (entry.key != RewardLevel.notSet) entry.key: entry.value,
  };
}

Map<DifficultyLevel, int> _withoutNotSetDifficulty(
  Map<DifficultyLevel, int> counts,
) {
  return {
    for (final entry in counts.entries)
      if (entry.key != DifficultyLevel.notSet) entry.key: entry.value,
  };
}

DateTime? _dateTimeMetadata(Object? value) {
  if (value is DateTime) {
    return value;
  }
  if (value is String) {
    return DateTime.tryParse(value)?.toUtc();
  }
  return null;
}

int? _intMetadata(Object? value) {
  return value is int && value >= 0 ? value : null;
}

int? _durationMinutes(TaskActivity activity, Task? task) {
  final metadataDuration = _intMetadata(activity.metadata['durationMinutes']);
  if (metadataDuration != null && metadataDuration > 0) {
    return metadataDuration;
  }

  final actualStart =
      _dateTimeMetadata(activity.metadata['actualStart']) ?? task?.actualStart;
  final actualEnd =
      _dateTimeMetadata(activity.metadata['actualEnd']) ?? task?.actualEnd;
  if (actualStart != null &&
      actualEnd != null &&
      actualStart.isBefore(actualEnd)) {
    final minutes = actualEnd.difference(actualStart).inMinutes;
    if (minutes > 0) {
      return minutes;
    }
  }

  return task?.durationMinutes;
}

int _pushesBeforeCompletion(Task? task) {
  if (task == null) {
    return 0;
  }
  return task.stats.manuallyPushedCount + task.stats.autoPushedCount;
}

RewardLevel? _rewardLevel(Object? value) {
  if (value is RewardLevel) {
    return value;
  }
  if (value is String) {
    return _enumByName(RewardLevel.values, value);
  }
  return null;
}

DifficultyLevel? _difficultyLevel(Object? value) {
  if (value is DifficultyLevel) {
    return value;
  }
  if (value is String) {
    return _enumByName(DifficultyLevel.values, value);
  }
  return null;
}

ReminderProfile? _reminderProfile(Object? value) {
  if (value is ReminderProfile) {
    return value;
  }
  if (value is String) {
    return _enumByName(ReminderProfile.values, value);
  }
  return null;
}

T? _enumByName<T extends Enum>(Iterable<T> values, String name) {
  for (final value in values) {
    if (value.name == name) {
      return value;
    }
  }
  return null;
}

String _requiredTrimmed(String value, String name) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ArgumentError.value(value, name, 'Value is required.');
  }
  return trimmed;
}
