import 'dart:convert'; import 'dart:math'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import '../../core/hijri_date.dart'; import '../local/models.dart'; class HijriCalendarService { HijriCalendarService._({http.Client? client}) : _client = client ?? http.Client(); static final HijriCalendarService instance = HijriCalendarService._(); static const String _baseUrl = 'https://api.myquran.com/v3/cal/hijr'; static const Duration _requestTimeout = Duration(seconds: 5); static final RegExp _flexDateKeyRegex = RegExp(r'^(\d{4})-(\d{1,2})-(\d{1,2})$'); final http.Client _client; final Map _cache = {}; final DateFormat _dateFormat = DateFormat('yyyy-MM-dd'); DateTime _dateOnly(DateTime value) => DateTime(value.year, value.month, value.day); String _dateKeyFromDate(DateTime value) => _dateFormat.format(_dateOnly(value)); String? _normalizeDateKey(String rawValue) { final trimmed = rawValue.trim(); if (trimmed.isEmpty) return null; final parsedIso = DateTime.tryParse(trimmed); if (parsedIso != null) return _dateKeyFromDate(parsedIso); final match = _flexDateKeyRegex.firstMatch(trimmed); if (match == null) return null; final year = int.tryParse(match.group(1) ?? ''); final month = int.tryParse(match.group(2) ?? ''); final day = int.tryParse(match.group(3) ?? ''); if (year == null || month == null || day == null) return null; if (month < 1 || month > 12 || day < 1 || day > 31) return null; return _dateKeyFromDate(DateTime(year, month, day)); } Box? _diskCacheBoxOrNull() { if (!Hive.isBoxOpen(HiveBoxes.hijriCache)) return null; return Hive.box(HiveBoxes.hijriCache); } String? _readDiskCache(String dateKey) => _diskCacheBoxOrNull()?.get(dateKey); Future _writeDiskCache(String dateKey, String label) async { final box = _diskCacheBoxOrNull(); if (box == null) return; await box.put(dateKey, label); } Future _fetchHijriLabelFromApi(String dateKey) async { try { final response = await _client .get(Uri.parse('$_baseUrl/$dateKey')) .timeout(_requestTimeout); if (response.statusCode != 200) return null; final payload = json.decode(response.body) as Map; return parseHijriLabel(payload); } catch (_) { return null; } } Future getHijriLabel(DateTime date) async { final dateOnly = _dateOnly(date); final dateKey = _dateKeyFromDate(dateOnly); final cached = _cache[dateKey]; if (cached != null) return cached; final diskCached = _readDiskCache(dateKey); if (diskCached != null && diskCached.trim().isNotEmpty) { _cache[dateKey] = diskCached; return diskCached; } final label = await _fetchHijriLabelFromApi(dateKey); if (label != null && label.trim().isNotEmpty) { _cache[dateKey] = label; await _writeDiskCache(dateKey, label); return label; } // Keep UI usable when the device is offline. final fallback = HijriDateFormatter.format(dateOnly); _cache[dateKey] = fallback; return fallback; } /// Prefetch and persist Hijri labels for a set of Gregorian date keys. /// Date keys should be in `yyyy-MM-dd`, but this method normalizes inputs. Future warmCacheForDateKeys( Iterable rawDateKeys, { int maxConcurrentRequests = 4, }) async { final uniqueKeys = {}; for (final rawKey in rawDateKeys) { final normalized = _normalizeDateKey(rawKey); if (normalized != null) uniqueKeys.add(normalized); } if (uniqueKeys.isEmpty) return; final pendingKeys = []; for (final dateKey in uniqueKeys) { final memory = _cache[dateKey]; if (memory != null && memory.trim().isNotEmpty) continue; final disk = _readDiskCache(dateKey); if (disk != null && disk.trim().isNotEmpty) { _cache[dateKey] = disk; continue; } pendingKeys.add(dateKey); } if (pendingKeys.isEmpty) return; final concurrency = maxConcurrentRequests.clamp(1, 8); for (var index = 0; index < pendingKeys.length; index += concurrency) { final end = min(index + concurrency, pendingKeys.length); final chunk = pendingKeys.sublist(index, end); await Future.wait(chunk.map((dateKey) async { final label = await _fetchHijriLabelFromApi(dateKey); if (label == null || label.trim().isEmpty) return; _cache[dateKey] = label; await _writeDiskCache(dateKey, label); })); } } static String? parseHijriLabel(Map payload) { if (payload['status'] != true) return null; final data = payload['data']; if (data is! Map) return null; final hijr = data['hijr']; if (hijr is! Map) return null; final today = hijr['today']?.toString().trim(); if (today != null && today.isNotEmpty) { final parts = today.split(','); return parts.length > 1 ? parts.last.trim() : today; } final day = hijr['day']; final monthName = hijr['monthName']?.toString().trim(); final year = hijr['year']; if (day != null && monthName != null && monthName.isNotEmpty && year != null) { return '$day $monthName $year H'; } return null; } }