import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../models/sake_item.dart'; import '../services/gemini_service.dart'; import '../services/sake_recommendation_service.dart'; import '../widgets/analyzing_dialog.dart'; import '../widgets/sake_3d_carousel_with_reason.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../providers/sake_list_provider.dart'; import 'sake_detail/sections/sake_pricing_section.dart'; import '../theme/app_colors.dart'; import '../constants/app_constants.dart'; import '../widgets/sake_detail/sake_detail_chart.dart'; import '../widgets/sake_detail/sake_detail_memo.dart'; import '../widgets/sake_detail/sake_detail_specs.dart'; import 'sake_detail/widgets/sake_photo_edit_modal.dart'; import 'sake_detail/sections/sake_mbti_stamp_section.dart'; import 'sake_detail/sections/sake_basic_info_section.dart'; import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart'; import '../services/mbti_compatibility_service.dart'; import '../widgets/sakenowa/sakenowa_detail_recommendation_section.dart'; // Note: google_fonts and theme_provider are now used in sake_basic_info_section.dart class SakeDetailScreen extends ConsumerStatefulWidget { final SakeItem sake; const SakeDetailScreen({super.key, required this.sake}); @override ConsumerState createState() => _SakeDetailScreenState(); } class _SakeDetailScreenState extends ConsumerState { // To trigger rebuilds if we don't switch to a stream late SakeItem _sake; int _currentImageIndex = 0; // Memo logic moved to SakeDetailMemo @override void initState() { super.initState(); _sake = widget.sake; // Memo init removed // AI分析情報の編集用コントローラーを初期化 } @override void dispose() { // Memo dispose removed // AI分析情報の編集用コントローラーを破棄 super.dispose(); } /// 五味チャートの値を手動更新し、Hiveに永続化 Future _updateTasteStats(Map newStats) async { final updatedSake = _sake.copyWith( tasteStats: newStats, isUserEdited: true, ); final box = Hive.box('sake_items'); await box.put(_sake.key, updatedSake); setState(() { _sake = updatedSake; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('チャートを更新しました'), duration: Duration(seconds: 2), ), ); } } @override Widget build(BuildContext context) { final appColors = Theme.of(context).extension()!; // スマートレコメンド (Phase 1-8 Enhanced) final allSakeAsync = ref.watch(allSakeItemsProvider); final allSake = allSakeAsync.asData?.value ?? []; // 新しいレコメンドエンジン使用(五味チャート類似度込み) final recommendations = SakeRecommendationService.getRecommendations( target: _sake, allItems: allSake, limit: AppConstants.recommendationLimit, ); final relatedItems = recommendations.map((rec) => rec.item).toList(); return Scaffold( body: CustomScrollView( slivers: [ SakeDetailSliverAppBar( sake: _sake, currentImageIndex: _currentImageIndex, onToggleFavorite: _toggleFavorite, onReanalyze: () => _reanalyze(context), onDelete: () { HapticFeedback.heavyImpact(); _showDeleteDialog(context); }, onPageChanged: (index) => setState(() => _currentImageIndex = index), onPhotoEdit: () => _showPhotoEditModal(context), ), SliverToBoxAdapter( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: Theme.of(context).brightness == Brightness.dark ? [ const Color(0xFF121212), // Scaffold Background const Color(0xFF1E1E1E), // Slightly lighter surface ] : [ Theme.of(context).scaffoldBackgroundColor, Theme.of(context).primaryColor.withValues(alpha: 0.05), ], ), ), padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Basic Info: AI確信度・MBTI相性・銘柄名・蔵元・タグ・キャッチコピー (Extracted) SakeBasicInfoSection( sake: _sake, onTapName: () => _showTextEditDialog( context, title: '銘柄名を編集', initialValue: _sake.displayData.displayName, onSave: (value) async { final box = Hive.box('sake_items'); final updated = _sake.copyWith(name: value, isUserEdited: true); await box.put(_sake.key, updated); setState(() => _sake = updated); }, ), onTapBrewery: () => _showBreweryEditDialog(context), onTapTags: () => _showTagEditDialog(context), onTapMbtiCompatibility: _showMbtiCompatibilityDialog, ), const SizedBox(height: 24), const Divider(), const SizedBox(height: 24), // Taste Radar Chart (Extracted) with Manual Edit SakeDetailChart( sake: _sake, onTasteStatsEdited: (newStats) => _updateTasteStats(newStats), ), const SizedBox(height: 24), const Divider(), const SizedBox(height: 24), // Description if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set) Text( _sake.hiddenSpecs.description!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.8, fontSize: 16, ), ), const SizedBox(height: 32), const Divider(), const SizedBox(height: 16), // AI Specs Accordion (Extracted) SakeDetailSpecs( sake: _sake, onUpdate: (updatedSake) { setState(() => _sake = updatedSake); }, ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), // Memo Field (Extracted) SakeDetailMemo( initialMemo: _sake.userData.memo, onUpdate: (value) async { // Auto-save final box = Hive.box('sake_items'); final updated = _sake.copyWith(memo: value, isUserEdited: true); await box.put(_sake.key, updated); // Note: setState is needed to update the 'updated' variable locally // But the text field manages its own state, so we don't strictly need to rebuild the text field // However, other parts might depend on _sake.userData.memo? Unlikely. // Actually, we should update _sake here to keep consistency. setState(() => _sake = updated); }, ), const SizedBox(height: 48), // Related Items 3D Carousel (Phase 1-8 Enhanced) if (_sake.itemType != ItemType.set) ...[ // Hide for Set Items Row( children: [ Icon(LucideIcons.sparkles, size: 16, color: Theme.of(context).colorScheme.onSurface), const SizedBox(width: 8), Text( 'おすすめの日本酒', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface, ), ), ], ), const SizedBox(height: 8), Text( '五味チャート・タグ・酒蔵・産地から自動選出', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: appColors.textSecondary, ), ), const SizedBox(height: 16), relatedItems.isNotEmpty ? Sake3DCarouselWithReason( recommendations: recommendations.take(6).toList(), height: 260, ) : Container( height: 120, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.info, color: appColors.iconSubtle, size: 32), const SizedBox(height: 8), Text( '関連する日本酒を追加すると\nおすすめが表示されます', textAlign: TextAlign.center, style: TextStyle(color: appColors.textSecondary, fontSize: 12), ), ], ), ), const SizedBox(height: 24), // さけのわ連携おすすめ(未飲銘柄) SakenowaDetailRecommendationSection( currentSakeName: _sake.displayData.displayName, currentTasteData: _sake.hiddenSpecs.activeTasteData, displayCount: 3, ), const SizedBox(height: 48), ], // MBTI Diagnostic Stamp Section (Phase C3) SakeMbtiStampSection(sake: _sake), const SizedBox(height: 24), ], ), ), ), // Phase 2-3: Business Pricing Section (Extracted) SliverToBoxAdapter( child: SakePricingSection( sake: _sake, onUpdated: (updated) => setState(() => _sake = updated), ), ), // Gap with Safe Area SliverPadding( padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), ), ], ), ); } bool _isAnalyzing = false; DateTime? _quotaLockoutTime; Future _toggleFavorite() async { HapticFeedback.mediumImpact(); final box = Hive.box('sake_items'); final messenger = ScaffoldMessenger.of(context); final newItem = _sake.copyWith(isFavorite: !_sake.userData.isFavorite); await box.put(_sake.key, newItem); setState(() { _sake = newItem; }); messenger.showSnackBar( SnackBar( content: Text(newItem.userData.isFavorite ? 'お気に入りに追加しました' : 'お気に入りを解除しました'), duration: const Duration(milliseconds: 1000), ), ); } Future _reanalyze(BuildContext context) async { // 1. Check Locks if (_isAnalyzing) return; // 2. Check Quota Lockout if (_quotaLockoutTime != null) { final remaining = _quotaLockoutTime!.difference(DateTime.now()); if (remaining.isNegative) { setState(() => _quotaLockoutTime = null); // Reset if time passed } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。')), ); return; } } if (_sake.displayData.imagePaths.isEmpty) return; setState(() => _isAnalyzing = true); try { showDialog( context: context, barrierDismissible: false, builder: (context) => const AnalyzingDialog(), ); final geminiService = GeminiService(); // 既存の画像パスを使用(すでに圧縮済みの想定) // 注: 既存のデータは未圧縮の可能性があるため、一括圧縮機能で対応 // forceRefresh: true でキャッシュを無視して再解析 final result = await geminiService.analyzeSakeLabel(_sake.displayData.imagePaths, forceRefresh: true); final newItem = _sake.copyWith( name: result.name ?? _sake.displayData.displayName, brand: result.brand ?? _sake.displayData.displayBrewery, prefecture: result.prefecture ?? _sake.displayData.displayPrefecture, description: result.description ?? _sake.hiddenSpecs.description, catchCopy: result.catchCopy ?? _sake.displayData.catchCopy, confidenceScore: result.confidenceScore, flavorTags: result.flavorTags.isNotEmpty ? result.flavorTags : _sake.hiddenSpecs.flavorTags, tasteStats: result.tasteStats.isNotEmpty ? result.tasteStats : _sake.hiddenSpecs.tasteStats, // New Fields specificDesignation: result.type ?? _sake.hiddenSpecs.type, alcoholContent: result.alcoholContent ?? _sake.hiddenSpecs.alcoholContent, polishingRatio: result.polishingRatio ?? _sake.hiddenSpecs.polishingRatio, sakeMeterValue: result.sakeMeterValue ?? _sake.hiddenSpecs.sakeMeterValue, riceVariety: result.riceVariety ?? _sake.hiddenSpecs.riceVariety, yeast: result.yeast ?? _sake.hiddenSpecs.yeast, manufacturingYearMonth: result.manufacturingYearMonth ?? _sake.hiddenSpecs.manufacturingYearMonth, itemType: ItemType.sake, ); final box = Hive.box('sake_items'); await box.put(_sake.key, newItem); setState(() { _sake = newItem; }); if (context.mounted) { Navigator.pop(context); // Close dialog ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('再解析が完了しました')), ); } } catch (e) { if (context.mounted) { Navigator.pop(context); // Close dialog (safely pops the top route, hopefully the dialog) // Check for Quota Error to set Lockout if (e.toString().contains('Quota') || e.toString().contains('429')) { setState(() { _quotaLockoutTime = DateTime.now().add(const Duration(minutes: 1)); }); } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('エラー: $e')), ); } } finally { if (mounted) { setState(() => _isAnalyzing = false); } } } void _showTagEditDialog(BuildContext context) { final TextEditingController tagController = TextEditingController(); final allTags = _sake.hiddenSpecs.flavorTags.toSet(); showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (context, setModalState) { return AlertDialog( title: const Text('タグ編集'), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.85, minWidth: 300, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, children: allTags.map((tag) => Chip( label: Text(tag), onDeleted: () { setModalState(() => allTags.remove(tag)); }, )).toList(), ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: tagController, decoration: const InputDecoration( hintText: '新しいタグを追加', isDense: true, ), onSubmitted: (val) { if (val.trim().isNotEmpty) { setModalState(() { allTags.add(val.trim()); tagController.clear(); }); } }, ), ), IconButton( icon: const Icon(LucideIcons.plus), onPressed: () { if (tagController.text.trim().isNotEmpty) { setModalState(() { allTags.add(tagController.text.trim()); tagController.clear(); }); } }, ), ], ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), TextButton( onPressed: () { _updateTags(allTags.toList()); Navigator.pop(context); }, child: const Text('保存'), ), ], ); } ), ); } Future _updateTags(List newTags) async { final box = Hive.box('sake_items'); final newItem = _sake.copyWith( flavorTags: newTags, isUserEdited: true, ); await box.put(_sake.key, newItem); setState(() => _sake = newItem); } Future _showDeleteDialog(BuildContext context) async { final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon(LucideIcons.alertTriangle, color: Theme.of(context).extension()!.warning, size: 24), const SizedBox(width: 8), const Text('削除確認'), ], ), content: Text('「${_sake.displayData.displayName}」を削除しますか?\nこの操作は取り消せません。'), actions: [ TextButton( child: const Text('キャンセル'), onPressed: () => Navigator.pop(context, false), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).extension()!.error, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16), ), onPressed: () => Navigator.pop(context, true), child: const Text('削除'), ), ], ), ); if (confirmed == true && mounted) { // nav/messenger captured above // Day 5: 画像ファイルを削除(ストレージクリーンアップ) for (final imagePath in _sake.displayData.imagePaths) { try { final imageFile = File(imagePath); if (await imageFile.exists()) { await imageFile.delete(); debugPrint('🗑️ Deleted image file: $imagePath'); } } catch (e) { debugPrint('⚠️ Failed to delete image file: $imagePath - $e'); } } // Hiveから削除 final box = Hive.box('sake_items'); await box.delete(_sake.key); if (mounted) { navigator.pop(); // Return to previous screen messenger.showSnackBar( const SnackBar(content: Text('削除しました')), ); } } } /// テキスト編集ダイアログを表示 Future _showTextEditDialog( BuildContext context, { required String title, required String initialValue, required Future Function(String) onSave, }) async { final controller = TextEditingController(text: initialValue); await showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: TextField( controller: controller, decoration: const InputDecoration( border: OutlineInputBorder(), ), autofocus: true, maxLines: null, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () async { await onSave(controller.text); if (context.mounted) Navigator.pop(context); }, child: const Text('保存'), ), ], ), ); } /// MBTI相性詳細ダイアログを表示 void _showMbtiCompatibilityDialog( BuildContext context, CompatibilityResult result, AppColors appColors, ) { final starColor = result.starRating >= 4 ? appColors.brandPrimary : result.starRating >= 3 ? appColors.textSecondary : appColors.textTertiary; showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: appColors.surfaceSubtle, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ Icon(LucideIcons.brainCircuit, color: appColors.brandPrimary, size: 24), const SizedBox(width: 8), Text( '${result.mbtiType}との相性', style: TextStyle( color: appColors.textPrimary, fontWeight: FontWeight.bold, ), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ // Star Rating Text( result.starDisplay, style: TextStyle( color: starColor, fontSize: 32, letterSpacing: 4, ), ), const SizedBox(height: 8), // Percentage & Level Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${result.percent}%', style: TextStyle( color: starColor, fontSize: 24, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: starColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( result.level, style: TextStyle( color: starColor, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ], ), const SizedBox(height: 20), // Match Reasons if (result.reasons.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, child: Text( 'マッチ理由', style: TextStyle( color: appColors.textSecondary, fontSize: 12, ), ), ), const SizedBox(height: 8), ...result.reasons.take(3).map((reason) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Icon(LucideIcons.check, size: 14, color: appColors.brandPrimary), const SizedBox(width: 8), Expanded( child: Text( reason, style: TextStyle( color: appColors.textPrimary, fontSize: 14, ), ), ), ], ), )), ], ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('閉じる', style: TextStyle(color: appColors.brandPrimary)), ), ], ), ); } /// 酒蔵・都道府県編集ダイアログを表示 Future _showBreweryEditDialog(BuildContext context) async { final breweryController = TextEditingController(text: _sake.displayData.displayBrewery); final prefectureController = TextEditingController(text: _sake.displayData.displayPrefecture); await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('酒蔵・都道府県を編集'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: breweryController, decoration: const InputDecoration( labelText: '酒蔵', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: prefectureController, decoration: const InputDecoration( labelText: '都道府県', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () async { final box = Hive.box('sake_items'); final updated = _sake.copyWith( brand: breweryController.text, prefecture: prefectureController.text, isUserEdited: true, ); await box.put(_sake.key, updated); setState(() => _sake = updated); if (context.mounted) Navigator.pop(context); }, child: const Text('保存'), ), ], ), ); } /// 写真編集モーダルを表示 Future _showPhotoEditModal(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => SakePhotoEditModal( sake: _sake, onUpdated: (updatedSake) { setState(() => _sake = updatedSake); }, ), ); } }