import 'dart:async';

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krezus/repositories/metadata/models/budget.dart';
import 'package:krezus/repositories/metadata/metadata_repository.dart';
import 'package:krezus/repositories/transactions/transactions_repository.dart';

part 'budget_event.dart';
part 'budget_state.dart';

class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
  final MetadataRepository _metadataRepository;
  final TransactionsRepository _transactionsRepository;
  Timer? setValueTimer;

  BudgetBloc({required MetadataRepository metadataRepository, required TransactionsRepository transactionsRepository}) : _metadataRepository = metadataRepository, _transactionsRepository = transactionsRepository, super(const BudgetState())  {
    on<BudgetsLoad>(_onBudgetsLoad);
    on<BudgetAdd>(_onBudgetAdd);
    on<BudgetRemove>(_onBudgetRemove);
    on<BudgetSetValue>(_onBudgetSetValue);
    on<BudgetSetLabel>(_onBudgetSetLabel);
    on<BudgetCompareNext>(_onBudgetCompareNext);
    on<BudgetComparePrevious>(_onBudgetComparePrevious);
    on<BudgetSetCompare>(_onBudgetSetCompare);
    on<BudgetSetInitial>(_onBudgetSetInitial);

    _metadataRepository
      .getBudgetsStream()
      .listen((budgets) => add(BudgetsLoad(budgets)));

    _transactionsRepository
      .getTransactionsStream()
      .listen((transactions) => add(BudgetSetCompare()));
  }

  _onBudgetsLoad(
    BudgetsLoad event, Emitter<BudgetState> emit
  ) {
    emit(_computeState(event.budgets, null));
  }

  FutureOr<void> _onBudgetAdd(BudgetAdd event, Emitter<BudgetState> emit) {
    Budget budget = Budget(
      label: event.label,
    );

    List<Budget> budgets = _computeBudgets(budget);
    _saveBudget(budgets);
    emit(_computeState(budgets, null));
  }

  FutureOr<void> _onBudgetRemove(BudgetRemove event, Emitter<BudgetState> emit) {
    List<Budget> budgets = _metadataRepository.getBudgets();
    Budget budgetToRemove = event.budget;

    budgets.removeWhere((budget) => budget.label == budgetToRemove.label);
    emit(_computeState(_metadataRepository.saveBudgets(budgets), null));
  }

  void _onBudgetSetValue(BudgetSetValue event, Emitter<BudgetState> emit) {
    Budget budgetToUpdate = event.budget;
    double newValue = event.value;

    if (state.remainingBudget - (event.value - budgetToUpdate.value) < 0) {
      newValue = event.budget.value + state.remainingBudget;
    }

    // if (state.remainingBudget - event.value < 0 && state.remainingBudget < 10) {
    //   budgetToUpdate.value = 
    // } else {
    //   budgetToUpdate.value = event.value;
    // }
    budgetToUpdate.value = newValue;
    List<Budget> budgets = _computeBudgets(budgetToUpdate);
    emit(_computeState(budgets, null));

    setValueTimer?.cancel();
    setValueTimer = Timer(
      const Duration(milliseconds: 100), 
      () {
        _saveBudget(budgets);
      }
    );
  }

  void _onBudgetSetLabel(BudgetSetLabel event, Emitter<BudgetState> emit) {
    Budget budgetToUpdate = event.budget;

    budgetToUpdate.label = event.label;
    List<Budget> budgets = _computeBudgets(budgetToUpdate);
    _saveBudget(budgets);
    emit(_computeState(budgets, null));
  }

  void _onBudgetCompareNext(BudgetCompareNext event, Emitter<BudgetState> emit) {
    num compareMonth = state.compareMonth;
    num compareYear = state.compareYear;
    if (state.compareMonth == 12) {
      compareMonth = 1;
      compareYear++;
    } else {
      compareMonth++;
    }

    if (state.lastDate != null && state.lastDate!.isBefore(DateTime(compareYear.toInt(), compareMonth.toInt()))) {
      return;
    }

    final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth);
    emit(state.copyWith(
      compareBudgets: compareResult.$1,
      otherBudgets: compareResult.$2,
      compareMonth: compareMonth,
      compareYear: compareYear,
    ));
  }

  void _onBudgetComparePrevious(BudgetComparePrevious event, Emitter<BudgetState> emit) {
    num compareMonth = state.compareMonth;
    num compareYear = state.compareYear;
    if (state.compareMonth == 1) {
      compareMonth = 12;
      compareYear--;
    } else {
      compareMonth--;
    }

    if (state.firstDate != null && state.firstDate!.isAfter(DateTime(compareYear.toInt(), compareMonth.toInt()))) {
      return;
    }

    final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth);
    emit(state.copyWith(
      compareBudgets: compareResult.$1,
      otherBudgets: compareResult.$2,
      compareMonth: compareMonth,
      compareYear: compareYear,
    ));
  }

  _onBudgetSetCompare(BudgetSetCompare event, Emitter<BudgetState> emit) {
    DateTime firstDate = DateTime.now();
    DateTime lastDate = DateTime.fromMicrosecondsSinceEpoch(0);

    for (var transaction in _transactionsRepository.getTransactions()) {
      if (firstDate.compareTo(transaction.date) > 0) {
        firstDate = transaction.date;
      }

      if (lastDate.compareTo(transaction.date) < 0) {
        lastDate = transaction.date;
      }
    }

    num compareMonth = state.compareMonth;
    num compareYear = state.compareYear;

    if (compareMonth == 1 && compareYear == 2000) {
      compareMonth = lastDate.month;
      compareYear = lastDate.year;
    }

    final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth);
    emit(state.copyWith(
      firstDate: firstDate,
      lastDate: lastDate,
      compareMonth: compareMonth,
      compareYear: compareYear,
      compareBudgets: compareResult.$1,
      otherBudgets: compareResult.$2,
    )); 
  }

  void _onBudgetSetInitial(BudgetSetInitial event, Emitter<BudgetState> emit) {
    emit(_computeState(state.budgets, event.value));
  }

  List<Budget> _computeBudgets(Budget budgetToSave) {
    List<Budget> budgets = _metadataRepository.getBudgets();

    try {
      Budget budgetFound = budgets.firstWhere((category) => category.label == budgetToSave.label);
      budgetFound.value = budgetToSave.value;
    } catch (e) {
      if (budgets.isEmpty) {
        budgets = [budgetToSave];
      } else {
        budgets.add(budgetToSave);
      }
    }

    return budgets;
  }

  List<Budget> _saveBudget(List<Budget> budgets) {
    return _metadataRepository.saveBudgets(budgets);
  }

  (List<Budget> compareBudgets, List<Budget> otherBudgets) _computeCompareBudget(List<Budget> budgets, num year, num month) {
    Map<String, Budget> compareBudgetMap = { for (var budget in budgets) budget.label : Budget(label: budget.label)};
    Budget otherBudget = Budget(label: 'Hors budget');
    Map<String, Budget> otherBudgetMap = {};
    List<Budget> compareBudgets = [];
    _transactionsRepository.getTransactions()
      .where((transaction) => transaction.value < 0 && transaction.date.year == year && transaction.date.month == month)
      .forEach((transaction) {
        Budget? budget = compareBudgetMap[transaction.category];
        if (budget == null) {
          otherBudget.value += transaction.value.abs();
          Budget? otherBudgetFromMap = otherBudgetMap[transaction.category];
          if (otherBudgetFromMap == null) {
            otherBudgetMap[transaction.category] = Budget(label: transaction.category, value: transaction.value.abs());
          } else {
            otherBudgetFromMap.value += transaction.value.abs();
          }
        } else {
          budget.value += transaction.value.abs();
        }
      });

    compareBudgets.addAll(compareBudgetMap.values);
    compareBudgets.add(otherBudget);
    return (compareBudgets, otherBudgetMap.values.toList()..sort((a, b) => b.value.compareTo(a.value)));
  }

  BudgetState _computeState(List<Budget> budgets, double? initialBudget) {
    final compareResult = _computeCompareBudget(state.budgets, state.compareYear, state.compareMonth);
    
    final budgetValues = budgets.map((budget) => budget.value);
    final budgetReducedValues = budgetValues.isEmpty ? 0 : budgetValues.reduce((value, element) => value + element);

    return state.copyWith(
      budgets: budgets,
      initialBudget: (initialBudget ?? state.initialBudget),
      remainingBudget: (initialBudget ?? state.initialBudget) - budgetReducedValues,
      compareBudgets: compareResult.$1,
      otherBudgets: compareResult.$2,
    );
  }
}