import { DbService, ListenerBelief } from './db.service.js';

// ---------------------------------------------------------------------------
// System C — Candidate Generators
// Each generator returns candidates with graph-path explanations.
// No scoring — the session director handles ranking.
// ---------------------------------------------------------------------------

export interface ClaimEdge {
  subjectType: string;
  subjectId: string;
  predicate: string;
  objectType: string;
  objectId: string;
  fusedValue: number;
}

export interface VibeCalendarContext {
  localHour: number;
  weekday: number;
  month: number;
  timeZone?: string;
}

/** A bounded key for short-lived, calendar-specific preference beliefs. */
export function calendarContextKey(context: VibeCalendarContext): string {
  const daypart = context.localHour < 6 ? 'night'
    : context.localHour < 12 ? 'morning'
      : context.localHour < 18 ? 'day'
        : 'evening';
  const dayType = context.weekday === 0 || context.weekday === 6 ? 'weekend' : 'weekday';
  const season = context.month === 12 || context.month <= 2 ? 'winter'
    : context.month <= 5 ? 'spring'
      : context.month <= 8 ? 'summer'
        : 'autumn';
  return `calendar:${daypart}:${dayType}:${season}`;
}

export interface Candidate {
  trackId: string;
  generatorId: string;
  explanation: ClaimEdge[];
  relevance: number;
  /**
   * Filled by the session director after sequence planning. Generators remain
   * deliberately unaware of slots and objectives, while the durable plan can
   * retain why this particular candidate won its position.
   */
  plan?: {
    slotRole: string;
    score: number;
    scoreBreakdown: Record<string, unknown>;
    explanation: Record<string, unknown>;
    /** Revision-level policy and constraint evidence, copied into the durable
     * objective snapshot by the coordinator. */
    objective?: Record<string, unknown>;
  };
}

export interface GeneratorContext {
  userId: string;
  seedTrackId: string | null;
  seedArtistId: string | null;
  beliefs: ListenerBelief[];
  recentExclusions: string[];
  toleranceMap: Record<string, number>;
  state: {
    energy: number;
    lastArtistIds: string[];
    lastGenreIds: string[];
    context: VibeCalendarContext | null;
    noveltyHunger: number;
    /** Session-local exploration controls; they never overwrite durable taste. */
    explorationCoefficient?: number;
    discoveryRadius?: number;
    sessionGoal?: { type: string; target: number; progress: number };
    sessionAgeMin: number;
  };
}

export type Generator = (db: DbService, ctx: GeneratorContext) => Promise<Candidate[]>;

const OBJECTIVE_USER = '00000000-0000-0000-0000-000000000000';
const FALLBACK_CANDIDATE_LIMIT = 60;
/** How deep into a comfort artist's catalogue the two proposed tracks are drawn from. */
const COMFORT_ARTIST_POOL = 10;

