import 'package:flutter/foundation.dart';
import '../core/api/meinweimar_api_client.dart';
import '../models/ad_campaign.dart';
import '../models/event.dart';
import '../models/partner.dart';
import '../models/sponsor_location.dart';
import '../models/poi.dart';
import '../models/tour.dart';
import '../repositories/ad_repository.dart';
import '../repositories/event_repository.dart';
import '../repositories/partner_repository.dart';
import '../repositories/sponsor_location_repository.dart';
import '../repositories/poi_repository.dart';
import '../repositories/tour_repository.dart';
import '../services/analytics_service.dart';
import '../services/offline_cache_service.dart';
import 'mock_data.dart';
enum AppDataStatus { idle, loading, ready, fallback }
class AppDataStore extends ChangeNotifier {
AppDataStore({MeinWeimarApiClient? api})
: _api = api ?? MeinWeimarApiClient() {
_pois = MockData.pois;
_tours = MockData.tours;
}
final MeinWeimarApiClient _api;
late List<Poi> _pois;
late List<Tour> _tours;
List<AdCampaign> _ads = const [];
List<Event> _events = const [];
List<Partner> _partners = const [];
AppDataStatus status = AppDataStatus.idle;
String? lastError;
String _language = 'de';
Future<void>? _loadFuture;
List<Poi> get pois => List.unmodifiable(_pois);
List<Tour> get tours => List.unmodifiable(_tours);
List<Partner> get partners => List.unmodifiable(_partners);
List<Event> get events => List.unmodifiable(_events);
bool get usingLiveData =>
status == AppDataStatus.ready && !_api.servedCachedResponse;
bool get usingCachedData =>
status == AppDataStatus.ready && _api.servedCachedResponse;
String get languageCode => _language;
Future<AdCampaign?> loadSponsorInterstitial({
required String languageCode,
}) async {
try {
final campaigns = await AdRepository(_api).fetchPlacement(
AdPlacement.sponsorInterstitial,
languageCode: languageCode,
);
final now = DateTime.now();
final active = campaigns
.where(
(campaign) =>
campaign.isActiveAt(now) &&
campaign.supports(AdPlacement.sponsorInterstitial),
)
.toList()
..sort((a, b) => b.priority.compareTo(a.priority));
if (campaigns.isNotEmpty) {
final otherPlacements = _ads.where(
(campaign) =>
campaign.resolvedPlacement != AdPlacement.sponsorInterstitial,
);
_ads = [
...otherPlacements,
...campaigns,
];
notifyListeners();
}
return active.isEmpty ? null : active.first;
} catch (_) {
return null;
}
}
Poi poiById(String id) => _pois.firstWhere(
(poi) => poi.id == id,
orElse: () => MockData.pois.firstWhere(
(poi) => poi.id == id,
orElse: () => _pois.first,
),
);
Future<EventActionUrls> eventActions(String id) async {
return EventRepository(_api).fetchActions(id: id);
}
Future<Event> eventDetail(String id) async {
Event? current;
for (final event in _events) {
if (event.id == id) {
current = event;
break;
}
}
try {
final detail = await EventRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _events.indexWhere((event) => event.id == id);
if (index >= 0) {
_events = [..._events]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
if (current != null) return current;
rethrow;
}
}
Future<Poi> poiDetail(String id) async {
try {
final detail = await PoiRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _pois.indexWhere((poi) => poi.id == id);
if (index >= 0) {
_pois = [..._pois]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
return poiById(id);
}
}
Future<AdCampaign> campaignDetail(String id) async {
AdCampaign? current;
for (final campaign in _ads) {
if (campaign.id == id) {
current = campaign;
break;
}
}
try {
final detail = await AdRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
// Do not replace the placement list entry with the detail response:
// some API detail payloads intentionally omit placement/targeting fields.
return detail;
} catch (_) {
if (current != null) return current;
rethrow;
}
}
Future<List<SponsorLocation>> sponsorLocations({
required String campaignId,
String? partnerId,
}) async {
return SponsorLocationRepository(_api).fetchAll(
languageCode: _language,
campaignId: campaignId,
partnerId: partnerId,
);
}
Future<Partner> partnerDetail(String id) async {
try {
final detail = await PartnerRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _partners.indexWhere((partner) => partner.id == id);
if (index >= 0) {
_partners = [..._partners]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
return _partners.firstWhere((partner) => partner.id == id);
}
}
Future<void> load({required String languageCode, bool force = false}) {
if (!force && status == AppDataStatus.loading && _loadFuture != null) {
return _loadFuture!;
}
if (!force && status == AppDataStatus.ready && _language == languageCode) {
return Future<void>.value();
}
_loadFuture = _performLoad(languageCode);
return _loadFuture!;
}
Future<void> _performLoad(String languageCode) async {
_language = languageCode;
_api.resetCacheSessionFlag();
status = AppDataStatus.loading;
lastError = null;
notifyListeners();
try {
final pois =
await PoiRepository(_api).fetchAll(languageCode: languageCode);
final poiMap = {for (final poi in pois) poi.id: poi};
final tours = await TourRepository(_api).fetchAll(
languageCode: languageCode,
pois: poiMap,
);
if (pois.isNotEmpty) _pois = pois;
if (tours.isNotEmpty) _tours = tours;
try {
_events =
await EventRepository(_api).fetchAll(languageCode: languageCode);
} catch (_) {
_events = const [];
}
try {
_partners =
await PartnerRepository(_api).fetchAll(languageCode: languageCode);
} catch (_) {
_partners = const [];
}
try {
final adLists = await Future.wait(
AdPlacement.values.map(
(placement) => AdRepository(_api).fetchPlacement(
placement,
languageCode: languageCode,
),
),
);
// Keep one resolved campaign payload per placement. The same
// campaign can intentionally use different creatives in Nearby,
// the side menu, tours and the sponsor splash.
_ads = adLists.expand((list) => list).toList(growable: false);
} catch (_) {
_ads = const [];
}
status = AppDataStatus.ready;
final cacheEntries = await OfflineCacheService.entryCount();
await AnalyticsService.instance.track(
'content_load',
properties: {
'language': languageCode,
'source': _api.servedCachedResponse ? 'cache' : 'live',
'cache_entries': cacheEntries,
'success': true,
},
);
} catch (error) {
lastError = error.toString();
status = AppDataStatus.fallback;
await AnalyticsService.instance.track(
'content_load',
properties: {
'language': languageCode,
'source': 'demo_fallback',
'success': false,
},
);
_ads = const [];
_partners = const [];
_events = const [];
} finally {
_loadFuture = null;
}
notifyListeners();
}
AdCampaign? campaignFor(AdPlacement placement, {String? category}) {
final now = DateTime.now();
final matches = _ads
.where(
(campaign) =>
campaign.isActiveAt(now) &&
campaign.resolvedPlacement == placement &&
campaign.supports(placement) &&
campaign.matchesCategory(category),
)
.toList()
..sort((a, b) => b.priority.compareTo(a.priority));
return matches.isEmpty ? null : matches.first;
}
@override
void dispose() {
_api.dispose();
super.dispose();
}
}

