using System.Collections.Concurrent;
using System.Net.Mail;
using Fast.Logic.CrudeOil;
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 CrudeInvoiceDistributeController : ODataController
{

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

    public CrudeInvoiceDistributeController(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 Crude", 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.VwInvoiceCrudes
            where selectedIds.Contains(q.Id) && q.IsApproved == "yes"
            orderby q.Month, q.InternalEntity, q.Counterparty
            select new InvoiceCrudeGridItem
            {
                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 Crude", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> Distribute([FromBody] InvoiceSelection[] invoiceSelections)
    {
        var result = new DistributeResponse();
        var concurrentMessages = new ConcurrentBag<string>();

        try
        {
            var invoiceSelectionIds = invoiceSelections.Select(x => x.id).Distinct().ToList();

            // Filter unapproved
            var unapprovedInvoices = await (
                from q in db.InvoiceCrudes
                where invoiceSelectionIds.Contains(q.Id)
                let hasLines = q.InvoiceCrudeLines.Count > 0
                let allApproved = q.InvoiceCrudeLines.All(x => x.ApprovedBy.HasValue)
                where !hasLines || !allApproved
                select q
            ).AsNoTracking().ToListAsync();

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

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

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

            var draftInvoices = await db.InvoiceCrudes
                .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);

            // Retrieve files
            var invoices = (
                from i in db.InvoiceCrudes
                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 invoiceInfo = invoices.Select(i => new
            {
                i.Id,
                i.InvoiceNum,
                i.Month,
                FileName = i.FileNameOnDisk,
                i.Counterparty,
                i.EmailAddresses
            }).ToList();

            // Warn about missing emails
            foreach (var inv in invoiceInfo.Where(x => string.IsNullOrWhiteSpace(x.EmailAddresses)))
                concurrentMessages.Add($"Error processing {inv.InvoiceNum}: missing email address");

            // Group for email send
            var emailGroups = invoiceInfo
                .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)
                        {
                            if (string.IsNullOrWhiteSpace(inv.FileName))
                                continue;

                            string crudeSubfolder = "Invoices/Crude";
                            var fileResponse = await fileService.DownloadFileAsync(crudeSubfolder, inv.FileName);
                            using var memoryStream = new MemoryStream();
                            await fileResponse.Stream.CopyToAsync(memoryStream, _);
                            pdfFiles.Add((inv.FileName, memoryStream.ToArray()));
                        }

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

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

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

                        string sendTo = group.Key.EmailAddresses;
                        string sentTo = await Task.Run(() => emailer.SendEmail(sendTo, subject, body, attachment));
                        concurrentMessages.Add($"Sent {invoiceNums} to {sentTo}");

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

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

        // Prepare response
        var ordered = concurrentMessages.OrderByDescending(x => x.Contains("Error"));
        var sb = new StringBuilder();
        bool hasErrors = ordered.Any(x => x.Contains("Error"));

        sb.AppendLine(hasErrors ? "Some files failed. See details below." : "All files sent successfully.")
          .AppendLine();

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

        return Ok(new DistributeResponse
        {
            hasErrors = hasErrors,
            message = sb.ToString()
        });
    }

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