//******************************************************************************************************
//  DeviceCSVProcessor.cs - Gbtc
//
//  Copyright © 2026, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
//  file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://opensource.org/licenses/MIT
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  01/15/2026 - J. Ritchie Carroll
//       Generated original version of source code.
//
//******************************************************************************************************

using System.Diagnostics;
using System.Text;
using DNP3ConfigLoader.TableOperations;
using Gemstone.Units;

namespace DNP3ConfigLoader;

/// <summary>
/// Processes device CSV files to create/update database records and generate DNP3 configuration files.
/// </summary>
public static class DeviceCSVProcessor
{
    private const string FlagsSource = "DNP3 Flags Source";
    private const string FlagsDestination = "DNP3 Flags Destination";
    private const string SBitInverted = "S-bit inverted";
    private const string XBitInverted = "X-bit inverted";
    private const string DefaultDigitalLabel = 
        "DIGITAL00       " + 
        "DIGITAL01       " + 
        "DIGITAL02       " + 
        "DIGITAL03       " + 
        "DIGITAL04       " + 
        "DIGITAL05       " +
        "DIGITAL06       " + 
        "DIGITAL07       " +
        "DIGITAL08       " +
        "DIGITAL09       " +
        "DIGITAL10       " +
        "DIGITAL11       " +
        "DIGITAL12       " +
        "DIGITAL13       " +
        "DIGITAL14       " +
        "DIGITAL15       ";
    //   0123456789012345

