//+------------------------------------------------------------------+
//|  RBSync — RB Trading Live Broker Auto-Sync (cTrader cBot)         |
//|  https://rbtrading.site                                          |
//+------------------------------------------------------------------+
//  READ-ONLY. This cBot only READS your account — it can NEVER place,
//  modify or close a trade. It pushes a snapshot of your open positions
//  and closed history to your RB Trading journal every few seconds and
//  on every trade event, exactly like the MetaTrader RBSync EA.
//
//  ── SETUP (about 2 minutes, one time) ────────────────────────────
//   1. In cTrader, open the "Automate" tab (top of the window).
//   2. cBots → New cBot → delete the template code → paste THIS WHOLE FILE.
//   3. Click Build (the hammer icon). It should say "Build succeeded".
//   4. In the journal on your PC: Settings → Live Broker Auto-Sync →
//      Set up live sync → copy your RBS-… code.
//   5. Back in cTrader: open a chart, find RBSync under cBots, click it,
//      paste your RBS-… code into the "Sync Code" box, then press ▶ Play.
//      (Accept the access prompt — it needs internet to reach the journal.)
//   Your trades now flow into the journal automatically. Leave cTrader open.
//------------------------------------------------------------------

using System;
using System.Globalization;
using System.Net.Http;
using System.Text;
using cAlgo.API;

namespace cAlgo.Robots
{
    // FullAccess is required so the cBot may reach the internet (the journal).
    [Robot(AccessRights = AccessRights.FullAccess, TimeZone = TimeZones.UTC, AddIndicators = false)]
    public class RBSync : Robot
    {
        [Parameter("Sync Code (RBS-…)", DefaultValue = "")]
        public string SyncCode { get; set; }

        [Parameter("Server URL", DefaultValue = "https://rbtrading.site/api/broker-sync")]
        public string ServerURL { get; set; }

        [Parameter("Heartbeat (seconds)", DefaultValue = 300)]
        public int HeartbeatSeconds { get; set; }

        [Parameter("History (days)", DefaultValue = 1825)]
        public int HistoryDays { get; set; }

        private static readonly HttpClient _http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
        private string _lastSig = "";
        private DateTime _lastSent = DateTime.MinValue;

        protected override void OnStart()
        {
            if (string.IsNullOrEmpty(SyncCode) || SyncCode.Trim().Length < 10)
            {
                Print("RBSync: paste your RBS- sync code into the 'Sync Code' parameter, then press Play again.");
                Stop();
                return;
            }
            // Push on any trade event for near-instant journaling, plus a 5s poll.
            Positions.Opened   += a => Push(true);
            Positions.Closed   += a => Push(true);
            Positions.Modified += a => Push(true);   // SL/TP added or amended mid-trade
            Timer.Start(5);
            Push(true);
            Print("RBSync: connected. Your trades will sync to RB Trading automatically.");
        }

        protected override void OnTimer() { Push(false); }
        protected override void OnStop()  { Push(true); }

        // ── helpers ──────────────────────────────────────────────────
        private static string D(double v, int digits) { return v.ToString("F" + digits, CultureInfo.InvariantCulture); }
        private static long Unix(DateTime dt) { return ((DateTimeOffset)DateTime.SpecifyKind(dt, DateTimeKind.Utc)).ToUnixTimeSeconds(); }
        private static string Clean(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            return s.Replace("\"", "").Replace("\\", "").Replace("<", "").Replace(">", "");
        }
        private int Dig(string symName) { var s = Symbols.GetSymbol(symName); return s != null ? s.Digits : 5; }
        private double Lots(string symName, double volUnits)
        {
            var s = Symbols.GetSymbol(symName);
            return s != null ? Math.Round(s.VolumeInUnitsToQuantity(volUnits), 2) : Math.Round(volUnits / 100000.0, 2);
        }