// ---------------------------------------------------------------------------
// 1. COMFORT — Top artists by longterm affinity > 0.5
// ---------------------------------------------------------------------------
async function comfortGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const topArtists = ctx.beliefs
    .filter(b => b.profile === 'longterm' && b.entity_type === 'artist' && b.dimension === 'affinity' && b.value > 0.5)
    .sort((a, b) => b.value - a.value)
    .slice(0, 20);

  if (topArtists.length === 0) return [];

  const artistValueMap = new Map(topArtists.map(b => [b.entity_id, b.value]));
  const artistIds = topArtists.map(b => b.entity_id);

  // Two tracks per artist, drawn at random from that artist's best known
  // COMFORT_ARTIST_POOL. Taking the top two by fused value instead proposed the
  // same handful of tracks in every session for as long as the listener's top
  // artists held still, which is most of why a Vibe felt like it never moved.
  const res = await db.pgClient.query(
    `SELECT sampled.id, sampled.artist_id
     FROM (
       SELECT ranked.id, ranked.artist_id,
         ROW_NUMBER() OVER (PARTITION BY ranked.artist_id ORDER BY RANDOM()) AS pick
       FROM (
         SELECT t.id, cf.object_id AS artist_id,
           ROW_NUMBER() OVER (PARTITION BY cf.object_id ORDER BY cf.fused_value DESC) AS rn
         FROM tracks t
         JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
           AND cf.predicate IN ('credited_main_on', 'featured_on')
           AND cf.object_type = 'artist'
           AND cf.object_id = ANY($1::uuid[])
           AND (cf.user_id = $2 OR cf.user_id = $3)
         WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
           AND NOT (t.id = ANY($4::uuid[]))
       ) ranked
       WHERE ranked.rn <= ${COMFORT_ARTIST_POOL}
     ) sampled
     WHERE sampled.pick <= 2
     ORDER BY sampled.artist_id, sampled.pick`,
    [artistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
  );

  return (res.rows as { id: string; artist_id: string }[]).map(row => {
    const value = artistValueMap.get(row.artist_id) ?? 0.5;
    return {
      trackId: row.id,
      generatorId: 'comfort',
      explanation: [{
        subjectType: 'artist',
        subjectId: row.artist_id,
        predicate: 'credited_main_on',
        objectType: 'track',
        objectId: row.id,
        fusedValue: value,
      }],
      relevance: value,
    };
  });
}

// ---------------------------------------------------------------------------
// 2. ADJACENT — Walk graph from seed artist, exclude comfort pool
// ---------------------------------------------------------------------------
async function adjacentGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  if (!ctx.seedArtistId) return [];

  const comfortArtistIds = new Set(
    ctx.beliefs
      .filter(b => b.profile === 'longterm' && b.entity_type === 'artist' && b.dimension === 'affinity' && b.value > 0.5)
      .map(b => b.entity_id)
  );

  // Walk: seedArtist -> (credited_main_on|featured_on) -> track -> (credited_main_on|featured_on) -> reachedArtist
  // cf1 finds tracks where seed artist appears; cf2 finds OTHER artists on those same tracks
  const reachedRes = await db.pgClient.query(
    `SELECT DISTINCT cf2.object_id AS artist_id
     FROM claim_fusion cf1
     JOIN claim_fusion cf2 ON cf2.subject_type = 'track'
       AND cf2.subject_id = cf1.subject_id
       AND cf2.predicate IN ('credited_main_on', 'featured_on')
       AND cf2.object_type = 'artist'
       AND cf2.object_id != $1
       AND (cf2.user_id = $2 OR cf2.user_id = $3)
     WHERE cf1.subject_type = 'track'
       AND cf1.predicate IN ('credited_main_on', 'featured_on')
       AND cf1.object_type = 'artist'
       AND cf1.object_id = $1
       AND (cf1.user_id = $2 OR cf1.user_id = $3)
     LIMIT 30`,
    [ctx.seedArtistId, OBJECTIVE_USER, ctx.userId]
  );

  const reachedArtistIds = (reachedRes.rows as { artist_id: string }[])
    .map(r => r.artist_id)
    .filter(id => !comfortArtistIds.has(id));

  if (reachedArtistIds.length === 0) return [];

  const trackRes = await db.pgClient.query(
    `SELECT id FROM (
       SELECT DISTINCT t.id
       FROM tracks t
       JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
         AND cf.predicate IN ('credited_main_on', 'featured_on')
         AND cf.object_type = 'artist'
         AND cf.object_id = ANY($1::uuid[])
         AND (cf.user_id = $2 OR cf.user_id = $3)
       WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($4::uuid[]))
     ) sub
     ORDER BY RANDOM()
     LIMIT 20`,
    [reachedArtistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
  );

  return (trackRes.rows as { id: string }[]).map(row => ({
    trackId: row.id,
    generatorId: 'adjacent',
    explanation: [{
      subjectType: 'artist',
      subjectId: ctx.seedArtistId!,
      predicate: 'credited_main_on',
      objectType: 'track',
      objectId: row.id,
      fusedValue: 0.6,
    }],
    relevance: 0.6,
  }));
}

