import '../models/sake_item.dart'; import '../models/mbti_result.dart'; import 'mbti_types.dart'; class MBTIDiagnosisService { MBTIResult diagnose(List items) { if (items.isEmpty) { return MBTIResult.insufficient(0); } // 1. Calculate Scores for each Axis (0.0 to 1.0) // > 0.5 leans towards the Left (E, S, T, J) // <= 0.5 leans towards the Right (I, N, F, P) // We can adjust thresholds if needed. final double eScore = _calculateEScore(items); // E vs I (Social) final double sScore = _calculateSScore(items); // S vs N (Recognition) final double tScore = _calculateTScore(items); // T vs F (Judgment) final double jScore = _calculateJScore(items); // J vs P (Tactics) // 2. Determine Axis Values final bool isE = eScore >= 0.5; final bool isS = sScore >= 0.5; final bool isT = tScore >= 0.5; final bool isJ = jScore >= 0.5; // 3. Construct 4-letter code final String code = [ isE ? 'E' : 'I', isS ? 'S' : 'N', isT ? 'T' : 'F', isJ ? 'J' : 'P', ].join(); // 4. Retrieve Type final type = MBTIType.types[code] ?? MBTIType.unknown(); // 5. Calculate Confidence (Simple Average of "distance from 0.5") // e.g. Score 0.9 -> distance 0.4. Score 0.51 -> distance 0.01. // Max distance is 0.5. So (dist / 0.5) * 100 is % confidence for that axis. final double avgDist = ( ((eScore - 0.5).abs()) + ((sScore - 0.5).abs()) + ((tScore - 0.5).abs()) + ((jScore - 0.5).abs()) ) / 4.0; // Normalize: Max possible sum of diffs is 0.5 * 4 = 2.0. // So avgDist max is 0.5. // Confidence = avgDist * 2. (0.5 * 2 = 1.0) final double confidence = avgDist * 2.0; return MBTIResult( type: type, sampleSize: items.length, confidence: confidence, axisScores: { 'E/I': isE, 'S/N': isS, 'T/F': isT, 'J/P': isJ, }, ); } // --- Axis Calculation Logic --- /// E (Extrovert) vs I (Introvert) /// Logic: Ratio of 'Set/Menu' items to total items. /// If user drinks "Sets" (Drinking comparison), they are likely 'Social/Party' (E). /// If user drinks "Single" items mainly, they are 'Solitary' (I). /// Threshold: Sets are rarer, so if > 10% are sets, lean E. double _calculateEScore(List items) { if (items.isEmpty) return 0.5; // Phase B fix: neutral default int setOrderCount = items.where((item) => item.itemType == ItemType.set).length; double ratio = setOrderCount / items.length; // Phase B fix: Neutral baseline (0.5) instead of I-biased (0.3) // Map 0.0 -> 0.5 (Neutral) // Map 0.1 -> 0.6 (Slight E) // Map 0.2 -> 0.7 (Strong E) // Formula: score = 0.5 + (ratio * 1.0) double score = 0.5 + ratio; return score.clamp(0.0, 1.0); } /// S (Sensing) vs N (Intuition) /// Logic: "Specs" vs "Vibes". /// S: Detailed numeric specs (SMV, Polishing, Alcohol, Rice, Yeast). /// N: Photos, Emotional Tags, Empty specs. double _calculateSScore(List items) { if (items.isEmpty) return 0.5; // Logic combined into the loop below. // Average fill rate of technical specs. // If user fills 3/5 avg, they are S. // If user fills 0/5 avg, they are N. double debugTotalSpecFills = 0; int maxPossibleSpecs = items.length * 5; for (var item in items) { final h = item.hiddenSpecs; if (h.sakeMeterValue != null) debugTotalSpecFills++; if (h.polishingRatio != null) debugTotalSpecFills++; if (h.alcoholContent != null) debugTotalSpecFills++; if (h.yeast != null && h.yeast!.isNotEmpty) debugTotalSpecFills++; if (h.riceVariety != null && h.riceVariety!.isNotEmpty) debugTotalSpecFills++; } double fillRatio = maxPossibleSpecs == 0 ? 0 : debugTotalSpecFills / maxPossibleSpecs; // Phase B fix: Neutral baseline (0.5) instead of N-biased (0.2) // Map 0.0 -> 0.5 (Neutral) // Map 0.3 -> 0.65 (Slight S) // Map 0.6 -> 0.8 (Strong S) // Map 1.0 -> 1.0 (Max S) // Formula: score = 0.5 + (fillRatio * 0.5) double score = 0.5 + (fillRatio * 0.5); return score.clamp(0.0, 1.0); } /// T (Thinking) vs F (Feeling) /// Logic: "Logic" vs "Emotion". /// T: Strict rating, more critical? Or just low favorites? /// F: High favorite ratio. "Love everything". double _calculateTScore(List items) { if (items.isEmpty) return 0.5; int favoriteCount = items.where((i) => i.userData.isFavorite).length; double favoriteRatio = favoriteCount / items.length; // Phase B fix: Neutral baseline (0.5) instead of extreme T-bias (1.0) // Map 0.0 -> 0.5 (Neutral) // Map 0.5 -> 0.25 (Slight F) // Map 1.0 -> 0.0 (Strong F) // High Favorite -> F (Low T score) // Low Favorite -> Neutral (T score 0.5) // Formula: score = 0.5 - (favoriteRatio * 0.5) return 0.5 - (favoriteRatio * 0.5); } /// J (Judging) vs P (Prospecting) /// Logic: "Planned/Completeness" vs "Random". /// J: High data completeness (user input fields), Organized. /// P: Low completeness (just photo and name), Spontaneous. /// Using "Input Completeness" as a proxy for J. /// Similar to S/N but includes subjective fields like Memo/Price/Location? /// Or "Prefecture Coverage"? /// Let's use "Input Completeness of REQUIRED BASIC fields" (Name, Brand, Prefecture) /// Almost everyone fills Name/Brand. /// Let's use "Has Memo" and "Has Price" and "Has Date". /// J types likely fill Price and Memo. double _calculateJScore(List items) { if (items.isEmpty) return 0.5; int detailedItemCount = 0; for (var item in items) { bool hasMemo = item.userData.memo != null && item.userData.memo!.isNotEmpty; bool hasPrice = item.userData.price != null || item.userData.costPrice != null; // bool hasPrefecture = item.displayData.displayPrefecture != 'Unknown'; // Default is often unknown if (hasMemo && hasPrice) { detailedItemCount++; } } double detailedRatio = detailedItemCount / items.length; // Phase B fix: Neutral baseline (0.5) instead of P-biased (0.2) // Map 0.0 -> 0.5 (Neutral) // Map 0.5 -> 0.75 (Slight J) // Map 1.0 -> 1.0 (Strong J) // If user inputs price AND memo for > 50% items -> J. // Else -> Neutral. // Formula: score = 0.5 + (detailedRatio * 0.5) double score = 0.5 + (detailedRatio * 0.5); return score.clamp(0.0, 1.0); } }