using Sandbox;
using System;

namespace GravCoin;

/// <summary>
/// Server-authoritative GravCoin market engine.
/// Note: market state is intentionally not persisted between server sessions.
/// </summary>
public sealed class MarketManager : Component
{
	public enum MarketPhase { Natural, Squeeze, Spring, Hook, Resistance, Exhaustion }

	private static MarketManager _instance;
	public static MarketManager Instance => _instance.IsValid() ? _instance : null;

	[Sync] public float CurrentPrice { get; private set; } = 50f;
	[Sync] public float OpenPrice { get; private set; } = 50f;
	[Sync] public float Change1h { get; private set; }
	[Sync] public float GlobalVolume { get; private set; }
	[Sync] public string ActiveEventName { get; private set; } = "";
	[Sync] public string ActiveEventDesc { get; private set; } = "";
	[Sync] public string ActiveEventColor { get; private set; } = "";
	[Sync] public string ActiveEventRarity { get; private set; } = "";
	[Sync] public float EventMultiplier { get; private set; } = 1f;
	[Sync] public float EventDrift { get; private set; }
	[Sync] public float EventTimeLeft { get; private set; }
	[Sync] public float InitialEventDuration { get; private set; }
	[Sync] public MarketPhase CurrentPhase { get; private set; } = MarketPhase.Natural;
	[Sync] public float PhaseTimeLeft { get; private set; }
	[Sync] public float PhaseDuration { get; private set; }
	[Sync] public NetList<MarketPhase> PhaseQueue { get; private set; } = new();
	[Sync] public float TaxPercent { get; private set; } = 4.0f;
	[Sync] public bool IsTutorial { get; private set; } = true;

	[Sync] public NetList<float> CandleData { get; private set; } = new();
	[Sync] public int CandleHeadIndex { get; private set; }
	[Sync] public NetList<float> FuturePrices { get; private set; } = new();

	private const int CandleCapacity = 14400; // 3600 candles x 4 values (OHLC)
	private const int LookaheadSize = 40;
	private const float StartPrice = 50f;
	private const float MinPrice = 0.05f;
	private static readonly string[] LegacyMarketFiles =
	{
		"grav_market.json",
		"gravcoin_market.json",
		"sandmoney/grav_market.json",
		"sandmoney/gravcoin_market.json"
	};

	private readonly Random _rng = new();
	private bool _wasProxy = true;
	private float _basePrice = StartPrice;
	private float _priceAt1h = StartPrice;
	private float _cOpen = StartPrice;
	private float _cHigh = StartPrice;
	private float _cLow = StartPrice;
	private TimeSince _lastTick;
	private TimeSince _lastHour;
	private TimeSince _tutorialTimer;
	private float _eventAnchorPrice = StartPrice;
	private int _eventWaveSeed;

	public int MaxLookahead => FuturePrices.Count;

	public float GetFuturePrice( int secondsAhead )
	{
		if ( FuturePrices.Count <= 0 )
			return CurrentPrice;

		int idx = Math.Clamp( secondsAhead - 1, 0, FuturePrices.Count - 1 );
		return FuturePrices[idx];
	}

	protected override void OnStart()
	{
		_instance = this;
		_wasProxy = IsProxy;
		if ( IsProxy ) return;

		DeleteLegacyMarketPersistence();
		InitializeFreshMarket();
	}

	protected override void OnFixedUpdate()
	{
		if ( _wasProxy && !IsProxy )
			RecoverFromHostMigration();
		_wasProxy = IsProxy;

		if ( EventTimeLeft > 0f )
		{
			EventTimeLeft -= Time.Delta;
			if ( EventTimeLeft <= 0f )
			{
				ClearEvent();
				ForceNaturalPhase();
			}
		}

		if ( IsProxy ) return;

		if ( IsTutorial && _tutorialTimer > 300f )
			IsTutorial = false;

		UpdatePhaseCycle();

		if ( _lastTick > 1f )
		{
			_lastTick = 0f;
			TickMarket();
		}

		if ( _lastHour > 3600f )
		{
			_lastHour = 0f;
			_priceAt1h = CurrentPrice;
		}
	}