// ---------------------------------------------------------------------------
// 3. DISCOVERY — Unfamiliar artists via graph edges from trusted artists
// ---------------------------------------------------------------------------
async function discoveryGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const trustedIds = ctx.beliefs
    .filter(b => b.profile === 'longterm' && b.entity_type === 'artist' && b.dimension === 'affinity' && b.value > 0.3)
    .map(b => b.entity_id);

  if (trustedIds.length === 0) return [];

  const noveltyTolerance = ctx.toleranceMap.novelty_tolerance ?? 0.3;
  const maxCandidates = Math.max(1, Math.floor(10 * noveltyTolerance));

  const unfamiliarRes = await db.pgClient.query(
    `SELECT cf.object_id AS artist_id, MAX(cf.fused_value) AS edge_strength
     FROM claim_fusion cf
     WHERE cf.subject_type = 'artist'
       AND cf.subject_id = ANY($1::uuid[])
       AND cf.predicate IN ('same_scene_as', 'same_label_as', 'produced')
       AND cf.object_type = 'artist'
       AND NOT EXISTS (
         SELECT 1 FROM listener_beliefs lb
         WHERE lb.user_id = $2
           AND lb.entity_type = 'artist'
           AND lb.entity_id = cf.object_id
           AND lb.profile IN ('longterm', 'obsession')
       )
     GROUP BY cf.object_id
     LIMIT 30`,
    [trustedIds, ctx.userId]
  );

  const unfamiliarRows = unfamiliarRes.rows as { artist_id: string; edge_strength: number }[];
  const unfamiliarArtistIds = unfamiliarRows.map(r => r.artist_id);
  const edgeStrengthMap = new Map(unfamiliarRows.map(r => [r.artist_id, r.edge_strength]));
  if (unfamiliarArtistIds.length === 0) return [];

  const trackRes = await db.pgClient.query(
    `SELECT id, artist_id FROM (
       SELECT DISTINCT ON (t.id) t.id, cf.object_id AS artist_id
       FROM tracks t
       JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
         AND cf.predicate IN ('credited_main_on', 'featured_on')
         AND cf.object_type = 'artist'
         AND cf.object_id = ANY($1::uuid[])
       WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($2::uuid[]))
       ORDER BY t.id, cf.fused_value DESC NULLS LAST
     ) sub
     ORDER BY RANDOM()
     LIMIT $3`,
    [unfamiliarArtistIds, ctx.recentExclusions, maxCandidates]
  );

  return (trackRes.rows as { id: string; artist_id: string }[]).map(row => {
    const relevance = edgeStrengthMap.get(row.artist_id) ?? 0.4;
    return {
      trackId: row.id,
      generatorId: 'discovery',
      explanation: [{
        subjectType: 'artist',
        subjectId: row.artist_id,
        predicate: 'credited_main_on',
        objectType: 'track',
        objectId: row.id,
        fusedValue: relevance,
      }],
      relevance,
    };
  });
}

