import 'dart:io' show File;

import 'package:app_settings/app_settings.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
import 'package:flutter/material.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';

import '../../../core/l10n/app_localizations.dart';
import '../../../core/theme/app_colors.dart';
import '../../../providers/app_version_provider.dart';

/// 本进程内缓存已下载的安装包路径；弹窗销毁后仍可避免「从设置返回再点安装」重复下包。
class _UpdateApkPathCache {
  _UpdateApkPathCache._();

  static String? _key;
  static String? _path;

  static String _composeKey(VersionCheckResult r) {
    final url = r.version.downloadUrl.trim();
    final ver = r.version.newVersion.trim();
    return '$url|$ver';
  }

  static String? peekPath(VersionCheckResult r) {
    final want = _composeKey(r);
    if (_path != null && _key == want) {
      return _path;
    }
    return null;
  }

  static void remember(VersionCheckResult r, String absolutePath) {
    _key = _composeKey(r);
    _path = absolutePath;
  }

  static void forget() {
    _key = null;
    _path = null;
  }
}

/// 版本更新弹窗。
///
/// Android：支持应用内下载 APK 并拉起系统安装界面，并提供「浏览器打开」备用入口。
/// iOS：仅系统浏览器跳转下载页（外链），无应用内 APK 流程。
class UpdateDialog extends StatefulWidget {
  const UpdateDialog({super.key, required this.result});

  final VersionCheckResult result;

  /// 显示版本更新弹窗。
  static void show(BuildContext context, VersionCheckResult result) {
    showDialog<void>(
      context: context,
      barrierDismissible: !result.isForce,
      builder: (_) => UpdateDialog(result: result),
    );
  }

  @override
  State<UpdateDialog> createState() => _UpdateDialogState();
}

class _UpdateDialogState extends State<UpdateDialog> {
  final CancelToken _cancelToken = CancelToken();

  bool _downloading = false;
  double? _downloadProgress;

  VersionCheckResult get result => widget.result;

  bool get _isAndroid =>
      !kIsWeb && defaultTargetPlatform == TargetPlatform.android;

  @override
  void dispose() {
    _cancelToken.cancel();
    super.dispose();
  }

  Uri? _parseDownloadUri() {
    final url = result.version.downloadUrl.trim();
    if (url.isEmpty) {
      return null;
    }
    final uri = Uri.tryParse(url);
    if (uri == null || !uri.hasScheme) {
      return null;
    }
    final s = uri.scheme.toLowerCase();
    if (s != 'http' && s != 'https') {
      return null;
    }
    if (uri.host.isEmpty) {
      return null;
    }
    return uri;
  }