	private void InitializeFreshMarket()
	{
		_basePrice = StartPrice;
		CurrentPrice = StartPrice;
		OpenPrice = StartPrice;
		_priceAt1h = StartPrice;
		GlobalVolume = 0f;
		IsTutorial = true;
		_tutorialTimer = 0f;
		_cOpen = _cHigh = _cLow = CurrentPrice;
		CandleData.Clear();
		CandleHeadIndex = 0;
		FuturePrices.Clear();
		ClearEvent();
		ResetPhaseCycle();

		// Warm up market history.
		for ( int i = 0; i < 2400; i++ )
		{
			float open = _basePrice;
			float close = SimulateNextPrice( _basePrice, 0f );
			AppendTick( open, close );
			_basePrice = close;
		}

		CurrentPrice = MathF.Round( _basePrice, 2 );
		_priceAt1h = GetCloseFromHistory( 3600 );
		OpenPrice = GetCloseFromHistory( 60 );
		RefillLookahead();
		Network.Refresh();
	}

	private void DeleteLegacyMarketPersistence()
	{
		var fs = FileSystem.Data;
		if ( fs is null ) return;

		foreach ( var file in LegacyMarketFiles )
		{
			try
			{
				if ( fs.FileExists( file ) )
					fs.DeleteFile( file );
			}
			catch
			{
			}
		}
	}

	private void TickMarket()
	{
		float open = CurrentPrice > 0f ? CurrentPrice : _basePrice;
		float next;
		if ( FuturePrices.Count > 0 )
		{
			next = FuturePrices[0];
			FuturePrices.RemoveAt( 0 );
		}
		else
		{
			next = SimulateNextPrice( _basePrice, EventDrift );
		}

		_basePrice = Math.Max( MinPrice, next );
		float close = MathF.Round( _basePrice, 2 );
		Change1h = _priceAt1h > 0 ? ((close - _priceAt1h) / _priceAt1h) * 100f : 0f;

		AppendTick( open, close );
		OpenPrice = GetCloseFromHistory( 60 );
		RefillLookahead();
	}

	private void UpdatePhaseCycle()
	{
		// Hook/Exhaustion are reserved for high-rarity events only.
		if ( !CanUseExtremePhases() && (CurrentPhase == MarketPhase.Hook || CurrentPhase == MarketPhase.Exhaustion) )
		{
			AdvancePhase();
			return;
		}

		if ( PhaseTimeLeft > 0f )
			PhaseTimeLeft = MathF.Max( 0f, PhaseTimeLeft - Time.Delta );

		if ( PhaseTimeLeft > 0f )
			return;

		AdvancePhase();
	}

	private void ForceNaturalPhase()
	{
		CurrentPhase = MarketPhase.Natural;
		PhaseDuration = NextRange( 180f, 300f );
		PhaseTimeLeft = PhaseDuration;
		PhaseQueue.Clear();
		FuturePrices.Clear();
		RefillLookahead();
	}

	private void ResetPhaseCycle()
	{
		CurrentPhase = RollNextPhase( allowExtremePhases: CanUseExtremePhases() );
		PhaseDuration = RollPhaseDuration( CurrentPhase );
		PhaseTimeLeft = PhaseDuration;
		
	}

	private void AdvancePhase()
	{
		if ( PhaseQueue.Count > 0 )
		{
			CurrentPhase = PhaseQueue[0];
			PhaseQueue.RemoveAt( 0 );
			
			// During an event, we want the phase to last exactly its allocated share of the total duration.
			// But if it's the last phase, we give it the remaining event time.
			if ( EventTimeLeft > 0 )
			{
				float div = Math.Max( 1, PhaseQueue.Count + 1 );
				PhaseDuration = EventTimeLeft / div;
			}
			else
			{
				PhaseDuration = RollPhaseDuration( CurrentPhase );
			}
		}
		else
		{
			CurrentPhase = RollNextPhase( allowExtremePhases: CanUseExtremePhases() );
			PhaseDuration = RollPhaseDuration( CurrentPhase );
		}

		PhaseTimeLeft = PhaseDuration;

		// Refresh lookahead immediately so UI predictions follow the new regime.
		FuturePrices.Clear();
		RefillLookahead();
	}

