Shop.cs
namespace NoChillquarium;

/// <summary>
/// Shop spends: fish, capacity, habitats, salt tank, meth, M80.
/// </summary>
public static class Shop
{
	public const int CapacityStep = 2;
	public const double BaseCapacityCost = 28;
	/// <summary>Ð added per slot already past the habitat floor.</summary>
	public const double CapacityCostPerExtra = 18;
	public const double MethCost = 45;
	public const double M80Cost = 28;
	public const double SaltTankCost = TankSim.SaltTankCost;

	static int _version;
	static int _methBags;
	static int _m80Bags;

	public static int Version => _version;
	public static int MethBags => _methBags;
	public static int M80Bags => _m80Bags;
	/// <summary>MainShell sets this while the laptop shop is open. Gifts wait.</summary>
	public static bool UiOpen { get; set; }
	public static string LastMessage { get; private set; } = "Pick a pack.";

	public static void SetLastMessage( string msg )
	{
		LastMessage = msg ?? "";
		_version++;
	}

	public static double CapacityCost =>
		BaseCapacityCost + Math.Max( 0, TankSim.Capacity - TankSim.DefaultCapacity ) * CapacityCostPerExtra;

	public static bool CanBuyFish( FishSpecies species ) =>
		species is not null
		&& TankSim.IsActive
		&& FishSpecies.IsShopUnlocked( species )
		&& Economy.Balance + 0.001 >= FishPurchaseTotal( species );

	/// <summary>Fish price, plus a capacity bump if this tank is packed.</summary>
	public static double FishPurchaseTotal( FishSpecies species )
	{
		if ( species is null )
			return 0;
		var total = species.BuyCost;
		if ( TankSim.IsActive && TankSim.FishCount >= TankSim.Capacity )
			total += CapacityCost;
		return total;
	}

	public static string FishActLabel( FishSpecies species )
	{
		if ( species is null || !TankSim.IsActive )
			return "—";
		if ( !FishSpecies.IsShopUnlocked( species ) )
			return "Soon";
		if ( CanBuyFish( species ) )
			return TankSim.FishCount >= TankSim.Capacity ? "Expand" : "Buy";
		if ( TankSim.FishCount >= TankSim.Capacity )
			return "Full";
		return "Need Ð";
	}

	public static string FishBuyBlockedReason( FishSpecies species )
	{
		if ( species is null )
			return "Invalid";
		if ( !TankSim.IsActive )
			return "No tank";
		if ( !FishSpecies.IsShopUnlocked( species ) )
			return FishSpecies.ShopUnlockHint( species );
		if ( TankSim.FishCount >= TankSim.Capacity )
		{
			var need = CapacityCost + species.BuyCost;
			if ( Economy.Balance + 0.001 < need )
				return "Tank full · +2 slots Ð" + Economy.FormatDoge( CapacityCost );
			return "";
		}
		if ( Economy.Balance < species.BuyCost )
			return $"Need Ð{Economy.FormatDoge( species.BuyCost )}";
		return "";
	}

	public static bool CanBuyCapacity() =>
		TankSim.IsActive && Economy.Balance >= CapacityCost;

	public static bool CanBuySaltTank() =>
		TankSim.IsActive && !TankSim.SaltUnlocked && Economy.Balance >= SaltTankCost;

	public static HabitatDef NextHabitatOffer => TankSim.NextHabitat;

	public static string HabitatUnlockText( HabitatDef def )
	{
		if ( def is null )
			return "";
		if ( Economy.PlaytimeSeconds >= def.UnlockPlaytime )
			return def.Note;
		var left = def.UnlockPlaytime - Economy.PlaytimeSeconds;
		var totalMin = (int)(left / 60f);
		if ( totalMin >= 60 )
		{
			var h = totalMin / 60;
			var m = totalMin % 60;
			return m > 0 ? $"Unlocks in {h}h {m}m" : $"Unlocks in {h}h";
		}
		var s = (int)(left % 60f);
		return $"Unlocks in {totalMin}:{s:00}";
	}

	public static bool CanBuyHabitat( HabitatDef def ) =>
		def is not null
		&& TankSim.CanUpgradeTo( def )
		&& Economy.Balance >= def.Cost;

	public static bool CanBuyMeth() =>
		TankSim.IsActive && Economy.Balance >= MethCost;