// ---------------------------------------------------------------------------
// 4. DEEP-DIVE — Albums from obsession artists, unplayed tracks first
// ---------------------------------------------------------------------------
async function deepDiveGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const obsessedIds = ctx.beliefs
    .filter(b => b.profile === 'obsession' && b.entity_type === 'artist' && b.dimension === 'affinity' && b.value > 0.3)
    .map(b => b.entity_id);

  if (obsessedIds.length === 0) return [];

  const albumRes = await db.pgClient.query(
    `SELECT al.id AS album_id, al.artist_id
     FROM albums al
     WHERE al.artist_id = ANY($1::uuid[])
     ORDER BY al.year ASC NULLS LAST, al.title ASC
     LIMIT 20`,
    [obsessedIds]
  );

  const albumRows = albumRes.rows as { album_id: string; artist_id: string }[];
  if (albumRows.length === 0) return [];

  const albumIds = albumRows.map(a => a.album_id);
  const albumArtistMap = new Map(albumRows.map(a => [a.album_id, a.artist_id]));
  const obsessionValueMap = new Map(
    ctx.beliefs
      .filter(b => b.profile === 'obsession' && b.entity_type === 'artist' && b.dimension === 'affinity' && b.value > 0.3)
      .map(b => [b.entity_id, b.value])
  );

  const trackRes = await db.pgClient.query(
    `SELECT sub.id, sub.album_id
     FROM (
       SELECT t.id, t.album_id,
         ROW_NUMBER() OVER (PARTITION BY t.album_id ORDER BY t.title ASC) AS rn
       FROM tracks t
       WHERE t.album_id = ANY($1::uuid[])
         AND (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($2::uuid[]))
     ) sub
     WHERE sub.rn <= 5
     ORDER BY sub.album_id, sub.rn`,
    [albumIds, ctx.recentExclusions]
  );

  return (trackRes.rows as { id: string; album_id: string }[]).map(row => {
    const artistId = albumArtistMap.get(row.album_id) ?? 'unknown';
    const relevance = obsessionValueMap.get(artistId) ?? 0.7;
    return {
      trackId: row.id,
      generatorId: 'deep-dive',
      explanation: [{
        subjectType: 'artist',
        subjectId: artistId,
        predicate: 'credited_main_on',
        objectType: 'track',
        objectId: row.id,
        fusedValue: relevance,
      }],
      relevance,
    };
  });
}

// ---------------------------------------------------------------------------
// 5. REVIVAL — Stale longterm affinity (last_reinforced > 90 days ago)
// ---------------------------------------------------------------------------
async function revivalGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const staleRes = await db.pgClient.query(
    `SELECT lb.entity_id AS artist_id, lb.value AS affinity
     FROM listener_beliefs lb
     WHERE lb.user_id = $1
       AND lb.profile = 'longterm'
       AND lb.entity_type = 'artist'
       AND lb.dimension = 'affinity'
       AND lb.value > 0.3
       AND lb.last_reinforced_at < NOW() - INTERVAL '90 days'
     ORDER BY lb.value DESC
     LIMIT 20`,
    [ctx.userId]
  );

  const staleArtists = staleRes.rows as { artist_id: string; affinity: number }[];
  if (staleArtists.length === 0) return [];

  const staleArtistIds = staleArtists.map(a => a.artist_id);
  const affinityMap = new Map(staleArtists.map(a => [a.artist_id, a.affinity]));

  const trackRes = await db.pgClient.query(
    `SELECT id, artist_id FROM (
       SELECT DISTINCT ON (t.id) t.id, cf.object_id AS artist_id
       FROM tracks t
       JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
         AND cf.predicate IN ('credited_main_on', 'featured_on')
         AND cf.object_type = 'artist'
         AND cf.object_id = ANY($1::uuid[])
         AND (cf.user_id = $2 OR cf.user_id = $3)
       WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($4::uuid[]))
       ORDER BY t.id, cf.fused_value DESC NULLS LAST
     ) sub
     ORDER BY RANDOM()
     LIMIT 20`,
    [staleArtistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
  );

  return (trackRes.rows as { id: string; artist_id: string }[]).map(row => {
    const relevance = affinityMap.get(row.artist_id) ?? 0.6;
    return {
      trackId: row.id,
      generatorId: 'revival',
      explanation: [{
        subjectType: 'artist',
        subjectId: row.artist_id,
        predicate: 'credited_main_on',
        objectType: 'track',
        objectId: row.id,
        fusedValue: relevance,
      }],
      relevance,
    };
  });
}