	private float RollPhaseDuration( MarketPhase phase )
	{
		return phase switch
		{
			MarketPhase.Natural => NextRange( 90f, 170f ),    // Slightly longer neutral/accumulation windows
			MarketPhase.Squeeze => NextRange( 90f, 165f ),    // Longer squeezes so pressure can really build/release
			MarketPhase.Spring => NextRange( 45f, 90f ),      // 45s - 1.5 min
			MarketPhase.Hook => NextRange( 70f, 140f ),
			MarketPhase.Resistance => NextRange( 90f, 150f ), // More frequent/longer accumulation stalls
			MarketPhase.Exhaustion => NextRange( 50f, 100f ),
			_ => NextRange( 100f, 180f )
		};
	}

	private float NextRange( float min, float max )
	{
		return min + ((float)_rng.NextDouble() * (max - min));
	}

	private MarketPhase RollNextPhase( bool allowExtremePhases )
	{
		int roll = _rng.Next( 100 );

		if ( allowExtremePhases )
		{
			// More accumulation (Natural/Resistance) before explosive phases.
			if ( roll < 22 ) return MarketPhase.Natural;      // 22%
			if ( roll < 62 ) return MarketPhase.Squeeze;      // 40% (stronger squeeze presence)
			if ( roll < 68 ) return MarketPhase.Spring;       // 6%
			if ( roll < 74 ) return MarketPhase.Hook;         // 12%
			if ( roll < 94 ) return MarketPhase.Resistance;   // 20%
			return MarketPhase.Exhaustion;                    // 6%
		}

		// Normal market: boost accumulation probability.
		if ( roll < 24 ) return MarketPhase.Natural;          // 24%
		if ( roll < 66 ) return MarketPhase.Squeeze;          // 42% (stronger squeeze presence)
		if ( roll < 74 ) return MarketPhase.Spring;           // 8%
		return MarketPhase.Resistance;                        // 26%
	}

	private bool CanUseExtremePhases()
	{
		if ( EventTimeLeft <= 0f )
			return false;

		return IsRarityAboveRare( ActiveEventRarity );
	}

	private static bool IsRarityAboveRare( string rarity )
	{
		if ( string.IsNullOrWhiteSpace( rarity ) )
			return false;

		string r = rarity.Trim().ToLowerInvariant();
		return r.Contains( "epic" )
			|| r.Contains( "legendary" )
			|| r.Contains( "ultra" )
			|| r.Contains( "epique" )
			|| r.Contains( "épique" )
			|| r.Contains( "legendaire" )
			|| r.Contains( "légendaire" );
	}

	private void AppendTick( float open, float close )
	{
		_cOpen = open;
		_cHigh = MathF.Max( open, close );
		_cLow = MathF.Min( open, close );
		CurrentPrice = close;

		if ( CandleData.Count < CandleCapacity )
		{
			CandleData.Add( _cOpen );
			CandleData.Add( _cHigh );
			CandleData.Add( _cLow );
			CandleData.Add( close );
		}
		else
		{
			CandleData[CandleHeadIndex] = _cOpen;
			CandleData[CandleHeadIndex + 1] = _cHigh;
			CandleData[CandleHeadIndex + 2] = _cLow;
			CandleData[CandleHeadIndex + 3] = close;
			CandleHeadIndex = (CandleHeadIndex + 4) % CandleCapacity;
		}
	}

