import 'dart:async';
import 'dart:developer';
import 'package:collection/collection.dart';

import 'package:flutter/widgets.dart';
import 'package:mnemo_cards/features/tests/test_state_holder.dart';
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
import 'package:mnemo_cards/features/packs/pack_cache_manager.dart';
import 'package:mnemo_cards/main.dart';
import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards/utils/string_helper.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:rxdart/rxdart.dart';

import '../../managers/repository/http_repository.dart';
import 'question_states/test_question_state.dart';

class TestManager extends ChangeNotifier {
  final PackCacheManager packCacheManager;
  final HttpRepository _repository;
  final PageController pageController = PageController();
  final TestHolder testHolder = TestHolder();
  final StreamController<void> _triggerController =
      StreamController.broadcast();

  TestDto? _test;

  Color get color => _test?.color?.asColor ?? borderGray;

  List<AbstractTestQuestion> get questions => _test?.questions ?? [];

  void triggerPoll() => _triggerController.add(1);

  Future<List<TestDto>> pollTests({String? packId}) async {
    print('polling tests');
    final tests = (await _repository.getTests(packId: packId ?? ''));
    return tests;
  }

  void skip(int id) {
    testHolder.skip(id);
    notifyListeners();
  }

  Stream<List<TestDto>> loadTestsStream({String? packId}) {
    return Rx.combineLatest2(
      _triggerController.stream.startWith(null),
      Stream.periodic(
        Duration(seconds: 120),
      ).startWith(null),
      (_, __) => null,
    )
        .throttleTime(Duration(seconds: 1))
        .asyncMap(
          (tests) async => pollTests(packId: packId)
              .catchError((_) => Future.value(<TestDto>[])),
        )
        .where((test) => test.isNotEmpty)
        .distinct((p, n) {
      if (p.length == n.length) {
        for (int i = 0; i < p.length; i++) {
          if (p[i].id != n[i].id ||
              p[i].version != n[i].version ||
              !ListEquality().equals(
                p[i].statistics?.words.words,
                n[i].statistics?.words.words,
              )) {
            return false;
          }
        }
      }
      return false;
    });
  }

  TestQuestionState? getState(int id) => testHolder.testQuestionState(id);

  Iterable<TestQuestionState> get states => testHolder.state.questions.values;

  Future<void> setActiveTest(String id, {String? packId}) async {
    clearStates();
    if (packId != null) {
      await packCacheManager.loadPackFromCache(packId);
    }
    try {
      _test = await _repository.getTest(id);
    } on Object catch (e, s) {
      log('error when loading test ${id}', error: e, stackTrace: s);
    }
    if (_test == null) {
      appRouter.maybePop();
      return;
    }
    // todo use uuid
    testHolder.setQuestions(
      _test!.questions,
      DateTime.timestamp().toString(),
    );
  }

  void setState(int id, TestQuestionState state) {
    testHolder.setQuestionState(id, state);
    log('Set state $id $state');
    notifyListeners();
  }

  TestManager(
    this.packCacheManager,
    this._repository,
  );

  void clearStates() {
    testHolder.clear();
    notifyListeners();
  }

  AbstractTestQuestion getTest(int index) => questions[index];

  void nextTest() {
    pageController.animateToPage(
      pageController.page!.toInt() + 1,
      duration: Duration(milliseconds: 300),
      curve: Curves.ease,
    );
  }

  void prevTest() {
    pageController.animateToPage(
      pageController.page!.toInt() - 1,
      duration: Duration(milliseconds: 300),
      curve: Curves.ease,
    );
  }

  Future<void> preloadImages(BuildContext context) async {
    for (final question in questions) {
      switch (question.questionType) {
        case TestQuestionType.simple:
          final images = [
            (question as SimpleTestQuestionBody).image,
            ...question.buttons.map((e) => e.image)
          ].whereNotNull();
          for (final image in images) {
            try {
              final testImage = TestImage.image(image);
              await precacheImage(
                (await testImage.loadImage())!,
                context,
              );
            } catch (e) {
              rethrow;
            }
          }
        case TestQuestionType.input_buttons:
          final images = [
            (question as InputButtonsTestQuestionBody).image,
          ].whereNotNull();
          for (final image in images) {
            try {
              final testImage = TestImage.image(image);
              await precacheImage(
                (await testImage.loadImage())!,
                context,
              );
            } catch (e) {
              rethrow;
            }
          }
        case TestQuestionType.matrix:
        // TODO: Handle this case.
        case TestQuestionType.match:
        // TODO: Handle this case.
        case TestQuestionType.undefined:
        // TODO: Handle this case.
      }
    }
  }

  Future<MemoryImage> loadImage(String packId, String path) async {
    final bytes = await packCacheManager.loadPackFile(packId, path);
    return MemoryImage(bytes!);
  }

  Future<void> sendStatistics() {
    final words = states
        .fold(<String, WordStatisticsDto>{}, (stats, s) {
          final existStat = stats[s.word] ?? WordStatisticsDto.empty(s.word);
          stats[s.word] = existStat.copyWith(
            correct: existStat.correct + (s.isCorrect ? 1 : 0),
            incorrect:
                existStat.incorrect + ((!s.isCorrect && s.isAnswered) ? 1 : 0),
            skipped: existStat.skipped + (s.isAnswered ? 0 : 1),
            questionTypes: {...existStat.questionTypes, s.testType},
          );
          return stats;
        })
        .values
        .toList();

    final dto = TestStatisticsDto(
      testId: int.tryParse(_test?.id ?? '') ?? -1,
      words: AllWordsStatisticsDto(words: words),
      sessionToken: testHolder.state.sessionToken,
    );
    return _repository.addTestStatistics(dto);
  }
}