// ---------------------------------------------------------------------------
// 6. NOVELTY — Recently released tracks by graph-adjacent artists
// ---------------------------------------------------------------------------
async function noveltyGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const trustedIds = ctx.beliefs
    .filter(b => b.entity_type === 'artist' && b.value > 0.3)
    .map(b => b.entity_id);

  if (trustedIds.length === 0) return [];

  const res = await db.pgClient.query(
    `SELECT id FROM (
       SELECT DISTINCT t.id, t.release_date
       FROM tracks t
       JOIN claim_fusion cf_edge ON cf_edge.subject_type = 'artist'
         AND cf_edge.subject_id = ANY($2::uuid[])
         AND cf_edge.predicate IN ('same_scene_as', 'same_label_as', 'produced')
         AND cf_edge.object_type = 'artist'
       JOIN claim_fusion cf_track ON cf_track.subject_type = 'track'
         AND cf_track.subject_id = t.id
         AND cf_track.predicate IN ('credited_main_on', 'featured_on')
         AND cf_track.object_type = 'artist'
         AND cf_track.object_id = cf_edge.object_id
       WHERE t.release_date IS NOT NULL
         AND t.release_date >= NOW() - INTERVAL '60 days'
         AND (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($1::uuid[]))
     ) sub
     ORDER BY release_date DESC
     LIMIT 20`,
    [ctx.recentExclusions.length > 0 ? ctx.recentExclusions : ['00000000-0000-0000-0000-000000000000'], trustedIds]
  );

  return res.rows.map((row: { id: string }) => ({
    trackId: row.id,
    generatorId: 'novelty',
    relevance: 0.5,
    explanation: [{
      subjectType: 'track', subjectId: row.id,
      predicate: 'release_date',
      objectType: 'date', objectId: 'recent',
      fusedValue: 0.5,
    }],
  }));
}

// ---------------------------------------------------------------------------
// 7. EXPERIMENTAL — Genres with high network distance from favourites
// ---------------------------------------------------------------------------
async function experimentalGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const favArtistIds = ctx.beliefs
    .filter(b => b.entity_type === 'artist' && b.value > 0.4)
    .map(b => b.entity_id);

  if (favArtistIds.length === 0) return [];

  const favGenreIds = ctx.beliefs
    .filter(b => b.entity_type === 'genre' && b.value > 0.2)
    .map(b => b.entity_id);

  const result = await db.pgClient.query(
    `WITH unfamiliar_genres AS (
       SELECT g.id, g.name,
         (SELECT COUNT(*) FROM track_genre tg2 WHERE tg2.genre_id = g.id) AS track_count
       FROM genre g
       WHERE NOT (g.id = ANY($1::uuid[]))
         AND EXISTS (SELECT 1 FROM track_genre tg WHERE tg.genre_id = g.id)
       ORDER BY RANDOM()
       LIMIT 3
     ),
     candidate_tracks AS (
       SELECT DISTINCT t.id, tg.genre_id
       FROM tracks t
       JOIN track_genre tg ON tg.track_id = t.id
       JOIN unfamiliar_genres ug ON ug.id = tg.genre_id
       WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($2::uuid[]))
       LIMIT 30
     )
     SELECT ct.id, ct.genre_id
     FROM candidate_tracks ct
     ORDER BY RANDOM()
     LIMIT 6`,
    [
      favGenreIds.length > 0 ? favGenreIds : ['00000000-0000-0000-0000-000000000000'],
      ctx.recentExclusions.length > 0 ? ctx.recentExclusions : ['00000000-0000-0000-0000-000000000000'],
    ]
  );

  return result.rows.map((row: { id: string; genre_id: string }) => ({
    trackId: row.id,
    generatorId: 'experimental',
    relevance: 0.2,
    explanation: [{
      subjectType: 'genre', subjectId: row.genre_id,
      predicate: 'belongs_to_genre',
      objectType: 'track', objectId: row.id,
      fusedValue: 0.2,
    }],
  }));
}