	private float SimulateNextPrice( float fromPrice, float drift )
	{
		// Phase-based market regimes layered on top of event-driven volatility.
		float phaseVolatilityMult = 1f;
		float phaseDriftBias = 0f;
		float phaseMeanRevertMult = 1f;
		float phaseShockChanceMult = 1f;
		float phaseShockPowerMult = 1f;

		// Determine the "Natural" direction of the market for this moment
		float currentTrendSign = drift >= 0 ? 1f : -1f;
		if ( string.IsNullOrEmpty(ActiveEventName) ) currentTrendSign = -0.5f; // Slight downward bias in normal times

		switch ( CurrentPhase )
		{
			case MarketPhase.Squeeze:
				// Stronger breakouts in both directions (up and down).
				phaseVolatilityMult = 2.30f;
				phaseDriftBias = 0.00120f * currentTrendSign;
				phaseMeanRevertMult = 0.50f;
				phaseShockChanceMult = 2.40f;
				phaseShockPowerMult = 2.35f;
				break;
			case MarketPhase.Spring:
				phaseVolatilityMult = 1.55f;
				phaseDriftBias = 0.00080f;
				phaseMeanRevertMult = 0.60f;
				phaseShockChanceMult = 1.40f;
				phaseShockPowerMult = 1.50f;
				break;
			case MarketPhase.Hook:
				// Hook follows the trend but with a "trap" feel
				phaseVolatilityMult = 1.40f;
				phaseDriftBias = -0.00035f * currentTrendSign; 
				phaseMeanRevertMult = 0.90f;
				phaseShockChanceMult = 1.30f;
				phaseShockPowerMult = 1.30f;
				break;
			case MarketPhase.Resistance:
				phaseVolatilityMult = 1.15f;
				phaseDriftBias = -0.00050f;
				phaseMeanRevertMult = 1.25f;
				phaseShockChanceMult = 0.85f;
				phaseShockPowerMult = 0.90f;
				break;
			case MarketPhase.Exhaustion:
				// Exhaustion is the "Climax": if Bull -> Hyper Moon, if Bear -> Total Crash
				phaseVolatilityMult = 1.60f;
				phaseDriftBias = 0.00120f * currentTrendSign; 
				phaseMeanRevertMult = 1.50f;
				phaseShockChanceMult = 2.00f;
				phaseShockPowerMult = 1.80f;
				break;
		}

		// Progressive Mean-reverting logic
		float meanTarget = 100f; 
		float dist = fromPrice - meanTarget;
		float attractFactor = 0.0003f; // Baseline very light (reduced pull toward 100)

		if ( dist > 0 ) // Upper range [100 -> 1000+]
		{
			// Progressive pull that ramps up as we approach 1000
			float ratio = dist / 900f; // 1.0 at 1000
			attractFactor += MathF.Pow( Math.Max(0, ratio), 2.5f ) * 0.022f;
		}
		else if ( dist < 0 ) // Lower range [1 -> 100]
		{
			// Ramps up as we approach 1
			float ratio = Math.Abs(dist) / 99f; // 1.0 at 1
			attractFactor += MathF.Pow( Math.Max(0, ratio), 3.0f ) * 0.032f;
		}

		float meanRevert = (meanTarget - fromPrice) / meanTarget * attractFactor * phaseMeanRevertMult;
		
		// Slightly reduced base volatility from 0.0062 to 0.0052 for a calmer feel
		float volatility = 0.0052f * phaseVolatilityMult * MathF.Max( 0.25f, EventMultiplier );
		
		// Use a more chaotic noise distribution: mix of uniform and occasional spikes
		float noise = ((float)_rng.NextDouble() * 2f - 1f) * volatility;

		// Reduced shock chance from 2% to 1.5% to make it less jittery in normal times
		float shock = 0f;
		if ( _rng.NextDouble() < 0.015f * phaseShockChanceMult )
		{
			// Shocks now favor the current trend but can also be "counter-shocks"
			float shockDir = (_rng.NextDouble() < 0.70 ? currentTrendSign : -currentTrendSign);
			shock = shockDir * (float)_rng.NextDouble() * 0.018f * phaseShockPowerMult;
		}

		float eventTrajectoryBias = ComputeEventTrajectoryBias( fromPrice, volatility );
		float next = fromPrice * (1f + noise + drift + phaseDriftBias + meanRevert + shock + eventTrajectoryBias);
		return MathF.Max( MinPrice, next );
	}

