using System.Runtime.Serialization.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.Extensions.FileProviders;

namespace Fast.Web.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class PipeRateScheduleController : ODataController
{
    private readonly MyDbContext db;
    private readonly AuthorizationHelper authHelper;
    private readonly string pipeRatesFolderPath;
    private readonly string pipeRateDiscountsFolderPath;
    private readonly IWebHostEnvironment env;

    public PipeRateScheduleController(MyDbContext db, IWebHostEnvironment env)
    {
        this.db = db;
        this.env = env;
        authHelper = new AuthorizationHelper(Main.IsAuthenticationEnabled);

        pipeRatesFolderPath = Path.Join(env?.ContentRootPath, "PipelineRates");
        if (!Directory.Exists(pipeRatesFolderPath))
            Directory.CreateDirectory(pipeRatesFolderPath);

        pipeRateDiscountsFolderPath = Path.Join(env?.ContentRootPath, "PipelineDiscounts");
        if (!Directory.Exists(pipeRateDiscountsFolderPath))
            Directory.CreateDirectory(pipeRateDiscountsFolderPath);
    }

    [Permission("Pipeline Rate Schedule", PermissionType.View)]
    [Route("/odata/GetPipeRateScheduleItems")]
    public async Task<IActionResult> GetItems(ODataQueryOptions<PipeRateScheduleListItem> queryOptions, bool isExport)
    {
        var itemsQueryable = GetItemsInternal();
        itemsQueryable = queryOptions.ApplyTo(itemsQueryable) as IQueryable<PipeRateScheduleListItem>;
        var items = itemsQueryable == null ? null : await itemsQueryable.ToListAsync();

        if (isExport)
            return File(Util.Excel.GetExportFileStream(items), "application/octet-stream");
        else
            return Ok(items);
    }

    private IQueryable<PipeRateScheduleListItem> GetItemsInternal()
    {
        IQueryable<PipeRateScheduleListItem>? itemsQueryable = null;

        itemsQueryable = (
            from q in db.PipelineRateSchedules
            join oi in db.VwPipeRateScheduleOverviewInfos on q.Id equals oi.PipeRateScheduleId into j1
            from oi in j1.DefaultIfEmpty()
            select new PipeRateScheduleListItem
            {
                Id = q.Id,
                ScheduleName = q.Name,
                Pipeline = q.Pipeline.Name ?? "",
                ContractType = q.ContractTypeNavigation.Name,
                Product = q.Product.Name,
            }
        ).AsNoTracking();

        return itemsQueryable;
    }