// ---------------------------------------------------------------------------
// 8. CONTEXTUAL — Contextual profile beliefs
// ---------------------------------------------------------------------------
async function contextualGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  if (!ctx.state.context) return [];

  const contextKey = calendarContextKey(ctx.state.context);

  const contextualBeliefs = await db.getListenerBeliefs({
    userId: ctx.userId,
    profile: 'contextual',
    limit: 30,
    orderBy: 'value',
    order: 'DESC',
  });

  const targetBeliefs = contextualBeliefs.filter(b =>
    b.entity_type === 'artist' && b.dimension === contextKey && b.value > 0.2,
  );
  const targetArtistIds = targetBeliefs.map(b => b.entity_id);
  const targetValueMap = new Map(targetBeliefs.map(b => [b.entity_id, b.value]));

  if (targetArtistIds.length === 0) return [];

  const trackRes = await db.pgClient.query(
    `SELECT id, artist_id FROM (
       SELECT DISTINCT ON (t.id) t.id, cf.object_id AS artist_id
       FROM tracks t
       JOIN claim_fusion cf ON cf.subject_type = 'track' AND cf.subject_id = t.id
         AND cf.predicate IN ('credited_main_on', 'featured_on')
         AND cf.object_type = 'artist'
         AND cf.object_id = ANY($1::uuid[])
         AND (cf.user_id = $2 OR cf.user_id = $3)
       WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
         AND NOT (t.id = ANY($4::uuid[]))
       ORDER BY t.id, cf.fused_value DESC NULLS LAST
     ) sub
     ORDER BY RANDOM()
     LIMIT 15`,
    [targetArtistIds, OBJECTIVE_USER, ctx.userId, ctx.recentExclusions]
  );

  return (trackRes.rows as { id: string; artist_id: string }[]).map(row => {
    const relevance = targetValueMap.get(row.artist_id) ?? 0.5;
    return {
      trackId: row.id,
      generatorId: 'contextual',
      explanation: [{
        subjectType: 'artist',
        subjectId: row.artist_id,
        predicate: 'credited_main_on',
        objectType: 'track',
        objectId: row.id,
        fusedValue: relevance,
      }],
      relevance,
    };
  });
}

// ---------------------------------------------------------------------------
// 9. LIBRARY FALLBACK — keeps a Vibe usable when the metadata graph is sparse
// ---------------------------------------------------------------------------
async function libraryFallbackGenerator(db: DbService, ctx: GeneratorContext): Promise<Candidate[]> {
  const res = await db.pgClient.query(
    // Probation belongs here too. This is the one generator that does not walk
    // the graph, so it is the only way an acquired recommendation nothing has
    // enriched yet can ever be offered — which is why Found tracks were sitting
    // on disk unplayed.
    `SELECT t.id
     FROM tracks t
     WHERE (t.state = 'LIBRARY' OR (t.state = 'RECOMMENDED' AND t.probation_status = 'probation'))
       AND NOT (t.id = ANY($1::uuid[]))
     ORDER BY RANDOM()
     LIMIT $2`,
    [ctx.recentExclusions, FALLBACK_CANDIDATE_LIMIT],
  );

  return (res.rows as { id: string }[]).map(row => ({
    trackId: row.id,
    generatorId: 'library-fallback',
    relevance: 0.05,
    explanation: [{
      subjectType: 'track', subjectId: row.id,
      predicate: 'library_fallback',
      objectType: 'track', objectId: row.id,
      fusedValue: 0.05,
    }],
  }));
}

// ---------------------------------------------------------------------------
// All generators, ordered by priority (comfort first, experimental last)
// ---------------------------------------------------------------------------
export const ALL_GENERATORS: Generator[] = [
  comfortGenerator,
  adjacentGenerator,
  deepDiveGenerator,
  revivalGenerator,
  discoveryGenerator,
  noveltyGenerator,
  contextualGenerator,
  experimentalGenerator,
  libraryFallbackGenerator,
];
