GrandmaReact.cs
namespace NoChillquarium;

/// <summary>
/// Mid-game porch reactions — one-shot dry-love Grandma lines on big firsts.
/// Queues if a gift/story card is open so nothing gets buried.
/// </summary>
public static class GrandmaReact
{
	static readonly HashSet<string> _said = new( StringComparer.OrdinalIgnoreCase );
	static string _pendingId = "";
	static string _pendingLine = "";
	/// <summary>Let system banners show first (train unlock, shady tab, fight payout).</summary>
	static float _holdTimer;
	static int _version;

	public static int Version => _version;

	// ---- Milestone ids (save as strings) ----
	public const string FirstFeed = "first_feed";
	public const string FirstTrain = "first_train";
	public const string FirstAbom = "first_abom";
	public const string FirstFightWin = "first_fight_win";
	public const string FirstFightLoss = "first_fight_loss";
	public const string FirstChamp = "first_champ";
	public const string FirstDeath = "first_death";
	public const string FirstSharkKill = "first_shark_kill";
	public const string FirstSalt = "first_salt";
	public const string FirstHabitat = "first_habitat";
	public const string FirstTankKick = "first_tank_kick";
	public const string FirstJanitorMeth = "first_janitor_meth";
	public const string RobbedTin = "robbed_tin";
	public const string DosedPipes = "dosed_pipes";
	public const string GiftedMeth = "gifted_meth";
	public const string FirstRansack = "first_ransack";
	public const string FirstSell = "first_sell";
	public const string FirstPainted = "first_painted";
	public const string FirstGolden = "first_golden";
	public const string FirstRainbow = "first_rainbow";
	public const string FirstFlush = "first_flush";

	public static void ResetForNewGame()
	{
		_said.Clear();
		_pendingId = "";
		_pendingLine = "";
		_holdTimer = 0f;
		_version++;
	}

	public static void Clear()
	{
		_pendingId = "";
		_pendingLine = "";
		_holdTimer = 0f;
		_version++;
	}

	public static void Load( SaveData data )
	{
		_said.Clear();
		_pendingId = "";
		_pendingLine = "";
		_holdTimer = 0f;
		if ( data?.GrandmaReactSaid is null )
		{
			// Old saves: treat past progress as already "heard" so we don't spam.
			SeedFromProgress( data );
			_version++;
			return;
		}
		foreach ( var id in data.GrandmaReactSaid )
		{
			if ( !string.IsNullOrWhiteSpace( id ) )
				_said.Add( id.Trim() );
		}
		_version++;
	}

	public static void WriteToSave( SaveData data )
	{
		if ( data is null )
			return;
		data.GrandmaReactSaid = _said.ToList();
	}

	/// <summary>Call from play tick — flush queued quip when UI is quiet.</summary>
	public static void Tick( float dt )
	{
		if ( string.IsNullOrEmpty( _pendingId ) )
			return;
		if ( _holdTimer > 0f )
		{
			_holdTimer -= MathF.Max( 0f, dt );
			return;
		}
		if ( IsUiBlocking() )
			return;

		var id = _pendingId;
		var line = _pendingLine;
		_pendingId = "";
		_pendingLine = "";
		SpeakNow( id, line );
	}

	/// <summary>One-shot Grandma banner. Always queues briefly so system tips show first.</summary>
	public static void Try( string id, string line )
	{
		if ( string.IsNullOrEmpty( id ) || string.IsNullOrEmpty( line ) )
			return;
		if ( _said.Contains( id ) )
			return;
		// Don't stack over an already-queued different quip — first wins.
		if ( !string.IsNullOrEmpty( _pendingId ) )
			return;

		_pendingId = id;
		_pendingLine = line;
		// Let the player sit with the system tip + porch beat before dry love.
		_holdTimer = IsUiBlocking() ? 0.8f : 4.5f;
		_version++;
	}

	// ---- Named firsts (call sites stay thin) ----

	public static void OnFirstFeed() =>
		Try( FirstFeed, "Grandma: \"They're eating. Cute. Don't get a big head. Cookies after you buy another one. That's called providing.\"" );

	public static void OnFirstTrain() =>
		Try( FirstTrain, "Grandma: \"Nice sweat. Now go make Brad regret opening that garage. Love you. Wipe your face.\"" );

	public static void OnFirstAbom() =>
		Try( FirstAbom, "Grandma: \"Ugly little angel. Keep making those. I want a whole ugly choir. Bingo's gonna hate us.\"" );

	public static void OnFirstFightWin() =>
		Try( FirstFightWin, "Grandma: \"I told Brad my punk hits. You just proved it. Sit. Drink water. Don't gloat at bingo.\"" );

	public static void OnFirstFightLoss() =>
		Try( FirstFightLoss, "Grandma: \"Cute try. Ice them, train again, bite back. I'm making soup either way. Soup doesn't care about receipts.\"" );

	public static void OnFirstChamp() =>
		Try( FirstChamp, "Grandma: \"Belt's crooked. Fix it. Then try not to look so proud at bingo. I will look proud. You don't get to.\"" );

	public static void OnFirstDeath() =>
		Try( FirstDeath, "Grandma: \"Supper's on. Love you. Still your fault. Wipe the glass. We do not skip wipe the glass.\"" );

