import 'dart:async';
import 'dart:ui';

import 'package:daily_challenge/daily_challenge.dart';
import 'package:daily_challenge/daily_challenge_package.dart';
import 'package:daily_challenge/data/db/daily_challenge_db.dart';
import 'package:daily_challenge/data/db/entities/entities.dart';
import 'package:daily_challenge/ui/pages/congratulations/congratulations_page.dart';
import 'package:guru_utils/id/identifiable.dart';
import 'package:guru_utils/database/batch/batch_data.dart';
import 'package:guru_utils/database/batch/batch_aware.dart';
import 'package:guru_utils/quiver/collection.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_popup/guru_popup.dart';
import 'package:rxdart/rxdart.dart';
import 'package:guru_utils/extensions/extensions.dart';

/// Created by Haoyi on 2023/10/9

class DailyChallengeStore with BatchAware<DailyChallengeEntity> {
  final int yearMonth;

  bool loaded;

  final BehaviorSubject<Map<int, List<DailyChallengeEntity>>>
      _dailyDataSubject =
      BehaviorSubject.seeded(<int, List<DailyChallengeEntity>>{});

  Map<int, List<DailyChallengeEntity>> get dailyData => _dailyDataSubject.value;

  Stream<Map<int, List<DailyChallengeEntity>>> get observableDailyMonthData =>
      _dailyDataSubject.stream;

  List<DailyChallengeEntity> getDailyData(int yearMonthDay) =>
      dailyData[yearMonthDay] ?? [];

  DailyChallengeStore(this.yearMonth) : loaded = false;

  void _refresh() {
    final Map<int, List<DailyChallengeEntity>> result = {};
    for (var entity in dataList) {
      final list = result[entity.yearMonthDay] ??= <DailyChallengeEntity>[];
      list.add(entity);
    }
    _dailyDataSubject.addEx(result);
  }

  Future loadEntities() async {
    final batch =
        await DailyChallengeDB.instance.loadDailyChallengeByYM(yearMonth);
    loaded = true;
    processBatchData(batch);
    _refresh();
  }

  Future updateEntity(DailyChallengeEntity entity) async {
    final batch = await DailyChallengeDB.instance.updateDailyChallenge(entity);
    processBatchData(batch);
    _refresh();
  }
}

class AwardStore with BatchAware<AwardEntity> {
  AwardStore();

  AwardEntity? getAward(int yearMonth) {
    return getData(yearMonth.toString());
  }

  Future loadEntities() async {
    final batch = await DailyChallengeDB.instance.loadAwards();
    processBatchData(batch);
  }

  Future updateEntity(AwardEntity entity) async {
    final batch = await DailyChallengeDB.instance.updateAward(entity);
    processBatchData(batch);
  }
}

class DailyChallengeManager {
  static final DailyChallengeManager instance = DailyChallengeManager._();

  late DailyChallengeDelegate delegate;

  late DailyChallengeConfig config;

  final AwardStore awardStore = AwardStore();

  DailyChallengeRequest? pendingRequest;

  DateTime? focusDateAfterPageResume;

  final List<DateTime> validMonths = [];

  DailyChallengeManager._();

  Stream<Map<String, AwardEntity>> get observableAwards =>
      awardStore.observableData;

  Stream<Map<int, List<DailyChallengeEntity>>>
      observableDailyChallengeMonthData(DateTime focusYearMonth) {
    return _useStore(DateTimeUtils.generateYearMonthNum(focusYearMonth))
        .observableDailyMonthData;
  }

  Stream<List<DailyChallengeEntity>> observableDailyChallengeData(
      DateTime focusDate) {
    return observableDailyChallengeMonthData(focusDate).map((event) =>
        event[DateTimeUtils.generateYearMonthDayNum(focusDate)] ?? []);
  }

  final LinkedLruHashMap<int, DailyChallengeStore> dailyChallengeStoreLruMap =
      LinkedLruHashMap(
          maximumSize: 32,
          onRemoved: (value) {
            value.disposeBatch();
          });

  DailyChallengeStore _useStore(int yearMonth) {
    final store = dailyChallengeStoreLruMap[yearMonth] ??=
        DailyChallengeStore(yearMonth)..loadEntities();
    return store;
  }

  Future<DailyChallengeStore> useLoadedStore(int yearMonth) async {
    final store =
        dailyChallengeStoreLruMap[yearMonth] ??= DailyChallengeStore(yearMonth);
    if (!store.loaded) {
      await store.loadEntities();
    }
    return store;
  }

  List<DailyChallengeEntity> getDailyChallengeByDate(DateTime date) {
    final yearMonth = DateTimeUtils.generateYearMonthNum(date);
    final yearMonthDay = DateTimeUtils.generateYearMonthDayNum(date);
    final store = _useStore(yearMonth);
    final entity = store.getDailyData(yearMonthDay);
    return entity;
  }

  Future<double> getAwardProgress(DateTime date) async {
    final yearMonth = DateTimeUtils.generateYearMonthNum(date);
    final store = await useLoadedStore(yearMonth);
    return getAchievedDays(store.dailyData) /
        DateTimeUtils.daysInMonth(date.year, date.month);
  }

  int getAchievedDays(Map<int, List<DailyChallengeEntity>> data) {
    return data.entries
        .where((element) =>
            getDailyProgress(
                DateTimeUtils.createDateTimeFromDateNum(element.key),
                element.value) >=
            100)
        .length;
  }

