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
{
    [Robot(AccessRights = AccessRights.FullAccess, AddIndicators = true)]
    public class MS_ExportM1 : Robot
    {
        // The M1Record struct defines the binary format for a single M1 bar.
        [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 56)]
        private struct M1Record
        {
            public double OADateTime;
            public long Open;
            public long High;
            public long Low;
            public long Close;
            public long Spread;
            public int TickVolume;
            public int Digits;
        }

        [Parameter("Output Path", DefaultValue = "\\\\COFFEE\\TickData\\Pepperstone", Group = "Output Settings")]
        public string DirectoryName { get; set; }

        // New parameter to control the batch file size in kilobytes.
        [Parameter("Max File Size (kB)", DefaultValue = 10000, Group = "Output Settings", MinValue = 1)]
        public int MaxFileSizeKb { get; set; }

        private Dictionary<string, List<M1Record>> _monthlyData;
        private double _priceFactor;

        protected override void OnStart()
        {
            if (!IsBacktesting)
            {
                Print("This cBot only operates in Backtesting mode.");
                Stop();
                return;
            }

            if (string.IsNullOrWhiteSpace(DirectoryName))
            {
                Print("Error: Output path is not specified.");
                Stop();
                return;
            }
            
            _monthlyData = new Dictionary<string, List<M1Record>>();
            _priceFactor = Math.Pow(10, Symbol.Digits);
            Directory.CreateDirectory(DirectoryName);
            Print("Starting M1 data accumulation...");
        }

        protected override void OnBar()
        {
            var barIndex = Bars.Count - 2;
            if (barIndex < 0) return;

            var bar = Bars[barIndex];
            var monthKey = $"{Symbol.Name}_{bar.OpenTime:yyyy_MM}";

            if (!_monthlyData.ContainsKey(monthKey))
            {
                _monthlyData[monthKey] = new List<M1Record>();
            }

            var record = new M1Record
            {
                OADateTime = bar.OpenTime.ToOADate(),
                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
            };
            
            _monthlyData[monthKey].Add(record);
        }
        
        protected override void OnTick()
        {
            // Not used
        }

        // The OnStop method contains the new batching logic.
        protected override void OnStop()
        {
            Print("Backtest finished. Writing accumulated data to batched files...");
            
            if (_monthlyData == null || _monthlyData.Count == 0)
            {
                Print("No data was accumulated to write.");
                return;
            }

            // The keys (e.g., "EURUSD_2023_01") are sorted to process months chronologically.
            var sortedMonthKeys = _monthlyData.Keys.ToList();
            sortedMonthKeys.Sort();

            var recordBatch = new List<M1Record>();
            string startMonthKey = null;
            long maxFileSizeBytes = MaxFileSizeKb * 1024;
            var recordSize = Marshal.SizeOf(typeof(M1Record));

            for (var i = 0; i < sortedMonthKeys.Count; i++)
            {
                var currentMonthKey = sortedMonthKeys[i];
                
                // Set the start month for the new batch.
                if (startMonthKey == null)
                {
                    startMonthKey = currentMonthKey;
                }

                // Add the entire month's data to the current batch.
                recordBatch.AddRange(_monthlyData[currentMonthKey]);

                long currentBatchSizeBytes = recordBatch.Count * recordSize;
                bool isLastMonth = (i == sortedMonthKeys.Count - 1);

                // Write the file if the size limit is exceeded OR if it's the last month.
                // This ensures a month's data is never split.
                if (currentBatchSizeBytes >= maxFileSizeBytes || isLastMonth)
                {
                    var symbol = Symbol.Name;
                    var startDate = startMonthKey.Substring(symbol.Length + 1).Replace('_', '-');
                    var endDate = currentMonthKey.Substring(symbol.Length + 1).Replace('_', '-');
                    var fileNameBase = $"{symbol}_{startDate}_{endDate}";
                    
                    WriteCompressedM1File(fileNameBase, recordBatch);

                    // Reset for the next batch.
                    recordBatch.Clear();
                    startMonthKey = null;
                }
            }
            
            Print("M1 data export completed successfully.");
        }

        private void WriteCompressedM1File(string fileNameBase, List<M1Record> records)
        {
            // The path for the final archive file with an .m1 extension.
            var archiveFilePath = Path.Combine(DirectoryName, $"{fileNameBase}.m1");
            // The name for the binary data file inside the archive.
            var entryFileName = $"{fileNameBase}.m1";

            var recordSize = Marshal.SizeOf(typeof(M1Record));
            var data = new byte[records.Count * recordSize];
            var handle = default(GCHandle);

            try
            {
                // This loop serializes the list of structs into a single byte array.
                for (var i = 0; i < records.Count; i++)
                {
                    var record = records[i];
                    handle = GCHandle.Alloc(record, GCHandleType.Pinned);
                    Marshal.Copy(handle.AddrOfPinnedObject(), data, i * recordSize, recordSize);
                    handle.Free();
                }
            }
            finally
            {
                if (handle.IsAllocated) handle.Free();
            }

            // Create the ZIP archive file.
            using (var fileStream = new FileStream(archiveFilePath, FileMode.Create))
            using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create))
            {
                // Create a new entry inside the archive for the binary data.
                var zipEntry = archive.CreateEntry(entryFileName, CompressionLevel.Optimal);

                // Open the entry's stream and write the serialized byte array to it.
                using (var entryStream = zipEntry.Open())
                {
                    entryStream.Write(data, 0, data.Length);
                }
            }

            Print($"Created archive: {archiveFilePath} (Uncompressed Size: {data.Length / 1024.0:F2} kB)");
        }    
    }
}