/* * MS_IncrementalExport
 * High-performance M1 and Tick data exporter.
 * Naming: {Symbol}_{Start-YYYY-MM}_{End-YYYY-MM}.{ext}
 */

using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using cAlgo.API;

namespace cAlgo.Robots
{
    public enum ExportMode { M1, Tick }

    [Robot(AccessRights = AccessRights.FullAccess, AddIndicators = true)]
    public class MS_IncrementalExport : Robot
    {
        [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 56)]
        private struct M1Record
        {
            public double OADateTime; // Must be at Offset 0
            public long Open, High, Low, Close, Spread;
            public int TickVolume, Digits;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)]
        private struct TickDataRecord
        {
            public double OADateTime; // Must be at Offset 0
            public float Ask;
            public float Bid;
        }

        [Parameter("Export Mode", DefaultValue = ExportMode.M1)]
        public ExportMode Mode { get; set; }

        [Parameter("Output Path", DefaultValue = "/mnt/Data")]
        public string DirectoryName { get; set; }

        [Parameter("Existing File Path", DefaultValue = "")]
        public string ExistingFilePath { get; set; }

        [Parameter("Max File Size (kB)", DefaultValue = 10000)]
        public int MaxFileSizeKb { get; set; }

        private List<M1Record> _m1Buffer = new List<M1Record>();
        private List<TickDataRecord> _tickBuffer = new List<TickDataRecord>();
        private double _lastTimestamp = double.MinValue;
        private double _priceFactor;

        protected override void OnStart()
        {
            _priceFactor = Math.Pow(10, Symbol.Digits);
            
            if (!string.IsNullOrWhiteSpace(ExistingFilePath) && File.Exists(ExistingFilePath))
            {
                LoadExistingData(ExistingFilePath);
            }
        }

        protected override void OnTick()
        {
            if (Mode != ExportMode.Tick) return;

            var time = Server.TimeInUtc.ToOADate();
            if (time <= _lastTimestamp) return;

            _tickBuffer.Add(new TickDataRecord
            {
                OADateTime = time,
                Ask = (float)Symbol.Ask,
                Bid = (float)Symbol.Bid
            });
        }

        protected override void OnBar()
        {
            if (Mode != ExportMode.M1) return;

            var barIndex = Bars.Count - 2;
            if (barIndex < 0) return;

            var bar = Bars[barIndex];
            var barTime = bar.OpenTime.ToOADate();

            if (barTime <= _lastTimestamp) return;

            _m1Buffer.Add(new M1Record
            {
                OADateTime = barTime,
                Open = (long)(bar.Open * _priceFactor),
                High = (long)(bar.High * _priceFactor),
                Low = (long)(bar.Low * _priceFactor),
                Close = (long)(bar.Close * _priceFactor),
                Spread = (long)(Symbol.Spread * _priceFactor),
                TickVolume = (int)bar.TickVolume,
                Digits = Symbol.Digits
            });
        }

        protected override void OnStop()
        {
            if (Mode == ExportMode.M1)
                ProcessExport(_m1Buffer, "m1");
            else
                ProcessExport(_tickBuffer, "tick");
        }

        private void ProcessExport<T>(List<T> buffer, string extension) where T : unmanaged
        {
            if (buffer.Count == 0) return;

            int recordSize = Marshal.SizeOf<T>();
            long maxBytes = (long)MaxFileSizeKb * 1024;
            int batchSize = (int)(maxBytes / recordSize);

            for (int i = 0; i < buffer.Count; i += batchSize)
            {
                var count = Math.Min(batchSize, buffer.Count - i);
                var batch = buffer.GetRange(i, count);
                WriteBatch(batch, extension);
            }
        }

        private void WriteBatch<T>(List<T> records, string ext) where T : unmanaged
        {
            ReadOnlySpan<T> span = CollectionsMarshal.AsSpan(records);
            
            // Extract timestamps via Cast to interpret the first 8 bytes as double
            double start = MemoryMarshal.Cast<T, double>(span)[0];
            double end = MemoryMarshal.Cast<T, double>(span.Slice(records.Count - 1))[0];

            string startStr = DateTime.FromOADate(start).ToString("yyyy-MM");
            string endStr = DateTime.FromOADate(end).ToString("yyyy-MM");
            
            string fileName = $"{Symbol.Name}_{startStr}_{endStr}.{ext}";
            string archivePath = Path.Combine(DirectoryName, fileName);

            ReadOnlySpan<byte> byteSpan = MemoryMarshal.Cast<T, byte>(span);

            using var fileStream = new FileStream(archivePath, FileMode.Create);
            using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create);
            var entry = archive.CreateEntry($"{Symbol.Name}.bin", CompressionLevel.Fastest);

            using var entryStream = entry.Open();
            entryStream.Write(byteSpan);
            
            Print("Saved: {0}", archivePath);
        }

        private void LoadExistingData(string path)
        {
            try
            {
                using var archive = ZipFile.OpenRead(path);
                var entry = archive.Entries.FirstOrDefault();
                if (entry == null) return;

                using var stream = entry.Open();
                using var ms = new MemoryStream();
                stream.CopyTo(ms);
                
                byte[] data = ms.ToArray();
                if (Mode == ExportMode.M1)
                {
                    var records = MemoryMarshal.Cast<byte, M1Record>(data);
                    foreach (var r in records) if (r.OADateTime > _lastTimestamp) _lastTimestamp = r.OADateTime;
                }
                else
                {
                    var records = MemoryMarshal.Cast<byte, TickDataRecord>(data);
                    foreach (var r in records) if (r.OADateTime > _lastTimestamp) _lastTimestamp = r.OADateTime;
                }
                
                Print("Loaded existing data. Last timestamp: {0}", DateTime.FromOADate(_lastTimestamp));
            }
            catch (Exception ex)
            {
                Print("Error loading existing data: {0}", ex.Message);
            }
        }
    }
}