using System.Globalization;
using System.Net;
using Fast.Shared.Logic.FileService;
using Fast.Web.Models;
using Renci.SshNet;
using Serilog;

namespace Fast.Web.Logic;

public class Argus
{
    private const string FtpUser = "afVisionEnergy";
    private const string FtpPass = "Rb37gU4";
    private const string BaseUrl = "ftp.argusmedia.com";
    private string baseLocalFolder = "";
    private readonly FileService fileService;

    Dictionary<string, ArgusIndex> indexes = new();

    private List<string> ValidPricePrefixes { get; } = new List<string> { "N ", "C ", "F " };

    public Argus(IWebHostEnvironment env)
    {
        var config = new FileServiceConfig(env.ContentRootPath);
        fileService = new FileService(config);
    }

    public async Task Import()
    {
        bool reprocessOldDates = true;
        DateOnly? reprocessStart = DateOnly.FromDateTime(DateTime.Today.AddDays(-10));
        DateOnly? reprocessEnd = DateOnly.FromDateTime(DateTime.Today);
        await Import(reprocessOldDates, reprocessStart, reprocessEnd);
    }

    public async Task Import(bool reprocessOldDates, DateOnly? reprocessStart, DateOnly? reprocessEnd)
    {
        baseLocalFolder = "ArgusPrices";

        await fileService.CreateDirectoryAsync(baseLocalFolder);

        var db = Main.CreateContext();

        indexes = (
            from q in db.MarketIndexAliases
            where q.IndexAliasTypeId == (int)Enums.IndexAliasType.ArgusId &&
                q.Index.Product.CategoryId == (int)Enums.ProductCategory.CrudeOil
            select new ArgusIndex { IndexId = q.Index.Id, Name = q.Index.Name ?? "", ArgusId = q.IndexAlias.Trim() }
        ).ToDictionary(x => x.ArgusId, x => x, StringComparer.OrdinalIgnoreCase);

        if (reprocessOldDates)
        {
            if (reprocessStart == null || reprocessEnd == null)
                throw new Exception("Reprocess start and end dates must be specified");

            Log.Information($"Reprocessing Argus prices from {reprocessStart.Value:yyyy-MM-dd} to {reprocessEnd.Value:yyyy-MM-dd}");
            DateOnly loopDate = reprocessStart.Value;
            while (loopDate <= reprocessEnd.Value)
            {
                await ImportByDate(loopDate);
                loopDate = loopDate.AddDays(1);
            }
        }
        else
        {
            await ImportByDate(DateOnly.FromDateTime(DateTime.Today));
        }

        await DeleteOldFilesAsync();
    }

    private async Task ImportByDate(DateOnly processDate)
    {
        string dateStr = processDate.ToString("yyyyMMdd");
        string remotePath = $"DCRDEUS/{dateStr}dhc.csv";

        List<ArgusItem> prices = await DownloadPricesAsync(remotePath, processDate);
        await ProcessPrices(indexes, prices, true);
    }

    private async Task<List<ArgusItem>> DownloadPricesAsync(string remotePath, DateOnly processDate)
    {
        List<ArgusItem> prices = new();
        var fileDate = GetRemoteFileTimestamp(remotePath);
        if (fileDate != DateTime.MinValue)
        {
            string fileName = Path.GetFileName(remotePath);

            byte[] fileBytes;
            using (var sftp = new SftpClient(BaseUrl, FtpUser, FtpPass))
            {
                sftp.Connect();
                using var memoryStream = new MemoryStream();

                sftp.DownloadFile(remotePath, memoryStream);
                sftp.ChangeDirectory("/");
                fileBytes = memoryStream.ToArray();
            }

            await fileService.WriteAllBytesAsync(baseLocalFolder, fileName, fileBytes, useSharding: false);

            string content = Encoding.UTF8.GetString(fileBytes);
            List<string> lines = content.Split('\n').ToList();
            //start at 1 since the first line is column headers
            for (int i = 1; i < lines.Count; i++)
            {
                var line = lines[i];
                if (string.IsNullOrWhiteSpace(line))
                    continue;

                var values = line.Split(',');
                ArgusItem item = new();
                item.Code = values[0].Trim('"');
                try
                {
                    item.TsType = int.Parse(values[1]);
                    item.PtCode = int.Parse(values[2]);
                    item.Date = DateOnly.ParseExact(values[3], "dd-MMM-yyyy", CultureInfo.InvariantCulture);
                    item.Value = decimal.Parse(values[4]);
                    item.FwdPeriod = int.Parse(values[5]);
                    item.Year = int.Parse(values[7]);
                    prices.Add(item);
                }
                catch (Exception ex)
                {
                    throw new Exception($"Error parsing Argus price record for {item.Code}", ex);
                }
            }
        }

        return prices;
    }

