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

namespace Fast.Web.Logic;

public class Platts
{
    private const string FtpUser = "bryce_pippitt";
    private const string FtpPass = "lH*6bPMi5HSXjya!o&PW";
    private const string BaseUrl = "sftp.platts.com";
    private readonly string baseLocalFolder = "PlattsPrices";
    private readonly IWebHostEnvironment env;
    private readonly NetworkCredential credentials;
    private readonly FileService fileService;

    Dictionary<string, PlattsIndex> dailyIndexes = new();
    Dictionary<string, PlattsIndex> monthlyIndexes = new();

    Dictionary<string, SymbolItem> dailySymbols = new();
    Dictionary<string, SymbolItem> monthlySymbols = new();

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

    public Platts(IWebHostEnvironment env)
    {
        this.env = env;
        var config = new FileServiceConfig(env.ContentRootPath);
        fileService = new FileService(config);
        credentials = new NetworkCredential(FtpUser, FtpPass);
    }

    public async Task DownloadPrices()
    {
        bool reprocessOldDates = true;
        //reprocessStart should be 3 days ago
        DateOnly? reprocessStart = DateOnly.FromDateTime(DateTime.Today.AddDays(-3));
        DateOnly? reprocessEnd = DateOnly.FromDateTime(DateTime.Today);
        await DownloadPrices(reprocessOldDates, reprocessStart, reprocessEnd);
    }

    public async Task DownloadPrices(bool reprocessOldDates, DateOnly? reprocessStart, DateOnly? reprocessEnd)
    {
        var db = Main.CreateContext();

        List<PlattsIndex> indexes = (
            from q in db.MarketIndexAliases
            where q.IndexAliasTypeId == (int)Enums.IndexAliasType.PlattsId &&
                q.Index.ProductId == (int)Enums.Product.NaturalGas &&
                q.Index.IndexTypeId != (int)Enums.MarketIndexType.Hybrid
            select new PlattsIndex { IndexId = q.Index.Id, Name = q.Index.Name ?? "", IndexTypeId = q.Index.IndexTypeId, PlattsSymbol = q.IndexAlias.Trim() }
        ).ToList();

        foreach (var index in indexes)
            index.PlattsSymbol = GetCleanSymbol(index.PlattsSymbol);

        dailyIndexes = indexes.Where(x => x.IndexTypeId == (int)Enums.MarketIndexType.Daily).ToDictionary(x => x.PlattsSymbol);
        monthlyIndexes = indexes.Where(x => x.IndexTypeId == (int)Enums.MarketIndexType.Monthly).ToDictionary(x => x.PlattsSymbol);
        dailySymbols = await GetSymbols(@"symbols/csv-version/GD_sym.csv", "gd", DateOnly.FromDateTime(DateTime.Today));
        monthlySymbols = await GetSymbols(@"symbols/csv-version/GM_sym.csv", "gm", DateOnly.FromDateTime(DateTime.Today));

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

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

        await DeleteOldFilesAsync();
    }

    private string GetCleanSymbol(string symbol)
    {
        var isEightChars = symbol.Length == 8;
        var endsWithC = symbol.EndsWith("c", StringComparison.OrdinalIgnoreCase);
        var endsWithU = symbol.EndsWith("u", StringComparison.OrdinalIgnoreCase);
        if (isEightChars && (endsWithC || endsWithU))
            symbol = symbol.Substring(0, 7);

        return symbol;
    }

    private async Task ProcessPrices(DateOnly processDate)
    {
        bool isToday = processDate == DateOnly.FromDateTime(DateTime.Today);

        string remoteFolder = isToday ? @"today" : processDate.ToString("yyyyMMdd");
        string remoteDailyPath = remoteFolder + @"/gd.ftp";
        string remoteMonthlyPath = remoteFolder + @"/gm.ftp";

        List<PriceItem> dailyPrices = await GetPrices(dailySymbols, remoteDailyPath, processDate);
        UpdatePrices(dailyIndexes, dailyPrices, false);

        List<PriceItem> monthlyPrices = await GetPrices(monthlySymbols, remoteMonthlyPath, processDate);
        UpdatePrices(monthlyIndexes, monthlyPrices, true);

        if (isToday)
            await ProcessPostClosePrices();
    }

