using System.Collections.Concurrent;
using System.Net.Mail;
using Fast.Logic.NaturalGas;
using Fast.Shared.Logic.FileService;
using Fast.Shared.Logic.Package;
using static Fast.Shared.Logic.Util.String;

namespace Fast.Web.Controllers;


[Authorize]
[ApiController]
[Route("api/[controller]")]
public class InvoiceDistributeController : ODataController
{

    private readonly MyDbContext db;
    private readonly AuthorizationHelper authHelper;
    private readonly FileService fileService;

    public InvoiceDistributeController(MyDbContext context, IWebHostEnvironment env)
    {
        db = context;
        authHelper = new AuthorizationHelper(Main.IsAuthenticationEnabled);
        var config = new FileServiceConfig(env.ContentRootPath);
        fileService = new FileService(config);
    }

    private class DistributeResponse
    {
        public bool hasErrors = false;
        public string? message;
    }

    public class InvoiceSelection
    {
        public int id;
    }

    [Permission("Invoice Natural Gas", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetItems(List<int> mySelection)
    {
        var selectedIds = mySelection.Select(x => (int?)x).ToList();

        var invoiceItems = await (
            from q in db.VwInvoiceGas
            where selectedIds.Contains(q.Id) && q.IsApproved == "yes"
            orderby q.Month, q.InternalEntity, q.Counterparty
            select new InvoiceGasGridItem
            {
                Id = q.Id,
                InvoiceNum = q.InvoiceNum,
                Month = q.Month!.Value,
                InternalEntityId = q.InternalEntityId!.Value,
                InternalEntity = q.InternalEntity!,
                CounterpartyId = q.CounterpartyId!.Value,
                Counterparty = q.Counterparty!,
                InvoiceType = q.InvoiceType!,
                DueDate = q.DueDate,
                FileNameOriginal = q.FileNameOriginal,
                FileNameOnDisk = q.FileNameOnDisk,
                IsApproved = q.IsApproved ?? "no",
                EmailedByName = q.EmailedByName,
                EmailedTime = q.EmailedTime,
                CounterpartyEmailAddresses = q.CounterpartyEmailAddresses,
                IsChecked = false
            }
        ).ToListAsync();

        return Ok(invoiceItems);
    }

    [Permission("Invoice Natural Gas", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> Distribute([FromBody] InvoiceSelection[] invoiceSelections)
    {
        var result = new DistributeResponse { hasErrors = false, message = "" };
        ConcurrentBag<string> concurrentMessages = new();

        try
        {
            // check for unapproved invoices
            var invoiceSelectionIds = invoiceSelections.Select(x => x.id).Distinct().Order().ToList();
            var unapprovedInvoices = await (
                from q in db.InvoiceGas
                where invoiceSelectionIds.Contains(q.Id)
                let hasDetailLines = q.InvoiceGasLines.Count > 0
                let allDetailLinesApproved = q.InvoiceGasLines.All(x => x.ApprovedBy.HasValue)
                let isInvoiceApproved = hasDetailLines && allDetailLinesApproved
                where !isInvoiceApproved
                select q
            ).AsNoTracking().ToListAsync();

            if (unapprovedInvoices.Count > 0)
            {
                var unapprovedInvoiceIds = unapprovedInvoices.Select(x => x.Id).ToList();
                invoiceSelections = invoiceSelections.Where(x => !unapprovedInvoiceIds.Contains(x.id)).ToArray();

                foreach (var invoice in unapprovedInvoices)
                {
                    concurrentMessages.Add($"Error processing {invoice.InvoiceNum}: invoice not approved");
                }
            }

            var invoiceIds = invoiceSelections.OrderBy(x => x.id).Select(x => x.id).ToArray();

            var draftInvoices = await db.InvoiceGas
                .Where(i => invoiceIds.Contains(i.Id) && i.InvoiceNum!.StartsWith("DRAFT"))
                .ToListAsync();

            foreach (var invoice in draftInvoices)
            {
                // Use a transaction to ensure Final InvoiceNum and Document Generation changes are atomic
                await db.Database.CreateExecutionStrategy().Execute(async () =>
                {
                    using var transaction = await db.Database.BeginTransactionAsync();
                    try
                    {
                        if (string.IsNullOrWhiteSpace(invoice.CounterpartyEmailAddresses))
                            invoice.InvoiceNum = invoice.InvoiceNum;
                        else
                            invoice.InvoiceNum = await GetNewFinalInvoiceNumAsync();

                        await db.SaveChangesAsync();

                        var invoiceDoc = new InvoiceDoc(fileService, "Invoices", "InvoiceTemplates", "Signatures", db);
                        var (fileNameOriginal, fileNameOnDisk) = await invoiceDoc.GenerateAsync(invoice.InvoiceNum);
                        invoice.FileNameOriginal = fileNameOriginal;
                        invoice.FileNameOnDisk = fileNameOnDisk;
                        await db.SaveChangesAsync();
                        await transaction.CommitAsync();
                    }
                    catch (Exception ex)
                    {
                        await transaction.RollbackAsync();
                        throw new Exception($"Error processing {invoice.InvoiceNum}: {GetExceptionMessage(ex)}");
                    }
                });
            }

            var appUserId = Util.GetAppUserId(User);

            var invoices = (
                from i in db.InvoiceGas
                where invoiceIds.Contains(i.Id)
                select new
                {
                    i.Id,
                    i.InvoiceNum,
                    i.Month,
                    i.FileNameOnDisk,
                    Counterparty = i.Counterparty.Name,
                    EmailAddresses = i.CounterpartyEmailAddresses
                }
            ).AsNoTracking().ToList();

            var invoiceList = invoices.Select(i => new
            {
                i.Id,
                i.InvoiceNum,
                i.Month,
                i.FileNameOnDisk,
                i.Counterparty,
                i.EmailAddresses
            }).ToList();

            foreach (var inv in invoiceList.Where(x => string.IsNullOrWhiteSpace(x.EmailAddresses)))
            {
                concurrentMessages.Add(
                    $"Error processing {inv.InvoiceNum}: this invoice is missing an email address");
            }

            // Email groups
            var emailGroups = invoiceList
                .Where(x => !string.IsNullOrWhiteSpace(x.EmailAddresses))
                .GroupBy(x => new { x.Month, x.Counterparty, x.EmailAddresses });

            await Parallel.ForEachAsync(emailGroups, new ParallelOptions { MaxDegreeOfParallelism = 2 },
                async (group, _) =>
                {
                    var invoiceNums = string.Join(",", group.Select(x => x.InvoiceNum));

                    try
                    {
                        var pdfFiles = new List<(string fileName, byte[] bytes)>();
                        foreach (var inv in group)
                        {
                            var fileName = inv.FileNameOnDisk;
                            if (string.IsNullOrWhiteSpace(fileName))
                                continue;

                            string gasSubfolder = "Invoices/Gas";
                            var fileResponse = await fileService.DownloadFileAsync(gasSubfolder, fileName);
                            using var memoryStream = new MemoryStream();
                            await fileResponse.Stream.CopyToAsync(memoryStream, _);
                            pdfFiles.Add((fileName, memoryStream.ToArray()));
                        }

                        if (pdfFiles.Count == 0)
                        {
                            concurrentMessages.Add($"Error processing {invoiceNums}: invoice file not found");
                            return;
                        }

                        using var combinedStream = await PdfConverter.MergePdfsFromBytes(pdfFiles);
                        var monthStr = string.Format("{0:MMMM yyyy}", group.Key.Month);
                        string subject = $"SUPERIOR - {group.Key.Counterparty} - {monthStr} Invoice - {invoiceNums}";
                        string body = subject + Environment.NewLine;
                        var combinedFileName = GetLegalFileName($"{subject}.pdf");
                        var attachment = new Attachment(combinedStream, combinedFileName);

                        using var context = Main.CreateContext();
                        var email = new Util.Email(context, Enums.NotificationType.Invoices);

                        string emailToAddresses = group.Key.EmailAddresses;
                        string sentTo = await Task.Run(() =>
                            email.SendEmail(emailToAddresses, subject, body, attachment));

                        concurrentMessages.Add($"Sent {invoiceNums} to {sentTo}");

                        foreach (var inv in group)
                        {
                            var dbInv = context.InvoiceGas.First(x => x.Id == inv.Id);
                            dbInv.EmailedBy = appUserId;
                            dbInv.EmailedTime = DateTime.UtcNow;
                        }

                        context.SaveChanges();
                    }
                    catch (Exception ex)
                    {
                        concurrentMessages.Add($"Error processing {invoiceNums}: {GetExceptionMessage(ex)}");
                    }
                });
        }
        catch (Exception ex)
        {
            concurrentMessages.Add($"General error: {GetExceptionMessage(ex)}");
        }

        var orderedMessages = concurrentMessages.OrderByDescending(x => x.Contains("Error"));
        result.hasErrors = orderedMessages.Any(x => x.Contains("Error"));

        var sb = new StringBuilder();
        if (!result.hasErrors)
            sb.AppendLine("All files sent successfully.").AppendLine();
        else
            sb.AppendLine("Some files failed. See details below.").AppendLine();

        foreach (var msg in orderedMessages)
            sb.AppendLine(msg);

        result.message = sb.ToString();

        return Ok(result);
    }

    private async Task<string> GetNewFinalInvoiceNumAsync()
    {
        return await Util.GetNewDealNumAsync(15, db);
    }
}
