import 'dart:async';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/version_checker/version_checker.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:auto_updater/auto_updater.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';

import '../startup.dart';

class AutoUpdateTask extends LaunchTask {
  AutoUpdateTask();

  static const _feedUrl =
      'https://github.com/AppFlowy-IO/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml';
  final _listener = _AppFlowyAutoUpdaterListener();

  @override
  Future<void> initialize(LaunchContext context) async {
    // the auto updater is not supported on mobile
    if (UniversalPlatform.isMobile) {
      return;
    }

    // don't use await here, because the auto updater is not a blocking operation
    unawaited(_setupAutoUpdater());

    ApplicationInfo.isCriticalUpdateNotifier.addListener(
      _showCriticalUpdateDialog,
    );
  }

  @override
  Future<void> dispose() async {
    autoUpdater.removeListener(_listener);

    ApplicationInfo.isCriticalUpdateNotifier.removeListener(
      _showCriticalUpdateDialog,
    );
  }

  // On macOS and windows, we use auto_updater to check for updates.
  // On linux, we use the version checker to check for updates because the auto_updater is not supported.
  Future<void> _setupAutoUpdater() async {
    Log.info(
      '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}',
    );

    // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch.
    final feedUrl = _feedUrl
        .replaceAll('{os}', ApplicationInfo.os)
        .replaceAll('{arch}', ApplicationInfo.architecture);

    // the auto updater is only supported on macOS and windows, so we don't need to check the platform
    if (UniversalPlatform.isMacOS || UniversalPlatform.isWindows) {
      autoUpdater.addListener(_listener);
    }

    Log.info('[AutoUpdate] feed url: $feedUrl');

    versionChecker.setFeedUrl(feedUrl);
    final item = await versionChecker.checkForUpdateInformation();
    if (item != null) {
      ApplicationInfo.latestAppcastItem = item;
      ApplicationInfo.latestVersionNotifier.value =
          item.displayVersionString ?? '';
    }
  }

  void _showCriticalUpdateDialog() {
    showCustomConfirmDialog(
      context: AppGlobals.rootNavKey.currentContext!,
      title: LocaleKeys.autoUpdate_criticalUpdateTitle.tr(),
      description: LocaleKeys.autoUpdate_criticalUpdateDescription.tr(
        namedArgs: {
          'currentVersion': ApplicationInfo.applicationVersion,
          'newVersion': ApplicationInfo.latestVersion,
        },
      ),
      builder: (context) => const SizedBox.shrink(),
      // if the update is critical, dont allow the user to dismiss the dialog
      barrierDismissible: false,
      showCloseButton: false,
      enableKeyboardListener: false,
      closeOnConfirm: false,
      confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(),
      onConfirm: () async {
        await versionChecker.checkForUpdate();
      },
    );
  }
}

class _AppFlowyAutoUpdaterListener extends UpdaterListener {
  @override
  void onUpdaterBeforeQuitForUpdate(AppcastItem? item) {}

  @override
  void onUpdaterCheckingForUpdate(Appcast? appcast) {
    // Due to the reason documented in the following link, the update will not be found if the user has skipped the update.
    // We have to check the skipped version manually.
    // https://sparkle-project.org/documentation/api-reference/Classes/SPUUpdater.html#/c:objc(cs)SPUUpdater(im)checkForUpdateInformation
    final items = appcast?.items;
    if (items != null) {
      final String? currentPlatform;
      if (UniversalPlatform.isMacOS) {
        currentPlatform = 'macos';
      } else if (UniversalPlatform.isWindows) {
        currentPlatform = 'windows';
      } else {
        currentPlatform = null;
      }

      final matchingItem = items.firstWhereOrNull(
        (item) => item.os == currentPlatform,
      );

      if (matchingItem != null) {
        _updateVersionNotifier(matchingItem);

        Log.info(
          '[AutoUpdate] latest version: ${matchingItem.displayVersionString}',
        );
      }
    }
  }

  @override
  void onUpdaterError(UpdaterError? error) {
    Log.error('[AutoUpdate] On update error: $error');
  }

  @override
  void onUpdaterUpdateNotAvailable(UpdaterError? error) {
    Log.info('[AutoUpdate] Update not available $error');
  }

  @override
  void onUpdaterUpdateAvailable(AppcastItem? item) {
    _updateVersionNotifier(item);

    Log.info('[AutoUpdate] Update available: ${item?.displayVersionString}');
  }

  @override
  void onUpdaterUpdateDownloaded(AppcastItem? item) {
    Log.info('[AutoUpdate] Update downloaded: ${item?.displayVersionString}');
  }

  @override
  void onUpdaterUpdateCancelled(AppcastItem? item) {
    _updateVersionNotifier(item);

    Log.info('[AutoUpdate] Update cancelled: ${item?.displayVersionString}');
  }

  @override
  void onUpdaterUpdateInstalled(AppcastItem? item) {
    _updateVersionNotifier(item);

    Log.info('[AutoUpdate] Update installed: ${item?.displayVersionString}');
  }

  @override
  void onUpdaterUpdateSkipped(AppcastItem? item) {
    _updateVersionNotifier(item);

    Log.info('[AutoUpdate] Update skipped: ${item?.displayVersionString}');
  }

  void _updateVersionNotifier(AppcastItem? item) {
    if (item != null) {
      ApplicationInfo.latestAppcastItem = item;
      ApplicationInfo.latestVersionNotifier.value =
          item.displayVersionString ?? '';
    }
  }
}

class AppFlowyAutoUpdateVersion {
  AppFlowyAutoUpdateVersion({
    required this.latestVersion,
    required this.currentVersion,
    required this.isForceUpdate,
  });

  factory AppFlowyAutoUpdateVersion.initial() => AppFlowyAutoUpdateVersion(
        latestVersion: '0.0.0',
        currentVersion: '0.0.0',
        isForceUpdate: false,
      );

  final String latestVersion;
  final String currentVersion;

  final bool isForceUpdate;

  bool get isUpdateAvailable => latestVersion != currentVersion;
}
