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 PipeContractController : ODataController
{
    private readonly MyDbContext db;
    private readonly AuthorizationHelper authHelper;
    private readonly string pipeContractsFolderPath;
    private readonly IWebHostEnvironment env;

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

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

    [Permission("Pipeline Contract", PermissionType.View)]
    [Route("/odata/GetPipeContractItems")]
    public async Task<IActionResult> GetItems(ODataQueryOptions<PipeContractListItem> queryOptions, bool isExport)
    {
        var itemsQueryable = GetItemsInternal();
        itemsQueryable = queryOptions.ApplyTo(itemsQueryable) as IQueryable<PipeContractListItem>;
        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<PipeContractListItem> GetItemsInternal()
    {
        DateOnly noStartDate = new(1900, 1, 1);
        DateOnly noEndDate = new(9999, 12, 31);
        IQueryable<PipeContractListItem>? itemsQueryable = null;

        itemsQueryable = (
            from q in db.PipelineContracts
            join oi in db.VwPipeContractOverviewInfos on q.Id equals oi.PipeContractId into j1
            from oi in j1.DefaultIfEmpty()
            join cd in db.PipelineContractDefaults
                on new { PipeContractId = q.Id, q.ProductId, q.PipelineId }
                equals new { cd.PipeContractId, cd.ProductId, cd.PipelineId } into j2
            from cd in j2.DefaultIfEmpty()
            select new PipeContractListItem
            {
                Id = q.Id,
                ContractNum = q.ContractId,
                StartDate = q.StartDate == null ? noStartDate : q.StartDate.Value,
                EndDate = q.EndDate == noEndDate ? null : q.EndDate,
                Pipeline = q.Pipeline.Name ?? "",
                ContractType = q.ContractType == null ? "" : q.ContractType.Name,
                RateSchedule = q.RateSchedule == null ? "" : q.RateSchedule.Name,
                Shipper = q.InternalEntity == null ? "" : q.InternalEntity.Name,
                ContractOwner = q.ContractOwner == null ? "" : q.ContractOwner.Name,
                CapacityRelease = q.IsCapacityRelease ? "yes" : "no",
                CapacityCounterparty = q.Counterparty == null ? "" : q.Counterparty.Name,
                ReceiptZones = oi.ReceiptZones ?? "",
                ReceiptMeters = oi.ReceiptMeters ?? "",
                DeliveryZones = oi.DeliveryZones ?? "",
                DeliveryMeters = oi.DeliveryMeters ?? "",
                IsDefaultContract = cd == null ? "no" : "yes",
                IsPtrContract = q.IsPtrContract ? "yes" : "no",
                IsDefaultPtrContract = q.Pipeline.DefaultPtrPipelineContractId == q.Id ? "yes" : "no",
                Product = q.Product.Name
            }
        ).AsNoTracking();

        return itemsQueryable;
    }

    [Permission("Pipeline Contract", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetRequiredData()
    {

        bool hasModifyPermission = false;
        List<IdName>? counterparties = null, contractTypes = null, productsInitial = null;
        List<PipeInfo> pipelines = new();
        var rateSchedules = new[] { new { Id = 0, Name = "", PipelineId = 0, ContractType = 0, ProductId = 0 } }.ToList();
        var zones = new[] { new { Id = 0, Name = "", PipelineId = 0 } }.ToList();
        var allZoneItem = new { Id = 0, Name = "ALL", PipelineId = 0 };
        List<MeterInfo>? meters = null;
        MeterInfo allMeterItem = new() { MeterId = 0, MeterName = "ALL", MeterNum = "ALL", PipeId = 0, ZoneId = 0 };
        var rateUnits = new List<IdName> { new(1, "Daily"), new(2, "Monthly") };

        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)).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 () => { rateSchedules = await (from q in Main.CreateContext().PipelineRateSchedules orderby q.Name select new { q.Id, q.Name, q.PipelineId, q.ContractType, q.ProductId }).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, rateSchedules, rateUnits, products };

        return Ok(result);
    }

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

        var detail = await (
            from q in db.PipelineContracts
            where q.Id == id
            join cd in db.PipelineContractDefaults
                on new { PipeContractId = q.Id, q.ProductId, q.PipelineId }
                equals new { cd.PipeContractId, cd.ProductId, cd.PipelineId } into j2
            from cd in j2.DefaultIfEmpty()
            select new PipeContractDetail
            {
                Id = q.Id,
                ContractNum = q.ContractId,
                StartDate = q.StartDate == null ? noStartDate : q.StartDate.Value,
                EndDate = q.EndDate == noEndDate ? null : q.EndDate,
                IsDefaultContract = cd != null,
                PipelineId = q.PipelineId,
                ContractTypeId = q.ContractTypeId == null ? 0 : q.ContractTypeId.Value,
                RateScheduleId = q.RateScheduleId == null ? 0 : q.RateScheduleId,
                ShipperId = q.InternalEntityId == null ? 0 : q.InternalEntityId.Value,
                ContractOwnerId = q.ContractOwnerId == null ? 0 : q.ContractOwnerId.Value,
                CapacityIsRelease = q.IsCapacityRelease,
                CapacityIsBuy = q.BuySell,
                CapacityCounterpartyId = q.CounterpartyId,
                CapacityDemandCharge = q.DemandCharge,
                CapacityRateUnitId = q.RateUnitTimeOption == null ? 0 : q.RateUnitTimeOption.Value,
                ReceiptZoneIds = q.IsAllReceiptZone ? allItem : q.PipeContractReceiptZones.Select(x => x.ZoneId).ToList(),
                ReceiptMeterIds = q.IsAllReceiptMeter ? allItem : q.PipeContractReceiptMeters.Select(x => x.MeterId).ToList(),
                DeliveryZoneIds = q.IsAllDeliveryZone ? allItem : q.PipeContractDeliveryZones.Select(x => x.ZoneId).ToList(),
                DeliveryMeterIds = q.IsAllDeliveryMeter ? allItem : q.PipeContractDeliveryMeters.Select(x => x.MeterId).ToList(),
                IsPtrContract = q.IsPtrContract,
                IsDefaultPtrContract = q.Pipeline.DefaultPtrPipelineContractId == q.Id,
                Documents = q.PipeContractDocs.Select(x => new DocItem { FileNameOriginal = x.FileNameOriginal, FileNameOnDisk = x.FileNameOnDisk }).ToList(),
                Notes = q.Comments ?? "",
                ProductId = q.ProductId
            }
        ).AsNoTracking().FirstAsync();

        return Ok(detail);
    }

    [Permission("Pipeline Contract", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveDetail(PipeContractDetail 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();
            PipelineContract? dbItem = null;
            if (saveType != Enums.SaveType.New)
            {
                dbItem = await (
                    from q in db.PipelineContracts
                        .Include(x => x.PipeContractDocs)
                        .Include(x => x.PipeContractReceiptZones)
                        .Include(x => x.PipeContractReceiptMeters)
                        .Include(x => x.PipeContractDeliveryZones)
                        .Include(x => x.PipeContractDeliveryMeters)
                        .Include(x => x.PipeContractDocs)
                    where q.Id == detail.Id
                    select q
                ).FirstOrDefaultAsync();
            }

            if (dbItem == null) //if the item does not exist then add it
            {
                dbItem = new PipelineContract();
                db.PipelineContracts.Add(dbItem);
            }
            else
            {
                //remove existing items so that they get completely re-inserted
                db.PipeContractDocs.RemoveRange(dbItem.PipeContractDocs);
                db.PipeContractReceiptZones.RemoveRange(dbItem.PipeContractReceiptZones);
                db.PipeContractReceiptMeters.RemoveRange(dbItem.PipeContractReceiptMeters);
                db.PipeContractDeliveryZones.RemoveRange(dbItem.PipeContractDeliveryZones);
                db.PipeContractDeliveryMeters.RemoveRange(dbItem.PipeContractDeliveryMeters);

                var oldDefaults = await db.PipelineContractDefaults.Where(x => x.PipeContractId == dbItem.Id).ToListAsync();
                db.PipelineContractDefaults.RemoveRange(oldDefaults);
            }

            var d = detail;
            dbItem.ContractId = d.ContractNum;
            dbItem.StartDate = d.StartDate;
            dbItem.EndDate = d.EndDate == null ? noEndDate : d.EndDate.Value;
            dbItem.PipelineId = d.PipelineId;
            dbItem.ContractTypeId = d.ContractTypeId;
            dbItem.RateScheduleId = d.RateScheduleId;
            dbItem.InternalEntityId = d.ShipperId;
            dbItem.ContractOwnerId = d.ContractOwnerId;
            dbItem.IsCapacityRelease = d.CapacityIsRelease;
            dbItem.BuySell = d.CapacityIsBuy;
            dbItem.CounterpartyId = d.CapacityCounterpartyId;
            dbItem.DemandCharge = d.CapacityDemandCharge;
            dbItem.RateUnitTimeOption = d.CapacityRateUnitId;
            dbItem.ProductId = d.ProductId;

            if (d.ReceiptZoneIds != null && d.ReceiptZoneIds.Contains(0))
            {
                dbItem.IsAllReceiptZone = true;
                dbItem.ReceiptZoneIdList = "ALL";
                dbItem.PipeContractReceiptZones.Clear();
            }
            else
            {
                dbItem.IsAllReceiptZone = false;
                dbItem.PipeContractReceiptZones.Clear();
                var newReceiptZones = d.ReceiptZoneIds?.Select(x => new PipeContractReceiptZone { PipeContractId = d.Id, ZoneId = x }).ToList() ?? new List<PipeContractReceiptZone>();
                foreach (var item in newReceiptZones)
                    dbItem.PipeContractReceiptZones.Add(item);
                dbItem.ReceiptZoneIdList = d.ReceiptZoneIds == null ? null : string.Join(',', d.ReceiptZoneIds);
            }

            if (d.ReceiptMeterIds != null && d.ReceiptMeterIds.Contains(0))
            {
                dbItem.IsAllReceiptMeter = true;
                dbItem.ReceiptMeterIdList = "ALL";
                dbItem.PipeContractReceiptMeters.Clear();
            }
            else
            {
                dbItem.IsAllReceiptMeter = false;
                dbItem.PipeContractReceiptMeters.Clear();
                var newReceiptMeters = d.ReceiptMeterIds?.Select(x => new PipeContractReceiptMeter { PipeContractId = d.Id, MeterId = x }).ToList() ?? new List<PipeContractReceiptMeter>();
                foreach (var item in newReceiptMeters)
                    dbItem.PipeContractReceiptMeters.Add(item);
                dbItem.ReceiptMeterIdList = d.ReceiptMeterIds?.Any() == true ? string.Join(',', d.ReceiptMeterIds) : null;
            }

            if (d.DeliveryZoneIds != null && d.DeliveryZoneIds.Contains(0))
            {
                dbItem.IsAllDeliveryZone = true;
                dbItem.DeliveryZoneIdList = "ALL";
                dbItem.PipeContractDeliveryZones.Clear();
            }
            else
            {
                dbItem.IsAllDeliveryZone = false;
                dbItem.PipeContractDeliveryZones.Clear();
                var newDeliveryZones = d.DeliveryZoneIds?.Select(x => new PipeContractDeliveryZone { PipeContractId = d.Id, ZoneId = x }).ToList() ?? new List<PipeContractDeliveryZone>();
                foreach (var item in newDeliveryZones)
                    dbItem.PipeContractDeliveryZones.Add(item);
                dbItem.DeliveryZoneIdList = d.DeliveryZoneIds?.Any() == true ? string.Join(',', d.DeliveryZoneIds) : null;
            }

            if (d.DeliveryMeterIds != null && d.DeliveryMeterIds.Contains(0))
            {
                dbItem.IsAllDeliveryMeter = true;
                dbItem.DeliveryMeterIdList = "ALL";
                dbItem.PipeContractDeliveryMeters.Clear();
            }
            else
            {
                dbItem.IsAllDeliveryMeter = false;
                dbItem.PipeContractDeliveryMeters.Clear();
                var newDeliveryMeters = d.DeliveryMeterIds?.Select(x => new PipeContractDeliveryMeter { PipeContractId = d.Id, MeterId = x }).ToList() ?? new List<PipeContractDeliveryMeter>();
                foreach (var item in newDeliveryMeters)
                    dbItem.PipeContractDeliveryMeters.Add(item);
                dbItem.DeliveryMeterIdList = d.DeliveryMeterIds?.Any() == true ? string.Join(',', d.DeliveryMeterIds) : null;
            }

            dbItem.IsPtrContract = d.IsPtrContract;
            //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
            dbItem.PipeContractDocs.Clear();
            var newDocs = d.Documents?.Select(x => new PipeContractDoc
            {
                FileNameOriginal = x.FileNameOriginal,
                FileNameOnDisk = x.FileNameOnDisk
            }).ToList() ?? new List<PipeContractDoc>();
            foreach (var item in newDocs)
                dbItem.PipeContractDocs.Add(item);

            dbItem.Comments = d.Notes;
            await db.SaveChangesAsync();
            resultId = dbItem.Id;
            var defaultPipelineContractId = dbItem.Id;

            if (d.IsDefaultContract)
            {
                var oldDefaults = await (
                    from q in db.PipelineContractDefaults
                    where q.PipelineId == d.PipelineId
                        && q.ProductId == d.ProductId
                    select q
                ).ToListAsync();
                db.PipelineContractDefaults.RemoveRange(oldDefaults);

                var newDefault = new PipelineContractDefault();
                newDefault.PipelineId = d.PipelineId;
                newDefault.ProductId = d.ProductId;
                newDefault.PipeContractId = defaultPipelineContractId;
                db.PipelineContractDefaults.Add(newDefault);

                await db.SaveChangesAsync();
            }

            FormattableString sqlPtr;
            if (d.IsDefaultPtrContract)
                sqlPtr = @$"update pipeline set default_ptr_pipeline_contract_id = null where id = {d.PipelineId} or default_ptr_pipeline_contract_id = {defaultPipelineContractId}
                         update pipeline set default_ptr_pipeline_contract_id = {defaultPipelineContractId} where ID = {d.PipelineId}";
            else
                sqlPtr = @$"update pipeline set default_ptr_pipeline_contract_id = null where default_ptr_pipeline_contract_id = {defaultPipelineContractId}";
            await db.Database.ExecuteSqlInterpolatedAsync(sqlPtr);

            await dbContextTransaction.CommitAsync();
        });

        return Ok(resultId);
    }

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

        return Ok();
    }

    [Permission("Pipeline Contract", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> UploadDoc(IEnumerable<IFormFile> files, [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 = "PipelineContracts";
        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 Contract", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> DownloadDoc(string fileNameOnDisk)
    {
        try
        {
            string subFolder = "PipelineContracts";
            var downloadResponse = await Util.File.GetFileAsync(env.ContentRootPath, subFolder, fileNameOnDisk);

            return File(downloadResponse.Stream, downloadResponse.ContentType, downloadResponse.FileName);
        }
        catch (Exception ex)
        {
            const string prefix = "PipelineContract";
            var bytes = Util.GetExceptionFilesBytes(ex, prefix);
            return File(bytes, "text/plain");
        }
    }
}