    private async static Task ProcessPrices(Dictionary<string, ArgusIndex> indexes, List<ArgusItem> prices, bool isMonthly)
    {
        var db = Main.CreateContext();
        var dbSystemUser = db.AppUsers.Where(x => x.DisplayName == "System").FirstOrDefault() ?? throw new Exception("Database user for uploading prices not found");
        int userId = dbSystemUser.Id;

        //PtCodes are from FTP Documentation/latestPricetype.csv
        //4 = index; 8 = midpoint; 49 = diff index
        var validPtCodes = new List<int> { 4, 8, 49 };

        prices = (
            from q in prices
            where validPtCodes.Contains(q.PtCode) &&
                q.Date.Year > 2000 &&
                q.Date.Year < 2200 &&
                q.FwdPeriod > 0 &&
                q.FwdPeriod < 13
            select q
        ).ToList();

        //for any prices with PtCode 4, ignore other prices with the same code if they have a PtCode of 8
        //this is because Argus has a tendency to publish both the index and midpoint prices for the same code
        //and we only want the index price since it is more accurate, but if it is not available then we will use the midpoint
        var codeAndDateWithIndexPriceHash = prices.Where(x => x.PtCode == 4).Select(x => new { Code = x.Code.ToLower(), x.Date }).ToHashSet();
        var filteredPrices = new List<ArgusItem>();
        foreach (var price in prices)
        {
            //skip midpoint prices if the index price is available
            var key = new { Code = price.Code.ToLower(), price.Date };
            if (price.PtCode == 8 && codeAndDateWithIndexPriceHash.Contains(key))
                continue;

            filteredPrices.Add(price);
        }

        await db.Database.CreateExecutionStrategy().Execute(async () =>
        {
            using var dbContextTransaction = await db.Database.BeginTransactionAsync();
            foreach (var price in filteredPrices)
            {
                //Superior adds a D to the end of the code for diff indexes since Argus
                //uses the same index code for different prices but with different PtCodes
                var alias = price.PtCode == 49 ? price.Code + "D" : price.Code;
                if (!indexes.ContainsKey(alias))
                    continue;

                var index = indexes[alias];
                var contractMonth = DateOnly.FromDateTime(new DateTime(price.Year, price.FwdPeriod, 1));

                //even though Argus prices are for a forward period, we store them as daily index prices
                //since Crude oil is physically settled for a forward period, not financially settled

                var marketPrice = (
                    from p in db.MarketPrices
                    where p.IndexId == index.IndexId &&
                    p.PriceDate == price.Date &&
                    p.MarketTypeId == (int)Enums.MarketType.DailyIndex &&
                    p.ContractMonth == contractMonth
                    select p
                ).FirstOrDefault();

                if (marketPrice == null)
                {
                    marketPrice = new MarketPrice();
                    db.MarketPrices.Add(marketPrice);
                }

                marketPrice.PriceDate = price.Date;
                marketPrice.MarketTypeId = (int)Enums.MarketType.DailyIndex;
                marketPrice.PublishDate = null;
                marketPrice.IndexId = index.IndexId;
                marketPrice.Price = Convert.ToDouble(price.Value);
                marketPrice.DataInputTime = DateTime.UtcNow;
                marketPrice.DataInputerId = userId;
                marketPrice.ContractMonth = contractMonth;
            }
            await db.SaveChangesAsync();
            await dbContextTransaction.CommitAsync();
        });
    }

    //return DateTime.MinValue if the remote path does not exist
    private static DateTime GetRemoteFileTimestamp(string remotePath)
    {
        DateTime timestamp = DateTime.MinValue;
        using (var sftp = new SftpClient(BaseUrl, FtpUser, FtpPass))
        {
            sftp.Connect();
            try
            {
                timestamp = sftp.GetLastWriteTime(remotePath);
            }
            catch (Exception)
            {
                timestamp = DateTime.MinValue;
            }
        }

        return timestamp;
    }

    private async Task DeleteOldFilesAsync()
    {
        int maxAgeInDays = 60;
        DateTime currentDate = DateTime.Now.Date;

        try
        {
            // Get all files with info to check dates
            var files = await fileService.GetFilesWithInfoAsync(baseLocalFolder, subFolder: null, searchPattern: "*.csv", recursive: false);

            foreach (var file in files)
            {
                // Check if file is older than maxAgeInDays
                if (file.LastModifiedDate.HasValue && (currentDate - file.LastModifiedDate.Value).TotalDays > maxAgeInDays)
                {
                    try
                    {
                        await fileService.DeleteFileAsync(baseLocalFolder, file.Name, useSharding: false);
                    }
                    catch (Exception)
                    {
                    }
                }
            }
        }
        catch (Exception)
        {
        }
    }

    public class ArgusItem
    {
        public string Code = "";
        public int TsType;
        public int PtCode;
        public DateOnly Date;
        public decimal Value;
        public int FwdPeriod;
        public int Year;
    }
}