	private float ComputeEventTrajectoryBias( float fromPrice, float volatility )
	{
		// Keep events less "straight line" for BOTH directions:
		// bullish -> fake pullbacks, bearish -> fake bounces, with strong final directional leg.
		if ( EventTimeLeft <= 0f || InitialEventDuration <= 0f || MathF.Abs( EventDrift ) < 0.00001f )
			return 0f;

		float progress = 1f - Math.Clamp( EventTimeLeft / InitialEventDuration, 0f, 1f ); // 0 -> 1 over event lifetime
		float trendSign = EventDrift >= 0f ? 1f : -1f;
		float midFocus = 1f - MathF.Abs( progress - 0.5f ) * 2f; // strongest in the middle
		float phase = progress * MathF.PI * 6.2f + _eventWaveSeed * 0.37f;

		// Repeated counter-trend jabs:
		// bull => downward fakes, bear => upward fakes.
		float counterPulse = MathF.Max( 0f, MathF.Sin( phase ) );
		counterPulse *= counterPulse;
		float fakeMoveBias = (-trendSign) * counterPulse
			* (0.0012f + volatility * 0.42f)
			* (0.35f + 0.65f * MathF.Max( 0f, midFocus ))
			* MathF.Max( 0.55f, EventMultiplier );

		// Early event compression: a bit more accumulation/stagnation before expansion.
		float earlyCompression = MathF.Max( 0f, (0.35f - progress) / 0.35f );
		float compressionBias = -EventDrift * 0.55f * earlyCompression;

		// Small oscillation to avoid robotic motion.
		float microChop = MathF.Sin( progress * MathF.PI * 12f + _eventWaveSeed * 0.19f )
			* (0.00022f + volatility * 0.07f);

		// Catch-up force to preserve a strong final upside despite fake pullbacks.
		float elapsedSeconds = progress * InitialEventDuration;
		float targetGrowthPerSec = MathF.Max( 0.00005f, 1f + EventDrift );
		float targetPrice = _eventAnchorPrice * MathF.Pow( targetGrowthPerSec, elapsedSeconds );
		float deviation = targetPrice > MinPrice ? (fromPrice - targetPrice) / targetPrice : 0f;
		float catchUpBias = Math.Clamp( -deviation * 0.22f, -0.0025f, 0.0036f );

		// Final leg acceleration to keep the ending move impactful in the event direction.
		float lateRamp = MathF.Max( 0f, (progress - 0.62f) / 0.38f );
		float closingDrive = trendSign * lateRamp * lateRamp * 0.0009f;

		return fakeMoveBias + compressionBias + microChop + catchUpBias + closingDrive;
	}

	private void RefillLookahead()
	{
		float sim = FuturePrices.Count > 0 ? FuturePrices[FuturePrices.Count - 1] : _basePrice;
		while ( FuturePrices.Count < LookaheadSize )
		{
			sim = SimulateNextPrice( sim, EventDrift );
			FuturePrices.Add( sim );
		}
	}

	private float GetCloseFromHistory( int secondsAgo )
	{
		int count = CandleData.Count;
		if ( count < 4 ) return CurrentPrice;

		int candles = count / 4;
		int clamped = Math.Clamp( secondsAgo, 0, candles - 1 );
		int closeIndex = (CandleHeadIndex + count - 1 - (clamped * 4)) % count;
		if ( closeIndex < 0 ) closeIndex += count;

		return MathF.Max( MinPrice, CandleData[closeIndex] );
	}

	private void ClearEvent()
	{
		ActiveEventName = "";
		ActiveEventDesc = "";
		ActiveEventColor = "";
		ActiveEventRarity = "";
		EventMultiplier = 1f;
		EventDrift = 0f;
		EventTimeLeft = 0f;
		InitialEventDuration = 0f;
		TaxPercent = 4.0f;
		_eventAnchorPrice = MathF.Max( MinPrice, _basePrice );
		_eventWaveSeed = 0;
	}