  Future<void> _launchBrowser(AppLocalizations l10n) async {
    final uri = _parseDownloadUri();
    if (uri == null) {
      return;
    }

    try {
      final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
      if (!ok && mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.cannotOpenLink)),
        );
      }
    } catch (_) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.cannotOpenLink)),
        );
      }
    }
  }

  Future<void> _downloadAndInstallApk(AppLocalizations l10n) async {
    final uri = _parseDownloadUri();
    if (uri == null) {
      return;
    }

    try {
      final dir = await getTemporaryDirectory();
      final defaultPath = '${dir.path}/coinvision_upgrade.apk';
      var cachedPath = _UpdateApkPathCache.peekPath(result);
      if (cachedPath != null && !_cachedApkStillOnDisk(cachedPath)) {
        _UpdateApkPathCache.forget();
        cachedPath = null;
      }
      final savePath = cachedPath ?? defaultPath;
      final needDownload = cachedPath == null;

      if (needDownload) {
        final dio = Dio(
          BaseOptions(
            connectTimeout: const Duration(seconds: 30),
            receiveTimeout: const Duration(minutes: 30),
          ),
        );

        setState(() {
          _downloading = true;
          _downloadProgress = 0;
        });

        await dio.download(
          uri.toString(),
          savePath,
          cancelToken: _cancelToken,
          onReceiveProgress: (received, total) {
            if (!mounted) {
              return;
            }
            if (total <= 0) {
              setState(() {
                _downloadProgress = null;
              });
            } else {
              setState(() {
                _downloadProgress = received / total;
              });
            }
          },
        );

        _UpdateApkPathCache.remember(result, savePath);

        if (!mounted) {
          return;
        }

        setState(() {
          _downloading = false;
          _downloadProgress = null;
        });
      }

      if (!mounted) {
        return;
      }

      final permitted = await _ensureInstallPackagesPermission(l10n);
      if (!permitted || !mounted) {
        return;
      }

      final openRes = await OpenFilex.open(
        savePath,
        type: 'application/vnd.android.package-archive',
      );
      if (!mounted) {
        return;
      }
      if (openRes.type != ResultType.done) {
        if (openRes.type == ResultType.fileNotFound) {
          _UpdateApkPathCache.forget();
        }
        final msg = openRes.type == ResultType.permissionDenied
            ? l10n.installPermissionRequired
            : l10n.upgradeDownloadOrInstallFailed;
        ScaffoldMessenger.maybeOf(context)?.showSnackBar(
          SnackBar(
            content: Text(msg),
            action: SnackBarAction(
              label: l10n.goToSettings,
              onPressed: () {
                AppSettings.openAppSettings(
                  type: AppSettingsType.manageUnknownAppSources,
                  asAnotherTask: true,
                );
              },
            ),
          ),
        );
      }
    } on DioException catch (_) {
      if (mounted) {
        setState(() {
          _downloading = false;
          _downloadProgress = null;
        });
        _UpdateApkPathCache.forget();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.upgradeDownloadOrInstallFailed)),
        );
      }
    } catch (_) {
      if (mounted) {
        setState(() {
          _downloading = false;
          _downloadProgress = null;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.upgradeDownloadOrInstallFailed)),
        );
      }
    }
  }

  /// 临时目录 APK 可能被系统清理；路径在缓存中但文件已删除时不应跳过下载。
  bool _cachedApkStillOnDisk(String absolutePath) {
    try {
      return File(absolutePath).existsSync();
    } catch (_) {
      return false;
    }
  }

  Future<bool> _ensureInstallPackagesPermission(AppLocalizations l10n) async {
    var status = await Permission.requestInstallPackages.status;
    if (status.isGranted) {
      return true;
    }

    status = await Permission.requestInstallPackages.request();
    if (status.isGranted) {
      return true;
    }

    // MIUI / 部分国产 ROM：系统不会弹窗，需在「允许安装未知应用」中为本应用单独放行。
    if (!mounted) {
      return false;
    }

    ScaffoldMessenger.maybeOf(context)?.showSnackBar(
      SnackBar(
        content: Text(l10n.installPermissionRequired),
        duration: const Duration(seconds: 8),
        action: SnackBarAction(
          label: l10n.goToSettings,
          onPressed: () {
            AppSettings.openAppSettings(
              type: AppSettingsType.manageUnknownAppSources,
              asAnotherTask: true,
            );
          },
        ),
      ),
    );

    return false;
  }

  Widget _buildActionArea(
    BuildContext context,
    ColorScheme cs,
    AppLocalizations l10n,
  ) {
    if (!_isAndroid) {
      return Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          FilledButton(
            onPressed: () => _launchBrowser(l10n),
            style: FilledButton.styleFrom(
              backgroundColor: AppColors.brand,
              foregroundColor: Colors.black,
              padding: const EdgeInsets.symmetric(vertical: 14),
            ),
            child: Text(
              l10n.updateNow,
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
          ),
          if (!result.isForce) ...[
            const SizedBox(height: 8),
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text(
                l10n.remindLater,
                style: TextStyle(color: cs.onSurface.withAlpha(153)),
              ),
            ),
          ],
        ],
      );
    }

    final primaryStyle = FilledButton.styleFrom(
      backgroundColor: AppColors.brand,
      foregroundColor: Colors.black,
      padding: const EdgeInsets.symmetric(vertical: 14),
      disabledBackgroundColor: AppColors.brand.withAlpha(120),
      disabledForegroundColor: Colors.black.withAlpha(120),
    );

    final secondaryOutline = OutlinedButton.styleFrom(
      foregroundColor: cs.onSurface.withAlpha(230),
      side: BorderSide(color: cs.outline.withAlpha(180)),
      padding: const EdgeInsets.symmetric(vertical: 12),
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FilledButton(
          onPressed:
              _downloading ? null : () => _downloadAndInstallApk(l10n),
          style: primaryStyle,
          child: Text(
            l10n.updateInAppInstall,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
        const SizedBox(height: 10),
        if (result.isForce)
          OutlinedButton(
            onPressed: _downloading ? null : () => _launchBrowser(l10n),
            style: secondaryOutline,
            child: Text(l10n.updateOpenInBrowser),
          )
        else
          // 勿用 stretch：外层 Column 为 stretch 时，子 Row 在交叉轴上收到 maxHeight=∞，
          // stretch 会生成 tightFor(height: ∞) 触发 BoxConstraints forces an infinite height。
          Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Expanded(
                child: TextButton(
                  onPressed: _downloading ? null : () => Navigator.of(context).pop(),
                  style: TextButton.styleFrom(
                    foregroundColor: cs.onSurface.withAlpha(153),
                    padding: const EdgeInsets.symmetric(vertical: 12),
                  ),
                  child: Text(
                    l10n.remindLater,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton(
                  onPressed: _downloading ? null : () => _launchBrowser(l10n),
                  style: secondaryOutline,
                  child: Text(
                    l10n.updateOpenInBrowser,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ),
            ],
          ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final l10n = AppLocalizations.of(context)!;
    final version = result.version;

    // [Dialog] 默认仅 minWidth≈280，maxWidth 为无限；Column + stretch 时子组件（如按钮 min 宽为 ∞）
    // 会破坏约束断言。限制 maxWidth/maxHeight，由 Column(mainAxis.min) 按内容高度收缩，避免全屏留白与崩溃。
    final mq = MediaQuery.sizeOf(context);
    final maxDialogW =
        mq.width <= 560 ? mq.width - 48.0 : 560.0;
    final maxDialogH = mq.height * 0.85;

    return PopScope(
      canPop: !result.isForce,
      child: Dialog(
        backgroundColor: cs.surface,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
        clipBehavior: Clip.antiAlias,
        constraints: BoxConstraints(
          minWidth: 280,
          maxWidth: maxDialogW.clamp(280.0, 560.0),
          maxHeight: maxDialogH,
        ),
        child: Padding(
          padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
                Row(
                  children: [
                    Icon(Icons.system_update, color: AppColors.brand, size: 24),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        l10n.newVersionFound,
                        style: TextStyle(
                          color: cs.onSurface,
                          fontSize: 16,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                Text(
                  '${l10n.latestVersion}：v${version.newVersion}',
                  style: TextStyle(
                    color: cs.onSurface,
                    fontSize: 14,
                    fontWeight: FontWeight.w500,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  '${l10n.currentVersion}：v${result.currentVersion}',
                  style: TextStyle(
                    color: cs.onSurface.withAlpha(153),
                    fontSize: 13,
                  ),
                ),
                const SizedBox(height: 16),
                if (version.updateDescription.isNotEmpty) ...[
                  Text(
                    l10n.updateContent,
                    style: TextStyle(
                      color: cs.onSurface,
                      fontSize: 14,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                  const SizedBox(height: 8),
                  ConstrainedBox(
                    constraints: const BoxConstraints(maxHeight: 200),
                    child: SingleChildScrollView(
                      physics: const ClampingScrollPhysics(),
                      child: Text(
                        version.updateDescription,
                        style: TextStyle(
                          color: cs.onSurface.withAlpha(200),
                          fontSize: 13,
                          height: 1.6,
                        ),
                      ),
                    ),
                  ),
                ],
                if (result.isForce) ...[
                  const SizedBox(height: 12),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
                    decoration: BoxDecoration(
                      color: AppColors.error.withAlpha(20),
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Icon(
                          Icons.warning_amber_rounded,
                          color: AppColors.error,
                          size: 16,
                        ),
                        const SizedBox(width: 4),
                        Expanded(
                          child: Text(
                            l10n.forceUpdateTip,
                            style: TextStyle(color: AppColors.error, fontSize: 12),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
                if (_downloading) ...[
                  const SizedBox(height: 14),
                  Text(
                    l10n.downloadingUpdate,
                    style: TextStyle(color: cs.onSurface.withAlpha(200), fontSize: 13),
                  ),
                  const SizedBox(height: 8),
                  ClipRRect(
                    borderRadius: BorderRadius.circular(4),
                    child: LinearProgressIndicator(
                      minHeight: 6,
                      value: (_downloadProgress != null &&
                              _downloadProgress! >= 0 &&
                              _downloadProgress! <= 1)
                          ? _downloadProgress
                          : null,
                      backgroundColor: cs.surfaceContainerHighest,
                      color: AppColors.brand,
                    ),
                  ),
                ],
                const SizedBox(height: 8),
                _buildActionArea(context, cs, l10n),
              ],
            ),
          ),
      ),
    );
  }
}
