import 'dart:async';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

import '../../core/contracts/app_update.dart';
import '../../core/infra/github_releases/github_releases_client.dart';
import '../../shared/app_info.dart';
import '../../shared/utils/app_logger.dart';
import '../../shared/utils/semver.dart';
import '../../shared/utils/window_bounds.dart';

class UpdateVerificationException implements Exception {
  UpdateVerificationException(this.message);
  final String message;
  @override
  String toString() => 'UpdateVerificationException: $message';
}

class UpdateService {
  static const _tag = 'Update';

  final GithubReleasesClient _client;
  final Dio _dio;
  final FutureOr<String> Function() _readCurrentVersion;

  UpdateService({
    GithubReleasesClient? client,
    Dio? dio,
    FutureOr<String> Function()? readCurrentVersion,
  }) : _readCurrentVersion = readCurrentVersion ?? getSmPlayerVersion,
       _client = client ?? GithubReleasesClient(),
       _dio =
           dio ??
           Dio(
             BaseOptions(
               connectTimeout: const Duration(seconds: 8),
               receiveTimeout: const Duration(seconds: 15),
               followRedirects: true,
             ),
           );


  static String? get currentPlatformKey {
    if (Platform.isWindows) return 'windows-x64';
    if (Platform.isMacOS) return 'macos';
    if (Platform.isAndroid) return 'android-arm64';
    return null;
  }

  Future<UpdateCheckResult> check() async {
    final platformKey = currentPlatformKey;
    if (platformKey == null) {
      return const UpdateCheckResult(hasUpdate: false);
    }

    final latest = await _client.fetchLatestRelease(platformKey: platformKey);
    final currentVersion = await _readCurrentVersion();

    final platformEntry = latest.platformAssets[platformKey];
    final selectedAsset = _selectPlatformAsset(latest);

    if (platformEntry == null && selectedAsset == null) {
      return const UpdateCheckResult(hasUpdate: false);
    }

    final effectiveRemoteVersion = (platformEntry?.version?.isNotEmpty ?? false)
        ? platformEntry!.version!
        : latest.version;

    if (!isNewerVersion(currentVersion, effectiveRemoteVersion)) {
      return const UpdateCheckResult(hasUpdate: false);
    }

    return UpdateCheckResult(
      hasUpdate: true,
      info: latest.copyWith(
        version: effectiveRemoteVersion,
        selectedAsset: selectedAsset,
        noAssetForPlatform: selectedAsset == null,
      ),
    );
  }

  static Future<String> computeSha256(File file) async {
    final bytes = await file.readAsBytes();
    return sha256.convert(bytes).toString();
  }

  Future<File> download(
    AppUpdateAsset asset, {
    required CancelToken cancelToken,
    void Function(int received, int total)? onReceiveProgress,
  }) async {
    final directory = Directory(
      '${(await getTemporaryDirectory()).path}${Platform.pathSeparator}'
      'smplayer-update',
    );
    if (await directory.exists()) {
      try {
        await directory.delete(recursive: true);
      } catch (e) {
        AppLogger.warn(_tag, 'clean update temp directory failed', e);
      }
    }
    await directory.create(recursive: true);

    final file = File(
      '${directory.path}${Platform.pathSeparator}${_safeFileName(asset.name)}',
    );
    await _dio.download(
      asset.downloadUrl,
      file.path,
      cancelToken: cancelToken,
      onReceiveProgress: onReceiveProgress,
    );
    final expected = asset.sha256;
    if (expected == null || expected.isEmpty) {
      throw UpdateVerificationException('更新包缺少校验信息，请前往发布页手动下载');
    }
    final actual = await computeSha256(file);
    if (actual.toLowerCase() != expected.toLowerCase()) {
      AppLogger.warn(_tag, 'sha256 mismatch expected=$expected actual=$actual');
      try {
        await file.delete();
      } catch (_) {}
      throw UpdateVerificationException('更新包校验失败，请前往发布页重试');
    }
    return file;
  }

  static const _apkInstallerChannel = MethodChannel('smplayer/apk_installer');

  Future<void> launchInstaller(String path) async {
    if (Platform.isWindows) {
      await Process.start(path, const [], mode: ProcessStartMode.detached);
      await saveWindowBounds();
      await Future<void>.delayed(const Duration(milliseconds: 500));
      exit(0);
    }

    if (Platform.isMacOS) {
      await Process.start('open', [path], mode: ProcessStartMode.detached);
      return;
    }

    if (Platform.isAndroid) {
      await _apkInstallerChannel.invokeMethod<void>('installApk', {
        'path': path,
      });
      return;
    }

    throw UnsupportedError('unsupported platform');
  }

  AppUpdateAsset? _selectPlatformAsset(AppUpdateInfo info) {
    final platformKey = currentPlatformKey;
    final platformAsset = platformKey == null
        ? null
        : info.platformAssets[platformKey];
    if (platformAsset != null) return platformAsset;
    return _selectFromAssets(info.assets);
  }

  AppUpdateAsset? _selectFromAssets(List<AppUpdateAsset> assets) {
    if (Platform.isWindows) {
      final setupPattern = RegExp(
        r'^smplayer-setup-.*-x64\.exe$',
        caseSensitive: false,
      );
      for (final asset in assets) {
        if (setupPattern.hasMatch(asset.name)) return asset;
      }
      for (final asset in assets) {
        if (asset.name.toLowerCase().endsWith('.exe')) return asset;
      }
    }

    if (Platform.isMacOS) {
      for (final asset in assets) {
        if (asset.name.toLowerCase().endsWith('.dmg')) return asset;
      }
    }

    if (Platform.isAndroid) {
      final arm64Pattern = RegExp(r'arm64|arm64-v8a', caseSensitive: false);
      for (final asset in assets) {
        final lowerName = asset.name.toLowerCase();
        if (lowerName.endsWith('.apk') && arm64Pattern.hasMatch(lowerName)) {
          return asset;
        }
      }
      for (final asset in assets) {
        if (asset.name.toLowerCase().endsWith('.apk')) return asset;
      }
    }

    return null;
  }

  String _safeFileName(String name) {
    return name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
  }
}