	public void RecoverFromHostMigration()
	{
		if ( IsProxy ) return;

		while ( CandleData.Count > 0 && CandleData.Count % 4 != 0 )
			CandleData.RemoveAt( CandleData.Count - 1 );

		if ( CandleData.Count >= 4 )
		{
			CandleHeadIndex = Math.Clamp( CandleHeadIndex, 0, Math.Max( 0, CandleData.Count - 4 ) );
			CandleHeadIndex -= CandleHeadIndex % 4;
			float latestClose = GetCloseFromHistory( 0 );
			_basePrice = latestClose;
			CurrentPrice = latestClose;
			_priceAt1h = GetCloseFromHistory( 3600 );
			OpenPrice = GetCloseFromHistory( 60 );
		}
		else
		{
			InitializeFreshMarket();
			return;
		}

		_cOpen = _cHigh = _cLow = CurrentPrice;
		if ( PhaseDuration <= 0f || PhaseTimeLeft <= 0f )
		{
			PhaseDuration = RollPhaseDuration( CurrentPhase );
			PhaseTimeLeft = PhaseDuration;
		}
		FuturePrices.Clear();
		RefillLookahead();
		_lastTick = 0f;
		_lastHour = 0f;
		Network.Refresh();
	}

	[Rpc.Owner]
	public void ApplyEvent( string name, float multiplier, float duration, float taxBonus = 0, float drift = 0f, string desc = "", string color = "", string rarity = "", float shock = 0f )
	{
		ActiveEventName = name ?? "";
		ActiveEventDesc = desc ?? "";
		ActiveEventColor = color ?? "";
		ActiveEventRarity = rarity ?? "";
		float scaledMultiplier = 1f + (MathF.Max( 0.1f, multiplier ) - 1f) * 0.93f;
		EventMultiplier = MathF.Max( 0.1f, scaledMultiplier );
		EventDrift = drift * 0.95f;
		EventTimeLeft = MathF.Max( 0f, duration );
		InitialEventDuration = EventTimeLeft;
		TaxPercent = MathF.Max( 0.1f, 4.0f + taxBonus );
		float appliedShock = shock * 0.91f;

		if ( appliedShock != 0f )
		{
			_basePrice = MathF.Max( MinPrice, _basePrice * (1f + appliedShock) );
			CurrentPrice = MathF.Round( _basePrice, 2 );
		}

		_eventAnchorPrice = MathF.Max( MinPrice, _basePrice );
		_eventWaveSeed = _rng.Next( 1, 10_000 );

		// Generate random phase sequence
		GenerateEventPhaseQueue( rarity );

		AdvancePhase();
	}

	private void GenerateEventPhaseQueue( string rarity )
	{
		PhaseQueue.Clear();
		bool allowExtreme = IsRarityAboveRare( rarity );

		// Build a trajectory with accumulation before expansion, instead of full random shuffle.
		PhaseQueue.Add( _rng.NextDouble() < 0.70 ? MarketPhase.Resistance : MarketPhase.Natural );

		// Frequently add a second accumulation/consolidation segment.
		if ( _rng.NextDouble() < 0.55 )
			PhaseQueue.Add( _rng.NextDouble() < 0.65 ? MarketPhase.Natural : MarketPhase.Resistance );

		PhaseQueue.Add( MarketPhase.Squeeze );

		// Add an extra squeeze sometimes to increase pressure/release feeling.
		if ( _rng.NextDouble() < 0.65 )
			PhaseQueue.Add( MarketPhase.Squeeze );

		// High-rarity events can chain one more squeeze for a stronger "coiled spring" behavior.
		if ( allowExtreme && _rng.NextDouble() < 0.35 )
			PhaseQueue.Add( MarketPhase.Squeeze );

		// Optional trap phase on higher rarities.
		if ( allowExtreme && _rng.NextDouble() < 0.45 )
			PhaseQueue.Add( MarketPhase.Hook );

		// Main expansion.
		PhaseQueue.Add( MarketPhase.Spring );

		// End with either final climax or one more consolidation/re-accumulation.
		if ( allowExtreme && _rng.NextDouble() < 0.55 )
		{
			PhaseQueue.Add( MarketPhase.Exhaustion );
		}
		else if ( _rng.NextDouble() < 0.40 )
		{
			PhaseQueue.Add( MarketPhase.Spring );
		}
		else
		{
			PhaseQueue.Add( MarketPhase.Resistance );
		}
	}

	[Rpc.Owner]
	public void AddGlobalVolume( float amount )
	{
		if ( amount <= 0f || float.IsNaN( amount ) || float.IsInfinity( amount ) )
			return;

		GlobalVolume += amount;
	}
}
