// Today-Hero story loader.
//
// Reads ~/.chain/today/active.yaml when present, validates the
// schema, and hands the parsed bilingual story back to the
// store page. On any failure (file missing, schema mismatch,
// banned-words hit, parse error) the caller falls back to the
// compiled-in const story so KRITIS / fresh installs always
// have something to render.
//
// See docs/today-pipeline.md for the full design rationale and
// tools/today/ for the operator-facing CLI.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';

/// Editorial action attached to a Today story. The set is kept
/// closed and ABI-stable — adding a new variant without bumping
/// the schema would let an old Studio render an unknown action
/// as a no-op button.
///
/// `filterCategory` and `runQuery` both consume the optional
/// [TodayStoryData.ctaPayload] — a wire-string the Store page
/// dispatches into its filter / search box. Keeping the action
/// surface inside the Store means a hero story can teach about a
/// section without forcing a page-route change.
enum TodayCta {
  none,
  openSettings,
  filterCategory,
  runQuery,
  showSuggestedSources,
}

/// Bilingual story rendered by the Today-Hero card. Either
/// loaded from disk or supplied as a const fallback.
@immutable
class TodayStoryData {
  final String badgeEn;
  final String badgeDe;
  final String titleEn;
  final String titleDe;
  final String bodyEn;
  final String bodyDe;
  final String ctaLabelEn;
  final String ctaLabelDe;
  final IconData icon;
  final TodayCta cta;

  /// Wire payload used by [TodayCta.filterCategory] (category id
  /// to apply to the Store grid) and [TodayCta.runQuery] (free-text
  /// to drop into the search box). Empty for other variants.
  final String ctaPayload;

  const TodayStoryData({
    required this.badgeEn,
    required this.badgeDe,
    required this.titleEn,
    required this.titleDe,
    required this.bodyEn,
    required this.bodyDe,
    required this.ctaLabelEn,
    required this.ctaLabelDe,
    required this.icon,
    required this.cta,
    this.ctaPayload = '',
  });
}

/// Static loader. Pure I/O + parsing, no widget concerns. The
/// store page calls `loadOrFallback` once per fetch and feeds
/// the result into `_StoreTodayHero`.
class TodayStoryLoader {
  /// Resolves to `~/.chain/today/active.yaml` on every supported
  /// host. The pipeline writes here from [tools/today/accept.sh].
  static String activePath() {
    final home =
        Platform.environment['HOME'] ??
        Platform.environment['USERPROFILE'] ??
        '';
    return p.join(home, '.fai', 'today', 'active.yaml');
  }

  /// Reads and validates the active story. Returns `fallback`
  /// when the file is missing, malformed, or fails schema /
  /// banned-words checks. Loud failures stay on disk in the
  /// pipeline tooling — Studio quietly falls back so the UI
  /// never crashes on bad operator input.
  static TodayStoryData loadOrFallback(TodayStoryData fallback) {
    try {
      final f = File(activePath());
      if (!f.existsSync()) return fallback;
      final raw = f.readAsStringSync();
      final story = _parse(raw);
      return story ?? fallback;
    } catch (_) {
      return fallback;
    }
  }

  static TodayStoryData? _parse(String raw) {
    final dynamic doc = loadYaml(raw);
    if (doc is! YamlMap) return null;

    if (doc['schema']?.toString() != 'today/v1') return null;

    String s(String key) => (doc[key] ?? '').toString().trim();
    if (_bannedWords.hasMatch(
      '${s('title_en')} ${s('title_de')} ${s('body_en')} ${s('body_de')}',
    )) {
      return null;
    }

    final required = [
      'badge_en',
      'badge_de',
      'title_en',
      'title_de',
      'body_en',
      'body_de',
      'icon',
      'cta',
    ];
    for (final k in required) {
      if (s(k).isEmpty) return null;
    }

    final cta = _ctaFor(s('cta'));
    if (cta == null) return null;
    if (cta != TodayCta.none) {
      if (s('cta_label_en').isEmpty || s('cta_label_de').isEmpty) {
        return null;
      }
    }
    final icon = _iconFor(s('icon'));
    if (icon == null) return null;

    return TodayStoryData(
      badgeEn: s('badge_en'),
      badgeDe: s('badge_de'),
      titleEn: s('title_en'),
      titleDe: s('title_de'),
      bodyEn: s('body_en'),
      bodyDe: s('body_de'),
      ctaLabelEn: s('cta_label_en'),
      ctaLabelDe: s('cta_label_de'),
      icon: icon,
      cta: cta,
      ctaPayload: s('cta_payload'),
    );
  }

  /// Same banned-word list as `tools/today/accept.sh`. Kept in
  /// sync by hand — a candidate that bypassed the shell gate
  /// (e.g. a hand-edited active.yaml) still won't reach the UI.
  static final RegExp _bannedWords = RegExp(
    r'\b('
    r'viral|killer|powerful|just\s+works|revolutionary|game[- ]changing|'
    r'seamless|next[- ]generation|cutting[- ]edge|world[- ]class|unprecedented'
    r')\b',
    caseSensitive: false,
  );

  static TodayCta? _ctaFor(String s) {
    switch (s) {
      case 'none':
        return TodayCta.none;
      case 'openSettings':
        return TodayCta.openSettings;
      case 'filterCategory':
        return TodayCta.filterCategory;
      case 'runQuery':
        return TodayCta.runQuery;
      case 'showSuggestedSources':
        return TodayCta.showSuggestedSources;
      default:
        return null;
    }
  }

  /// Whitelist of icons the prompt template is allowed to
  /// produce. Keeping the set explicit means an LLM hallucinated
  /// icon name fails the load (and we fall back) rather than
  /// rendering a random glyph.
  static IconData? _iconFor(String name) {
    switch (name) {
      case 'hub_outlined':
        return Icons.hub_outlined;
      case 'account_tree_outlined':
        return Icons.account_tree_outlined;
      case 'shield_outlined':
        return Icons.shield_outlined;
      case 'extension':
        return Icons.extension;
      case 'bolt':
        return Icons.bolt;
      case 'menu_book_outlined':
        return Icons.menu_book_outlined;
      case 'security':
        return Icons.security;
      case 'auto_awesome':
        return Icons.auto_awesome;
      case 'rocket_launch':
        return Icons.rocket_launch;
      case 'timeline_outlined':
        return Icons.timeline_outlined;
      default:
        return null;
    }
  }
}