	public static void OnFirstSharkKill() =>
		Try( FirstSharkKill, "Grandma: \"You punched a cocaine shark. Bingo will hear about this. I'm so proud. And concerned. Mostly concerned. Sit.\"" );

	public static void OnFirstSalt() =>
		Try( FirstSalt, "Grandma: \"Two tanks. Fancy. Wrong water still means supper. I will say it at church. I will say it twice.\"" );

	public static void OnFirstHabitat() =>
		Try( FirstHabitat, "Grandma: \"Bigger glass. Still my porch. Don't flood the steps. Love you. Shoo. That's the whole speech.\"" );

	public static void OnFirstTankKick() =>
		Try( FirstTankKick, "Grandma: \"That's a living room. You just assaulted furniture. I'm making soup. The soup is disappointed. The soup has a lawyer.\"" );

	public static void OnFirstJanitorMeth() =>
		Try( FirstJanitorMeth, "Grandma: \"You tried to drug the help. He drinks gas-station coffee. I raised a punk, not a pharmacist. Sit. Think about that.\"" );

	public static void OnRobbedTin() =>
		Try( RobbedTin, "Grandma: \"That tin was for bingo and a funeral. I can still count. Put it back in spirit. Love you. Thief. Sit anyway.\"" );

	public static void OnDosedPipes() =>
		Try( DosedPipes, "Grandma: \"The toilet is not a laboratory. If the city calls I'm telling them your full government name. I remember it.\"" );

	public static void OnGiftedMeth() =>
		Try( GiftedMeth, "Grandma: \"I put it in the soup. The soup said no. The soup is grounded. You're still my idiot. Sit. Don't season anything.\"" );

	public static void OnFirstRansack() =>
		Try( FirstRansack, "Grandma: \"You robbed a garage. Brad will laminate it. I'm proud in a way that requires a lawyer. Drink water.\"" );

	public static void OnFirstSell() =>
		Try( FirstSell, "Grandma: \"That's budgeting. Keep the shiny ones. The tin likes both kinds of love. Don't make it a TED talk.\"" );

	public static void OnFirstPainted() =>
		Try( FirstPainted, "Grandma: \"Painted. Like a crime. Like a compliment. Name it. Don't sell it. I will know.\"" );

	public static void OnFirstGolden() =>
		Try( FirstGolden, "Grandma: \"Gold in my glass. Don't sell that one. I will know. Love you. That's not a request.\"" );

	public static void OnFirstRainbow() =>
		Try( FirstRainbow, "Grandma: \"That one looks like a fever. Keep it. Bingo would riot. I would let them.\"" );

	public static void OnFirstFlush() =>
		Try( FirstFlush, "Grandma: \"I heard the splash. I didn't look. The faucet's going to gossip. Love you. Menace. Shoo.\"" );

	/// <summary>Keeper chase — one-shot per tier so the tin and the glass stay the same story.</summary>
	public static void OnKeeper( FishRarity rarity )
	{
		if ( rarity >= FishRarity.Mythical )
			OnFirstRainbow();
		else if ( rarity >= FishRarity.Legendary )
			OnFirstGolden();
		else if ( rarity >= FishRarity.PaintedRare )
			OnFirstPainted();
	}

	// ---- Internals ----

	static bool IsUiBlocking() =>
		GrandmaGifts.IsBusy
		|| StorySystem.CardOpen
		|| BattleSystem.Phase == BattlePhase.Result
		|| BattleSystem.Phase == BattlePhase.Fighting;

	static void SpeakNow( string id, string line )
	{
		if ( _said.Contains( id ) )
			return;
		_said.Add( id );
		TankSim.ShowBanner( line );
		_version++;
		SaveGame.TrySave( quiet: true );
	}

	/// <summary>Continuing mid-empire: don't re-fire early tutorial quips.</summary>
	static void SeedFromProgress( SaveData data )
	{
		if ( data is null )
			return;
		if ( data.StoryFedOnce || data.PlaytimeSeconds > 120f )
			_said.Add( FirstFeed );
		if ( data.StoryTrainedOnce )
			_said.Add( FirstTrain );
		if ( data.StoryCombinedOnce || data.AbominationsCreated > 0 )
			_said.Add( FirstAbom );
		if ( data.BattleWins > 0 )
			_said.Add( FirstFightWin );
		if ( data.BattleLosses > 0 )
			_said.Add( FirstFightLoss );
		if ( data.ChampionshipTitles > 0 )
			_said.Add( FirstChamp );
		if ( data.FishDeaths > 0 )
			_said.Add( FirstDeath );
		if ( data.SharkKills > 0 )
			_said.Add( FirstSharkKill );
		if ( data.SaltUnlocked )
			_said.Add( FirstSalt );
		if ( data.StorySoldOnce )
			_said.Add( FirstSell );
		if ( data.AdventureFlushed )
			_said.Add( FirstFlush );
		// Any non-starter habitat id means they already upgraded once.
		if ( !string.IsNullOrEmpty( data.FreshHabitatId )
			&& !string.Equals( data.FreshHabitatId, "tank_fresh_1", StringComparison.OrdinalIgnoreCase ) )
			_said.Add( FirstHabitat );
		else if ( !string.IsNullOrEmpty( data.SaltHabitatId )
			&& !string.Equals( data.SaltHabitatId, "tank_salt_1", StringComparison.OrdinalIgnoreCase ) )
			_said.Add( FirstHabitat );
	}
}