    private async Task ProcessPostClosePrices()
    {
        List<PriceItem> dailyPostClosePrices = await GetPrices(dailySymbols, @"postclose/gd.ftp", DateOnly.FromDateTime(DateTime.Today));
        UpdatePrices(dailyIndexes, dailyPostClosePrices, false);

        List<PriceItem> monthlyPostClosePrices = await GetPrices(monthlySymbols, @"postclose/gm.ftp", DateOnly.FromDateTime(DateTime.Today));
        UpdatePrices(monthlyIndexes, monthlyPostClosePrices, true);
    }

    private static void UpdatePrices(Dictionary<string, PlattsIndex> indexes, List<PriceItem> prices, bool isMonthly)
    {
        var db = Main.CreateContext();
        var dbSystemUser = db.AppUsers.Where(x => x.DisplayName == "System").FirstOrDefault();
        if (dbSystemUser == null)
            throw new Exception("Database user for uploading prices not found");

        int userId = dbSystemUser.Id;

        List<MarketPrice> pricesToRemove = new();
        List<MarketPrice> pricesToAdd = new();

        foreach (var price in prices)
        {
            if (indexes.ContainsKey(price.Symbol))
            {
                var index = indexes[price.Symbol];

                int marketTypeID = isMonthly ? 3 : 2;

                var marketPrice = (
                    from p in db.MarketPrices
                    where p.IndexId == index.IndexId &&
                    p.PriceDate == price.Date &&
                    p.MarketTypeId == marketTypeID
                    select p
                ).FirstOrDefault();

                if (marketPrice != null)
                    pricesToRemove.Add(marketPrice);

                DateOnly? publishDate = isMonthly ? DateOnly.FromDateTime(DateTime.Now) : (DateOnly?)null;

                MarketPrice newMarketPrice = new();
                newMarketPrice.PriceDate = price.Date;
                newMarketPrice.MarketTypeId = marketTypeID;
                newMarketPrice.PublishDate = publishDate;
                newMarketPrice.IndexId = index.IndexId;
                newMarketPrice.Price = Convert.ToDouble(price.Price);
                newMarketPrice.DataInputTime = DateTime.UtcNow;
                newMarketPrice.DataInputerId = userId;
                //for the ContractMonth on daily indexes, we use the first day of the month of the PriceDate
                //for the ContractMonth on monthly indexes we use 1/1/1900 since the month is stored in the PriceDate field, not the ContractMonth field
                newMarketPrice.ContractMonth = isMonthly ? new DateOnly(1900, 1, 1) : Util.Date.FirstDayOfMonth(price.Date);
                pricesToAdd.Add(newMarketPrice);
            }
        }

        if (pricesToRemove != null && pricesToRemove.Any())
        {
            db.MarketPrices.RemoveRange(pricesToRemove);
            db.SaveChanges();
        }

        if (pricesToAdd != null && pricesToAdd.Any())
        {
            db.MarketPrices.AddRange(pricesToAdd);
            db.SaveChanges();
        }
    }

