import '../about/external/data/CommitmentEvent.dart';
import '../about/external/data/ExcuseCluster.dart';
import '../about/internal/application/EventType.dart';

/// Clusters excuses over time and turns the pattern into a confrontation:
/// "Too tired has appeared 14 times this month, 11 of them on gym days,
/// 9 of them after 7pm. Consider moving gym to morning."
///
/// The on-device pass is a cheap keyword bucketing so the confrontation works
/// offline; the server refines clusters and overwrites [ExcuseCluster.insight].
class ExcuseAnalyser {
  /// Excuse families the local pass recognises. Order matters — the first
  /// family whose keyword appears wins.
  static const Map<String, List<String>> families = <String, List<String>>{
    "Too tired": <String>["tired", "exhausted", "knackered", "no energy", "sleepy"],
    "No time": <String>["no time", "busy", "ran out of time", "swamped"],
    "Not feeling it": <String>["not feeling", "no motivation", "cant be", "cannot be"],
    "Unwell": <String>["sick", "ill", "headache", "pain", "sore", "injured"],
    "Interrupted": <String>["interrupted", "came up", "emergency", "had to"],
    "Forgot": <String>["forgot", "slipped my mind", "missed it"],
    "Weather": <String>["rain", "cold", "hot", "weather"],
    "Travel": <String>["travel", "away", "trip", "commute", "traffic"],
  };

  static const String unclustered = "Other";

  /// The family this excuse belongs to.
  static String classify(String excuse) {
    final String text = excuse.toLowerCase();

    for (MapEntry<String, List<String>> family in families.entries) {
      for (String keyword in family.value) {
        if (text.contains(keyword)) {
          return family.key;
        }
      }
    }

    return unclustered;
  }

  /// Build the taxonomy from the event log. Only deferrals and misses carry
  /// excuses worth clustering.
  static List<ExcuseCluster> cluster(List<CommitmentEvent> events) {
    final Map<String, ExcuseCluster> clusters = <String, ExcuseCluster>{};
    final Map<String, Map<String, int>> categoryCounts =
        <String, Map<String, int>>{};

    for (CommitmentEvent event in events) {
      if (event.event != EventType.DEFERRED &&
          event.event != EventType.MISSED &&
          event.event != EventType.ABANDONED) {
        continue;
      }

      if (event.excuseText.isEmpty) {
        continue;
      }

      final String label = classify(event.excuseText);

      final ExcuseCluster cluster =
          clusters.putIfAbsent(label, () => ExcuseCluster(label: label));

      cluster.occurrences = cluster.occurrences + 1;

      if (event.at != null) {
        final int weekday = event.at!.weekday;
        final int hour = event.at!.hour;
        cluster.byWeekday[weekday] = (cluster.byWeekday[weekday] ?? 0) + 1;
        cluster.byHour[hour] = (cluster.byHour[hour] ?? 0) + 1;
      }

      if (event.excuseClusterId != null &&
          event.excuseClusterId!.isNotEmpty) {
        final Map<String, int> counts =
            categoryCounts.putIfAbsent(label, () => <String, int>{});
        counts[event.excuseClusterId!] =
            (counts[event.excuseClusterId!] ?? 0) + 1;
      }
    }

    final List<ExcuseCluster> result = clusters.values.toList();

    for (ExcuseCluster cluster in result) {
      cluster.dominantCategory = _dominant(categoryCounts[cluster.label]);
      cluster.insight = describe(cluster);
    }

    result.sort((a, b) => b.occurrences.compareTo(a.occurrences));

    return result;
  }

  /// The confrontation copy. Only the concentrations that are actually
  /// meaningful get mentioned — a flat distribution says nothing.
  static String describe(ExcuseCluster cluster) {
    if (cluster.occurrences < 3) {
      return "";
    }

    final StringBuffer buffer = StringBuffer();

    buffer.write(
        "'${cluster.label}' has appeared ${cluster.occurrences} times");

    final MapEntry<int, int>? weekday = _peak(cluster.byWeekday);
    if (weekday != null && weekday.value >= (cluster.occurrences * 0.4)) {
      buffer.write(", ${weekday.value} of them on ${_weekdayName(weekday.key)}s");
    }

    final MapEntry<int, int>? hour = _peak(cluster.byHour);
    if (hour != null && hour.value >= (cluster.occurrences * 0.35)) {
      buffer.write(", ${hour.value} of them after ${_hourLabel(hour.key)}");
    }

    buffer.write(".");

    final String suggestion = _suggest(cluster, weekday, hour);
    if (suggestion.isNotEmpty) {
      buffer.write(" $suggestion");
    }

    return buffer.toString();
  }

  static String _suggest(
    ExcuseCluster cluster,
    MapEntry<int, int>? weekday,
    MapEntry<int, int>? hour,
  ) {
    if (hour == null) {
      return "";
    }

    if (hour.key >= 18 && cluster.label == "Too tired") {
      return "Consider moving this to the morning.";
    }

    if (hour.key >= 18) {
      return "Evenings are not working for this. Try scheduling it earlier.";
    }

    if (weekday != null) {
      return "${_weekdayName(weekday.key)} is where this keeps failing.";
    }

    return "";
  }

  static MapEntry<int, int>? _peak(Map<int, int> histogram) {
    if (histogram.isEmpty) {
      return null;
    }

    MapEntry<int, int>? peak;
    for (MapEntry<int, int> entry in histogram.entries) {
      if (peak == null || entry.value > peak.value) {
        peak = entry;
      }
    }
    return peak;
  }

  static String _dominant(Map<String, int>? counts) {
    if (counts == null || counts.isEmpty) {
      return "";
    }

    String label = "";
    int best = 0;

    for (MapEntry<String, int> entry in counts.entries) {
      if (entry.value > best) {
        best = entry.value;
        label = entry.key;
      }
    }

    return label;
  }

  static String _weekdayName(int weekday) {
    const List<String> names = <String>[
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday",
    ];
    if (weekday < 1 || weekday > 7) {
      return "";
    }
    return names[weekday - 1];
  }

  static String _hourLabel(int hour) {
    if (hour == 0) {
      return "midnight";
    }
    if (hour < 12) {
      return "${hour}am";
    }
    if (hour == 12) {
      return "noon";
    }
    return "${hour - 12}pm";
  }
}