  int getDailyProgress(DateTime date, List<DailyChallengeEntity> entities) {
    return delegate.getDailyProgress(date, entities);
  }

  int getMonthIndex(DateTime dateTime) {
    return (dateTime.year - config.start.year) * 12 +
        (dateTime.month - config.start.month);
  }

  String getTrophyImage(int focusYearMonth, {int? achieved}) {
    final baseYearMonth =
        DateTimeUtils.generateYearMonthNum(DateTime(config.start.year, 1, 1));
    final trophy = config
        .trophies[(focusYearMonth - baseYearMonth) % config.trophies.length];
    if (achieved != null && achieved < trophy.achieved.length) {
      return trophy.achieved[achieved];
    }

    final award = awardStore.getAward(focusYearMonth);
    if (award == null) {
      return trophy.initial;
    }
    final resIdx = award.achieved.clamp(0, trophy.achieved.length - 1);
    return trophy.achieved[resIdx];
  }

  int getAchieved(int yearMonth) {
    final award = awardStore.getAward(yearMonth);
    return award?.achieved ?? -1;
  }

  String getExhibitedTrophyImage(int focusYearMonth, {int? achieved}) {
    if (config.exhibitedTrophies.isEmpty) {
      return getTrophyImage(focusYearMonth, achieved: achieved);
    }

    final baseYearMonth =
        DateTimeUtils.generateYearMonthNum(DateTime(config.start.year, 1, 1));
    final trophy = config.exhibitedTrophies[
        (focusYearMonth - baseYearMonth) % config.exhibitedTrophies.length];
    if (achieved != null && achieved < trophy.achieved.length) {
      return trophy.achieved[achieved];
    }
    final award = awardStore.getAward(focusYearMonth);
    if (award == null) {
      return trophy.initial;
    }
    final resIdx = award.achieved.clamp(0, trophy.achieved.length - 1);
    return trophy.achieved[resIdx];
  }

  Future updateDailyChallenge(DailyChallengeEntity entity) async {
    final yearMonth = DateTimeUtils.yyyyMMddStr2yyyyMMNum(entity.id);
    final store = await useLoadedStore(yearMonth);
    await store.updateEntity(entity);
  }

  Future<bool> updateDailyChallengeState(
      DateTime focusDate, DailyChallengeState state) async {
    final updateEntity = DailyChallengeEntity.fromState(focusDate, state);
    await DailyChallengeManager.instance.updateDailyChallenge(updateEntity);
    return updateEntity.status == DailyChallengeStatus.completed;
  }

  void playOrReplay(DateTime focusDate, List<DailyChallengeEntity> entities,
      {VoidCallback? onCompleted}) {
    pendingRequest?.dispose();
    final progress =
        DailyChallengeManager.instance.getDailyProgress(focusDate, entities);
    final request = DailyChallengeRequest(focusDate, (state) async {
      Log.d("playOrReplay update state:${state.progress}");
      // 当前游戏的进度要大于 0，总的进度要小于 100
      if (state.progress > 0 && progress < 100) {
        final completed = await updateDailyChallengeState(focusDate, state);
        if (completed) {
          onCompleted?.call();
        }
      }
    }, entities: entities);
    DailyChallengeManager.instance.delegate.playOrReplay(request);
    pendingRequest = request;
  }

  Future checkAndCollect(DateTime focusDate) async {
    final progress = await getAwardProgress(focusDate);
    for (int achieved = config.policy.achievedPercentages.length - 1;
        achieved >= 0;
        achieved--) {
      final percentage = config.policy.achievedPercentages[achieved];
      if (progress >= percentage) {
        Log.d("progress:$progress percentage:$percentage achieved:$achieved");
        await GuruPopup.instance.showDailyChallengeCongrats(focusDate);
        await awardStore.updateEntity(AwardEntity.create(focusDate, achieved));
        break;
      }
    }
  }

  DailyChallengeAccessMode getAccessMode(DateTime dateTime) {
    final entities = getDailyChallengeByDate(dateTime);
    return delegate.getAccessMode(dateTime, entities);
  }

  void setFocusDateAfterPageResume(DateTime dateTime) {
    focusDateAfterPageResume = dateTime;
  }

  DateTime? consumeFocusDateAfterPageResume() {
    final result = focusDateAfterPageResume;
    focusDateAfterPageResume = null;
    return result;
  }

  Future init(DailyChallengeDelegate delegate) async {
    this.delegate = delegate;
    await DailyChallengeDB.instance.initDatabase();
    await awardStore.loadEntities();
    config = await delegate.loadConfig();
    final yearMonthDateTime = DateTime.now();
    final prevYearMonthDateTime =
        yearMonthDateTime.subtract(const Duration(days: 1));
    final nextYearMonthDateTime =
        yearMonthDateTime.add(const Duration(days: 1));

    final currentMonthDateTime =
        DateTime(yearMonthDateTime.year, yearMonthDateTime.month);

    for (var dt = config.start;
        dt.compareTo(currentMonthDateTime) <= 0;
        dt = DateTime(dt.year, dt.month + 1)) {
      validMonths.add(dt);
    }

    _useStore(DateTimeUtils.generateYearMonthNum(yearMonthDateTime));
    _useStore(DateTimeUtils.generateYearMonthNum(prevYearMonthDateTime));
    _useStore(DateTimeUtils.generateYearMonthNum(nextYearMonthDateTime));
  }
}
