import 'dart:math';
import 'package:ernaehrung/android/components/meal_page_text/days_component.dart';
import 'package:ernaehrung/android/config/cast_helper.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive/hive.dart';
import '../models/food.dart';
import '../models/user.dart';
import 'format_helper.dart';

class StatisticsService {

  static final StatisticsService _instance = StatisticsService._internal();
  factory StatisticsService() => _instance;

  static StatisticsService get instance => _instance;

  StatisticsService._internal() {
    initBoxes();
  }
  final String todayStatisticsBoxName = dotenv.env['STATISTICS_TODAY_BOX'] ?? 'STATISTICS_TODAY_BOX';
  final String mainStatisticsBoxName =  dotenv.env['STATISTICS_MAIN_BOX'] ?? 'STATISTICS_MAIN_BOX';
  final String progressStatisticsBoxName = dotenv.env['STATISTICS_PROGRESS_BOX'] ?? 'STATISTICS_PROGRESS_BOX';

  ValueNotifier<int> eatenCalories = ValueNotifier<int>(0);
  /*
  * fat 0
  * protein 1
  * carbon 2
  * fat for one day, week, month, 3
  * protein for one day, week, month 4
  * carbon for one day, week, month 5
  * calories for one day, week, month 6
  * */
  ValueNotifier<List<double>> ingredients = ValueNotifier<List<double>>([0,0,0,0,0,0,0]);
  ValueNotifier<int> dailyAverageForCurrentWeek = ValueNotifier<int>(0);
  ValueNotifier<List<Food>> weeklyCaloryRanking = ValueNotifier<List<Food>>([]);
  ValueNotifier<List<BarChartGroupData>> barChartData = ValueNotifier<List<BarChartGroupData>>([]);

  initBoxes()async{
    Box reducedBox = Hive.box(todayStatisticsBoxName);
    Box progressBox = Hive.box(progressStatisticsBoxName);

    putIfKeyNotExists([reducedBox,progressBox], 'FRÜHSTÜCK', []);
    putIfKeyNotExists([reducedBox,progressBox], 'MITTAGESSEN', []);
    putIfKeyNotExists([reducedBox,progressBox], 'ABENDESSEN', []);
    updateStatisticsTodayBoxByTimespan(TimeSpan.day);
  }

  void putIfKeyNotExists(List<Box> boxes, String key, dynamic value) {
    for(int i = 0; i < boxes.length;i++){
      if (!boxes[i].containsKey(key)) {
        boxes[i].put(key, value);
      }
    }
  }

  updateStatisticsTodayBoxByTimespan(TimeSpan timespan){
    clearBoxByBox(Hive.box(todayStatisticsBoxName));
    int timestamp = getTimestampFromNow();
    int dayLen = 1;
    switch(timespan){
       case TimeSpan.day:
        getNewFoodAndUpdateBoxByTimestampAndBox(timestamp, Hive.box(todayStatisticsBoxName));
        break;
       case TimeSpan.week:
         List<int> currentWeek = getTimestampsByTimestampAndTimespan(TimeSpan.week,timestamp);
         for(int i = 0;i < currentWeek.length;i++){
           getNewFoodAndUpdateBoxByTimestampAndBox(currentWeek[i],Hive.box(todayStatisticsBoxName));
         }
         dayLen = currentWeek.length;
         break;
      case TimeSpan.month:
        List<int> currentMonth = getTimestampsByTimestampAndTimespan(TimeSpan.month,timestamp);
        for(int i = 0;i < currentMonth.length;i++){
          getNewFoodAndUpdateBoxByTimestampAndBox(currentMonth[i],Hive.box(todayStatisticsBoxName));
        }
        dayLen = currentMonth.length;
        break;
     }
    updateCalculationsAndNotfiyListenersForPorgressStatistics(dayLen);
  }

  void updateCalculationsAndNotfiyListenersForPorgressStatistics(int dayLen){
    eatenCalories.value = getAllEatenCaloriesByBox(Hive.box(todayStatisticsBoxName));
    ingredients.value = getAllEatenIngredientsForTodayStatistics(dayLen.roundToDouble());
  }

  void getNewFoodAndUpdateBoxByTimestampAndBox(int timestamp, Box box){
    Map<String,List<Food>> newFood = getFoodMapForGivenTimestampFromMainBox(timestamp);
    if(newFood.keys.isNotEmpty){
      setElementsOfBoxByBox(newFood, box);
    }
  }

  List<int> getTimestampsByTimestampAndTimespan(TimeSpan timespan, int timestamp) {
    int range = timespan == TimeSpan.week ? 7 : 31;
    int targetWeekday = DateTime.monday; // Example target weekday (Monday)
    DateTime currentDateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
    int currentWeekday = currentDateTime.weekday;
    int daysToAdd = targetWeekday - currentWeekday;
    DateTime targetDateTime = currentDateTime.add(Duration(days: daysToAdd));

    List<int> timestampsForWeekdays = [];
    for (int i = 0; i < range; i++) {
      timestampsForWeekdays.add(targetDateTime.millisecondsSinceEpoch ~/ 1000);
      targetDateTime = targetDateTime.add(const Duration(days: 1));
    }
    return timestampsForWeekdays;
  }

