using System.Globalization;
using Fast.Shared.Logic.FileService;
using Fast.Web.Models;

namespace Fast.Web.Logic;

public class Cme
{
    private readonly IWebHostEnvironment env;
    private readonly CmeDataMineApiClient apiClient;
    private readonly int systemUserId;
    private readonly int cmeFileCount;
    private readonly HashSet<DateOnly> holidays;
    private readonly FileService fileService;

    public Cme(IWebHostEnvironment env)
    {
        this.env = env;
        var config = new FileServiceConfig(env.ContentRootPath);
        fileService = new FileService(config);
        apiClient = new CmeDataMineApiClient(env, fileService);

        var db = Main.CreateContext();
        systemUserId = db.AppUsers.Where(x => x.UserName == "System").First().Id;
        cmeFileCount = int.Parse(db.AppSettings.Where(x => x.Name == "CmeFileCount").First().Value);
        holidays = db.Holidays.Where(x => x.HolidayDate != null).Select(x => x.HolidayDate!.Value).ToHashSet();
    }

    public async Task<bool> DownloadPrices()
    {
        await ReprocessOldPriceFiles();

        var apiResponse = await apiClient.GetListResponseAsync();
        //Settle Files
        var setlFileDetails = (
            from q in apiResponse.Files
            where q.Dataset.ToUpperInvariant() == "BASPLS_NYMEX"
                && q.FileName.ToUpperInvariant().EndsWith(".CSV")
            orderby q.Period descending
            select q
        ).ToList();

        if (setlFileDetails.Count == 0)
            throw new Exception("No files were available for processing.");

        if (setlFileDetails.Count != 0) await DownloadFiles(setlFileDetails);

        return true;
    }

    private async Task ReprocessOldPriceFiles()
    {
        string reprocessDir = Path.Combine("CmePrices", "reprocess");
        await fileService.CreateDirectoryAsync(reprocessDir);

        var allFiles = await fileService.GetFilesAsync(reprocessDir, recursive: false);
        var filesToProcess = allFiles
                                  .Where(f => f.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) && System.Text.RegularExpressions.Regex.IsMatch(f, @"^\d{8}.*\.csv$"))
                                  .ToArray();

        var cmeProcessor = new Cme(env);

