import 'package:flutter/foundation.dart'; import '../models/sake_item.dart'; import '../models/sakenowa/sakenowa_models.dart'; import '../utils/string_similarity.dart'; import 'sakenowa_service.dart'; /// マッチング結果 class MatchResult { final SakenowaBrand? brand; final SakenowaBrewery? brewery; final SakenowaArea? area; final SakenowaFlavorChart? flavorChart; final double score; // 0.0-1.0 final bool isConfident; // スコア >= 0.8 MatchResult({ this.brand, this.brewery, this.area, this.flavorChart, required this.score, required this.isConfident, }); bool get hasMatch => brand != null; } /// さけのわ自動マッチングサービス /// /// ユーザーが登録した日本酒を自動的にさけのわデータベースとマッチング /// 銘柄名の統一、正確なフレーバーチャート取得に使用 class SakenowaAutoMatchingService { final SakenowaService _sakenowaService; SakenowaAutoMatchingService(this._sakenowaService); /// 日本酒をさけのわデータベースとマッチング /// /// [sakeItem]: マッチング対象の日本酒 /// [minScore]: 最小類似度スコア(デフォルト: 0.7) /// [autoApply]: マッチング結果を自動で適用(デフォルト: false) /// /// Returns: マッチング結果(見つからない場合はscoreが0) Future matchSake({ required SakeItem sakeItem, double minScore = 0.7, bool autoApply = false, }) async { try { debugPrint('🔍 [SakenowaAutoMatching] 開始: ${sakeItem.displayData.displayName}'); // さけのわデータ取得 final brands = await _sakenowaService.getBrands(); final breweries = await _sakenowaService.getBreweries(); final areas = await _sakenowaService.getAreas(); final flavorCharts = await _sakenowaService.getFlavorCharts(); debugPrint('📊 [SakenowaAutoMatching] データ取得完了: ${brands.length} brands'); // マップ化 final breweryMap = {for (var b in breweries) b.id: b}; final areaMap = {for (var a in areas) a.id: a}; final chartMap = {for (var c in flavorCharts) c.brandId: c}; // 最良マッチを探す SakenowaBrand? bestBrand; double bestScore = 0.0; for (final brand in brands) { final score = StringSimilarity.sakenowaMatchScore( sakeItem.displayData.displayName, brand.name, ); if (score > bestScore) { bestScore = score; bestBrand = brand; } } // 最小スコア未満なら失敗 if (bestScore < minScore) { debugPrint('❌ [SakenowaAutoMatching] スコア不足: $bestScore < $minScore'); return MatchResult( score: bestScore, isConfident: false, ); } // マッチング成功 final brewery = bestBrand != null ? breweryMap[bestBrand.breweryId] : null; final area = brewery != null ? areaMap[brewery.areaId] : null; final chart = bestBrand != null ? chartMap[bestBrand.id] : null; debugPrint('✅ [SakenowaAutoMatching] マッチング成功!'); debugPrint(' 銘柄: ${bestBrand?.name} (スコア: $bestScore)'); debugPrint(' 酒蔵: ${brewery?.name}'); debugPrint(' 地域: ${area?.name}'); final result = MatchResult( brand: bestBrand, brewery: brewery, area: area, flavorChart: chart, score: bestScore, isConfident: bestScore >= 0.8, ); // 自動適用 if (autoApply && result.hasMatch) { await applyMatch(sakeItem, result); } return result; } catch (e, stackTrace) { debugPrint('💥 [SakenowaAutoMatching] エラー: $e'); debugPrint('Stack trace: $stackTrace'); return MatchResult( score: 0.0, isConfident: false, ); } } /// マッチング結果をSakeItemに適用 /// /// DisplayDataのsakenowaフィールドとHiddenSpecsを更新 Future applyMatch(SakeItem sakeItem, MatchResult result) async { if (!result.hasMatch) { debugPrint('⚠️ [SakenowaAutoMatching] マッチなし、適用スキップ'); return; } try { debugPrint('💾 [SakenowaAutoMatching] マッチング結果を適用中...'); // DisplayData更新(さけのわ統一名称) final updatedDisplayData = sakeItem.displayData.copyWith( sakenowaName: result.brand?.name, sakenowaBrewery: result.brewery?.name, sakenowaPrefecture: result.area?.name, ); // HiddenSpecs更新(6軸フレーバーチャート) Map? flavorChartMap; if (result.flavorChart != null) { flavorChartMap = { 'f1': result.flavorChart!.f1, 'f2': result.flavorChart!.f2, 'f3': result.flavorChart!.f3, 'f4': result.flavorChart!.f4, 'f5': result.flavorChart!.f5, 'f6': result.flavorChart!.f6, }; } final updatedHiddenSpecs = sakeItem.hiddenSpecs.copyWith( sakenowaBrandId: result.brand?.id, sakenowaFlavorChart: flavorChartMap, ); // SakeItem更新 sakeItem.displayData = updatedDisplayData; sakeItem.hiddenSpecs = updatedHiddenSpecs; // Hiveに保存 await sakeItem.save(); debugPrint('✅ [SakenowaAutoMatching] 適用完了!'); debugPrint(' displayName: ${sakeItem.displayData.displayName}'); debugPrint(' displayBrewery: ${sakeItem.displayData.displayBrewery}'); debugPrint(' displayPrefecture: ${sakeItem.displayData.displayPrefecture}'); } catch (e, stackTrace) { debugPrint('💥 [SakenowaAutoMatching] 適用エラー: $e'); debugPrint('Stack trace: $stackTrace'); } } /// 複数の日本酒を一括マッチング /// /// [sakeItems]: マッチング対象のリスト /// [minScore]: 最小類似度スコア /// [autoApply]: マッチング結果を自動で適用 /// /// Returns: マッチング成功数 Future matchBatch({ required List sakeItems, double minScore = 0.7, bool autoApply = false, }) async { debugPrint('🔄 [SakenowaAutoMatching] バッチ処理開始: ${sakeItems.length} 件'); int successCount = 0; for (final sake in sakeItems) { final result = await matchSake( sakeItem: sake, minScore: minScore, autoApply: autoApply, ); if (result.hasMatch && result.isConfident) { successCount++; } } debugPrint('✅ [SakenowaAutoMatching] バッチ処理完了: $successCount/${sakeItems.length} 成功'); return successCount; } /// 既存データのさけのわフィールドをクリア /// /// デバッグ・テスト用 Future clearSakenowaData(SakeItem sakeItem) async { debugPrint('🧹 [SakenowaAutoMatching] さけのわデータクリア: ${sakeItem.displayData.displayName}'); final clearedDisplayData = sakeItem.displayData.copyWith( sakenowaName: null, sakenowaBrewery: null, sakenowaPrefecture: null, ); final clearedHiddenSpecs = sakeItem.hiddenSpecs.copyWith( sakenowaBrandId: null, sakenowaFlavorChart: null, ); sakeItem.displayData = clearedDisplayData; sakeItem.hiddenSpecs = clearedHiddenSpecs; await sakeItem.save(); debugPrint('✅ [SakenowaAutoMatching] クリア完了'); } }