  clearBoxByBox(Box box){
    for(int i = 0; i < box.keys.length;i++){
      box.put(box.keys.elementAt(i), []);
    }
  }

  setElementsOfBoxByBox(Map<String,List<Food>> newFood,Box box){
    Iterable<String> keys = newFood.keys;
    for(int i = 0; i < keys.length;i++){
      List<Food> alreadyExisting = castDynamicToListFood(box.get(keys.elementAt(i)));
      alreadyExisting.addAll(newFood[keys.elementAt(i)] as Iterable<Food>);
      box.put(keys.elementAt(i), alreadyExisting);
    }
  }

  getDayAsIntFromTimestamp(int timestamp){
    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
    return dateTime.day;
  }

  Map<String,List<Food>> getFoodMapForGivenTimestampFromMainBox(int timestamp){
    Box box = Hive.box(mainStatisticsBoxName);
    dynamic matchingTimestamp = getMatchingTimestamp(box, timestamp);
    if(matchingTimestamp != null){
      return castDynamicMap(box.get(matchingTimestamp));
    }
    return <String,List<Food>>{};
  }

  getMatchingTimestamp(Box box,int newTimestamp){
    if(box.keys.isNotEmpty){
      for(int i = 0; i < box.keys.length;i++){
        int timestamp = box.keys.elementAt(i);
        if(isDateEqual(newTimestamp, timestamp)){
          return timestamp;
        }
      }
      return null;
    }
  }

  getMonthAsIntFromTimestamp(int timestamp){
    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
    return dateTime.month;
  }

  getYearAsIntFromTimestamp(int timestamp){
    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
    return dateTime.year;
  }

  isDateEqual(int timestamp1, int timestamp2){
    return
      getDayAsIntFromTimestamp(timestamp1) == getDayAsIntFromTimestamp(timestamp2) &&
      getMonthAsIntFromTimestamp(timestamp1) == getMonthAsIntFromTimestamp(timestamp2) &&
      getYearAsIntFromTimestamp(timestamp1) == getYearAsIntFromTimestamp(timestamp2);
  }

  addItemToMainBox(Food value, String mealType){
    // Hive.deleteFromDisk();
    Box box = Hive.box(mainStatisticsBoxName);
    DateTime dateTime = DateTime.now();

    // DEBUG
    //DateTime dateTime = getRandomTimestampForTesting();

    int newTimestamp = dateTime.millisecondsSinceEpoch.toInt() ~/ 1000;

    dynamic matchingTimestamp = getMatchingTimestamp(box, newTimestamp);
    if(matchingTimestamp != null){
      newTimestamp = matchingTimestamp;
    }
    Map<String, List<Food>> valueMap = castDynamicMap(box.get(newTimestamp));
    List<Food> values = [];
    if(valueMap.containsKey(mealType)){
      values = valueMap[mealType]!;
    }
    values.add(value);
    valueMap[mealType] = values;
    box.put(newTimestamp, valueMap);
  }

  getRandomTimestampForTesting(){
    DateTime now = DateTime.now();
    DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
    DateTime endOfWeek = startOfWeek.add(const Duration(days: 6));

    Random random = Random();
    int randomMilliseconds = random.nextInt(endOfWeek.millisecondsSinceEpoch - startOfWeek.millisecondsSinceEpoch);
    DateTime randomTimestamp = startOfWeek.add(Duration(milliseconds: randomMilliseconds));

    return randomTimestamp;
  }

  int getAllEatenCaloriesByBox(Box box){
    num sum = 0;
    for(int i = 0; i < box.keys.length;i++){
      for(Food food in box.get(box.keys.elementAt(i))){
        sum += food.calories;
      }
    }
    return sum as int;
  }

  List<double> getAllEatenIngredientsForTodayStatistics(double dayLen){
    Box box = Hive.box(todayStatisticsBoxName);
    final Box userBox = Hive.box<User>("USER_BOX");
    final calorieSummary = userBox.get("USER").kalorien;

    const double fatPercentPerThousandCalorie = 3.7;
    const double proteinPercentPerThousandCalorie = 4.5;
    const double carbsPercentPerThousandCalorie = 12.8;

    double fatSummary = (calorieSummary / 100) * fatPercentPerThousandCalorie;
    double carbSummary = (calorieSummary / 100) * carbsPercentPerThousandCalorie;
    double proteinSummary = (calorieSummary / 100) * proteinPercentPerThousandCalorie;

    num fat = 0;
    num protein = 0;
    num carbs = 0;
    for(int i = 0; i < box.keys.length;i++){
      for(Food food in box.get(box.keys.elementAt(i))){
        fat += food.fatg;
        protein += food.proteing;
        carbs += food.carbohydrateg;
      }
    }

    return [
      fat.roundToDouble(),
      protein.roundToDouble(),
      carbs.roundToDouble(),
      fatSummary * dayLen,
      proteinSummary * dayLen,
      carbSummary * dayLen,
      calorieSummary * dayLen
    ];
  }

