//+------------------------------------------------------------------+
//|                                                FrictionGuard.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_FRICTION_GUARD_MQH
#define SCALPER_GUARD_FRICTION_GUARD_MQH

#include "CoreDefines.mqh"
#include "CircularTickBuffer.mqh"
#include "TickVelocityTracker.mqh"
#include "SpreadAnalyzer.mqh"
#include "FrictionClassifier.mqh"

//--- Telemetry dashboard integration with standalone fallback stub
#ifndef DISABLE_TELEMETRY_DASHBOARD
#include "TelemetryDashboard.mqh"
#else
//--- Lightweight headless stub dashboard for standalone or test execution
class CTelemetryDashboard
  {
public:
   bool              Initialize(const string, const bool, const string, const string, const int, const int, const int, const int) { return true; }
   void              Render(const SFrictionMetrics&) {}
   void              Clear(void) {}
   void              OnChartEvent(const int, const long&, const double&, const string&) {}
  };
#endif

//+------------------------------------------------------------------+
//| Class: CFrictionGuard                                            |
//| Master Pre-Trade Friction Gatekeeper & Analytical Facade         |
//+------------------------------------------------------------------+
class CFrictionGuard
  {
private:
   string                mSymbol;                 // Monitored symbol name
   double                mPoint;                  // Symbol point size
   int                   mDigits;                 // Symbol digits precision
   int                   mMinWarmupTicks;         // Minimum samples needed before gating
   double                mSlippageEstimatePoints; // Base slippage points configured
   bool                  mIsInitialized;          // Status flag

   SSymbolProfile        mProfile;            // Auto-detected symbol profile metadata
   CCircularTickBuffer  *mBuffer;             // Pre-allocated ring buffer
   CTickVelocityTracker *mVelocityTracker;    // Price velocity and momentum tracker
   CSpreadAnalyzer      *mSpreadAnalyzer;     // Welford variance spread profiler
   CFrictionClassifier  *mClassifier;         // Friction ratio and regime classifier
   CTelemetryDashboard  *mDashboard;          // On-chart visual telemetry HUD

   SFrictionMetrics      mCurrentMetrics;     // Cached telemetry and performance snapshot

   //+------------------------------------------------------------------+
   //| Safely releases all heap-allocated objects and nulls pointers    |
   //+------------------------------------------------------------------+
   void              CleanMemory(void)
     {
      if(CheckPointer(mBuffer) == POINTER_DYNAMIC)
        {
         delete mBuffer;
         mBuffer = NULL;
        }
      if(CheckPointer(mVelocityTracker) == POINTER_DYNAMIC)
        {
         delete mVelocityTracker;
         mVelocityTracker = NULL;
        }
      if(CheckPointer(mSpreadAnalyzer) == POINTER_DYNAMIC)
        {
         delete mSpreadAnalyzer;
         mSpreadAnalyzer = NULL;
        }
      if(CheckPointer(mClassifier) == POINTER_DYNAMIC)
        {
         delete mClassifier;
         mClassifier = NULL;
        }
      if(CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         delete mDashboard;
         mDashboard = NULL;
        }
      mIsInitialized = false;
     }

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CFrictionGuard(void)
     {
      mSymbol = "";
      mPoint = 0.00001;
      mDigits = 5;
      mMinWarmupTicks = DEFAULT_WARMUP_TICKS;
      mSlippageEstimatePoints = DEFAULT_SLIPPAGE_ESTIMATE_PTS;
      mIsInitialized = false;

      ZeroMemory(mProfile);
      mBuffer = NULL;
      mVelocityTracker = NULL;
      mSpreadAnalyzer = NULL;
      mClassifier = NULL;
      mDashboard = NULL;

      ZeroMemory(mCurrentMetrics);
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CFrictionGuard(void)
     {
      CleanMemory();
     }

   //+------------------------------------------------------------------+
   //| Initializes the master controller and all sub-components         |
   //+------------------------------------------------------------------+
   bool              Initialize(const string symbol,
                                const int    bufferCapacity           = DEFAULT_BUFFER_CAPACITY,
                                const double velocityWindowSeconds    = DEFAULT_VELOCITY_WINDOW_SEC,
                                const double minVelocityPointsPerSec  = DEFAULT_MIN_VELOCITY_PTS_SEC,
                                const double spreadZScoreLimit        = DEFAULT_SPREAD_ZSCORE_LIMIT,
                                const double hardSpreadLimitPoints    = DEFAULT_HARD_SPREAD_MAX_PTS,
                                const double maxFrictionRatio         = DEFAULT_MAX_FRICTION_RATIO,
                                const double expectedHoldingSeconds   = DEFAULT_EXPECTED_HOLDING_SEC,
                                const double minExpectedMovePoints    = DEFAULT_MIN_EXPECTED_MOVE_PTS,
                                const double slippageEstimatePoints   = DEFAULT_SLIPPAGE_ESTIMATE_PTS,
                                const bool   enableDashboard          = true,
                                const string dashboardTitle           = "INTELLIGENT FRICTION GUARD",
                                const double strategyTakeProfitPoints = 0.0,
                                const double commissionEquivalentPoints = 0.0,
                                const double commissionPerLot         = 0.0,
                                const int    warmupTicks              = DEFAULT_WARMUP_TICKS,
                                const int    dashboardX               = 15,
                                const int    dashboardY               = 20,
                                const int    dashboardWidth           = 540,
                                const int    dashboardFontSize        = 8)
     {
      //--- Clean any previously active instances
      CleanMemory();

      mSymbol = (symbol == "" || symbol == NULL) ? _Symbol : symbol;
      mPoint = SymbolInfoDouble(mSymbol, SYMBOL_POINT);
      mDigits = (int)SymbolInfoInteger(mSymbol, SYMBOL_DIGITS);
      mSlippageEstimatePoints = slippageEstimatePoints;

      if(mPoint <= 0.0)
        {
         mPoint = (_Point > 0.0) ? _Point : 0.00001;
        }

      //--- Auto-detect symbol profile metadata
      mProfile.mSymbol = mSymbol;
      mProfile.mDigits = mDigits;
      mProfile.mPoint = mPoint;
      mProfile.mTickSize = SymbolInfoDouble(mSymbol, SYMBOL_TRADE_TICK_SIZE);
      mProfile.mTickValue = SymbolInfoDouble(mSymbol, SYMBOL_TRADE_TICK_VALUE);
      mProfile.mSpreadType = (SymbolInfoInteger(mSymbol, SYMBOL_SPREAD_FLOAT) != 0) ? SPREAD_TYPE_FLOATING : SPREAD_TYPE_FIXED;
      mProfile.mStopsLevel = (int)SymbolInfoInteger(mSymbol, SYMBOL_TRADE_STOPS_LEVEL);
      mProfile.mFreezeLevel = (int)SymbolInfoInteger(mSymbol, SYMBOL_TRADE_FREEZE_LEVEL);

      MqlTick curTick;
      if(SymbolInfoTick(mSymbol, curTick) && mPoint > 0.0)
        {
         mProfile.mTypicalSpreadPts = (curTick.ask - curTick.bid) / mPoint;
        }
      else
        {
         mProfile.mTypicalSpreadPts = (double)SymbolInfoInteger(mSymbol, SYMBOL_SPREAD);
        }
      mProfile.mIsZeroSpreadAccount = (mProfile.mTypicalSpreadPts <= 2.0);
      mProfile.mCommissionPerLot = commissionPerLot;

      //--- Calculate commission equivalent points from per-lot value if points not explicitly supplied
      if(commissionEquivalentPoints > 0.0)
        {
         mProfile.mCommissionPoints = commissionEquivalentPoints;
        }
      else
         if(commissionPerLot > 0.0 && mProfile.mTickValue > 0.0 && mProfile.mTickSize > 0.0)
           {
            double pointsPerUnit = mPoint / mProfile.mTickSize;
            double pointValue = mProfile.mTickValue * pointsPerUnit;
            if(pointValue > 0.0)
              {
               mProfile.mCommissionPoints = commissionPerLot / pointValue;
              }
            else
              {
               mProfile.mCommissionPoints = 0.0;
              }
           }
         else
           {
            mProfile.mCommissionPoints = 0.0;
           }

      //--- 1. Allocate and initialize Circular Buffer
      mBuffer = new CCircularTickBuffer();
      if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.Initialize(bufferCapacity))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CCircularTickBuffer.");
         CleanMemory();
         return false;
        }

      //--- 2. Allocate and initialize Velocity Tracker
      mVelocityTracker = new CTickVelocityTracker();
      if(CheckPointer(mVelocityTracker) == POINTER_INVALID ||
         !mVelocityTracker.Initialize(mPoint, velocityWindowSeconds, minVelocityPointsPerSec))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CTickVelocityTracker.");
         CleanMemory();
         return false;
        }

      //--- 3. Allocate and initialize Spread Analyzer
      mSpreadAnalyzer = new CSpreadAnalyzer();
      if(CheckPointer(mSpreadAnalyzer) == POINTER_INVALID ||
         !mSpreadAnalyzer.Initialize(spreadZScoreLimit, hardSpreadLimitPoints, 0.1, warmupTicks))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CSpreadAnalyzer.");
         CleanMemory();
         return false;
        }

      //--- 4. Allocate and initialize Friction Classifier
      mClassifier = new CFrictionClassifier();
      if(CheckPointer(mClassifier) == POINTER_INVALID ||
         !mClassifier.Initialize(maxFrictionRatio, expectedHoldingSeconds, minExpectedMovePoints, slippageEstimatePoints, minVelocityPointsPerSec, mProfile.mCommissionPoints, spreadZScoreLimit))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CFrictionClassifier.");
         CleanMemory();
         return false;
        }

      //--- 5. Validate parameter scaling against symbol digits and strategy targets
      string scaleWarning = "";
      bool isScaleValid = mClassifier.ValidateScale(mProfile, strategyTakeProfitPoints, minVelocityPointsPerSec, hardSpreadLimitPoints, scaleWarning);

      //--- 6. Allocate and initialize Telemetry Dashboard (if enabled)
      if(enableDashboard)
        {
         mDashboard = new CTelemetryDashboard();
         if(CheckPointer(mDashboard) == POINTER_INVALID ||
            !mDashboard.Initialize(mSymbol, true, dashboardTitle, "FrictionHUD_", dashboardX, dashboardY, dashboardWidth, dashboardFontSize))
           {
            Print("[-] CFrictionGuard: Failed to allocate/initialize CTelemetryDashboard.");
            CleanMemory();
            return false;
           }
        }

      mMinWarmupTicks = (warmupTicks > 1) ? warmupTicks : DEFAULT_WARMUP_TICKS;
      if(bufferCapacity < mMinWarmupTicks)
        {
         mMinWarmupTicks = bufferCapacity;
        }

      ZeroMemory(mCurrentMetrics);
      mCurrentMetrics.mDigits = mDigits;
      mCurrentMetrics.mPoint = mPoint;
      mCurrentMetrics.mSpreadType = mProfile.mSpreadType;
      mCurrentMetrics.mCommissionPoints = mProfile.mCommissionPoints;
      mCurrentMetrics.mIsScaleValid = isScaleValid;
      mCurrentMetrics.mScaleWarningMessage = scaleWarning;

      if(!isScaleValid)
        {
         PrintFormat("[!] CFrictionGuard WARNING on %s: %s (Trading PAUSED)", mSymbol, scaleWarning);
         mCurrentMetrics.mRegime = REGIME_FRICTION_LOCKED;
         mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
         mCurrentMetrics.mIsSafeToTrade = false;
        }
      else
        {
         mCurrentMetrics.mRegime = REGIME_OPTIMAL;
         mCurrentMetrics.mReason = REASON_NONE;
         mCurrentMetrics.mIsSafeToTrade = true;
        }

      mIsInitialized = true;
      return true;
     }

   //+------------------------------------------------------------------+
   //| Ingests raw market tick into analytical pipeline                 |
   //+------------------------------------------------------------------+
   void              ProcessTick(const MqlTick &tick)
     {
      if(!mIsInitialized || CheckPointer(mBuffer) == POINTER_INVALID)
        {
         return;
        }

      //--- Pre-ingestion quote sanity validation: reject corrupted or crossed quotes
      if(tick.bid <= 0.0 || tick.ask <= 0.0 || tick.ask < tick.bid)
        {
         mCurrentMetrics.mReason = REASON_INVALID_TICK;
         mCurrentMetrics.mIsSafeToTrade = false;
         return;
        }

      //--- Pack raw tick into STickSample
      STickSample sample;
      sample.mTimestampMs = (tick.time_msc > 0) ? (ulong)tick.time_msc : ((ulong)tick.time * 1000);
      sample.mBid = tick.bid;
      sample.mAsk = tick.ask;
      sample.mMidPrice = (tick.bid + tick.ask) / 2.0;
      sample.mSpreadPoints = (mPoint > 0.0) ? (tick.ask - tick.bid) / mPoint : 0.0;
      sample.mVolume = (tick.volume_real > 0.0) ? tick.volume_real : (double)tick.volume;

      //--- 1. Evaluate spread anomaly BEFORE updating distribution so outlier tick does not pollute baseline
      double zScore = 0.0;
      bool isSpike = false;
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         isSpike = mSpreadAnalyzer.IsSpreadSpike(sample.mSpreadPoints, zScore);
        }

      //--- 2. Push sample into ring buffer
      mBuffer.Push(sample);

      //--- 3. Update Welford / EWMA spread distribution
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         mSpreadAnalyzer.UpdateDistribution(sample.mSpreadPoints);
        }

      //--- 4. Compute real-time kinetic velocity
      double absVelocity = 0.0;
      double signedVelocity = 0.0;
      if(CheckPointer(mVelocityTracker) != POINTER_INVALID)
        {
         mVelocityTracker.ComputeVelocity(mBuffer, absVelocity, signedVelocity);
        }

      //--- 5. Update cached metrics
      mCurrentMetrics.mCurrentSpreadPoints = sample.mSpreadPoints;
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         mCurrentMetrics.mMeanSpreadPoints = mSpreadAnalyzer.GetMean();
         mCurrentMetrics.mStdDevSpreadPoints = mSpreadAnalyzer.GetStdDev();
         mCurrentMetrics.mSpreadZScore = zScore;
         mCurrentMetrics.mIsZeroSpreadAccount = (mProfile.mIsZeroSpreadAccount || mSpreadAnalyzer.IsZeroSpreadProfile());
        }
      mCurrentMetrics.mTickVelocityPointsPerSec = absVelocity;
      mCurrentMetrics.mSignedVelocity = signedVelocity;
      if(CheckPointer(mVelocityTracker) != POINTER_INVALID)
        {
         mCurrentMetrics.mTickFrequency = mVelocityTracker.GetLastTickFrequency();
        }
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         mCurrentMetrics.mCommissionPoints = mClassifier.GetCommissionEquivalentPoints();
        }
      mCurrentMetrics.mEffectiveFrictionPoints = sample.mSpreadPoints + mCurrentMetrics.mCommissionPoints + mSlippageEstimatePoints;

      //--- 6. Compute friction ratio and test regime
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         ENUM_REJECTION_REASON reason = REASON_NONE;
         double frictionRatio = 0.0;

         ENUM_MARKET_REGIME regime = mClassifier.ClassifyRegime(
                                        sample.mSpreadPoints,
                                        zScore,
                                        absVelocity,
                                        isSpike,
                                        reason,
                                        frictionRatio
                                     );

         mCurrentMetrics.mRegime = regime;
         mCurrentMetrics.mReason = reason;
         mCurrentMetrics.mFrictionRatio = frictionRatio;
         mCurrentMetrics.mIsSafeToTrade = (regime == REGIME_OPTIMAL || regime == REGIME_MODERATE);

         //--- 6. If ring buffer has not accumulated warmup samples, mark unready
         if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.IsReady(mMinWarmupTicks))
           {
            mCurrentMetrics.mReason = REASON_BUFFER_NOT_READY;
            mCurrentMetrics.mIsSafeToTrade = false;
           }

         //--- 7. If input scaling is invalid, permanently lock out trading until parameters are corrected
         if(!mCurrentMetrics.mIsScaleValid)
           {
            mCurrentMetrics.mRegime = REGIME_FRICTION_LOCKED;
            mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
            mCurrentMetrics.mIsSafeToTrade = false;
           }
        }
     }

   //+------------------------------------------------------------------+
   //| Pre-trade gatekeeper check: boolean clearance decision           |
   //| Supports optional directional momentum verification              |
   //+------------------------------------------------------------------+
   bool              IsSafeToTrade(const ENUM_ORDER_TYPE orderType = (ENUM_ORDER_TYPE)-1)
     {
      if(!mIsInitialized)
        {
         return false;
        }

      mCurrentMetrics.mTotalEvaluations++;

      //--- 1. Check out-of-scale safety lock
      if(!mCurrentMetrics.mIsScaleValid)
        {
         mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
         return false;
        }

      //--- 2. Check if buffer has sufficient warmup ticks
      if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.IsReady(mMinWarmupTicks))
        {
         mCurrentMetrics.mReason = REASON_BUFFER_NOT_READY;
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
         return false;
        }

      //--- 3. Evaluate current regime; a rejected quote invalidates the cached snapshot
      bool isSafe = (mCurrentMetrics.mRegime == REGIME_OPTIMAL || mCurrentMetrics.mRegime == REGIME_MODERATE)
                    && mCurrentMetrics.mReason != REASON_INVALID_TICK;

      //--- 4. Directional momentum check (if specified)
      if(isSafe && orderType != (ENUM_ORDER_TYPE)-1)
        {
         if(orderType == ORDER_TYPE_BUY && mCurrentMetrics.mSignedVelocity < 0.0)
           {
            isSafe = false;
            mCurrentMetrics.mReason = REASON_ADVERSE_MOMENTUM;
           }
         else
            if(orderType == ORDER_TYPE_SELL && mCurrentMetrics.mSignedVelocity > 0.0)
              {
               isSafe = false;
               mCurrentMetrics.mReason = REASON_ADVERSE_MOMENTUM;
              }
        }

      if(isSafe)
        {
         mCurrentMetrics.mApprovedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = true;
        }
      else
        {
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
        }

      return isSafe;
     }

   //+------------------------------------------------------------------+
   //| Renders the non-blocking HUD telemetry to chart                  |
   //+------------------------------------------------------------------+
   void              RenderTelemetry(void)
     {
      if(mIsInitialized && CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.Render(mCurrentMetrics);
        }
     }

   //+------------------------------------------------------------------+
   //| Clears HUD from chart                                            |
   //+------------------------------------------------------------------+
   void              ClearTelemetry(void)
     {
      if(CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.Clear();
        }
     }

   //+------------------------------------------------------------------+
   //| Dispatches chart GUI interactions (dragging, resizing)           |
   //+------------------------------------------------------------------+
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
     {
      if(mIsInitialized && CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.OnChartEvent(id, lparam, dparam, sparam);
        }
     }

   //+------------------------------------------------------------------+
   //| Returns pointer to internal telemetry dashboard instance         |
   //+------------------------------------------------------------------+
   CTelemetryDashboard* GetDashboard(void)
     {
      return mDashboard;
     }

   //+------------------------------------------------------------------+
   //| Sets commission cost in equivalent symbol points                 |
   //+------------------------------------------------------------------+
   void              SetCommissionEquivalentPoints(const double points)
     {
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         mClassifier.SetCommissionEquivalentPoints(points);
        }
      mProfile.mCommissionPoints = points;
      mCurrentMetrics.mCommissionPoints = points;
     }

   //+------------------------------------------------------------------+
   //| Retrieves current commission cost in equivalent symbol points    |
   //+------------------------------------------------------------------+
   double            GetCommissionEquivalentPoints(void) const
     {
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         return mClassifier.GetCommissionEquivalentPoints();
        }
      return 0.0;
     }

   //+------------------------------------------------------------------+
   //| Retrieves consolidated snapshot of analytical metrics            |
   //+------------------------------------------------------------------+
   void              GetMetrics(SFrictionMetrics &outMetrics) const
     {
      outMetrics = mCurrentMetrics;
     }

   //+------------------------------------------------------------------+
   //| Retrieves auto-detected symbol profile metadata                  |
   //+------------------------------------------------------------------+
   void              GetProfile(SSymbolProfile &outProfile) const
     {
      outProfile = mProfile;
     }

   //+------------------------------------------------------------------+
   //| Resets cumulative trade evaluation counters                      |
   //+------------------------------------------------------------------+
   void              ResetCounters(void)
     {
      mCurrentMetrics.mTotalEvaluations = 0;
      mCurrentMetrics.mApprovedEvaluations = 0;
      mCurrentMetrics.mBlockedEvaluations = 0;
     }

   //+------------------------------------------------------------------+
   //| Explicit shutdown for lifecycle termination                      |
   //+------------------------------------------------------------------+
   void              Shutdown(void)
     {
      ClearTelemetry();
      CleanMemory();
     }

   //+------------------------------------------------------------------+
   //| Status check                                                     |
   //+------------------------------------------------------------------+
   bool              IsInitialized(void) const
     {
      return mIsInitialized;
     }
  };

#endif // SCALPER_GUARD_FRICTION_GUARD_MQH
