import '../about/external/data/ExercisePrescription.dart';
import '../about/external/data/SessionLog.dart';
import '../about/external/data/SetLog.dart';

/// Did you do the session, or a watered-down version of it?
///
/// ```
/// integrity = 0.5 x (prescribed_sets_done / prescribed_sets)
///           + 0.3 x (1 - mean |actual_reps - target| / target)
///           + 0.2 x (hard_exercises_done / hard_exercises_prescribed)
/// ```
///
/// Skipping the hard exercise while doing the easy ones is the most common
/// form of self-deception in training, and the third term is what detects it.
class IntegrityEngine {
  static const double setsWeight = 0.5;
  static const double repsWeight = 0.3;
  static const double hardWeight = 0.2;

  static double score(
    SessionLog session,
    List<ExercisePrescription> prescribed,
  ) {
    if (prescribed.isEmpty) {
      return 0;
    }

    return (setsWeight * _setCompletion(session, prescribed)) +
        (repsWeight * _repAccuracy(session, prescribed)) +
        (hardWeight * _hardCompletion(session, prescribed));
  }

  /// Prescribed sets actually done, capped at 1 so overshooting one lift does
  /// not paper over skipping another.
  static double _setCompletion(
    SessionLog session,
    List<ExercisePrescription> prescribed,
  ) {
    int prescribedSets = 0;
    int doneSets = 0;

    for (ExercisePrescription item in prescribed) {
      prescribedSets = prescribedSets + item.sets;

      final int done = session.sets
          .where((entry) =>
              entry.exerciseId == item.exerciseId && entry.isPrescribed)
          .length;

      doneSets = doneSets + (done > item.sets ? item.sets : done);
    }

    if (prescribedSets == 0) {
      return 0;
    }

    return doneSets / prescribedSets;
  }

  /// How close the reps landed to target, averaged across prescribed sets.
  static double _repAccuracy(
    SessionLog session,
    List<ExercisePrescription> prescribed,
  ) {
    double totalError = 0;
    int counted = 0;

    for (ExercisePrescription item in prescribed) {
      if (item.targetReps <= 0) {
        continue;
      }

      final List<SetLog> done = session.sets
          .where((entry) =>
              entry.exerciseId == item.exerciseId && entry.isPrescribed)
          .toList();

      for (SetLog entry in done) {
        final double error =
            (entry.reps - item.targetReps).abs() / item.targetReps;
        totalError = totalError + (error > 1 ? 1 : error);
        counted = counted + 1;
      }
    }

    if (counted == 0) {
      return 0;
    }

    return 1 - (totalError / counted);
  }

  /// The term that catches cherry-picking. "Hard" comes from your historical
  /// RPE on that exercise, not a static label.
  static double _hardCompletion(
    SessionLog session,
    List<ExercisePrescription> prescribed,
  ) {
    final List<ExercisePrescription> hard =
        prescribed.where((item) => item.hard).toList();

    if (hard.isEmpty) {
      return 1;
    }

    int done = 0;

    for (ExercisePrescription item in hard) {
      final bool touched = session.sets.any((entry) =>
          entry.exerciseId == item.exerciseId && entry.isPrescribed);
      if (touched) {
        done = done + 1;
      }
    }

    return done / hard.length;
  }

  /// Whether an exercise counts as hard for this user, derived from historical
  /// RPE rather than asserted up front.
  static bool isHard(List<double> historicalRpe, {double cutoff = 8.0}) {
    if (historicalRpe.isEmpty) {
      return false;
    }

    double total = 0;
    for (double value in historicalRpe) {
      total = total + value;
    }

    return (total / historicalRpe.length) >= cutoff;
  }

  /// Weekly volume per muscle group, for under-target warnings — "you went to
  /// the gym" is not a metric.
  static Map<String, int> weeklySetsByMuscleGroup(List<SessionLog> sessions) {
    final Map<String, int> volume = <String, int>{};

    for (SessionLog session in sessions) {
      for (SetLog entry in session.sets) {
        if (entry.muscleGroup.isEmpty) {
          continue;
        }
        volume[entry.muscleGroup] = (volume[entry.muscleGroup] ?? 0) + 1;
      }
    }

    return volume;
  }

  /// Total plyometric ground contacts across the week.
  static int weeklyContacts(List<SessionLog> sessions) {
    int total = 0;
    for (SessionLog session in sessions) {
      total = total + session.contacts;
    }
    return total;
  }

  /// Plyo is the one modality where the app stops you rather than pushes you.
  /// CNS and connective tissue do not recover on a motivation schedule.
  static bool contactCeilingBreached(List<SessionLog> sessions, int ceiling) {
    if (ceiling <= 0) {
      return false;
    }
    return weeklyContacts(sessions) >= ceiling;
  }

  /// Whether enough recovery has passed since the last high-intensity lower
  /// body session.
  static bool recoveredEnough(
    DateTime? lastHighIntensityLowerBody,
    int requiredHours, {
    DateTime? now,
  }) {
    if (lastHighIntensityLowerBody == null) {
      return true;
    }
    final DateTime moment = now ?? DateTime.now();
    return moment.difference(lastHighIntensityLowerBody).inHours >=
        requiredHours;
  }

  /// Training through a deload logs as non-compliance, the same as skipping.
  static bool violatesDeload(SessionLog session, bool isDeloadWeek) {
    if (!isDeloadWeek) {
      return false;
    }
    return session.sets.any((entry) => entry.rpe >= 8);
  }
}
