using System.Text.RegularExpressions;
using DocumentFormat.OpenXml.Wordprocessing;
using Fast.Shared.Logic.FileService;
using Fast.Shared.Logic.Package;
using static Fast.Shared.Models.Enums;

namespace Fast.Logic.NaturalGas;

public partial class InvoiceDoc(IFileService fileService, string filesFolderName, string templatesFolderName, string signaturesFolderName, MyDbContext db)
    : DocBaseOpenXml(fileService, filesFolderName, templatesFolderName, signaturesFolderName, string.Empty, true)
{
    private const int ColIndexLineNo = 0;
    private const int ColIndexDealNo = 1;
    private const int ColIndexFlowDays = 2;
    private const int ColIndexPipeline = 3;
    private const int ColIndexDeliveryMeter = 4;
    private const int ColIndexQuantity = 5;
    private const int ColIndexPrice = 6;
    private const int ColIndexAdder = 7;
    private const int ColIndexInvoicePrice = 8;
    private const int ColIndexAmount = 9;

    protected override string TemplateFileName => "GasInvoiceTemplate.docx";

    public async Task<(string fileNameOriginal, string fileNameOnDisk)> GenerateAsync(string invoiceNum)
    {
        // Get the invoice from the database without tracking so that we don't modify data in the database
        var invoice = (
            from q in db.InvoiceGas
                .Include(x => x.InvoiceGasLines)
                    .ThenInclude(x => x.Deal)
                .Include(x => x.CounterpartyState)
                .Include(x => x.InternalEntityState)
                .Include(x => x.ContractPaymentType)
                .Include(x => x.Counterparty)
                .Include(x => x.InternalEntity)
                .Include(x => x.InvoiceType)
            where q.InvoiceNum == invoiceNum
            select q
        ).AsNoTracking().First();

        var internalEntityName = invoice.InternalEntity.Name;
        var counterpartyName = invoice.Counterparty.Name;
        var internalInvoiceAddress = invoice.InternalEntityAddressLine1 + " " + invoice.InternalEntityAddressLine2;
        var internalCityStateZip = GetCityStateZipStr(invoice.InternalEntityCity, invoice.InternalEntityState?.Name, invoice.InternalEntityZip);
        var counterpartyAddress = invoice.CounterpartyAddressLine1 + " " + invoice.CounterpartyAddressLine2;
        var counterpartyCityStateZip = GetCityStateZipStr(invoice.CounterpartyCity, invoice.CounterpartyState?.Name, invoice.CounterpartyZip);
        var customerNum = invoice.Counterparty.InternalCustomerNum;
        var paymentType = invoice.ContractPaymentType == null ? "" :
            invoice.ContractPaymentType.IsWire ? "Wire" : "ACH";

        // Fill in template placeholders
        SetText("Version", invoice.InvoiceType.Name);
        SetText("InternalEntity", internalEntityName);
        SetText("InternalAddress", internalInvoiceAddress);
        SetText("InternalCityStateZip", internalCityStateZip);
        SetText("InternalPhone", invoice.InternalEntityTelephoneNum ?? "");
        SetText("InternalFax", invoice.InternalEntityFaxNum ?? "");
        SetText("InternalContactAttn", invoice.InternalEntityAttn ?? "");
        SetText("InternalContactPhone", invoice.ContactPhone ?? "");
        SetText("InternalContactEmail", invoice.ContactEmail ?? "");
        SetText("InternalAbaNumber", invoice.PaymentAccountNum ?? "");
        SetText("InternalBank", invoice.PaymentBankName ?? "");
        SetText("InternalBankAccountName", invoice.PaymentAccountName ?? "");
        SetText("InternalBankAddress", invoice.PaymentCityState ?? "");
        SetText("InternalWireNum", invoice.PaymentAbaNumWire ?? "");
        SetText("CounterpartyName", counterpartyName);
        SetText("CounterpartyAddress", counterpartyAddress);
        SetText("CounterpartyCityStateZip", counterpartyCityStateZip);
        SetText("CounterpartyAttn", invoice.CounterpartyAttn ?? "");
        SetText("InternalNumber", invoice.InvoiceNum);
        SetText("InternalDate", invoice.InvoiceDate.ToString("MM/dd/yyyy"));
        SetText("InternalDueDate", invoice.DueDate?.ToString("MM/dd/yyyy") ?? "");
        SetText("PaymentType", paymentType);
        SetText("InternalCustomerNo", customerNum ?? "");
        SetText("InternalMonth", invoice.Month.ToString("MM/yyyy"));

        await SetLinesAsync(invoice);

        var fileNameOriginal = GetNewFileName(invoice.Month, invoice.CounterpartyId, invoice.InvoiceNum);
        var existingFileNameOnDisk = db.InvoiceGas.First(x => x.InvoiceNum == invoiceNum).FileNameOnDisk;
        var newFileNameOnDisk = Guid.NewGuid().ToString("N") + ".pdf";
        var fileNameOnDisk = existingFileNameOnDisk ?? newFileNameOnDisk;

        wordprocessingDocument.PackageProperties.Title = fileNameOriginal;
        string baseGasPath = Path.Join(filesFolderName, "Gas");

        using var ms = SaveToMemoryStream();
        await PdfConverter.ConvertDocxAsync(ms, fileService, baseGasPath, fileNameOnDisk, useSharding: true);

        return (fileNameOriginal, fileNameOnDisk);
    }

    protected override HashSet<string> GetDocKeys()
    {
        HashSet<string> keys = new(StringComparer.OrdinalIgnoreCase)
        {
            "Version",
            "InternalEntity",
            "InternalAddress",
            "InternalCityStateZip",
            "InternalPhone",
            "InternalFax",
            "InternalContactAttn",
            "InternalContactPhone",
            "InternalContactEmail",
            "InternalBank",
            "InternalBankAccountName",
            "InternalBankAddress",
            "InternalWireNum",
            "InternalAbaNumber",
            "CounterpartyName",
            "CounterpartyAddress",
            "CounterpartyCityStateZip",
            "CounterpartyAttn",
            "InternalNumber",
            "InternalDate",
            "InternalDueDate",
            "PaymentType",
            "InternalCustomerNo",
            "InternalMonth",
        };

        return keys;
    }

    private async Task SetLinesAsync(InvoiceGa invoice)
    {
        var allLines = invoice.InvoiceGasLines.OrderBy(x => x.LineNum).ToList();
        var lineGroups = GetInvoiceLineGroups(allLines);
        var lineSupportData = await GetLineSupportDataAsync();

        var table = doc.MainDocumentPart?.Document?.Body?
            .Descendants<Table>()
            .LastOrDefault()
            ?? throw new InvalidOperationException("No table found in document");

        foreach (var group in lineGroups)
        {
            AddGroupLines(group, table, lineSupportData);
            AddGroupSubtotalLine(group, table);
        }

        AddTaxLine(invoice, table);
        AddGrandTotalLine(invoice, table);
    }

    private class InvoiceLineGroup
    {
        public List<InvoiceGasLine> InvoiceLines { get; } = new();
    }

    private static List<InvoiceLineGroup> GetInvoiceLineGroups(List<InvoiceGasLine> invoiceLines)
    {
        List<InvoiceLineGroup> allInvoiceLineGroups = new();
        InvoiceLineGroup? currentGroup = null;
        string? previousDealNum = null;
        foreach (var line in invoiceLines)
        {
            var currentDealNum = line.Deal?.TicketNum;
            var hasDealNum = !string.IsNullOrWhiteSpace(currentDealNum);
            var dealNumChanged = currentDealNum != previousDealNum;

            if (!hasDealNum || dealNumChanged)
            {
                //create a new group
                currentGroup = new InvoiceLineGroup();
                allInvoiceLineGroups.Add(currentGroup);
                previousDealNum = currentDealNum;
            }
            //add line to current group
            currentGroup!.InvoiceLines.Add(line);
        }

        return allInvoiceLineGroups;
    }

    private class LineRequiredData
    {
        public List<MeterInfo> Meters { get; set; } = new();
        public Dictionary<int, string> PipelinesDict { get; set; } = new();
        public Dictionary<int, string> MeterNameDict { get; set; } = new();
    }

    private static async Task<LineRequiredData> GetLineSupportDataAsync()
    {
        using var db = Main.CreateContext();
        var data = new LineRequiredData();

        data.Meters = await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.NaturalGasAndLng);
        data.PipelinesDict = await (from q in db.Pipelines select new { q.Id, q.PipeShort }).AsNoTracking().ToDictionaryAsync(x => x.Id, x => x.PipeShort);
        data.MeterNameDict = data.Meters.Select(x => new { x.MeterId, x.MeterName }).Distinct().ToDictionary(x => x.MeterId, x => x.MeterName);

        return data;
    }

    private static void AddGroupLines(InvoiceLineGroup group, Table table, LineRequiredData data)
    {
        foreach (var line in group.InvoiceLines)
        {
            //the template file table should already have a blank row for the first line
            //so we only add rows for subsequent lines
            if (line.LineNum != 1)
                AddBlankRow(table);

            var pipelineShortName = "";
            var pipeId = DataHelper.GetPipeForMeterProduct(data.Meters, line.MeterId, line.Deal?.ProductId);
            if (line.MeterId.HasValue && pipeId.HasValue)
                pipelineShortName = data.PipelinesDict[pipeId.Value];
            else if (line.PipelineCustomText != null)
                pipelineShortName = line.PipelineCustomText;

            var meterName = "";
            if (line.MeterId.HasValue && data.MeterNameDict.TryGetValue(line.MeterId.Value, out var name))
                meterName = name;
            else if (line.MeterCustomText != null)
                meterName = line.MeterCustomText;

            var invoicePrice = line.Price.GetValueOrDefault() + line.Adder.GetValueOrDefault();

            SetTableColText(table, ColIndexLineNo, line.LineNum.ToString());
            SetTableColText(table, ColIndexDealNo, line.Deal?.TicketNum ?? "");
            SetTableColText(table, ColIndexFlowDays, line.FlowDays ?? "");
            SetTableColText(table, ColIndexPipeline, pipelineShortName);
            SetTableColText(table, ColIndexDeliveryMeter, meterName);
            SetTableColText(table, ColIndexQuantity, FormatNum(line.Quantity, NumFormat.n0));
            SetTableColText(table, ColIndexPrice, FormatNum(line.Price, NumFormat.n5));
            SetTableColText(table, ColIndexAdder, FormatNum(line.Adder, NumFormat.n5));
            SetTableColText(table, ColIndexInvoicePrice, FormatNum(invoicePrice, NumFormat.n5));
            SetTableColText(table, ColIndexAmount, FormatNum(line.Amount, NumFormat.c2));
        }
    }

    private static void AddGroupSubtotalLine(InvoiceLineGroup group, Table table)
    {
        if (group.InvoiceLines.Count <= 1)
        {
            //even if there is no subtotal being added, we still want a blank line for separation
            AddBlankRow(table);
            return;
        }

        //this is for the subtotal row
        AddBlankRow(table);

        var dealNum = group.InvoiceLines.First().Deal?.TicketNum ?? "";
        var totalQuantity = group.InvoiceLines.Sum(x => x.Quantity);
        var totalAmount = group.InvoiceLines.Sum(x => x.Amount);

        SetTableColText(table, ColIndexDeliveryMeter, $"SUBTOTAL FOR {dealNum}");
        SetTableColText(table, ColIndexQuantity, FormatNum(totalQuantity, NumFormat.n0));
        SetTableColText(table, ColIndexAmount, FormatNum(totalAmount, NumFormat.c2));

        //this is for the blank row after the subtotal
        AddBlankRow(table);
    }

    private static void AddTaxLine(InvoiceGa invoice, Table table)
    {
        if (invoice.SalesTaxAmount.GetValueOrDefault() != 0)
        {
            AddBlankRow(table);
            SetTableColText(table, ColIndexDeliveryMeter, "TAX");
            SetTableColText(table, ColIndexAmount, FormatNum(invoice.SalesTaxAmount, NumFormat.c2));
        }
    }

    private static void AddGrandTotalLine(InvoiceGa invoice, Table table)
    {
        AddBlankRow(table);
        SetTableColText(table, ColIndexDeliveryMeter, "TOTAL AMOUNT DUE", true);
        SetTableColText(table, ColIndexQuantity, FormatNum(invoice.TotalQuantity, NumFormat.n0), true);
        SetTableColText(table, ColIndexAmount, FormatNum(invoice.GrandTotal, NumFormat.c2), true);
    }

    private static void AddBlankRow(Table lineTable)
    {
        // clones the last row of the table and replaces any existing data with blanks
        var lastRow = lineTable.Elements<TableRow>().LastOrDefault()
            ?? throw new InvalidOperationException("No rows found in table to clone");

        var newRow = (TableRow)lastRow.CloneNode(true);
        var cells = newRow.Elements<TableCell>().ToList();

        SetTableCellText(cells[ColIndexLineNo], "");
        SetTableCellText(cells[ColIndexDealNo], "");
        SetTableCellText(cells[ColIndexFlowDays], "");
        SetTableCellText(cells[ColIndexPipeline], "");
        SetTableCellText(cells[ColIndexDeliveryMeter], "");
        SetTableCellText(cells[ColIndexQuantity], "");
        SetTableCellText(cells[ColIndexPrice], "");
        SetTableCellText(cells[ColIndexAdder], "");
        SetTableCellText(cells[ColIndexInvoicePrice], "");
        SetTableCellText(cells[ColIndexAmount], "");

        // insert the new row after the last row
        lineTable.InsertAfter(newRow, lastRow);
    }

    private static void SetTableColText(Table table, int colIndex, string text, bool bold = false)
    {
        var lastRow = table.Elements<TableRow>().LastOrDefault()
            ?? throw new InvalidOperationException("No rows found in table");

        var cells = lastRow.Elements<TableCell>().ToList();

        if (colIndex >= cells.Count || colIndex < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(colIndex),
                $"Column index {colIndex} is out of range. Table has {cells.Count} columns.");
        }

        var cell = cells[colIndex];
        SetTableCellText(cell, text, bold);
    }

    private static string GetCityStateZipStr(string? city, string? state, string? zip)
    {
        city = string.IsNullOrWhiteSpace(city) ? "" : city;
        state = string.IsNullOrWhiteSpace(state) ? "" : state;
        zip = string.IsNullOrWhiteSpace(zip) ? "" : " " + zip;
        var comma = city == "" ? "" : ", ";
        var cityStateZip = $"{city}{comma}{state}{zip}";
        return cityStateZip;
    }

    private string GetNewFileName(DateOnly month, int counterpartyId, string invoiceNum)
    {
        var monthStr = month.ToString("yyyy-MM");
        string counterparty = db.Counterparties.Where(x => x.Id == counterpartyId).Select(x => x.Name).First();
        counterparty = RegexAlphaNumeric.Replace(counterparty, "");
        string newFileName = $"{monthStr}-{counterparty}-{invoiceNum}.pdf";
        return newFileName;
    }

    private static readonly Regex RegexAlphaNumeric = new(@"[^0-9a-zA-Z]+", RegexOptions.Compiled);
}
