import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:flutter/services.dart' show rootBundle; import '../../core/constants.dart'; import '../db/app_database.dart'; /// fn-seed-importer /// /// Idempotent seed loader for the 8 catalog tables. Runs once per install /// (gated by meta_kv[`seeded_v1`]). Transactional: either all 8 catalogs /// import or none do. /// /// JSON files live under `assets/seed/*.json` and ship as a top-level array. class SeedImporter { final AppDatabase db; final Future Function(String path) loadAsset; SeedImporter(this.db, {Future Function(String path)? loadAsset}) : loadAsset = loadAsset ?? rootBundle.loadString; /// Import all catalogs if not already seeded. Returns true if the import /// ran, false if it was a no-op. Future importIfNeeded() async { final marker = await (db.select(db.metaKv) ..where((t) => t.key.equals(kSeededV1Flag))) .getSingleOrNull(); if (marker != null && marker.value == 'true') return false; await db.transaction(() async { await _importProtocols(); await _importBreakProtocols(); await _importCommonFrames(); await _importMethodologies(); await _importFramePatterns(); await _importRewardMenuItems(); await _importReferences(); await _importDietPatterns(); await db.into(db.metaKv).insertOnConflictUpdate( MetaKvCompanion.insert(key: kSeededV1Flag, value: 'true'), ); }); return true; } Future> _loadJsonArray(String fileName) async { final raw = await loadAsset('assets/seed/$fileName'); final decoded = json.decode(raw); if (decoded is! List) { throw FormatException('$fileName: expected top-level JSON array'); } return decoded; } String? _jsonField(Map m, String key) { final v = m[key]; if (v == null) return null; return json.encode(v); } // ---- 8 catalog adapters ---- Future _importProtocols() async { final rows = await _loadJsonArray('protocols.json'); for (final r in rows.cast>()) { await db.into(db.protocols).insertOnConflictUpdate(ProtocolsCompanion.insert( id: r['id'] as String, category: r['category'] as String, title: r['title'] as String, titleEn: Value(r['title_en'] as String?), what: r['what'] as String, whenText: r['when'] as String, dose: r['dose'] as String, why: r['why'] as String, howJson: _jsonField(r, 'how') ?? '[]', checkText: r['check'] as String, caution: Value(r['caution'] as String?), defaultAnchorJson: Value(_jsonField(r, 'default_anchor')), minDoseForStart: Value(r['min_dose_for_start'] as String?), referenceIdsJson: Value(_jsonField(r, 'reference_ids')), evidenceStrength: Value(r['evidence_strength'] as String?), sourceDoc: Value(r['source_doc'] as String?), )); } } Future _importBreakProtocols() async { final rows = await _loadJsonArray('break_protocols.json'); for (final r in rows.cast>()) { await db.into(db.breakProtocols).insertOnConflictUpdate( BreakProtocolsCompanion.insert( id: r['id'] as String, category: r['category'] as String, title: r['title'] as String, hubermanSummary: r['huberman_summary'] as String, frameExamplesJson: Value(_jsonField(r, 'frame_examples')), phasesJson: _jsonField(r, 'phases') ?? '[]', defaultCommonFramesJson: _jsonField(r, 'default_common_frames') ?? '[]', toolsJson: Value(_jsonField(r, 'tools')), medicalWarning: Value(r['medical_warning'] as String?), referenceIdsJson: Value(_jsonField(r, 'reference_ids')), ), ); } } Future _importCommonFrames() async { final rows = await _loadJsonArray('common_frames.json'); for (final r in rows.cast>()) { await db.into(db.commonFrames).insertOnConflictUpdate( CommonFramesCompanion.insert( id: r['id'] as String, title: r['title'] as String, what: r['what'] as String, why: r['why'] as String, dose: Value(r['dose'] as String?), howJson: Value(_jsonField(r, 'how')), checkText: r['check'] as String, applicableBreakCategoriesJson: Value(_jsonField(r, 'applicable_break_categories')), referenceIdsJson: Value(_jsonField(r, 'reference_ids')), ), ); } } Future _importMethodologies() async { final rows = await _loadJsonArray('methodologies.json'); for (final r in rows.cast>()) { await db.into(db.methodologies).insertOnConflictUpdate( MethodologiesCompanion.insert( id: r['id'] as String, name: r['name'] as String, originator: r['originator'] as String, oneLineDefinition: r['one_line_definition'] as String, coreUnit: r['core_unit'] as String, procedureJson: Value(_jsonField(r, 'procedure')), toolsJson: Value(_jsonField(r, 'tools')), strengthsJson: Value(_jsonField(r, 'strengths')), weaknessesJson: Value(_jsonField(r, 'weaknesses')), goodFor: Value(r['good_for'] as String?), hubermanFitScore: r['huberman_fit_score'] as int, isCoreEngine: Value(r['is_core_engine'] as bool? ?? false), referenceIdsJson: Value(_jsonField(r, 'reference_ids')), ), ); } } Future _importFramePatterns() async { final rows = await _loadJsonArray('frame_patterns.json'); for (final r in rows.cast>()) { await db.into(db.framePatterns).insertOnConflictUpdate( FramePatternsCompanion.insert( id: r['id'] as String, domain: Value(r['domain'] as String?), avoidanceKeyword: r['avoidance_keyword'] as String, l0Example: r['l0_example'] as String, l1SimpleReplace: Value(r['l1_simple_replace'] as String?), l2Suggestion: r['l2_suggestion'] as String, l3Identity: Value(r['l3_identity'] as String?), ), ); } } Future _importRewardMenuItems() async { final rows = await _loadJsonArray('reward_menu_items.json'); for (final r in rows.cast>()) { await db.into(db.rewardMenuItems).insertOnConflictUpdate( RewardMenuItemsCompanion.insert( id: r['id'] as String, tierRecommended: r['tier_recommended'] as String, title: r['title'] as String, description: Value(r['description'] as String?), estimatedCostKrwMin: Value(r['estimated_cost_krw_min'] as int?), estimatedCostKrwMax: Value(r['estimated_cost_krw_max'] as int?), isEffortTied: Value(r['is_effort_tied'] as bool?), tagsJson: Value(_jsonField(r, 'tags')), avoidForBreakHabitsJson: Value(_jsonField(r, 'avoid_for_break_habits')), ), ); } } Future _importReferences() async { final rows = await _loadJsonArray('references.json'); for (final r in rows.cast>()) { await db.into(db.references).insertOnConflictUpdate( ReferencesCompanion.insert( id: r['id'] as String, kind: r['kind'] as String, title: r['title'] as String, authorsJson: Value(_jsonField(r, 'authors')), year: Value(r['year'] as int?), journal: Value(r['journal'] as String?), doi: Value(r['doi'] as String?), url: Value(r['url'] as String?), episodeNumber: Value(r['episode_number'] as int?), publisher: Value(r['publisher'] as String?), evidenceStrength: Value(r['evidence_strength'] as String?), verified: Value(r['verified'] as bool?), note: Value(r['note'] as String?), ), ); } } Future _importDietPatterns() async { final rows = await _loadJsonArray('diet_patterns.json'); for (final r in rows.cast>()) { await db.into(db.dietPatterns).insertOnConflictUpdate( DietPatternsCompanion.insert( id: r['id'] as String, name: r['name'] as String, core: r['core'] as String, strengthsJson: Value(_jsonField(r, 'strengths')), weaknessesJson: Value(_jsonField(r, 'weaknesses')), evidenceStrength: r['evidence_strength'] as String, koreanContextFit: Value(r['korean_context_fit'] as String?), starterLeversJson: Value(_jsonField(r, 'starter_levers')), medicalWarning: Value(r['medical_warning'] as String?), linkedProtocolIdsJson: Value(_jsonField(r, 'linked_protocol_ids')), referenceIdsJson: Value(_jsonField(r, 'reference_ids')), ), ); } } }