    [Permission("Pipeline Rate Schedule", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetRequiredData()
    {
        var hasModifyPermission = await authHelper.IsAuthorizedAsync(User, "Pipeline Rate Schedule", PermissionType.Modify);
        List<IdName>? counterparties = null, contractTypes = null, rateTypes = null, applicationRules = null, productsInitial = null;
        List<PipeInfo> pipelines = new();
        var zones = new[] { new { Id = 0, Name = "", PipelineId = 0 } }.ToList();
        var allZoneItem = new { Id = 0, Name = "ALL", PipelineId = 0 };
        var allCounterpartyItem = new IdName(0, "ALL");
        var months = DataHelper.GetMonths();
        List<MeterInfo>? meters = null;
        MeterInfo allMeterItem = new() { MeterId = 0, MeterName = "ALL", MeterNum = "ALL", PipeId = 0, ZoneId = 0 };

        var tasks = new List<Task>
        {
            Task.Run(async () => { hasModifyPermission = await authHelper.IsAuthorizedAsync(User, "Pipeline Contract", PermissionType.Modify); }),
            Task.Run(async () => { counterparties = (await DataHelper.GetCounterpartiesAsync(false, Enums.ProductCategory.All)).Select(x => new IdName(x.EntityId, x.FullName)).Prepend(allCounterpartyItem).ToList(); }),
            Task.Run(async () => {
                pipelines = (await DataHelper.GetPipelinesAsync(false))
                    .Select(x => new PipeInfo {
                        PipeId = x.PipeId,
                        PipeName = x.PipeName,
                        PipeShort = x.PipeShort,
                        IsCrudePipe = x.IsCrudePipe,
                        IsGasPipe = x.IsGasPipe,
                        ProductIds = x.ProductIds
                    }).ToList();
            }),
            Task.Run(async () => { zones = (await (from q in Main.CreateContext().Zones orderby q.Name select new { q.Id, q.Name, q.PipelineId }).AsNoTracking().ToListAsync()).Prepend(allZoneItem).ToList(); }),
            Task.Run(async () => { meters = (await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.All)).Prepend(allMeterItem).ToList(); }),
            Task.Run(async () => { contractTypes = await (from q in Main.CreateContext().PipelineContractTypes orderby q.Name select new IdName(q.Id, q.Name)).AsNoTracking().ToListAsync(); }),
            Task.Run(async () => { rateTypes = await (from q in Main.CreateContext().PipelineTariffTypes orderby q.Name select new IdName(q.Id, q.Name)).AsNoTracking().ToListAsync(); }),
            Task.Run(async () => { applicationRules = await (from q in Main.CreateContext().PipeRateApplicationRules orderby q.Name select new IdName(q.Id, q.Name ?? "")).AsNoTracking().ToListAsync(); }),
            Task.Run(async () => { productsInitial = await DataHelper.GetProductsAsync();})
        };
        await Task.WhenAll(tasks);

        var products = (
            from q in productsInitial
            where
                q.Id == (int)Enums.Product.NaturalGas
                || q.Id == (int)Enums.Product.CrudeOil
                || q.Id == (int)Enums.Product.Condensate
                || q.Id == (int)Enums.Product.Retrograde
            select q
        );
        var result = new { hasModifyPermission, counterparties, pipelines, zones, meters, contractTypes, rateTypes, applicationRules, months, products };

        return Ok(result);
    }

    [Permission("Pipeline Rate Schedule", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetDetail(int id)
    {
        DateOnly noEndDate = new(9999, 12, 31);
        var allItem = new List<int>() { 0 };

        var detail = await (
            from q in db.PipelineRateSchedules
            where q.Id == id
            select new PipeRateScheduleDetail
            {
                Id = q.Id,
                ScheduleName = q.Name,
                PipelineId = q.PipelineId,
                ContractTypeId = q.ContractType,
                ProductId = q.ProductId,
                Rates = q.PipelineTariffs.Select(rate => new PipeRateScheduleDetailRate
                {
                    Id = rate.Id,
                    RateTypeId = rate.TariffTypeId,
                    EffectiveDate = rate.EffectiveDate,
                    ApplicationRuleId = rate.ApplicationRuleId,
                    SingleRate = rate.TariffRate,
                    Documents = rate.PipeRateDocs.Select(x => new DocItem { FileNameOriginal = x.FileNameOriginal, FileNameOnDisk = x.FileNameOnDisk }).ToList(),
                    Notes = rate.Comments ?? "",
                    MultiRateData = rate.PipelineTariffData.Select(data => new PipeRateScheduleDetailRateData
                    {
                        OrderId = data.SeasonTableId,
                        StartMonth = data.SeasonStartMonth,
                        EndMonth = data.SeasonEndMonth,
                        FromZoneId = data.ZoneFromId,
                        ToZoneId = data.ZoneToId,
                        Rate = data.Rate
                    }).OrderBy(x => x.OrderId).ToList()
                }).ToList(),
                Discounts = q.PipelineRateDiscounteds.Select(disc => new PipeRateScheduleDetailDiscount
                {
                    Id = disc.Id,
                    RateTypeId = disc.TariffTypeId,
                    EffectiveDate = disc.EffectiveDate,
                    ExpirationDate = disc.EndDate == noEndDate ? null : disc.EndDate,
                    Rate = disc.Rate,
                    FromZoneIds = disc.AreAllFromZonesSelected ? allItem : disc.PipelineRateDiscountedFromTos.Where(x => x.IsFrom == true).Select(x => x.ZoneId).Distinct().ToList(),
                    FromMeterIds = disc.AreAllFromMetersSelected ? allItem : disc.PipelineRateDiscountedFromTos.Where(x => x.MeterId.HasValue && x.IsFrom == true).Distinct().Select(x => x.MeterId == null ? 0 : x.MeterId.Value).ToList(),
                    ToZoneIds = disc.AreAllToZonesSelected ? allItem : disc.PipelineRateDiscountedFromTos.Where(x => x.IsFrom == false).Select(x => x.ZoneId).Distinct().ToList(),
                    ToMeterIds = disc.AreAllToMetersSelected ? allItem : disc.PipelineRateDiscountedFromTos.Where(x => x.MeterId.HasValue && x.IsFrom == false).Distinct().Select(x => x.MeterId == null ? 0 : x.MeterId.Value).ToList(),
                    CounterpartyIds = disc.AreAllCounterpartySelected ? allItem : disc.PipelineRateDiscountedCounterparties.Select(x => x.EntityId).ToList(),
                    Documents = disc.PipeDiscountDocs.Select(x => new DocItem { FileNameOriginal = x.FileNameOriginal, FileNameOnDisk = x.FileNameOnDisk }).ToList(),
                    Notes = disc.Notes ?? ""
                }).ToList()
            }
        ).AsNoTracking().FirstAsync();

        return Ok(detail);
    }

    [Permission("Pipeline Rate Schedule", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveDetail(PipeRateScheduleDetail detail, Enums.SaveType saveType)
    {
        DateOnly noEndDate = new(9999, 12, 31);
        int resultId = 0;

        await db.Database.CreateExecutionStrategy().Execute(async () =>
        {
            using var dbContextTransaction = await db.Database.BeginTransactionAsync();
            PipelineRateSchedule? dbItem = null;
            if (saveType != Enums.SaveType.New)
            {
                dbItem = await (
                    from q in db.PipelineRateSchedules
                        .Include(x => x.PipelineTariffs)
                        .ThenInclude(x => x.PipeRateDocs)
                        .Include(x => x.PipelineTariffs)
                        .ThenInclude(x => x.PipelineTariffData)
                        .Include(x => x.PipelineRateDiscounteds)
                        .ThenInclude(x => x.PipeDiscountDocs)
                        .Include(x => x.PipelineRateDiscounteds)
                        .ThenInclude(x => x.PipelineRateDiscountedFromTos)
                        .Include(x => x.PipelineRateDiscounteds)
                        .ThenInclude(x => x.PipelineRateDiscountedCounterparties)
                    where q.Id == detail.Id
                    select q
                ).FirstOrDefaultAsync();
            }

            if (dbItem == null) //if the item does not exist then add it
            {
                dbItem = new PipelineRateSchedule();
                db.PipelineRateSchedules.Add(dbItem);
            }
            else
            {
                db.PipelineTariffs.RemoveRange(dbItem.PipelineTariffs);
                db.PipelineRateDiscountedFromTos.RemoveRange(dbItem.PipelineRateDiscounteds.SelectMany(x => x.PipelineRateDiscountedFromTos));
                db.PipelineRateDiscounteds.RemoveRange(dbItem.PipelineRateDiscounteds);
            }

            var d = detail;
            dbItem.Name = d.ScheduleName;
            dbItem.PipelineId = d.PipelineId;
            dbItem.ContractType = d.ContractTypeId;
            dbItem.ProductId = d.ProductId;

            foreach (var rate in d.Rates)
            {
                PipelineTariff t = new();
                t.ApplicationRuleId = rate.ApplicationRuleId;
                t.EffectiveDate = rate.EffectiveDate;

                //if a "Save New" is performed then we might have two different db records pointing to the same FileNameOnDisk
                //this is not a problem because files are only removed from the disk when they no longer have any references to them
                t.PipeRateDocs.Clear();
                var newDocs = rate.Documents?.Select(x => new PipeRateDoc
                {
                    FileNameOriginal = x.FileNameOriginal,
                    FileNameOnDisk = x.FileNameOnDisk
                }).ToList() ?? new List<PipeRateDoc>();
                foreach (var doc in newDocs)
                    t.PipeRateDocs.Add(doc);

                t.Comments = rate.Notes;
                t.TariffTypeId = rate.RateTypeId;
                t.TariffRate = rate.SingleRate;

                if (rate.RateTypeId != 4) //annual charge adjustment {
                {
                    int orderId = 1;
                    foreach (var rateData in rate.MultiRateData)
                    {
                        PipelineTariffDatum td = new();
                        td.SeasonTableId = orderId;
                        orderId++;
                        td.SeasonStartDay = 1;
                        td.SeasonStartMonth = rateData.StartMonth == null ? 1 : rateData.StartMonth.Value;
                        td.SeasonEndDay = 31;
                        td.SeasonEndMonth = rateData.EndMonth == null ? 12 : rateData.EndMonth.Value;
                        td.Rate = rateData.Rate;
                        td.ZoneFromId = rateData.FromZoneId == null ? 0 : rateData.FromZoneId.Value;
                        td.ZoneToId = rateData.ToZoneId == null ? 0 : rateData.ToZoneId.Value;

                        t.PipelineTariffData.Add(td);
                    }
                }
                dbItem.PipelineTariffs.Add(t);
            }

            var zoneIdsByMeterId = new Dictionary<int, int>();
            var allMeterIds = new List<int>();
            if (d.Discounts?.Any() == true)
            {
                var fromMeterIds = d.Discounts.Where(x => x.FromMeterIds != null).SelectMany(x => x.FromMeterIds).ToList();
                var toMeterIds = d.Discounts.Where(x => x.ToMeterIds != null).SelectMany(x => x.ToMeterIds).ToList();
                if (fromMeterIds?.Any() == true)
                    allMeterIds.AddRange(fromMeterIds);
                if (toMeterIds?.Any() == true)
                    allMeterIds.AddRange(toMeterIds);

                allMeterIds = allMeterIds.Distinct().ToList();
            }

            if (allMeterIds?.Any() == true)
            {
                if (detail.ProductId != (int)Enums.Product.NaturalGas
                    && detail.ProductId != (int)Enums.Product.CrudeOil
                    && detail.ProductId != (int)Enums.Product.Condensate
                    && detail.ProductId != (int)Enums.Product.Retrograde
                )
                    throw new NotImplementedException($"can't handle product id: {detail.ProductId}");

                var asOfDate = DateOnly.FromDateTime(DateTime.Now.Date);
                zoneIdsByMeterId = await (
                    from q in db.Meters
                    where allMeterIds.Contains(q.Id)
                    let mp = q.MeterProducts
                        .Where(x => x.ProductId == detail.ProductId && x.EffectiveDate <= asOfDate).OrderByDescending(x => x.EffectiveDate).FirstOrDefault()
                    select new { MeterId = q.Id, ProductId = mp.ProductId, SourceZoneId = mp.SourceZoneId ?? 0 }
                ).ToDictionaryAsync(x => x.MeterId, x => x.SourceZoneId);
            }

            if (d.Discounts != null)
            {
                foreach (var disc in d.Discounts)
                {
                    PipelineRateDiscounted prd = new();
                    prd.TariffTypeId = disc.RateTypeId;
                    prd.EffectiveDate = disc.EffectiveDate;
                    prd.EndDate = disc.ExpirationDate == null ? noEndDate : disc.ExpirationDate.Value;

                    //if a "Save New" is performed then we might have two different db records pointing to the same FileNameOnDisk
                    //this is not a problem because files are only removed from the disk when they no longer have any references to them
                    prd.PipeDiscountDocs.Clear();
                    var newDocs = disc.Documents?.Select(x => new PipeDiscountDoc
                    {
                        FileNameOriginal = x.FileNameOriginal,
                        FileNameOnDisk = x.FileNameOnDisk
                    }).ToList() ?? new List<PipeDiscountDoc>();
                    foreach (var doc in newDocs)
                        prd.PipeDiscountDocs.Add(doc);

                    prd.Notes = disc.Notes;
                    prd.Rate = disc.Rate;

                    if (disc.FromZoneIds != null && disc.FromZoneIds.Contains(0))
                        prd.AreAllFromZonesSelected = true;

                    if (disc.FromMeterIds != null && disc.FromMeterIds.Contains(0))
                        prd.AreAllFromMetersSelected = true;

                    if (disc.ToZoneIds != null && disc.ToZoneIds.Contains(0))
                        prd.AreAllToZonesSelected = true;

                    if (disc.ToMeterIds != null && disc.ToMeterIds.Contains(0))
                        prd.AreAllToMetersSelected = true;

                    if (disc.CounterpartyIds != null && disc.CounterpartyIds.Contains(0))
                        prd.AreAllCounterpartySelected = true;

                    var normalFromMeterIds = new List<int>();
                    if (!prd.AreAllFromMetersSelected && disc.FromMeterIds?.Any() == true)
                        normalFromMeterIds = disc.FromMeterIds.ToList();

                    foreach (var meterId in normalFromMeterIds)
                    {
                        PipelineRateDiscountedFromTo prdFt = new();
                        prdFt.MeterId = meterId;
                        prdFt.IsFrom = true;
                        prdFt.ZoneId = zoneIdsByMeterId[meterId];
                        prd.PipelineRateDiscountedFromTos.Add(prdFt);
                    }

                    var normalToMeterIds = new List<int>();
                    if (!prd.AreAllToMetersSelected && disc.ToMeterIds?.Any() == true)
                        normalToMeterIds = disc.ToMeterIds.ToList();

                    foreach (var meterId in normalToMeterIds)
                    {
                        PipelineRateDiscountedFromTo prdFt = new();
                        prdFt.MeterId = meterId;
                        prdFt.IsFrom = false;
                        prdFt.ZoneId = zoneIdsByMeterId[meterId];
                        prd.PipelineRateDiscountedFromTos.Add(prdFt);
                    }

                    var zoneIdsAlreadyPushed = prd.PipelineRateDiscountedFromTos.Select(x => x.ZoneId).ToHashSet();

                    var normalFromZoneIdsWithoutMeters = new List<int>();
                    if (!prd.AreAllFromZonesSelected && disc.FromZoneIds?.Any() == true)
                        normalFromZoneIdsWithoutMeters = disc.FromZoneIds.Where(x => !zoneIdsAlreadyPushed.Contains(x)).ToList();

                    foreach (var zoneId in normalFromZoneIdsWithoutMeters)
                    {
                        PipelineRateDiscountedFromTo prdFt = new();
                        prdFt.MeterId = null;
                        prdFt.IsFrom = true;
                        prdFt.ZoneId = zoneId;
                        prd.PipelineRateDiscountedFromTos.Add(prdFt);
                    }

                    var normalToZoneIdsWithoutMeters = new List<int>();
                    if (!prd.AreAllToZonesSelected && disc.ToZoneIds?.Any() == true)
                        normalToZoneIdsWithoutMeters = disc.ToZoneIds.Where(x => !zoneIdsAlreadyPushed.Contains(x)).ToList();

                    foreach (var zoneId in normalToZoneIdsWithoutMeters)
                    {
                        PipelineRateDiscountedFromTo prdFt = new();
                        prdFt.MeterId = null;
                        prdFt.IsFrom = false;
                        prdFt.ZoneId = zoneId;
                        prd.PipelineRateDiscountedFromTos.Add(prdFt);
                    }

                    var normalCounterpartyIds = new List<int>();
                    if (!prd.AreAllCounterpartySelected && disc.CounterpartyIds?.Any() == true)
                        normalCounterpartyIds = disc.CounterpartyIds.ToList();

                    foreach (var counterpartyId in normalCounterpartyIds)
                    {
                        PipelineRateDiscountedCounterparty prdC = new();
                        prdC.EntityId = counterpartyId;
                        prd.PipelineRateDiscountedCounterparties.Add(prdC);
                    }

                    dbItem.PipelineRateDiscounteds.Add(prd);
                }
            }

            await db.SaveChangesAsync();
            resultId = dbItem.Id;

            await dbContextTransaction.CommitAsync();
        });

        return Ok(resultId);
    }

    [Permission("Pipeline Rate Schedule", PermissionType.Modify)]
    [Route("[action]/{id}")]
    public async Task<IActionResult> DeleteDetail(int id)
    {
        PipelineRateSchedule dbItem = await db.PipelineRateSchedules.Where(x => x.Id == id).FirstAsync();
        db.Entry(dbItem).State = EntityState.Deleted;
        await db.SaveChangesAsync();

        return Ok();
    }

    public enum RateDocType
    {
        RateDoc = 1,
        RateDiscountDoc = 2
    }

    [Permission("Pipeline Rate Schedule", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> UploadDoc(IEnumerable<IFormFile> files, RateDocType rateDocType, [FromODataUri] string metaData)
    {
        var docItems = new List<DocItem>();
        using var ms = new MemoryStream(Encoding.UTF8.GetBytes(metaData));
        var serializer = new DataContractJsonSerializer(typeof(ChunkMetaData));
        var chunkData = serializer.ReadObject(ms) as ChunkMetaData;
        if (chunkData == null)
            return BadRequest("Invalid metadata.");

        string? fileNameOnDisk = null;
        string subFolder = rateDocType == RateDocType.RateDoc ? "PipelineRates" : "PipelineDiscounts";
        if (files != null)
        {
            foreach (var file in files)
            {
                if (file.Length > 0)
                {
                    fileNameOnDisk = await Util.File.SaveFileAsync(env.ContentRootPath, subFolder, file);
                }
            }
        }

        if (!string.IsNullOrEmpty(fileNameOnDisk) &&
            chunkData.ChunkIndex == chunkData.TotalChunks - 1)
        {
            docItems.Add(new DocItem
            {
                FileNameOriginal = chunkData.FileName,
                FileNameOnDisk = fileNameOnDisk
            });
            return Ok(docItems);
        }

        return Ok();
    }

    [Permission("Pipeline Rate Schedule", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> DownloadDoc(string fileNameOnDisk, RateDocType rateDocType)
    {
        try
        {
            string subFolder = rateDocType == RateDocType.RateDoc ? "PipelineRates" : "PipelineDiscounts";
            var downloadResponse = await Util.File.GetFileAsync(env.ContentRootPath, subFolder, fileNameOnDisk);

            return File(downloadResponse.Stream, downloadResponse.ContentType, downloadResponse.FileName);
        }
        catch (Exception ex)
        {
            string rateDocTypeStr = rateDocType == RateDocType.RateDoc
                ? "Rate Doc"
                : "Rate Discount Doc";
            string messagePrefix = $"Rate Doc Type: {rateDocTypeStr}";
            var bytes = Util.GetExceptionFilesBytes(ex, messagePrefix);
            return File(bytes, "text/plain");
        }
    }
}