        foreach (var fileName in filesToProcess)
        {
            var fileResponse = await fileService.DownloadFileAsync(reprocessDir, fileName, useSharding: false);
            using var reader = new StreamReader(fileResponse.Stream);
            string fileText = await reader.ReadToEndAsync();

            var records = CmeDataMineApiClient.GetRecordsFromFileText(fileText);
            string dateString = fileName.Substring(0, 8);
            DateOnly priceDate = DateOnly.ParseExact(dateString, "yyyyMMdd", CultureInfo.InvariantCulture);
            await cmeProcessor.ProcessFile(priceDate, records);
            await fileService.DeleteFileAsync(reprocessDir, fileName, useSharding: false);
        }
    }

    private async Task DownloadFiles(List<DataMineFileDetail> fileDetails)
    {
        //process the most recent files up to CmeFileCount
        int numFilesToProcess = cmeFileCount;
        if (numFilesToProcess > fileDetails.Count)
            numFilesToProcess = fileDetails.Count;

        for (int i = 0; i < numFilesToProcess; i++)
        {
            var file = fileDetails[i];
            await DownloadFile(file);
        }
    }

    private async Task DownloadFile(DataMineFileDetail fileDetail)
    {
        var priceDate = DateOnly.ParseExact(fileDetail.Yyyymmdd, "yyyyMMdd", CultureInfo.InvariantCulture);

        //Nymex sometimes correctly publishes prices the day before a holiday,
        //but then also mistakenly publishes the same prices again on the holiday
        //so we want to skip the holiday prices
        if (holidays.Contains(priceDate))
            return;

        var records = await apiClient.GetSettlementDataAsync(fileDetail.Fid);
        await ProcessFile(priceDate, records);
    }

    private async Task ProcessFile(DateOnly priceDate, List<DataMineSettlementRecord> records)
    {
        var db = Main.CreateContext();
        var relevantIndexes = (
                    from q in db.MarketIndexAliases
                    where q.IndexAliasTypeId == (int)Enums.IndexAliasType.CmeName
                    select new { q.IndexAlias, q.IndexId }
                ).ToDictionary(x => x.IndexAlias, x => x.IndexId, StringComparer.OrdinalIgnoreCase);

        var expirationProductsBySymbol = new Dictionary<string, int> {
            { "NG", (int)Enums.Product.NaturalGas },
            { "CL", (int)Enums.Product.CrudeOil },
            { "BZ", (int)Enums.Product.CrudeOil }
        };

        var expirationBenchmarksBySymbol = new Dictionary<string, int> {
            { "NG", (int)Enums.Product.NaturalGas },
            { "CL", (int)Enums.Benchmark.WTICrudeOil },
            { "BZ", (int)Enums.Benchmark.BrentCrudeOil }
        };

        var expirationProductIds = expirationProductsBySymbol.Values.ToList();

        var existingExpirationItems = await (
            from q in db.ProductExpirations
            where expirationProductIds.Contains(q.ProductId)
            select new { q.ProductId, q.ContractMonth, q.BenchmarkId, q.ExpirationDate }
        ).ToDictionaryAsync(x => (x.ProductId, x.ContractMonth, x.BenchmarkId), x => x.ExpirationDate);

        var relevantRecords = records.Where(x => relevantIndexes.ContainsKey(x.Sym) && x.SecTyp.Equals("fut", StringComparison.OrdinalIgnoreCase)).ToList();

        await db.Database.CreateExecutionStrategy().Execute(async () =>
        {
            using var dbContextTransaction = await db.Database.BeginTransactionAsync();
            foreach (var rec in relevantRecords)
            {
                string symbol;
                double settlePrice;
                DateOnly contractMonth;

                var hasSym = !string.IsNullOrWhiteSpace(rec.Sym);
                var hasSettlePrice = rec.SettlePrice.HasValue;
                var hasContractMonth = rec.MMY.HasValue && rec.MMY.Value.Day == 1;

                if (!hasSym || !hasSettlePrice || !hasContractMonth)
                    continue;

                symbol = rec.Sym;
                settlePrice = Convert.ToDouble(rec.SettlePrice.GetValueOrDefault());
                contractMonth = DateOnly.FromDateTime(rec.MMY.GetValueOrDefault().ToDateTime(TimeOnly.MinValue));

                //process expiration dates
                var isExpirationSymbol = expirationProductsBySymbol.ContainsKey(symbol);
                var hasExpirationDate = rec.MatDt.HasValue;
                if (isExpirationSymbol && hasExpirationDate)
                {
                    var productId = expirationProductsBySymbol[symbol];
                    var benchmarkId = expirationBenchmarksBySymbol[symbol];
                    var expirationDateExists = existingExpirationItems.ContainsKey((productId, contractMonth, benchmarkId));
                    if (!expirationDateExists)
                    {
                        var expirationItem = new ProductExpiration();
                        expirationItem.ProductId = productId;
                        expirationItem.BenchmarkId = benchmarkId;
                        expirationItem.ContractMonth = contractMonth;
                        expirationItem.ExpirationDate = DateOnly.FromDateTime(rec.MatDt.GetValueOrDefault().ToDateTime(TimeOnly.MinValue));
                        db.ProductExpirations.Add(expirationItem);
                    }
                }

                //process prices
                var indexId = relevantIndexes[rec.Sym];
                var marketItem = await (
                    from q in db.MarketPrices
                    where indexId == q.IndexId
                        && q.MarketTypeId == (int)Enums.MarketType.ForwardCurve
                        && q.PriceDate == priceDate
                        && q.ContractMonth == contractMonth
                    select q
                ).FirstOrDefaultAsync();

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

                marketItem.IndexId = indexId;
                marketItem.MarketTypeId = (int)Enums.MarketType.ForwardCurve;
                marketItem.PriceDate = priceDate;
                marketItem.ContractMonth = contractMonth;
                marketItem.PublishDate = null;
                marketItem.Price = settlePrice;
                marketItem.DataInputerId = systemUserId;
                marketItem.DataInputTime = DateTime.UtcNow;
            }

            await db.SaveChangesAsync();
            await dbContextTransaction.CommitAsync();
        });
    }
}
