import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/sake_item.dart'; import '../models/schema/sake_taste_stats.dart'; final shukoDiagnosisServiceProvider = Provider((ref) => ShukoDiagnosisService()); class ShukoDiagnosisService { ShukoProfile diagnose(List items) { if (items.isEmpty) { return ShukoProfile.empty(); } // 1. Calculate Average Stats (only from items with valid data) double totalAroma = 0; double totalBitterness = 0; double totalSweetness = 0; double totalAcidity = 0; double totalBody = 0; int count = 0; debugPrint('🍶🍶🍶 SHUKO DIAGNOSIS START: Total items = ${items.length}'); for (var item in items) { final stats = item.hiddenSpecs.sakeTasteStats; // Skip items with empty tasteStats (all zeros) if (stats.aroma == 0 && stats.bitterness == 0 && stats.sweetness == 0 && stats.acidity == 0 && stats.body == 0) { debugPrint('🍶 SKIPPED item (all zeros)'); continue; } totalAroma += stats.aroma; totalBitterness += stats.bitterness; totalSweetness += stats.sweetness; totalAcidity += stats.acidity; totalBody += stats.body; count++; } debugPrint('🍶🍶🍶 Analyzed $count out of ${items.length} items'); if (count == 0) { debugPrint('🍶🍶🍶 WARNING: No items to analyze, returning empty profile'); return ShukoProfile.empty(); } final avgStats = SakeTasteStats( aroma: totalAroma / count, bitterness: totalBitterness / count, sweetness: totalSweetness / count, acidity: totalAcidity / count, body: totalBody / count, ); // 2. Determine Title based on dominant traits final title = _determineTitle(avgStats); return ShukoProfile( title: title.title, description: title.description, avgStats: avgStats, totalSakeCount: items.length, analyzedCount: count, ); } ShukoTitle _determineTitle(SakeTasteStats stats) { // DEBUG: Print average stats debugPrint('🔍 DEBUG avgStats: aroma=${stats.aroma.toStringAsFixed(2)}, bitterness=${stats.bitterness.toStringAsFixed(2)}, sweetness=${stats.sweetness.toStringAsFixed(2)}, acidity=${stats.acidity.toStringAsFixed(2)}, body=${stats.body.toStringAsFixed(2)}'); // Scoring-based logic to handle overlapping traits final Map scores = {}; // 1. 辛口サムライ (Dry Samurai) // High Bitterness (Sharpness) + Low Sweetness // Old: alcoholFeeling + Low sweetness scores['辛口サムライ'] = _calculateDryScore(stats); // 2. フルーティーマスター (Fruity Master) // High Aroma + High Sweetness (Modern Fruity Ginjo Style) // Old: fruitiness + High sweetness scores['フルーティーマスター'] = _calculateFruityScore(stats); // 3. 旨口探求者 (Umami Explorer) // High Body (Richness) // Old: richness scores['旨口探求者'] = _calculateRichnessScore(stats); // 4. 香りの貴族 (Aroma Noble) // High Aroma (dominant trait) scores['香りの貴族'] = _calculateAromaScore(stats); // 5. バランスの賢者 (Balance Sage) // All stats moderate and balanced scores['バランスの賢者'] = _calculateBalanceScore(stats); // DEBUG: Print all scores debugPrint('🔍 DEBUG scores: ${scores.entries.map((e) => '${e.key}=${e.value.toStringAsFixed(2)}').join(', ')}'); // Find the title with the highest score final maxEntry = scores.entries.reduce((a, b) => a.value > b.value ? a : b); debugPrint('🔍 DEBUG winner: ${maxEntry.key} with score ${maxEntry.value.toStringAsFixed(2)}'); // Threshold: require minimum score to avoid false positives // Lowered to 1.5 to be more forgiving for "Standard" sake if (maxEntry.value < 1.0) { debugPrint('🔍 DEBUG: Score too low (${maxEntry.value.toStringAsFixed(2)} < 1.0), returning default title'); // Proposed New Default Titles return const ShukoTitle( title: '酒道の旅人', description: '未知なる味を求めて各地を巡る、終わりのない旅の途中。', ); } // Return the winning title return _getTitleByName(maxEntry.key); } // Scoring functions for each type double _calculateDryScore(SakeTasteStats stats) { double score = 0; // Dry = Sharp/Bitter + Not Sweet if (stats.bitterness > 0.1) { if (stats.bitterness > 3.0) score += (stats.bitterness - 3.0) * 2; // Lowered from 3.2 // Also verify Acidity contributions (Acid + Bitter = Dry) if (stats.acidity > 3.0) score += (stats.acidity - 3.0); // Penalize if too sweet if (stats.sweetness < 2.5) { score += (2.5 - stats.sweetness) * 2; } else if (stats.sweetness > 3.5) { score -= (stats.sweetness - 3.5) * 2; } } return score; } double _calculateFruityScore(SakeTasteStats stats) { double score = 0; // Fruity = High Aroma + Moderate/High Sweetness if (stats.aroma > 0.1) { if (stats.aroma > 2.8) score += (stats.aroma - 2.8) * 1.5; // Lowered from 3.0 // Verify Sweetness support if (stats.sweetness > 2.8) score += (stats.sweetness - 2.8) * 1.5; // Bonus if Body is not too heavy (Light + Sweet + Aroma = Fruity) if (stats.body < 3.5) score += 0.5; } return score; } double _calculateRichnessScore(SakeTasteStats stats) { double score = 0; // Richness = High Body (Kokumi) + Sweetness or Bitterness if (stats.body > 0.1) { // Body is the primary driver if (stats.body > 3.0) score += (stats.body - 3.0) * 2.5; // Lowered from 3.3 // Bonus for complexity if (stats.bitterness > 3.0) score += 0.5; } return score; } double _calculateAromaScore(SakeTasteStats stats) { double score = 0; // Pure Aroma focus (Daiginjo style) // Lowered threshold significantly to capture "Aroma Type" even if not extreme if (stats.aroma > 3.0) { score += (stats.aroma - 3.0) * 3; } // Boost score if it is the dominant trait if (stats.aroma > stats.body && stats.aroma > stats.bitterness) { score += 1.0; } return score; } double _calculateBalanceScore(SakeTasteStats stats) { double score = 0; // Check range (Max - Min) final values = [stats.aroma, stats.sweetness, stats.acidity, stats.bitterness, stats.body]; final maxVal = values.reduce((a, b) => a > b ? a : b); final minVal = values.reduce((a, b) => a < b ? a : b); final spread = maxVal - minVal; // Strict requirement for "Balance": // The difference between the highest and lowest trait must be small. if (spread > 1.5) { return 0; // Not balanced if there's a spike } int validStats = 0; double sumDiffFrom3 = 0; // Check deviation from 3.0 (Center) void checkStat(double val) { if (val > 0.1) { validStats++; sumDiffFrom3 += (val - 3.0).abs(); } } checkStat(stats.aroma); checkStat(stats.sweetness); checkStat(stats.acidity); checkStat(stats.bitterness); checkStat(stats.body); if (validStats >= 3) { double avgDev = sumDiffFrom3 / validStats; // If average deviation is small (< 0.7), it's balanced if (avgDev < 0.7) { score = (0.8 - avgDev) * 5; // Higher score for tighter balance } } return score; } ShukoTitle _getTitleByName(String name) { switch (name) { case '辛口サムライ': return const ShukoTitle( title: '辛口サムライ', description: 'キレのある辛口を好む、硬派な日本酒ファン。', // Updated Description? ); case 'フルーティーマスター': return const ShukoTitle( title: 'フルーティーマスター', description: '果実のような香りと甘みを愛する、華やかな飲み手。', ); case '旨口探求者': return const ShukoTitle( title: '旨口探求者', description: 'お米本来の旨みやコクを重視する、通な舌の持ち主。', ); case '香りの貴族': return const ShukoTitle( title: '香りの貴族', description: '吟醸香など、鼻に抜ける香りを何より楽しむタイプ。', ); case 'バランスの賢者': return const ShukoTitle( title: 'バランスの賢者', description: '偏りなく様々な酒を楽しむ、オールラウンダー。', ); default: // New Default Title return const ShukoTitle( title: '酒道の旅人', description: '未知なる味を求めて各地を巡る、終わりのない旅の途中。', ); } } // v1.1: Personalization Logic String getGreeting(String? nickname) { if (nickname == null || nickname.trim().isEmpty) { return 'ようこそ!'; } return 'ようこそ、$nicknameさん'; } ShukoTitle personalizeTitle(ShukoTitle original, String? gender) { if (gender == null) return original; String newTitle = original.title; // Simple customization logic if (gender == 'female') { if (newTitle.contains('サムライ')) newTitle = newTitle.replaceAll('サムライ', '麗人'); if (newTitle.contains('貴族')) newTitle = newTitle.replaceAll('貴族', 'プリンセス'); if (newTitle.contains('賢者')) newTitle = newTitle.replaceAll('賢者', 'ミューズ'); if (newTitle.contains('愛好家')) newTitle = newTitle.replaceAll('愛好家', '目利き'); // Feminine variant } else if (gender == 'male') { if (newTitle.contains('貴族')) newTitle = newTitle.replaceAll('貴族', '貴公子'); } return ShukoTitle( title: newTitle, description: original.description, ); } } class ShukoProfile { final String title; final String description; final SakeTasteStats avgStats; final int totalSakeCount; final int analyzedCount; ShukoProfile({ required this.title, required this.description, required this.avgStats, required this.totalSakeCount, required this.analyzedCount, }); factory ShukoProfile.empty() { return ShukoProfile( title: '旅の始まり', description: 'まずは日本酒を記録して、\n自分の好みを発見しましょう。', avgStats: SakeTasteStats(aroma: 0, bitterness: 0, sweetness: 0, acidity: 0, body: 0), totalSakeCount: 0, analyzedCount: 0, ); } } class ShukoTitle { final String title; final String description; const ShukoTitle({required this.title, required this.description}); }