import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:open_file/open_file.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:xml/xml.dart';

import '../core/utils/app_logger.dart';
import '../data/models/app_update_model.dart';

class UpdateService {
  UpdateService._();

  static final UpdateService instance = UpdateService._();

  static final Uri releasesRssUrl = Uri.parse(
    'https://git.saont.net/gongyun/app/releases.rss',
  );
  static final Uri releasesPageUrl = Uri.parse(
    'https://git.saont.net/gongyun/app/releases',
  );

  final Dio _dio = Dio(
    BaseOptions(
      connectTimeout: const Duration(seconds: 12),
      receiveTimeout: const Duration(seconds: 20),
      followRedirects: true,
      responseType: ResponseType.plain,
    ),
  );

  bool _checking = false;

  Future<AppUpdateInfo?> checkForUpdate() async {
    if (_checking) return null;
    if (!_supportsAutoDownload) return null;

    _checking = true;
    try {
      final current = await PackageInfo.fromPlatform();
      final latest = await _fetchLatestRelease(currentVersion: current.version);
      if (latest == null) return null;
      if (_compareVersions(latest.version, current.version) <= 0) return null;
      return latest;
    } catch (e, stackTrace) {
      AppLogger.e('检查更新失败: $e\n$stackTrace');
      return null;
    } finally {
      _checking = false;
    }
  }

  Future<File?> downloadUpdate(
    AppUpdateInfo update, {
    void Function(int received, int total)? onProgress,
  }) async {
    final url = update.downloadUrl;
    if (url == null) {
      return null;
    }

    final dir = await getTemporaryDirectory();
    final updateDir = Directory(p.join(dir.path, 'app_updates'));
    await updateDir.create(recursive: true);

    final fileName = update.fileName ?? _fileNameFromUrl(url);
    final file = File(p.join(updateDir.path, fileName));
    if (await file.exists()) {
      await file.delete();
    }

    await _dio.downloadUri(
      url,
      file.path,
      onReceiveProgress: onProgress,
      options: Options(responseType: ResponseType.bytes),
    );
    return file;
  }

  Future<void> openInstaller(File file) async {
    final result = await OpenFile.open(file.path);
    if (result.type != ResultType.done) {
      throw Exception(result.message);
    }
  }

