import 'dart:convert';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:open_filex/open_filex.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';

class AppUpdateInfo {
  final String latestVersion;
  final int versionCode;
  final String apkUrl;
  final DateTime? publishedAt;
  final String notes;
  final String sha256;
  final int minSupportedVersionCode;

  const AppUpdateInfo({
    required this.latestVersion,
    required this.versionCode,
    required this.apkUrl,
    required this.publishedAt,
    required this.notes,
    required this.sha256,
    required this.minSupportedVersionCode,
  });

  static DateTime? _parsePublishedAt(String? rawValue) {
    final value = (rawValue ?? '').trim();
    if (value.isEmpty) return null;

    // Accept timestamps with timezone offset both as +07:00 and +0700.
    final normalized = value.replaceFirstMapped(
      RegExp(r'([+-]\d{2})(\d{2})$'),
      (match) => '${match.group(1)}:${match.group(2)}',
    );
    return DateTime.tryParse(normalized);
  }

  factory AppUpdateInfo.fromJson(Map<String, dynamic> json) {
    return AppUpdateInfo(
      latestVersion: (json['latest_version'] as String? ?? '').trim(),
      versionCode: (json['version_code'] as num?)?.toInt() ?? 0,
      apkUrl: (json['apk_url'] as String? ?? '').trim(),
      publishedAt: _parsePublishedAt(json['published_at'] as String?),
      notes: (json['notes'] as String? ?? '').trim(),
      sha256: (json['sha256'] as String? ?? '').trim(),
      minSupportedVersionCode:
          (json['min_supported_version_code'] as num?)?.toInt() ?? 1,
    );
  }

  bool get isValid =>
      latestVersion.isNotEmpty && versionCode > 0 && apkUrl.isNotEmpty;
}

class AppVersionInfo {
  final String versionName;
  final int versionCode;

  const AppVersionInfo({
    required this.versionName,
    required this.versionCode,
  });

  String get displayLabel => '$versionName+$versionCode';
}

class UpdateCheckResult {
  final AppVersionInfo current;
  final AppUpdateInfo? remote;
  final bool updateAvailable;
  final String? errorMessage;

  const UpdateCheckResult({
    required this.current,
    required this.remote,
    required this.updateAvailable,
    required this.errorMessage,
  });

  bool get hasError => errorMessage != null;

  const UpdateCheckResult.error({
    required AppVersionInfo current,
    required String message,
  }) : this(
          current: current,
          remote: null,
          updateAvailable: false,
          errorMessage: message,
        );

  const UpdateCheckResult.success({
    required AppVersionInfo current,
    required AppUpdateInfo remote,
    required bool updateAvailable,
  }) : this(
          current: current,
          remote: remote,
          updateAvailable: updateAvailable,
          errorMessage: null,
        );
}

class UpdateService {
  UpdateService._();

  static const String metadataUrl = 'https://files.jamshalat.com/latest.json';
  static final UpdateService instance = UpdateService._();

  Future<AppVersionInfo> getCurrentVersion() async {
    final packageInfo = await PackageInfo.fromPlatform();
    return AppVersionInfo(
      versionName: packageInfo.version,
      versionCode: int.tryParse(packageInfo.buildNumber) ?? 0,
    );
  }

  Future<UpdateCheckResult> checkForUpdate() async {
    final current = await getCurrentVersion();

    try {
      final response = await http.get(
        Uri.parse(metadataUrl),
        headers: const {
          'Accept': 'application/json',
          'Cache-Control': 'no-cache',
        },
      );

      if (response.statusCode != 200) {
        return UpdateCheckResult.error(
          current: current,
          message: 'Gagal mengambil data update (${response.statusCode}).',
        );
      }

      final payload = json.decode(response.body) as Map<String, dynamic>;
      final remote = AppUpdateInfo.fromJson(payload);

      if (!remote.isValid) {
        return UpdateCheckResult.error(
          current: current,
          message: 'Format update tidak valid.',
        );
      }

      final updateAvailable = remote.versionCode > current.versionCode;
      return UpdateCheckResult.success(
        current: current,
        remote: remote,
        updateAvailable: updateAvailable,
      );
    } catch (_) {
      return UpdateCheckResult.error(
        current: current,
        message: 'Tidak dapat memeriksa update saat ini.',
      );
    }
  }

  Future<UpdateInstallResult> downloadAndTriggerInstall(
    AppUpdateInfo info, {
    void Function(double progress)? onProgress,
  }) async {
    if (!Platform.isAndroid) {
      return const UpdateInstallResult(
        success: false,
        message: 'Install update otomatis hanya didukung di Android.',
      );
    }

    final uri = Uri.tryParse(info.apkUrl);
    if (uri == null) {
      return const UpdateInstallResult(
        success: false,
        message: 'URL APK tidak valid.',
      );
    }

    final client = http.Client();
    IOSink? sink;
    try {
      final request = http.Request('GET', uri);
      final response = await client.send(request);
      if (response.statusCode != 200) {
        return UpdateInstallResult(
          success: false,
          message: 'Download APK gagal (${response.statusCode}).',
        );
      }

      final tempDir = await getTemporaryDirectory();
      final file = File(
        '${tempDir.path}/jamshalat-update-v${info.latestVersion}-b${info.versionCode}.apk',
      );
      if (file.existsSync()) {
        await file.delete();
      }

      sink = file.openWrite();

      final totalBytes = response.contentLength ?? 0;
      var receivedBytes = 0;
      await for (final chunk in response.stream) {
        sink.add(chunk);
        receivedBytes += chunk.length;
        if (totalBytes > 0) {
          onProgress?.call(receivedBytes / totalBytes);
        }
      }

      await sink.flush();
      await sink.close();
      sink = null;
      onProgress?.call(1);

      final expectedSha = info.sha256.trim().toLowerCase();
      final downloadedSha = sha256
          .convert(await file.readAsBytes())
          .toString()
          .toLowerCase();
      if (expectedSha.isNotEmpty && expectedSha != downloadedSha) {
        await file.delete();
        return const UpdateInstallResult(
          success: false,
          message: 'Checksum APK tidak cocok. Download dibatalkan.',
        );
      }

      final openResult = await OpenFilex.open(
        file.path,
        type: 'application/vnd.android.package-archive',
      );
      if (openResult.type != ResultType.done) {
        final msg = openResult.message.trim().isNotEmpty
            ? openResult.message.trim()
            : 'Tidak dapat membuka installer Android.';
        return UpdateInstallResult(
          success: false,
          message: msg,
        );
      }

      return UpdateInstallResult(
        success: true,
        message: 'APK selesai diunduh. Silakan lanjutkan instalasi pada prompt Android.',
        downloadedFilePath: file.path,
      );
    } catch (_) {
      return const UpdateInstallResult(
        success: false,
        message: 'Terjadi kesalahan saat mengunduh atau memasang update.',
      );
    } finally {
      await sink?.close();
      client.close();
    }
  }
}

class UpdateInstallResult {
  final bool success;
  final String message;
  final String? downloadedFilePath;

  const UpdateInstallResult({
    required this.success,
    required this.message,
    this.downloadedFilePath,
  });
}
