import 'dart:math';

import '../about/external/data/Commitment.dart';
import '../about/external/data/Habit.dart';
import '../about/internal/application/CommitmentClass.dart';
import '../about/internal/application/CommitmentStatus.dart';

/// The single number the whole system runs on.
///
/// ```
/// debt = Sum over open/missed commitments:
///          w(class) x severity(days_overdue) x recency_decay(t)
///
/// w(class):        non-negotiable 5.0 | standard 2.0 | elective 0.0
/// severity(d):     1 + log2(1 + d)
/// recency_decay:   0.5 ^ (days_since / half_life)
/// abandonment:     one-time +2x w(class), no decay for 30 days
/// late_complete:   debt reduced to 30% of accrued, not 0
/// ```
///
/// Sublinear severity is the load-bearing choice: with linear growth a single
/// ancient task swamps the score and the number stops meaning anything.
class DebtEngine {
  /// Half-life of the recency decay, in days.
  static const double halfLifeDays = 14;

  /// Multiplier applied once when a commitment is abandoned.
  static const double abandonmentMultiplier = 2.0;

  /// Days an abandonment resists decay.
  static const int abandonmentProtectionDays = 30;

  /// What a late completion leaves behind. Never zero — otherwise you learn
  /// that everything is negotiable.
  static const double lateCompleteRetention = 0.30;

  /// Debt weight per habit shortfall unit.
  static const double habitShortfallWeight = 1.0;

  /// `severity(d) = 1 + log2(1 + d)`.
  static double severity(int daysOverdue) {
    final int days = daysOverdue < 0 ? 0 : daysOverdue;
    return 1 + (log(1 + days) / ln2);
  }

  /// `recency_decay(t) = 0.5 ^ (days_since / half_life)`.
  static double recencyDecay(int daysSince) {
    final int days = daysSince < 0 ? 0 : daysSince;
    return pow(0.5, days / halfLifeDays).toDouble();
  }

  /// Debt contributed by one commitment, as of [now].
  static double commitmentDebt(Commitment commitment, {DateTime? now}) {
    final DateTime moment = now ?? DateTime.now();

    final double weight = classWeight(commitment.commitmentClass);

    // Electives never accrue debt, whatever happens to them.
    if (weight == 0) {
      return 0;
    }

    // Cleanly completed and archived items are settled.
    if (commitment.status == CommitmentStatus.Completed ||
        commitment.status == CommitmentStatus.Archived) {
      return 0;
    }

    if (commitment.dueEnd == null) {
      return 0;
    }

    final int daysSince = moment.difference(commitment.dueEnd!).inDays;

    // The window is still open — nothing is owed yet.
    if (daysSince < 0) {
      return 0;
    }

    if (commitment.status == CommitmentStatus.Abandoned) {
      // Abandonment is the most expensive outcome and resists decay for a
      // month, so it cannot be waited out.
      final double base = weight * abandonmentMultiplier * severity(daysSince);
      if (daysSince <= abandonmentProtectionDays) {
        return base;
      }
      return base * recencyDecay(daysSince - abandonmentProtectionDays);
    }

    final double accrued =
        weight * severity(daysSince) * recencyDecay(daysSince);

    if (commitment.status == CommitmentStatus.LateCompleted) {
      return accrued * lateCompleteRetention;
    }

    return accrued;
  }

  /// Debt contributed by a habit. A single miss costs nothing — only falling
  /// below the frequency target in the rolling window does.
  static double habitDebt(Habit habit) {
    if (!habit.behindTarget) {
      return 0;
    }
    return habit.shortfall * habitShortfallWeight;
  }

  /// The whole score, as of [now].
  static double totalDebt(
    List<Commitment> commitments, {
    List<Habit> habits = const <Habit>[],
    DateTime? now,
  }) {
    final DateTime moment = now ?? DateTime.now();

    double total = 0;

    for (Commitment commitment in commitments) {
      total = total + commitmentDebt(commitment, now: moment);
    }

    for (Habit habit in habits) {
      total = total + habitDebt(habit);
    }

    return total;
  }

  /// Open items past their window — the count that [Thresholds.maxOpenOverdue]
  /// caps.
  static int openOverdueCount(List<Commitment> commitments) {
    return commitments
        .where((commitment) =>
            commitment.status == CommitmentStatus.Overdue ||
            (commitment.status == CommitmentStatus.Open &&
                commitment.windowClosed))
        .length;
  }

  /// Missed non-negotiables — three of these force Grounded regardless of the
  /// numeric score.
  static int missedNonNegotiables(List<Commitment> commitments) {
    return commitments
        .where((commitment) =>
            commitment.commitmentClass == CommitmentClass.NonNegotiable &&
            (commitment.status == CommitmentStatus.Overdue ||
                commitment.status == CommitmentStatus.Abandoned ||
                (commitment.status == CommitmentStatus.Open &&
                    commitment.windowClosed)))
        .length;
  }

  /// What clearing this item would remove from the score — used to show the
  /// user the actual price of each row in the overdue queue.
  static double reliefFromClearing(Commitment commitment, {DateTime? now}) {
    return commitmentDebt(commitment, now: now);
  }
}
