using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using DocumentFormat.OpenXml.ExtendedProperties;
using Fast.Shared.Logic.ValuationByPath;
using Humanizer;

namespace Fast.Web.Logic;

public class BcExporter() : ODataController
{
    private readonly MyDbContext db = Main.CreateContext();
    private readonly BcApiHelper bcApiHelper = new();
    private Dictionary<int, (int? LocationId, string? StateAbbr, string MeterName)>? _meterCache;
    private Dictionary<int, (int? PipelineTypeId, string? PipeCode)>? _pipeCache;
    private Dictionary<int, (string? PipeCode, string? MeterNum)>? _meterProductCache;
    private Dictionary<string, BcInvoice>? _existingInvoicesCache;
    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };

    public async Task<string> TestBc(DateOnly month)
    {
        var bcInvoices = await GetBcInvoiceHeadersOnly(month);

        // bcInvoices = bcInvoices.Where(x => x.Number == "INVC80").ToList();
        bcInvoices.ForEach(x => x.Number = x.Number.Replace("INVC", "TEST-"));

        var valLinesLookup = await GetValuationResults(month);

        await bcApiHelper.Authenticate();
        _existingInvoicesCache = await bcApiHelper.FetchExistingInvoicesAsync();

        var resultStr = "";
        var successCount = 0;
        var failCount = 0;
        var duplicateCount = 0;

        foreach (var invoice in bcInvoices)
        {
            // check duplicates first before processing to save time
            if (_existingInvoicesCache.ContainsKey(invoice.Number))
            {
                duplicateCount++;
                continue;
            }

            try
            {
                var valLines = valLinesLookup[(invoice.InternalEntityId, invoice.CounterpartyId)].ToList();
                var bcLines = await GetBcLinesFromValuation(valLines, month, invoice.Number);
                // order by ascending SdealNumber for easier review in BC
                invoice.InvoiceLines = bcLines.OrderBy(x => x.SdealNumber).ToList();
                if (invoice.SalesTaxAmount != 0 && invoice.SalesTaxAmount != null)
                {
                    BcInvoiceLine taxLine = await GetTaxLine(invoice.SalesTaxAmount);
                    invoice.InvoiceLines.Add(taxLine);
                }

                // Handle rounding differences between BC and our app
                var chargeOffLine = GetRoundingChargeOffLine(invoice);
                if (chargeOffLine != null)
                    invoice.InvoiceLines.Add(chargeOffLine);

                ValidateInvoice(invoice);
                Debug.WriteLine($"Processing invoice {invoice.Number}");
                await bcApiHelper.CreateInvoiceAsync(invoice);
            }
            catch (Exception ex)
            {
                failCount++;
                var errorMessage = FormatBcApiException(ex, invoice.Number);
                resultStr += $"• {errorMessage}\n\n";
                continue;
            }
            successCount++;
        }

        string failStr = "invoice".ToQuantity(failCount);
        string successStr = "invoice".ToQuantity(successCount);
        string duplicateMessage = "";

        if (duplicateCount > 0)
        {
            var duplicateHumanized = "duplicate".ToQuantity(duplicateCount);
            duplicateMessage = $"{duplicateHumanized} skipped.";
        }

        if (!string.IsNullOrWhiteSpace(resultStr))
            resultStr = $"""
                Some invoices failed. See below for details.
                {failStr} failed.
                {successStr} created successfully.
                {duplicateMessage}

                {resultStr}
                """;
        else
            resultStr = $"""
                All Invoices Succeeded.
                {successStr} created successfully.
                {duplicateMessage}
                """;

        return resultStr;
    }

    public void ValidateInvoice(BcInvoice inv)
    {
        if (inv.InvoiceLines.Count == 0)
            throw new Exception($"{inv.Number}: No actualization lines found");

        if (string.IsNullOrWhiteSpace(inv.CustomerNum))
            throw new Exception($"{inv.Number}: Customer number is not set");

        if (inv.DueDate == null)
            throw new Exception($"{inv.Number}: Due Date is not set");
    }

    public async Task<List<BcInvoice>> GetBcInvoiceHeadersOnly(DateOnly month)
    {
        var headers = await (
            from q in db.InvoiceGas
            where q.Month == month
                && q.InvoiceNum.StartsWith("INVC")
            orderby q.InvoiceNum
            select new BcInvoice
            {
                Number = q.InvoiceNum,
                CounterpartyId = q.CounterpartyId,
                InternalEntityId = q.InternalEntityId,
                ExternalDocumentNumber = "",
                CustomerNum = q.Counterparty.InternalCustomerNum,
                DocumentDate = q.InvoiceDate,
                PostingDate = Util.Date.LastDayOfMonth(month),
                DueDate = q.DueDate,
                SalesTaxAmount = q.SalesTaxAmount,
                GrandTotal = q.GrandTotal
            }
        ).AsNoTracking().ToListAsync();

        return headers;
    }

    private async Task<List<BcInvoiceLine>> GetBcLinesFromValuation(List<PathValuationResult> valLines, DateOnly month, string invoiceNum)
    {
        var newLines = new List<BcInvoiceLine>();
        var delayedErrorMessages = new List<string>();

        foreach (var val in valLines)
        {
            BcInvoiceLine newLine = new();
            var pipecodeName = await GetPipecodeForLine(val.DeliveryPipeId, val.DeliveryPipeContract);
            newLine.LineType = "Item";
            newLine.LineTypeNumber = GetBcItemTypeForLine(val.DeliveryInternalEntityId, val.DeliveryDealPurposeId);
            newLine.UnitPrice = val.DeliveryInvoicePrice;
            newLine.Quantity = val.DeliveryActualVol ?? val.DeliveryNomVol;
            newLine.TaxCode = "NONTAXABLE";
            newLine.ProdMonth = month.ToString("yyyy-MM");
            newLine.MeterCode = await GetBcMeterForLineAsync(val.DeliveryMeterId);
            newLine.StateLocation = GetBcStateLocationForLine(val.DeliveryMeterId, invoiceNum, out var delayedErrorMessage);
            if (!string.IsNullOrWhiteSpace(delayedErrorMessage))
                delayedErrorMessages.Add(delayedErrorMessage);

            newLine.PdealNumber = "PD-" + val.ReceiptDeal + "-D" + val.Day.Day.ToString("D2");
            newLine.SdealNumber = "SD-" + val.DeliveryDeal + "-D" + val.Day.Day.ToString("D2");
            newLine.PipeCode = string.IsNullOrWhiteSpace(pipecodeName) ? null : pipecodeName;

            newLines.Add(newLine);
        }

        if (delayedErrorMessages.Count > 0)
        {
            delayedErrorMessages = delayedErrorMessages.Distinct().ToList();
            var combinedErrorMessage = string.Join($"\n• ", delayedErrorMessages);
            throw new Exception(combinedErrorMessage);
        }

        return newLines;
    }

    private static async Task<BcInvoiceLine> GetTaxLine(decimal? salesTaxAmount)
    {
        BcInvoiceLine newLine = new();
        newLine.LineType = "Item";
        newLine.LineTypeNumber = "SALESTAX";
        newLine.UnitPrice = salesTaxAmount ?? 0;
        newLine.Quantity = 1;
        newLine.TaxCode = "NONTAXABLE"; // oxymoron, but BC requires a tax code on sales tax lines

        return newLine;
    }

    // BC calculates invoice totals by multiplying each line's UnitPrice by Quantity, rounding to 2 decimals,
    // then summing all amounts. Our app may calculate the total differently. This method adds a CHARGE-OFF
    // line to reconcile small differences (under $5). Throws an exception for larger differences.
    private static BcInvoiceLine? GetRoundingChargeOffLine(BcInvoice invoice)
    {
        // Calculate BC's total: sum of each line's (UnitPrice * Quantity) rounded to 2 decimals
        decimal bcTotal = 0m;
        foreach (var line in invoice.InvoiceLines)
        {
            var lineAmount = (line.UnitPrice ?? 0m) * (line.Quantity ?? 0m);
            bcTotal += Math.Round(lineAmount, 2, MidpointRounding.AwayFromZero);
        }

        // Our app's grand total from the database
        decimal ourTotal = invoice.GrandTotal ?? 0m;

        // Calculate the difference
        decimal difference = ourTotal - bcTotal;

        // If no difference, no charge-off line needed
        if (difference == 0m)
            return null;

        // If difference is $5 or more, throw an error
        if (Math.Abs(difference) >= 5m)
        {
            throw new Exception($"""
                Invoice {invoice.Number}: Difference too large.
                Our invoice total: {ourTotal:C2}, BC total: {bcTotal:C2}, Difference: {difference:C2}
                """);
        }

        // Add a CHARGE-OFF line to make up the difference
        return new BcInvoiceLine
        {
            LineType = "Item",
            LineTypeNumber = "CHARGE-OFF",
            UnitPrice = difference,
            Quantity = 1,
            TaxCode = "NONTAXABLE"
        };
    }


    enum InternalEntityType
    {
        Superior = 1,
        Walter = 2,
        Other = 3
    }

    private static string GetBcItemTypeForLine(int? internalEntityId, int? dealPurposeId)
    {
        const int superiorId = 315;
        const int superiorPrincipalId = 352;
        const int superiorWtId = 382;
        const int superiorProcessingId = 334;
        const int walterId = 333;
        const int walterKwId = 375;

        var internalEntityTypeId = (internalEntityId ?? 0) switch
        {
            superiorId or superiorPrincipalId or superiorWtId or superiorProcessingId => (int)InternalEntityType.Superior,
            walterId or walterKwId => (int)InternalEntityType.Walter,
            _ => (int)InternalEntityType.Other
        };

        const int cashout = (int)Enums.DealPurpose.Cashout;
        const int directSaleInternalPaid = (int)Enums.DealPurpose.DirectSaleInternalPaid;
        const int superior = (int)InternalEntityType.Superior;
        const int walter = (int)InternalEntityType.Walter;
        const int other = (int)InternalEntityType.Other;

        return (internalEntityTypeId, dealPurposeId) switch
        {
            (superior, cashout) => "NATGAS C/O",
            (walter, cashout) => "NATGAS C/O P-T WOG",
            (other, cashout) => "NATGAS C/O P-T",
            (walter, directSaleInternalPaid) => "NATGAS WOGAGY",
            (other, directSaleInternalPaid) => "NATGAS P-T",

            _ => "NATGAS"
        };
    }

    private string? GetBcStateLocationForLine(int deliveryMeterId, string invoiceNum, out string? missingMessage)
    {
        _meterCache ??= (
            from q in db.Meters
            select new
            {
                q.Id,
                q.LocationId,
                StateAbbr = q.State == null ? null : q.State.Abbreviation,
                MeterName = q.Name
            }
        ).ToDictionary(x => x.Id, x => (x.LocationId, (string?)x.StateAbbr, x.MeterName));

        var (locationId, stateAbbr, meterName) = _meterCache[deliveryMeterId];

        var locationName = locationId switch
        {
            (int)Enums.Location.OffshoreFederal => "OFF FEDERAL",
            (int)Enums.Location.Onshore => "ONSHORE",
            (int)Enums.Location.OffshoreState => "OFF STATE",
            _ => ""
        };

        string? result = $"STATE-{stateAbbr}-{locationName}";

        missingMessage = null;
        if (string.IsNullOrWhiteSpace(stateAbbr) || string.IsNullOrWhiteSpace(locationName))
            missingMessage = $"{invoiceNum}: State or Location is missing for meter `{meterName}`";

        return result;
    }

    private async Task<string?> GetPipecodeForLine(int deliveryPipeId, string deliveryContract)
    {
        _pipeCache ??= await (
            from q in db.Pipelines
            select new
            {
                q.Id,
                q.PipelineTypeId,
                q.PipeCode
            }
        ).ToDictionaryAsync(x => x.Id, x => ((int?)x.PipelineTypeId, (string?)x.PipeCode));

        var (pipelineTypeId, pipeCode) = _pipeCache[deliveryPipeId];

        var typeName = pipelineTypeId switch
        {
            (int)Enums.PipelineType.Intrastate => "INTRA",
            (int)Enums.PipelineType.Interstate => "INTER",
            (int)Enums.PipelineType.NonJurisdictional => "NJD",
            _ => ""
        };

        string? result = null;
        if (!string.IsNullOrWhiteSpace(typeName) && !string.IsNullOrWhiteSpace(pipeCode) && !string.IsNullOrWhiteSpace(deliveryContract))
            result = typeName + "-" + pipeCode + "-" + deliveryContract;

        return result;
    }

    private async Task<string?> GetBcMeterForLineAsync(int deliveryMeterId)
    {
        _meterProductCache ??= (await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.NaturalGasAndLng))
            .DistinctBy(x => x.MeterId)
            .ToDictionary(x => x.MeterId, x => ((string?)x.PipeCode, (string?)x.MeterNum));

        var (pipeCode, meterNum) = _meterProductCache[deliveryMeterId];

        string? result = null;
        if (!string.IsNullOrWhiteSpace(pipeCode) && !string.IsNullOrWhiteSpace(meterNum))
            result = $"PIPE-{pipeCode}-{meterNum}";

        return result;
    }

    private static async Task<ILookup<(int? DeliveryInternalEntityId, int? DeliveryCounterpartyId), PathValuationResult>>
        GetValuationResults(DateOnly month)
    {
        var firstOfMonth = Util.Date.FirstDayOfMonth(month);
        var lastOfMonth = Util.Date.LastDayOfMonth(month);
        var valParams = new ValParams();

        valParams.TransactionTypeIds.Add((int)Enums.TransactionType.PhysicalGas);
        valParams.PositionDateRanges.Add(new DateRange(Enums.DateStyle.MonthRange, firstOfMonth, lastOfMonth));
        valParams.IncludeBasisInContractPrice = false;

        var val = new PathValuaterGas();
        var results = (await val.GetValuationValues(valParams)).ToLookup(x => (x.DeliveryInternalEntityId, x.DeliveryCounterpartyId));

        return results;
    }

    private static bool TryExtractJson(string input, out string json)
    {
        json = "";

        var startIndex = input.IndexOf('{');
        if (startIndex < 0)
            return false;

        json = input[startIndex..];
        return true;
    }

    private static string FormatBcApiException(Exception ex, string invoiceNum)
    {
        if (!TryExtractJson(ex.Message, out var json))
            return ex.Message;

        try
        {
            var bcError = JsonSerializer.Deserialize<BcApiError>(
                json,
                JsonOptions
            );

            if (bcError?.Error == null)
                return ex.Message;

            var msg = $"""
                {invoiceNum}: Error of Type {bcError.Error.Code}.
                Message: {bcError.Error.Message};
                """;
            return msg;
        }
        catch
        {
            // If anything goes wrong, fall back to raw message
            return ex.Message;
        }
    }
}