  void updateCalculationsAndNotfiyListenersForTodayStatistics(){
    dailyAverageForCurrentWeek.value = getAverageCaloriesForCurrentWeekOnDailyBasis();
    weeklyCaloryRanking.value = getWeeklyCaloryRanking();
    barChartData.value = getBarChartData();
  }

  void updateProgressBoxValues(){
    Box box = Hive.box(progressStatisticsBoxName);
    box.clear();
    int timestamp = getTimestampFromNow();
    List<int> currentWeek = getTimestampsByTimestampAndTimespan(TimeSpan.week,timestamp);
    for(int i = 0;i < currentWeek.length;i++){
      getNewFoodAndUpdateBoxByTimestampAndBox(currentWeek[i],box);
    }
    updateCalculationsAndNotfiyListenersForTodayStatistics();
  }

  int getAverageCaloriesForCurrentWeekOnDailyBasis(){
    Box box = Hive.box(progressStatisticsBoxName);
    return getAllEatenCaloriesByBox(box)~/7;
  }

  List<Food> getWeeklyCaloryRanking(){
    int timestamp = getTimestampFromNow();
    List<int> currentWeek = getTimestampsByTimestampAndTimespan(TimeSpan.week,timestamp);
    List<Food> allFoodsOfWeek = [];
    for(int i = 0;i < currentWeek.length;i++){
      Map<String,List<Food>> foodFromMainBox = getFoodMapForGivenTimestampFromMainBox(currentWeek[i]);
      Iterable<String> keys = foodFromMainBox.keys;
      if(keys.isNotEmpty){
        for(int i = 0; i < keys.length;i++ ){
          allFoodsOfWeek.addAll(foodFromMainBox[keys.elementAt(i)] as Iterable<Food>);
        }
      }
    }
    allFoodsOfWeek.sort((a, b) => b.calories - a.calories);
    return getListOfDistinctElements(allFoodsOfWeek);
  }

  int getSumOfCaloriesByFoodList(List<Food> foods){
    int sum = 0;
    for(int i = 0; i < foods.length;i++){
      sum += foods[i].calories as int;
    }
    return sum;
  }

  List<BarChartGroupData> getBarChartData(){
    int timestamp = getTimestampFromNow();
    List<int> currentWeek = getTimestampsByTimestampAndTimespan(TimeSpan.week,timestamp);
    int maxValue = 0;
    List<List<int>> result =
    [
      [0,0,0],
      [0,0,0],
      [0,0,0],
      [0,0,0],
      [0,0,0],
      [0,0,0],
      [0,0,0],
    ]; //[[breakfast,lunch,dinner],[breakfast,lunch,dinner] ...]
    for(int i = 0;i < currentWeek.length;i++){
      Map<String,List<Food>> foodMapFromMainBoxByTimestamp = getFoodMapForGivenTimestampFromMainBox(currentWeek[i]);
      Iterable<String> keys = foodMapFromMainBoxByTimestamp.keys;
      if(keys.isNotEmpty){
        for(int j = 0; j < keys.length;j++){
          int value = getSumOfCaloriesByFoodList(foodMapFromMainBoxByTimestamp[keys.elementAt(j)] ?? []);
          if(value > maxValue){
            maxValue = value;
          }
          result[i][j] = value;
        }
      }
    }
    return  [
      BarChartGroupData(
        x: 0,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[0][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[0][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[0][2].toDouble(), // Second segment color
        ),],
      ),
      BarChartGroupData(
        x: 1,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[1][0].toDouble(),
          // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[1][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[1][2].toDouble(),  // Second segment color
        ),],
      ),
      BarChartGroupData(
        x: 2,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[2][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[2][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[2][2].toDouble(),  // Second segment color
        ),],
      ),
      BarChartGroupData(
        x: 3,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[3][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[3][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[3][2].toDouble(),  // Second segment color
        ),],
        showingTooltipIndicators: [0],
      ),
      BarChartGroupData(
        x: 4,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[4][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[4][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[4][2].toDouble(),  // Second segment color
        ),],
      ),
      BarChartGroupData(
        x: 5,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[5][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[5][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[5][2].toDouble(),  // Second segment color
        ),],
      ),
      BarChartGroupData(
        x: 6,
        barRods: [BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.yellow,
          toY: result[6][0].toDouble(), // First segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.red,
          toY: result[6][1].toDouble(),  // Second segment color
        ), BarChartRodData(
          width: 7.5, // Adjust the width of the bar if needed
          color: Colors.green,
          toY: result[6][2].toDouble(),  // Second segment color
        ),],        showingTooltipIndicators: [0],
      ),
    ];
  }

  getTimestampFromNow(){
    DateTime now = DateTime.now();
    return now.millisecondsSinceEpoch.toInt() ~/ 1000;
  }

  List<Food> getMealsOfTodayByMealtype(String mealtype){
    int timestamp = getTimestampFromNow();
    return getFoodMapForGivenTimestampFromMainBox(timestamp)[mealtype] ?? [];

  }

}