	public static bool CanBuyM80() =>
		TankSim.IsActive && Economy.Balance >= M80Cost;

	public static bool CanFeedMeth() =>
		TankSim.IsActive && _methBags > 0;

	public static bool CanArmM80() =>
		TankSim.IsActive && _m80Bags > 0;

	public static void ResetInventory()
	{
		_methBags = 0;
		_m80Bags = 0;
		LastMessage = "Pick a pack.";
		DarkWeb.Clear();
		_version++;
	}

	public static void LoadInventory( int methBags, int m80Bags = 0 )
	{
		_methBags = Math.Max( 0, methBags );
		_m80Bags = Math.Max( 0, m80Bags );
		_version++;
	}

	public static void WriteInventory( SaveData data )
	{
		if ( data is not null )
		{
			data.MethBags = _methBags;
			data.M80Bags = _m80Bags;
		}
	}

	public static bool TryBuyFish( FishSpecies species )
	{
		if ( species is null || !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( !FishSpecies.IsShopUnlocked( species ) )
		{
			Fail( FishSpecies.ShopUnlockHint( species ) );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		var expanded = false;
		if ( TankSim.FishCount >= TankSim.Capacity )
		{
			var cap = CapacityCost;
			if ( Economy.Balance + 0.001 < cap + species.BuyCost )
			{
				Fail( "Tank full. +2 slots Ð" + Economy.FormatDoge( cap )
					+ " · then Ð" + Economy.FormatDoge( species.BuyCost ) + " for the fish." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}
			if ( !Economy.TrySpend( cap ) )
			{
				Fail( "Tank full. Need Ð" + Economy.FormatDoge( cap ) + " to add slots." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}
			TankSim.IncreaseCapacity( CapacityStep );
			expanded = true;
		}

		if ( !Economy.TrySpend( species.BuyCost ) )
		{
			Fail( expanded
				? $"+{CapacityStep} slots bought. Need Ð{Economy.FormatDoge( species.BuyCost )} for the fish."
				: $"Need Ð{Economy.FormatDoge( species.BuyCost )}." );
			GameAudio.PlayUi( GameAudio.Error );
			_version++;
			return false;
		}

		if ( !TankSim.Spawn( species ) )
		{
			Economy.Add( species.BuyCost );
			Fail( "Could not spawn fish." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		var rarity = TankSim.LastSpawnRarity;
		var warn = species.Water != TankSim.Water && species.Water != WaterType.Any
			? " · WRONG WATER ⚠"
			: "";
		LastMessage = expanded
			? $"+{CapacityStep} slots · unfolding {species.Name} · Ð{Economy.FormatDoge( CapacityCost + species.BuyCost )}{warn}"
			: $"Unfolding {species.Name}… Ð{Economy.FormatDoge( species.BuyCost )}{warn}";
		// Juice + rarity stamp live on the card — don't spoil the flip here.
		BuyReveal.Begin( species, rarity );
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool TryBuyCapacity()
	{
		if ( !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		var cost = CapacityCost;
		if ( !Economy.TrySpend( cost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( cost )} for +{CapacityStep} slots." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		TankSim.IncreaseCapacity( CapacityStep );
		LastMessage = $"+{CapacityStep} capacity on {TankSim.TankName} (now {TankSim.Capacity}).";
		GameAudio.PlayUi( GameAudio.Upgrade );
		BoomFeel.SoftPunch( 0.3f );
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool TryBuySaltTank()
	{
		if ( !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( TankSim.SaltUnlocked )
		{
			Fail( "You already own a salt tank." );
			return false;
		}

		if ( !Economy.TrySpend( SaltTankCost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( SaltTankCost )} for a salt tank." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		TankSim.TryUnlockSaltTank( announce: false );
		LastMessage = $"Salt tank + clownfish · Ð{Economy.FormatDoge( SaltTankCost )}";
		AchievementSystem.Unlock( "salt" );
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool TryBuyHabitat( HabitatDef def )
	{
		if ( def is null || !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( !TankSim.CanUpgradeTo( def ) )
		{
			if ( Economy.PlaytimeSeconds < def.UnlockPlaytime )
				Fail( HabitatUnlockText( def ) );
			else if ( def.Rank != TankSim.HabitatRank + 1 )
				Fail( "Buy the next habitat in order." );
			else
				Fail( "Can't move into that habitat yet." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		if ( !Economy.TrySpend( def.Cost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		if ( !TankSim.TryUpgradeHabitat( def, announce: false ) )
		{
			Economy.Add( def.Cost );
			Fail( "Habitat move failed." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		// Housewarming tip scales with rank so upgrades feel like progression.
		var houseTip = Math.Round( 10 + def.Rank * 8.0, 1 );
		Economy.AddForced( houseTip );
		BoomFeel.SoftPunch( 0.5f + def.Rank * 0.08f );
		LastMessage = $"Moved into {def.Name} · cap {TankSim.Capacity} · +Ð{Economy.FormatDoge( houseTip )}";
		TankSim.ShowBanner( $"{def.Name}! Cap {TankSim.Capacity}. Housewarming +Ð{Economy.FormatDoge( houseTip )}" );
		GameAudio.PlayBigCash();
		if ( def.Rank == 1 )
			GrandmaReact.OnFirstHabitat();
		AchievementSystem.Unlock( "habitat" );
		Chaos.Add( 2f + def.Rank * 0.5f );
		Karma.Add( 3f + def.Rank * 0.5f ); // better homes = good-boy-ish
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool TryBuyMeth()
	{
		if ( !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( !Economy.TrySpend( MethCost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( MethCost )} for meth." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		_methBags++;
		LastMessage = $"Bought meth ×{_methBags} · Ð{Economy.FormatDoge( MethCost )}";
		GameAudio.PlayUi( GameAudio.Trade );
		Chaos.Add( 5f );
		JanitorPeep.On( "meth" );
		AchievementSystem.Unlock( "shady" );
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool TryBuyM80()
	{
		if ( !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( !Economy.TrySpend( M80Cost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( M80Cost )} for an M80." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		_m80Bags++;
		// First stick is a two-pack so shady unlock feels generous.
		if ( _m80Bags == 1 )
			_m80Bags = 2;
		LastMessage = $"Bought M80 ×{_m80Bags} · Ð{Economy.FormatDoge( M80Cost )}";
		GameAudio.PlayUi( GameAudio.Trade );
		Chaos.Add( 3f );
		AchievementSystem.Unlock( "shady" );
		StorySystem.Evaluate();
		SaveGame.TrySave();
		_version++;
		return true;
	}

	/// <summary>Spend M80 stock when detonating (1 per taped fish).</summary>
	public static bool TryConsumeM80( int count )
	{
		if ( count <= 0 )
			return true;
		if ( _m80Bags < count )
			return false;
		_m80Bags -= count;
		_version++;
		return true;
	}

	/// <summary>Fight loot / hush money / drawer swipe — no shop banner.</summary>
	public static void GrantM80( int count )
	{
		if ( count <= 0 )
			return;
		_m80Bags += count;
		_version++;
	}

	public static void GrantMeth( int count )
	{
		if ( count <= 0 )
			return;
		_methBags += count;
		_version++;
	}

	public static bool TryTakeMeth( int count = 1 )
	{
		if ( count <= 0 )
			return true;
		if ( _methBags < count )
			return false;
		_methBags -= count;
		_version++;
		return true;
	}

	public static bool TryFeedMeth()
	{
		if ( !CanFeedMeth() )
		{
			Fail( "No meth bags. Buy some in Shady." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		_methBags--;
		var n = TankSim.ApplyMethToActiveTank();
		GameAudio.PlayUi( n > 0 ? GameAudio.Confirm : GameAudio.Error );
		if ( n > 0 )
		{
			Karma.Add( -6f - n * 0.5f );
			var saved = 0;
			foreach ( var f in TankSim.Fish )
			{
				if ( f.OnMeth && f.IsWrongWater( TankSim.Water ) )
					saved++;
			}
			// Contain the thrash or the glass pays for it.
			MethRush.TryStart();
			AchievementSystem.Unlock( "meth" );
		}
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool CanBuyDecor( DecorDef def )
	{
		if ( def is null || !TankSim.IsActive )
			return false;
		if ( def.IsBackdrop )
		{
			var hung = TankSim.ActiveBackdropId;
			if ( def.Id == DecorDef.BackdropNone.Id )
				return !string.IsNullOrEmpty( hung );
			if ( string.Equals( hung, def.Id, StringComparison.Ordinal ) )
				return false;
			return Economy.Balance >= def.Cost;
		}
		if ( def.IsSubstrate )
		{
			var laid = TankSim.ActiveGravelId;
			if ( def.Id == DecorDef.GravelNone.Id )
				return !string.IsNullOrEmpty( laid );
			if ( string.Equals( laid, def.Id, StringComparison.Ordinal ) )
				return false;
			return Economy.Balance >= def.Cost;
		}
		return TankSim.DecorCount < TankSim.MaxDecor && Economy.Balance >= def.Cost;
	}

	public static bool TryBuyDecor( DecorDef def )
	{
		if ( def is null || !TankSim.IsActive )
		{
			Fail( "No tank open." );
			return false;
		}

		if ( def.IsBackdrop )
		{
			var hung = TankSim.ActiveBackdropId;
			if ( def.Id == DecorDef.BackdropNone.Id )
			{
				if ( string.IsNullOrEmpty( hung ) )
				{
					Fail( "Already plain glass." );
					return false;
				}
				TankSim.TrySetBackdrop( DecorDef.BackdropNone, announce: false );
				LastMessage = "Peeled the poster. Plain glass.";
				GameAudio.PlayUi( GameAudio.Upgrade );
				SaveGame.TrySave();
				_version++;
				return true;
			}

			if ( string.Equals( hung, def.Id, StringComparison.Ordinal ) )
			{
				Fail( $"{def.Name} is already hung." );
				return false;
			}

			if ( !Economy.TrySpend( def.Cost ) )
			{
				Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}

			if ( !TankSim.TrySetBackdrop( def, announce: false ) )
			{
				Economy.Add( def.Cost );
				Fail( "Could not hang backdrop." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}

			LastMessage = $"Hung {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
			GameAudio.PlayUi( GameAudio.Upgrade );
			Karma.Add( def.ChaosOnPlace > 0f ? -1f : 1.2f );
			SaveGame.TrySave();
			_version++;
			return true;
		}

		if ( def.IsSubstrate )
		{
			var laid = TankSim.ActiveGravelId;
			if ( def.Id == DecorDef.GravelNone.Id )
			{
				if ( string.IsNullOrEmpty( laid ) )
				{
					Fail( "Already lot dirt." );
					return false;
				}
				TankSim.TrySetGravel( DecorDef.GravelNone, announce: false );
				LastMessage = "Scooped back to lot dirt.";
				GameAudio.PlayUi( GameAudio.Upgrade );
				SaveGame.TrySave();
				_version++;
				return true;
			}

			if ( string.Equals( laid, def.Id, StringComparison.Ordinal ) )
			{
				Fail( $"{def.Name} is already down." );
				return false;
			}

			if ( !Economy.TrySpend( def.Cost ) )
			{
				Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}

			if ( !TankSim.TrySetGravel( def, announce: false ) )
			{
				Economy.Add( def.Cost );
				Fail( "Could not dump gravel." );
				GameAudio.PlayUi( GameAudio.Error );
				return false;
			}

			LastMessage = $"Dumped {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
			GameAudio.PlayUi( GameAudio.Upgrade );
			Karma.Add( def.ChaosOnPlace > 0f ? -0.4f : 0.6f );
			SaveGame.TrySave();
			_version++;
			return true;
		}

		if ( TankSim.DecorCount >= TankSim.MaxDecor )
		{
			Fail( $"Decor full ({TankSim.MaxDecor} max on this tank)." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		if ( !Economy.TrySpend( def.Cost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		// Shop owns the status line — no tank banner (avoids double toast + layout thrash).
		if ( !TankSim.TryPlaceDecor( def, announce: false ) )
		{
			Economy.Add( def.Cost );
			Fail( "Could not place decor." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		LastMessage = $"Placed {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
		GameAudio.PlayUi( GameAudio.Upgrade );
		Karma.Add( def.ChaosOnPlace > 0f ? -1f : 1.5f );
		SaveGame.TrySave();
		_version++;
		return true;
	}

	static void Fail( string msg )
	{
		LastMessage = msg;
		_version++;
	}

	/// <summary>Set shop banner from other systems (WMD etc.).</summary>
	public static void FailPublic( string msg ) => Fail( msg );

	public static void OkPublic( string msg )
	{
		LastMessage = msg ?? "";
		_version++;
	}
}