    /// <summary>
    /// Map of DNP3 map types to time-series framework signal types.
    /// </summary>
    private static readonly Dictionary<string, string> s_mapTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        ["binary"] = "DIGI",
        ["doubleBitBinary"] = "DIGI",
        ["analog"] = "ALOG",
        ["counter"] = "ALOG",
        ["frozenCounter"] = "ALOG",
        ["controlStatus"] = "DIGI",
        ["setpointStatus"] = "DIGI"
    };

    /// <summary>
    /// Processes the device CSV file.
    /// </summary>
    public static void ProcessDeviceCSV(ProcessParameters processParams)
    {
        List<CIMDevice> devices = ReadDeviceCSV(processParams.Settings.DeviceCSVFilePath, processParams.Settings.CommentChar);
        int totalRows = devices.Count;

        Console.WriteLine($"\nProcessing device CSV file \"{processParams.Settings.DeviceCSVFilePath}\" with {totalRows:N0} rows...");

        int processedCount = 0;
        double totalProcessingTime = 0.0;
        int totalProcessedTagMapRows = 0;

        foreach (CIMDevice deviceInfo in devices)
        {
            Stopwatch rowTimer = Stopwatch.StartNew();

            (string? deviceAcronym, int tagMapRowsProcessed) = ProcessDeviceCSVRow(processParams, deviceInfo);

            if (deviceAcronym is null)
                continue;

            rowTimer.Stop();
            totalProcessingTime += rowTimer.Elapsed.TotalSeconds;
            totalProcessedTagMapRows += tagMapRowsProcessed;
            processedCount++;

            // Calculate estimated time remaining
            string etaStr;

            if (totalProcessedTagMapRows > 0)
            {
                double averageTimePerTagRow = totalProcessingTime / totalProcessedTagMapRows;
                double avgTagRowsPerDevice = (double)totalProcessedTagMapRows / processedCount;
                int remainingDevices = totalRows - processedCount;
                double estimatedRemainingTagRows = remainingDevices * avgTagRowsPerDevice;
                double estimatedTimeRemaining = averageTimePerTagRow * estimatedRemainingTagRows;

                etaStr = Time.ToElapsedTimeString(estimatedTimeRemaining, 2);
            }
            else
            {
                etaStr = "calculating...";
            }

            Console.WriteLine($"\nProcessing complete for {deviceAcronym}\n");
            Console.WriteLine($"{processedCount:N0} out of {totalRows:N0} CSV device rows processed, {(double)processedCount / totalRows * 100:F2}% complete...");
            Console.WriteLine($"{totalProcessedTagMapRows:N0} total tag map rows processed");

            if (totalRows - processedCount > 0)
                Console.WriteLine($"Estimated time remaining: {etaStr}");

            Console.WriteLine();
            Console.WriteLine(new string('-', 80));

            processParams.ProcessedAcronyms.Add(deviceAcronym);
        }
    }

    private static List<CIMDevice> ReadDeviceCSV(string csvPath, char commentChar)
    {
        List<CIMDevice> devices = [];

        using StreamReader reader = new(csvPath);

        // Skip header
        string? header = reader.ReadLine();

        if (header is null)
            return devices;

        string[] columns = Common.ParseCSVLine(header);

        while (reader.ReadLine() is { } line)
        {
            // Skip comment lines
            if (line.StartsWith(commentChar) || line.StartsWith(';'))
                continue;

            string[] values = Common.ParseCSVLine(line);
            Dictionary<string, string?> row = [];

            for (int i = 0; i < columns.Length && i < values.Length; i++)
                row[columns[i].Trim()] = values[i].Trim();

            if (!row.TryGetValue("Name", out string? name) || string.IsNullOrWhiteSpace(name))
                continue;

            devices.Add(new CIMDevice
            {
                Name = name,
                Substation = row.GetValueOrDefault("Substation", name) ?? name,
                Connection = row.GetValueOrDefault("Connection", "") ?? "",
                TagMapFileName = row.GetValueOrDefault("TagMapFileName", "") ?? ""
            });
        }

        return devices;
    }

    private static (string? DeviceAcronym, int TagMapRowsProcessed) ProcessDeviceCSVRow(ProcessParameters processParams, CIMDevice deviceInfo)
    {
        string deviceName = deviceInfo.Name;

        // If device name starts with '#' or ';' character, skip this row
        if (deviceName.StartsWith('#') || deviceName.StartsWith(';'))
        {
            Console.WriteLine($">> Skipping device: {deviceName[1..]} -- row commented out in CSV file");
            return (null, 0);
        }

        // Create valid device acronym from device name (with -DNP3 suffix)
        string deviceAcronym = string.Format(ProcessParameters.DeviceAcronymTemplate, Common.GetCleanIdentifier(deviceName));

        Console.WriteLine();
        Console.WriteLine($">> Processing device: {deviceName}");

        // Establish device configuration file and mapping file paths
        string deviceConfigFilePath = $"{processParams.OutputDirectory}{deviceAcronym}-config.xml";
        string deviceMappingFilePath = $"{processParams.OutputDirectory}{deviceAcronym}-mapping.xml";

        // Build device connection string (matches Python format)
        string connectionString =
            $"CommsFilePath={deviceConfigFilePath.Replace('/', '\\')}; " +
            $"MappingFilePath={deviceMappingFilePath.Replace('/', '\\')}; " +
            $"MapQualityToStateFlags={processParams.Settings.MapQualityToStateFlags.ToString().ToLowerInvariant()}; " +
            $"PublishFlagsAsSeparateMeasurements={processParams.Settings.PublishFlagsAsSeparateMeasurements.ToString().ToLowerInvariant()}; " +
            $"TagMatchPattern={processParams.Settings.TagMatchPattern}; " +
            $"QualityTagSuffix={processParams.Settings.QualityTagSuffix}; " +
            $"PollingInterval={processParams.Settings.PollingInterval}";

        string substationName = string.IsNullOrWhiteSpace(deviceInfo.Substation) ? deviceName : deviceInfo.Substation;

        // Add or update DNP3 device record
        Device? deviceRecord = DeviceOperations.GetDeviceRecord(
            processParams.DeviceTable,
            processParams.NodeID,
            processParams.HistorianID,
            processParams.ProtocolID,
            deviceAcronym,
            substationName,
            connectionString,
            processParams.Settings.DefaultEnableDevices);

        if (deviceRecord is null)
            return (null, 0);

        // Parse "Connection" column value into host IP and port
        string connection = deviceInfo.Connection;
        string[] connectionParts = connection.Split(':');
        string hostIP = connectionParts.Length > 0 ? connectionParts[0] : "127.0.0.1";
        string port = connectionParts.Length > 1 ? connectionParts[1] : processParams.Settings.DefaultDNP3Port.ToString();

        // Load a copy of the 'Device.xml' template and replace parameters
        string deviceTemplate = processParams.DeviceTemplate
            .Replace("{HostIP}", hostIP)
            .Replace("{Port}", port);

        // Write target device XML configuration file to output directory
        File.WriteAllText(deviceConfigFilePath, deviceTemplate);

        // Get tag map file name
        string tagMapFileName = deviceInfo.TagMapFileName;

        // Create dictionary of map sections to hold processed tag map file rows
        Dictionary<string, List<IndexMapping>> mapSections = [];

        foreach (string key in s_mapTypes.Keys)
            mapSections[key] = [];

        // Read and process tag map file
        List<TagMapRow> tagMapRows = ReadTagMapCSV(tagMapFileName, processParams.Settings.CommentChar);
        int totalRows = tagMapRows.Count;

        Console.WriteLine($"      Processing tag map \"{Path.GetFileName(tagMapFileName)}\" with {totalRows:N0} rows...");

        // Preload all existing measurements for this device into an in-memory cache to
        // avoid individual database round-trips during validation of unchanged records
        processParams.LoadMeasurementCache(deviceRecord.ID);

        Dictionary<string, int> indexes = new()
        {
            ["digitalIndex"] = 0,
            ["analogIndex"] = 0
        };

        int processedCount = 0;
        DateTime lastUpdateTime = DateTime.Now;

        foreach (TagMapRow tagMapRow in tagMapRows)
        {
            List<string> newAdapterAcronyms = ProcessTagMapCSVRow(
                processParams,
                tagMapRow,
                deviceRecord,
                indexes,
                mapSections);

            processParams.ProcessedAcronyms.UnionWith(newAdapterAcronyms);

            processedCount++;

            if ((DateTime.Now - lastUpdateTime).TotalSeconds < 2.0D)
                continue;

            Console.WriteLine($"         >> Processed {processedCount:N0} tag map rows so far, {(double)processedCount / totalRows * 100:F2}% complete...");
            lastUpdateTime = DateTime.Now;
        }

        Console.WriteLine($"      Done! Processed {processedCount:N0} tag map rows for \"{Path.GetFileName(tagMapFileName)}\"");

        // Clear the per-device measurement cache
        processParams.ClearMeasurementCache();

        // Remove any leftover digitals beyond the last index
        Console.WriteLine("\n      Checking for unreferenced DIGI measurements...");
        
        MeasurementOperations.RemoveUnreferencedMeasurements(
            processParams.MeasurementTable,
            indexes["digitalIndex"],
            deviceRecord.ID,
            processParams.SignalTypeIDs["DIGI"],
            "DIGI");

        // Remove any leftover analogs beyond the last index
        Console.WriteLine("\n      Checking for unreferenced ALOG measurements...");

        MeasurementOperations.RemoveUnreferencedMeasurements(
            processParams.MeasurementTable,
            indexes["analogIndex"],
            deviceRecord.ID,
            processParams.SignalTypeIDs["ALOG"],
            "ALOG");

        // Build the map section string for the mapping XML file
        StringBuilder mapSectionLines = new();

        foreach ((string mapType, List<IndexMapping> mapSectionList) in mapSections)
        {
            if (mapSectionList.Count == 0)
                continue;

            mapSectionLines.AppendLine($"  <{mapType}Map>");

            foreach (IndexMapping mapSection in mapSectionList)
            {
                string indexMapTemplate = processParams.IndexMapTemplate
                    .Replace("{ID}", mapSection.ID.ToString())
                    .Replace("{Source}", mapSection.Source)
                    .Replace("{Index}", mapSection.Index.ToString());

                mapSectionLines.AppendLine(indexMapTemplate);
            }

            mapSectionLines.AppendLine($"  </{mapType}Map>");
        }

        // Load a copy of the 'Mapping.xml' template and replace parameters
        string mappingTemplate = processParams.MappingTemplate
            .Replace("{MapSections}", mapSectionLines.ToString());

        // Write target mapping XML configuration file to output directory
        File.WriteAllText(deviceMappingFilePath, mappingTemplate);

        return (deviceAcronym, processedCount);
    }

    private static List<TagMapRow> ReadTagMapCSV(string tagMapPath, char commentChar)
    {
        List<TagMapRow> rows = [];

        if (!File.Exists(tagMapPath))
            return rows;

        using StreamReader reader = new(tagMapPath);

        // Read header
        string? header = reader.ReadLine();

        if (header is null)
            return rows;

        string[] columns = Common.ParseCSVLine(header);

        while (reader.ReadLine() is { } line)
        {
            if (line.StartsWith(commentChar))
                continue;

            string[] values = Common.ParseCSVLine(line);
            Dictionary<string, string?> row = [];

            for (int j = 0; j < columns.Length && j < values.Length; j++)
                row[columns[j].Trim()] = values[j].Trim();

            if (!row.TryGetValue("DNPIndex", out string? indexStr) || !int.TryParse(indexStr, out int dnpIndex))
                continue;

            if (!row.TryGetValue("TagName", out string? tagName) || string.IsNullOrWhiteSpace(tagName))
                continue;

            if (!row.TryGetValue("Scale", out string? scaleStr) || !double.TryParse(scaleStr, out double scale))
                scale = 1.0D;

            rows.Add(new TagMapRow
            {
                MapType = row.GetValueOrDefault("MapType", "binary") ?? "binary",
                DNPIndex = dnpIndex,
                TagName = tagName,
                AlternateTag = row.GetValueOrDefault("AlternateTag", "") ?? "",
                Scale = scale,
                Negate = string.Equals(row.GetValueOrDefault("Negate", "FALSE"), "TRUE", StringComparison.OrdinalIgnoreCase),
                Concatenate = string.Equals(row.GetValueOrDefault("Concatenate", "FALSE"), "TRUE", StringComparison.OrdinalIgnoreCase),
                InvertSBit = string.Equals(row.GetValueOrDefault("InvertSBit", "FALSE"), "TRUE", StringComparison.OrdinalIgnoreCase),
                InvertXBit = string.Equals(row.GetValueOrDefault("InvertXBit", "FALSE"), "TRUE", StringComparison.OrdinalIgnoreCase)
            });
        }

        return rows;
    }

    private static List<string> ProcessTagMapCSVRow(
        ProcessParameters processParams,
        TagMapRow tagMapRow,
        Device deviceRecord,
        Dictionary<string, int> indexes,
        Dictionary<string, List<IndexMapping>> mapSections)
    {
        List<string> newAdapterAcronyms = [];

        string mapType = tagMapRow.MapType;

        // Validate map type
        if (!s_mapTypes.TryGetValue(mapType, out string? signalType))
        {
            Console.WriteLine($"     ERROR: invalid map type name \"{mapType}\" for device - mapping row skipped");
            return newAdapterAcronyms;
        }

        bool isBinary = string.Equals(mapType, "binary", StringComparison.OrdinalIgnoreCase);
        int index;
        string signalTypeSuffix;

        if (signalType == "DIGI")
        {
            indexes["digitalIndex"]++;
            index = indexes["digitalIndex"];
            signalTypeSuffix = "DV";
        }
        else
        {
            indexes["analogIndex"]++;
            index = indexes["analogIndex"];
            signalTypeSuffix = "AV";
        }

        int dnp3Index = tagMapRow.DNPIndex;
        string pointTag = Common.GetCleanIdentifier(tagMapRow.TagName);
        string signalReference = $"{deviceRecord.Acronym}-{signalTypeSuffix}{index}";
        string description = $"{deviceRecord.Name} {mapType} {pointTag}, DNP3 index: {dnp3Index} [{signalType}{index:D4}]";
        string alternateTag = tagMapRow.AlternateTag;
        bool negated = tagMapRow.Negate;
        bool concatenated = tagMapRow.Concatenate;
        bool invertSBit = tagMapRow.InvertSBit;
        bool invertXBit = tagMapRow.InvertXBit;

        // Handle alternate tag formatting based on IEEE C37.118 version
        if (signalType == "ALOG")
        {
            if (processParams.Settings.IEEEC37_118Version < 2011)
                alternateTag = alternateTag.Length > 16 ? alternateTag[..16] : alternateTag.PadRight(16);
        }
        else
        {
            if (processParams.Settings.IEEEC37_118Version < 2011)
            {
                string prefix = alternateTag.Length > 16 ? alternateTag[..16] : alternateTag.PadRight(16);
                alternateTag = prefix + DefaultDigitalLabel[16..];
            }
        }

        double scale = tagMapRow.Scale;

        // Negate the scale if specified
        if (negated)
            scale = -scale;

        Measurement? firstIntermediateMeasurement = null;
        Measurement? secondIntermediateMeasurement = null;

        if (concatenated)
        {
            // Validate that concatenation is only specified for binary points
            if (!isBinary)
            {
                Console.WriteLine($"     ERROR: concatenation only supported for \"binary\" type - mapping row for \"{pointTag}\" skipped");
                return newAdapterAcronyms;
            }

            // Define an intermediate measurement point for the first part (low bit)
            firstIntermediateMeasurement = MeasurementOperations.GetMeasurementRecord(
                processParams.MeasurementTable,
                deviceRecord.ID,
                deviceRecord.HistorianID,
                processParams.SignalTypeIDs["CALC"],
                $"{pointTag}!LOW",
                $"{alternateTag}!LOW",
                $"{deviceRecord.Acronym}.{pointTag}-CV1",
                scale,
                $"{deviceRecord.Name} first intermediate {mapType} (low bit) of concatenated measurement for {pointTag}, DNP3 index: {dnp3Index} ({FlagsSource})",
                processParams.MeasurementCache);

            // Define an intermediate measurement point for the second part (high bit)
            secondIntermediateMeasurement = MeasurementOperations.GetMeasurementRecord(
                processParams.MeasurementTable,
                deviceRecord.ID,
                deviceRecord.HistorianID,
                processParams.SignalTypeIDs["CALC"],
                $"{pointTag}!HIGH",
                $"{alternateTag}!HIGH",
                $"{deviceRecord.Acronym}.{pointTag}-CV2",
                scale,
                $"{deviceRecord.Name} second intermediate {mapType} (high bit) of concatenated measurement for {pointTag}, DNP3 index: {dnp3Index + 1}",
                processParams.MeasurementCache);

            // Define a modified description for the concatenated measurement
            description = $"{deviceRecord.Name} concatenated {pointTag}, DNP3 indexes: {dnp3Index} and {dnp3Index + 1} [{signalType}{index:D4}] ({FlagsDestination})";
        }

        if (invertSBit)
        {
            if (isBinary)
            {
                description += $" ({SBitInverted})";
            }
            else
            {
                Console.WriteLine($"     WARNING: S-bit inversion only supported for \"binary\" type - S-bit inversion for \"{pointTag}\" skipped");
                invertSBit = false;
            }
        }

        if (invertXBit)
        {
            // TODO: Assuming X-bit only applies to concatenated binary points, need to confirm -- source CIM export defines no X-bit enabled columns
            if (isBinary && concatenated)
            {
                description += $" ({XBitInverted})";
            }
            else
            {
                Console.WriteLine($"     WARNING: X-bit inversion only supported for concatenated \"binary\" type - X-bit inversion for \"{pointTag}\" skipped");
                invertXBit = false;
            }
        }

        // Add or update DNP3 measurement record
        Measurement? measurementRecord = MeasurementOperations.GetMeasurementRecord(
            processParams.MeasurementTable,
            deviceRecord.ID,
            deviceRecord.HistorianID,
            processParams.SignalTypeIDs[signalType],
            pointTag,
            alternateTag,
            signalReference,
            scale,
            description,
            processParams.MeasurementCache);

        if (measurementRecord is null)
            return newAdapterAcronyms;

        if (invertSBit)
        {
            const string AdapterName = "DNP3!S-BIT-INVERTER";

            // Only query/create the singleton adapter once per run
            if (processParams.VerifiedAdapterNames.Add(AdapterName))
            {
                // Create S-bit inverter filter adapter
                const string VariableList = $"value[]=FILTER ActiveMeasurements WHERE Description LIKE '%{SBitInverted}%'";
                const string AdapterConnectionString = 
                    $"VariableList={{{VariableList}}}; " +
                    "ExpressionText={(CAST(value[INDEX], int) XOR 1)}; " +
                    "FilterOperation=ValueAugmentation; UsePrecisionTimer=false";

                CustomFilterAdapterOperations.GetCustomFilterAdapterRecord(
                    processParams.CustomFilterAdapterTable,
                    processParams.NodeID,
                    AdapterName,
                    "DynamicCalculator.dll",
                    "DynamicCalculator.DynamicFilter",
                    AdapterConnectionString);
            }

            newAdapterAcronyms.Add(AdapterName);
        }

        if (invertXBit)
        {
            const string AdapterName = "DNP3!X-BIT-INVERTER";

            // Only query/create the singleton adapter once per run
            if (processParams.VerifiedAdapterNames.Add(AdapterName))
            {
                // Create X-bit inverter filter adapter
                const string VariableList = $"value[]=FILTER ActiveMeasurements WHERE Description LIKE '%{XBitInverted}%'";
                const string AdapterConnectionString = 
                    $"VariableList={{{VariableList}}}; " +
                    "ExpressionText={(CAST(value[INDEX], int) XOR 2)}; " +
                    "FilterOperation=ValueAugmentation; UsePrecisionTimer=false";

                CustomFilterAdapterOperations.GetCustomFilterAdapterRecord(
                    processParams.CustomFilterAdapterTable,
                    processParams.NodeID,
                    AdapterName,
                    "DynamicCalculator.dll",
                    "DynamicCalculator.DynamicFilter",
                    AdapterConnectionString);
            }

            newAdapterAcronyms.Add(AdapterName);
        }

        if (concatenated)
        {
            if (firstIntermediateMeasurement is null || secondIntermediateMeasurement is null)
            {
                Console.WriteLine($"     ERROR: concatenation specified for \"{pointTag}\" but intermediate measurements not found - mapping row skipped");
                return newAdapterAcronyms;
            }

            const string FlagsAdapterName = "DNP3!STATE-FLAGS-TRANSFERER";

            // Only query/create the singleton adapter once per run
            if (processParams.VerifiedAdapterNames.Add(FlagsAdapterName))
            {
                // Create state flags transfer filter adapter
                const string FlagsConnectionString = 
                    $"SourceMeasurements=FILTER ActiveMeasurements WHERE Description LIKE '%{FlagsSource}%' ORDER BY AlternateTag; " +
                    $"DestinationMeasurements=FILTER ActiveMeasurements WHERE Description LIKE '%{FlagsDestination}%' ORDER BY AlternateTag; " +
                    "Flags=0xFFFFFFFF";

                CustomFilterAdapterOperations.GetCustomFilterAdapterRecord(
                    processParams.CustomFilterAdapterTable,
                    processParams.NodeID,
                    FlagsAdapterName,
                    "DataQualityMonitoring.dll",
                    "DataQualityMonitoring.StateFlagsTransferAdapter",
                    FlagsConnectionString);
            }

            newAdapterAcronyms.Add(FlagsAdapterName);

            // Only intermediate measurements need to be added to map sections for reading from DNP3 for
            // concatenated points since the final measurement is calculated from the two intermediate
            // points and not directly mapped to a DNP3 index
            mapSections[mapType].Add(new IndexMapping
            {
                ID = firstIntermediateMeasurement.PointID,
                Source = processParams.Settings.TargetSource,
                Index = dnp3Index
            });

            mapSections[mapType].Add(new IndexMapping
            {
                ID = secondIntermediateMeasurement.PointID,
                Source = processParams.Settings.TargetSource,
                Index = dnp3Index + 1
            });

            // Create dynamic calculator for concatenation, two binary points concatenated as (second << 1) | first
            string variableList =
                $"first={firstIntermediateMeasurement.SignalID}; " +
                $"second={secondIntermediateMeasurement.SignalID}";

            string calcConnectionString =
                $"VariableList={{{variableList}}}; " +
                $"OutputMeasurements={measurementRecord.SignalID}; " +
                "ExpressionText={(CAST(first, int) AND 1) OR ((CAST(second, int) AND 1) << 1)}; " +
                $"CalculationInterval={processParams.Settings.PollingInterval}; " +
                $"LagTime={processParams.Settings.ValidTimeRange}; " +
                $"LeadTime={processParams.Settings.ValidTimeRange}; " +
                "FramesPerSecond=1; UsePrecisionTimer=false; UseLatestValues=true; " +
                "TimestampSource=LocalClock; UseLocalClockAsRealTime=true; " +
                "IgnoreBadTimestamps=true; RoundToNearestTimestamp=true; " +
                "PerformTimestampReasonabilityCheck=false; " +
                "OutlierOperation=PublishWithBadState";

            string calcAdapterName = $"{deviceRecord.Acronym}.{pointTag}!CONCAT-CALC";

            CustomActionAdapterOperations.GetCustomActionAdapterRecord(
                processParams.CustomActionAdapterTable,
                processParams.NodeID,
                calcAdapterName,
                "DynamicCalculator.dll",
                "DynamicCalculator.DynamicCalculator",
                calcConnectionString);

            newAdapterAcronyms.Add(calcAdapterName);
        }
        else
        {
            // Add the DNP3 measurement record to the map sections for reading from DNP3
            mapSections[mapType].Add(new IndexMapping
            {
                ID = measurementRecord.PointID,
                Source = processParams.Settings.TargetSource,
                Index = dnp3Index
            });
        }

        // Handle quality flag measurements
        if (!processParams.Settings.PublishFlagsAsSeparateMeasurements)
            return newAdapterAcronyms;

        // Add or update DNP3 quality flag measurement record
        signalType = "ALOG";
        indexes["analogIndex"]++;
        index = indexes["analogIndex"];
        signalTypeSuffix = "AV";

        // Use the first intermediate low value for quality flags if concatenated
        string qualityPointTag = $"{pointTag}{(concatenated ? "!LOW" : "")}{processParams.Settings.QualityTagSuffix}";
        signalReference = $"{deviceRecord.Acronym}-{signalTypeSuffix}{index}";
        description = $"{deviceRecord.Name} {(concatenated ? "concatenated" : mapType)} {pointTag} Quality Flags [{signalType}{index:D4}]";
        string qualityAlternateTag = tagMapRow.AlternateTag;

        if (processParams.Settings.IEEEC37_118Version == 2011)
        {
            qualityAlternateTag = $"{qualityAlternateTag}{processParams.Settings.QualityTagSuffix}";
        }
        else
        {
            string suffix = processParams.Settings.QualityTagSuffix;
            int suffixLen = Math.Min(suffix.Length, 10);
            suffix = suffix[..suffixLen];
            int prefixLen = 16 - suffixLen;
            string prefix = qualityAlternateTag.Length > prefixLen ? qualityAlternateTag[..prefixLen] : qualityAlternateTag.PadRight(prefixLen);
            qualityAlternateTag = prefix + suffix;
        }

        MeasurementOperations.GetMeasurementRecord(
            processParams.MeasurementTable,
            deviceRecord.ID,
            deviceRecord.HistorianID,
            processParams.SignalTypeIDs[signalType],
            qualityPointTag,
            qualityAlternateTag,
            signalReference,
            1.0,
            description,
            processParams.MeasurementCache);

        return newAdapterAcronyms;
    }
}