  Future<void> openReleasePage(AppUpdateInfo update) async {
    final url = update.pageUrl;
    if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
      throw Exception('无法打开更新页面: $url');
    }
  }

  Future<AppUpdateInfo?> _fetchLatestRelease({String? currentVersion}) async {
    final response = await _dio.getUri<String>(releasesRssUrl);
    final body = response.data;
    if (body == null || body.trim().isEmpty) return null;

    final document = XmlDocument.parse(body);
    final items = document.findAllElements('item').toList();
    if (items.isEmpty) return null;

    final releases =
        items.map(_releaseFromRssItem).whereType<_RssRelease>().toList()
          ..sort(_compareRssReleaseDesc);
    if (releases.isEmpty) return null;

    for (final release in releases) {
      if (currentVersion != null &&
          _compareVersions(release.version, currentVersion) <= 0) {
        continue;
      }

      final update = await _buildUpdateInfo(release);
      if (update == null) continue;

      if (update.downloadUrl == null) continue;
      return update;
    }

    return null;
  }

  _RssRelease? _releaseFromRssItem(XmlElement item) {
    final title = _elementText(item, 'title') ?? '';
    final link = _elementText(item, 'link') ?? releasesPageUrl.toString();
    final description = _elementText(item, 'description');
    final content = _elementText(item, 'encoded');
    final enclosure = item
        .findElements('enclosure')
        .map((e) => e.getAttribute('url'))
        .whereType<String>()
        .map(_absoluteUriOrNull)
        .whereType<Uri>()
        .toList();

    final candidates = <Uri>[
      ...enclosure,
      ..._extractLinks(description ?? ''),
      ..._extractLinks(content ?? ''),
      ..._extractLinks(item.toXmlString()),
    ];

    final pageUrl = _absoluteUriOrNull(link) ?? releasesPageUrl;
    final version = _versionFromText(
      '$title $link ${description ?? ''} ${content ?? ''}',
    );
    if (version == null) return null;

    return _RssRelease(
      version: version,
      title: title.isEmpty ? 'v$version' : title,
      description: _stripHtml(
        (content ?? '').isNotEmpty ? content : description,
      ),
      pageUrl: pageUrl,
      candidates: candidates,
      publishedAt: _parseRssDate(_elementText(item, 'pubDate')),
    );
  }

  Future<AppUpdateInfo?> _buildUpdateInfo(_RssRelease release) async {
    final candidates = <Uri>[...release.candidates];
    final downloadUrl = _pickDownloadUrl(candidates);
    Uri? resolvedDownloadUrl = downloadUrl;

    resolvedDownloadUrl ??= await _downloadUrlFromReleasePage(release.pageUrl);

    return AppUpdateInfo(
      version: release.version,
      title: release.title,
      description: release.description,
      pageUrl: release.pageUrl,
      downloadUrl: resolvedDownloadUrl,
      fileName: resolvedDownloadUrl == null
          ? null
          : _fileNameFromUrl(resolvedDownloadUrl),
      publishedAt: release.publishedAt,
    );
  }

  Future<Uri?> _downloadUrlFromReleasePage(Uri pageUrl) async {
    try {
      final response = await _dio.getUri<String>(pageUrl);
      final body = response.data;
      if (body == null || body.trim().isEmpty) return null;
      return _pickDownloadUrl(_extractLinks(body));
    } catch (e) {
      AppLogger.w('鑾峰彇鏇存柊闄勪欢澶辫触: $pageUrl $e');
      return null;
    }
  }

  Uri? _pickDownloadUrl(List<Uri> candidates) {
    final normalized = candidates.toSet().toList();
    final preferred = normalized
        .where(_isReleaseAttachment)
        .where(_isSupportedPackage)
        .toList();
    if (preferred.isNotEmpty) return preferred.first;
    return null;
  }

  bool get _supportsAutoDownload => Platform.isAndroid || Platform.isWindows;

  bool _isReleaseAttachment(Uri uri) {
    return uri.path.contains('/releases/download/');
  }

  bool _isSupportedPackage(Uri uri) {
    final name = _fileNameFromUrl(uri).toLowerCase();
    if (Platform.isAndroid) {
      return name.endsWith('.apk');
    }
    if (Platform.isWindows) {
      return name.endsWith('.exe') ||
          name.endsWith('.msix') ||
          name.endsWith('.msi') ||
          name.endsWith('.zip');
    }
    if (Platform.isLinux) {
      return name.endsWith('.appimage') ||
          name.endsWith('.deb') ||
          name.endsWith('.rpm') ||
          name.endsWith('.tar.gz') ||
          name.endsWith('.zip');
    }
    if (Platform.isMacOS) {
      return name.endsWith('.dmg') ||
          name.endsWith('.pkg') ||
          name.endsWith('.zip');
    }
    return false;
  }

  List<Uri> _extractLinks(String html) {
    final matches = RegExp(
      r'''https?://[^\s"'<>]+|href=["']([^"']+)["']''',
      caseSensitive: false,
    ).allMatches(html);
    return matches
        .map((m) => m.group(1) ?? m.group(0))
        .whereType<String>()
        .map(_absoluteUriOrNull)
        .whereType<Uri>()
        .toList();
  }

  Uri? _absoluteUriOrNull(String value) {
    final cleaned = _decodeHtmlEntities(value.trim());
    final parsed = Uri.tryParse(cleaned);
    if (parsed == null) return null;
    if (parsed.hasScheme) return parsed;
    return releasesPageUrl.resolveUri(parsed);
  }

  String? _elementText(XmlElement element, String name) {
    for (final child in element.children.whereType<XmlElement>()) {
      if (child.name.qualified == name || child.name.local == name) {
        return child.innerText.trim();
      }
    }
    return null;
  }

  String? _versionFromText(String text) {
    final match = RegExp(
      r'v?(\d+\.\d+\.\d+(?:[+\-][0-9A-Za-z.\-]+)?)',
    ).firstMatch(text);
    return match?.group(1);
  }

  String? _stripHtml(String? value) {
    if (value == null) return null;
    final withoutTags = value.replaceAll(RegExp(r'<[^>]+>'), ' ');
    return _decodeHtmlEntities(
      withoutTags,
    ).replaceAll(RegExp(r'\s+'), ' ').trim();
  }

  String _decodeHtmlEntities(String value) {
    return value
        .replaceAll('&nbsp;', ' ')
        .replaceAll('&amp;', '&')
        .replaceAll('&quot;', '"')
        .replaceAll('&#34;', '"')
        .replaceAll('&#39;', "'")
        .replaceAll('&#43;', '+')
        .replaceAll('&lt;', '<')
        .replaceAll('&gt;', '>');
  }

  DateTime? _parseRssDate(String? value) {
    if (value == null || value.isEmpty) return null;
    final direct = DateTime.tryParse(value);
    if (direct != null) return direct;
    try {
      return HttpDate.parse(value);
    } catch (_) {
      return null;
    }
  }

  int _compareRssReleaseDesc(_RssRelease a, _RssRelease b) {
    final version = _compareVersions(b.version, a.version);
    if (version != 0) return version;

    final left = a.publishedAt;
    final right = b.publishedAt;
    if (left == null && right == null) return 0;
    if (left == null) return 1;
    if (right == null) return -1;
    return right.compareTo(left);
  }

  int _compareVersions(String a, String b) {
    final left = _versionParts(a);
    final right = _versionParts(b);
    final length = left.length > right.length ? left.length : right.length;
    for (var i = 0; i < length; i++) {
      final l = i < left.length ? left[i] : 0;
      final r = i < right.length ? right[i] : 0;
      if (l != r) return l.compareTo(r);
    }
    return 0;
  }

  List<int> _versionParts(String version) {
    final core = version.split(RegExp(r'[+\-]')).first;
    return core
        .split('.')
        .map((part) => int.tryParse(part) ?? 0)
        .toList(growable: false);
  }

  String _fileNameFromUrl(Uri uri) {
    final segment = uri.pathSegments.isEmpty
        ? 'app_update'
        : uri.pathSegments.last;
    return Uri.decodeComponent(segment);
  }

  @visibleForTesting
  Future<AppUpdateInfo?> debugFetchLatestRelease() => _fetchLatestRelease();
}

class _RssRelease {
  final String version;
  final String title;
  final String? description;
  final Uri pageUrl;
  final List<Uri> candidates;
  final DateTime? publishedAt;

  const _RssRelease({
    required this.version,
    required this.title,
    required this.pageUrl,
    required this.candidates,
    this.description,
    this.publishedAt,
  });
}