        private void Push(bool force)
        {
            try
            {
                if (string.IsNullOrEmpty(SyncCode) || SyncCode.Trim().Length < 10) return;
                var code = SyncCode.Trim();
                var sig = new StringBuilder();

                // ── Open positions (source of truth for live SL/TP) ──────────
                var open = new StringBuilder(); int nOpen = 0;
                foreach (var p in Positions)
                {
                    if (nOpen >= 100) break;
                    int d = Dig(p.SymbolName);
                    if (nOpen > 0) open.Append(",");
                    open.Append("{\"ticket\":\"").Append(p.Id).Append("\"")
                        .Append(",\"sym\":\"").Append(Clean(p.SymbolName)).Append("\"")
                        .Append(",\"dir\":\"").Append(p.TradeType == TradeType.Buy ? "buy" : "sell").Append("\"")
                        .Append(",\"lots\":").Append(D(Lots(p.SymbolName, p.VolumeInUnits), 2))
                        .Append(",\"entry\":").Append(D(p.EntryPrice, d))
                        .Append(",\"sl\":").Append(D(p.StopLoss ?? 0, d))
                        .Append(",\"tp\":").Append(D(p.TakeProfit ?? 0, d))
                        .Append(",\"profit\":").Append(D(p.GrossProfit, 2))
                        .Append(",\"comm\":").Append(D(p.Commissions, 2))
                        .Append(",\"swap\":").Append(D(p.Swap, 2))
                        .Append(",\"openTime\":").Append(Unix(p.EntryTime)).Append("}");
                    sig.Append(p.Id).Append("|").Append(D(p.StopLoss ?? 0, d)).Append("|")
                       .Append(D(p.TakeProfit ?? 0, d)).Append("|").Append(D(p.VolumeInUnits, 0)).Append(";");
                    nOpen++;
                }

                // ── Closed history. Each HistoricalTrade is one fill/slice with a
                //    UNIQUE Id; a partial close (scale-out) produces several slices
                //    that share PositionId. We send each with its own ticket so the
                //    journal keeps every slice (correct total P&L). SL/TP aren't in
                //    cTrader history — the journal backfills them from the live
                //    open snapshot it saw while the position was running.
                var closed = new StringBuilder(); int nClosed = 0;
                var cutoff = Server.Time.AddDays(-HistoryDays);
                for (int i = History.Count - 1; i >= 0 && nClosed < 1000; i--)
                {
                    var h = History[i];
                    if (h.ClosingTime < cutoff) break;
                    int d = Dig(h.SymbolName);
                    if (nClosed > 0) closed.Append(",");
                    closed.Append("{\"ticket\":\"").Append(h.Id).Append("\"")
                          .Append(",\"sym\":\"").Append(Clean(h.SymbolName)).Append("\"")
                          .Append(",\"dir\":\"").Append(h.TradeType == TradeType.Buy ? "buy" : "sell").Append("\"")
                          .Append(",\"lots\":").Append(D(Lots(h.SymbolName, h.VolumeInUnits), 2))
                          .Append(",\"entry\":").Append(D(h.EntryPrice, d))
                          .Append(",\"exit\":").Append(D(h.ClosingPrice, d))
                          .Append(",\"sl\":0,\"tp\":0")
                          .Append(",\"profit\":").Append(D(h.NetProfit, 2))
                          .Append(",\"comm\":").Append(D(h.Commissions, 2))
                          .Append(",\"swap\":").Append(D(h.Swap, 2))
                          .Append(",\"openTime\":").Append(Unix(h.EntryTime))
                          .Append(",\"closeTime\":").Append(Unix(h.ClosingTime)).Append("}");
                    sig.Append("C").Append(h.Id).Append(":").Append(D(h.NetProfit, 2)).Append(";");
                    nClosed++;
                }

                var currency = Account.Asset != null ? Account.Asset.Name : "USD";
                var json = new StringBuilder();
                json.Append("{\"token\":\"").Append(Clean(code)).Append("\"")
                    .Append(",\"account\":{\"login\":\"").Append(Account.Number).Append("\"")
                    .Append(",\"server\":\"").Append(Clean(Account.BrokerName)).Append("\"")
                    .Append(",\"currency\":\"").Append(Clean(currency)).Append("\"")
                    .Append(",\"balance\":").Append(D(Account.Balance, 2))
                    .Append(",\"equity\":").Append(D(Account.Equity, 2)).Append("}")
                    .Append(",\"open\":[").Append(open).Append("]")
                    .Append(",\"closed\":[").Append(closed).Append("]}");

                // Skip redundant sends: same structural snapshot AND within the
                // heartbeat window → don't POST (cuts idle traffic ~10-15x).
                var s = sig.ToString();
                if (!force && s == _lastSig && (Server.Time - _lastSent).TotalSeconds < HeartbeatSeconds) return;
                _lastSig = s; _lastSent = Server.Time;

                var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
                var resp = _http.PostAsync(ServerURL, content).Result;
                if (!resp.IsSuccessStatusCode)
                    Print("RBSync: server replied " + (int)resp.StatusCode + ". Check your Sync Code is correct.");
            }
            catch (Exception ex)
            {
                Print("RBSync: could not sync — " + ex.Message);
            }
        }
    }
}