    private async Task<List<PriceItem>> GetPrices(Dictionary<string, SymbolItem> symbols, string remotePath, DateOnly processDate)
    {
        List<PriceItem> prices = new();
        PriceItem.PublishDate = DateOnly.FromDateTime(GetRemoteFileTimestamp(remotePath));

        if (PriceItem.PublishDate != DateOnly.MinValue)
        {
            //the remote path is either "today" or a date
            //if the remote path is the same as the process date,
            //then the local folder is processDateStr/today/remotePath
            //otherwise, the local folder is processDateStr/remotePath (which sometimes includes the word "today" already)
            //we don't want to create a dated folder within another dated folder
            string processDateStr = processDate.ToString("yyyy-MM-dd");
            string remotePathDateStr = processDate.ToString("yyyyMMdd");
            bool remotePathStartsWithDate = remotePath.StartsWith(remotePathDateStr);
            if (remotePathStartsWithDate)
                remotePath = remotePath.Replace(remotePathDateStr + "/", "");

            string localFolder = Path.Join(processDateStr, remotePathStartsWithDate ? "today" : "");
            string fileName = Path.GetFileName(remotePath);
            string? subFolder = Path.GetDirectoryName(remotePath)?.Replace("\\", "/");
            if (!string.IsNullOrEmpty(subFolder))
                localFolder = Path.Join(localFolder, subFolder);

            await fileService.CreateDirectoryAsync(baseLocalFolder, localFolder);

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

                if (remotePathStartsWithDate)
                    sftp.ChangeDirectory(remotePathDateStr);

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

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

            string content = Encoding.UTF8.GetString(fileBytes);
            List<string> lines = content.Split('\n').ToList();

            foreach (var line in lines)
            {
                if (line.Length < 2)
                    continue;
                var linePrefix = line.Substring(0, 2);
                if (ValidPricePrefixes.Contains(linePrefix))
                {
                    string[] lineParts = line.Split(' ');
                    PriceItem item = new();
                    string symbolPart = lineParts[1][0..^1]; //remove the last character
                    string symbolBate = lineParts[1].Last().ToString(); //get the last character

                    if (symbols.ContainsKey(symbolPart))
                    {
                        SymbolItem symbolItem = symbols[symbolPart];
                        if (symbolBate.Equals("u", StringComparison.OrdinalIgnoreCase) || (!symbolItem.Bates.Contains("u") && symbolBate == "c"))
                        {
                            item.Index = symbols[symbolPart].Index;
                            item.Date = DateOnly.ParseExact(lineParts[2].Substring(0, 8), "yyyyMMdd", CultureInfo.InvariantCulture);
                            item.Price = decimal.Parse(lineParts[3]);
                            item.Symbol = symbolPart;
                            prices.Add(item);
                        }
                    }
                }
            }
        }

        return prices;
    }

    //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<Dictionary<string, SymbolItem>> GetSymbols(string remotePath, string priceType, DateOnly processDate)
    {
        string localFolder = Path.Join(processDate.ToString("yyyy-MM-dd"), Path.GetDirectoryName(remotePath)?.Replace("\\", "/") ?? "");
        string fileName = Path.GetFileName(remotePath);

        await fileService.CreateDirectoryAsync(baseLocalFolder, localFolder);

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

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

        string content = Encoding.UTF8.GetString(fileBytes);
        List<string> lines = content.Split('\n').ToList();
        List<SymbolItem> symbols = new();

        foreach (var line in lines)
        {
            string[] lineParts = line.Split(',');
            if (string.Equals(lineParts[0], priceType, StringComparison.OrdinalIgnoreCase))
            {
                SymbolItem item = new()
                {
                    Symbol = lineParts[2],
                    Bates = lineParts[3],
                    Index = lineParts[13]
                };
                symbols.Add(item);
            }
        }

        return symbols.ToDictionary(x => x.Symbol);
    }

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

        try
        {
            var directories = await fileService.GetDirectoriesAsync(baseLocalFolder);

            foreach (var dir in directories)
            {
                string dirName = Path.GetFileName(dir);
                DateTime.TryParseExact(dirName, "yyyy-MM-dd", null, DateTimeStyles.AssumeLocal, out DateTime directoryDate);

                if (directoryDate != DateTime.MinValue && (currentDate - directoryDate).TotalDays > maxAgeInDays)
                {
                    try
                    {
                        // Delete all files in the directory
                        var files = await fileService.GetFilesAsync(baseLocalFolder, dirName, "*", recursive: true);
                        foreach (var file in files)
                        {
                            string fileName = Path.GetFileName(file);
                            string subFolder = Path.GetDirectoryName(file)?.Replace("\\", "/") ?? "";
                            await fileService.DeleteFileAsync(baseLocalFolder, Path.Join(dirName, subFolder, fileName), useSharding: false);
                        }
                    }
                    catch (Exception)
                    {
                    }
                }
            }
        }
        catch (Exception)
        {
        }
    }

    public class SymbolItem
    {
        public string Symbol = "";
        public string Bates = "";
        public string Index = "";
    }

    public class PriceItem
    {
        private static DateOnly publishDate;

        public string Index = "";
        public DateOnly Date;
        public decimal Price;
        public string Symbol = "";

        public static DateOnly PublishDate { get => publishDate; set => publishDate = value; }
    }
}