using Edge.Core.Configuration;
using Edge.Core.Database;
using Edge.Core.Database.Models;
using Edge.Core.IndustryStandardInterface.ATG;
using Edge.Core.IndustryStandardInterface.Pump;
using Edge.Core.Processor;
using Edge.Core.Processor.Dispatcher.Attributes;
using Edge.Core.UniversalApi;
using FdcServerHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Wayne.FDCPOSInterface;
using Wayne.FDCPOSLibrary;

namespace Applications.FDC
{
    [UniversalApi(Name = "OnFdcControllerStateChange", EventDataType = typeof(FdcPumpControllerOnStateChangeEventArg), Description = "When pump state changed, the event will fired")]
    [UniversalApi(Name = "OnError", EventDataType = typeof(string))]
    [UniversalApi(Name = "OnCurrentFuellingStatusChange", EventDataType = typeof(FdcServerTransactionDoneEventArg), Description = "When pump in fueling state, the fueling progress will be reported via this event")]
    [UniversalApi(Name = "OnFdcFuelSaleTransactinStateChange", EventDataType = typeof(FdcFuelSaleTransactinStateChangeEventArg), Description = "When pump transaction state changed, the event will fired")]
    [MetaPartsDescriptor(
        "lang-zh-cn:Fdc 服务器 Applang-en-us:Fdc Server App",
        "lang-zh-cn:用于管理已经配置和连接在此FCC中的所有加油机,并提供基于 IFSF-POS-FDC 协议定义的各类接口" +
        "lang-en-us:Used for manage all pumps configed and connected in this FCC, and providing varies of APIs based on IFSF-FDC-POS protocol",
        new[] { "lang-zh-cn:加油机lang-en-us:Pump", "lang-zh-cn:IfsfFdcServerlang-en-us:IfsfFdcServer" })]
    public class FdcServerHostApp : IAppProcessor
    {
        [UniversalApi(Description = "Get the overall pumps, nozzles info.")]
        public async Task<IEnumerable<object>> GetPumpsLayout(ApiData input)
        {
            //if (input == null || input.Parameters == null || !input.Parameters.Any())
            //    return this.fdcPumpControllers;
            //var targetPumpIds = input.Parameters.Where(v => v.Name != null && v.Name.ToLower() == "pumpid" && int.TryParse(v.Value, out _))
            //    .Select(v => int.Parse(v.Value));
            ////if (!targetPumpIds.Any()) return this.fdcPumpControllers;
            return this.fdcPumpControllers.Select(p => new
            {
                p.Name,
                p.PumpId,
                Nozzles = p.Nozzles.Select(n => new
                {
                    n.LogicalId,
                    n.RealPriceOnPhysicalPump,
                    SiteLevelNozzleId = this.nozzleExtraInfos.FirstOrDefault(c =>
                        c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.SiteLevelNozzleId,
                    ProductBarcode = this.nozzleExtraInfos.FirstOrDefault(c =>
                        c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.ProductBarcode,
                    ProductName = this.nozzleExtraInfos.FirstOrDefault(c =>
                        c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.ProductName,
                }),
                p.AmountDecimalDigits,
                p.VolumeDecimalDigits,
                p.PriceDecimalDigits,
                p.VolumeTotalizerDecimalDigits
            });
        }

        [UniversalApi(Description = "Get FuelSaleTrx Details info by searching conditions. </br>" +
            "Searching condition 1: by providing para.Name=='ReleaseToken' will ignore other conditions.</br>" +
            "Searching condition 2: para.Name=='PumpId' and para.Name=='LogicalNozzleId' must be provided, and para.Name=='TransactionNumber' is optional.")]
        public async Task<IEnumerable<object>> GetFuelSaleTrxDetailsAsync(ApiData input)
        {
            if (input == null || input.Parameters == null || !input.Parameters.Any()) throw new ArgumentException(nameof(input));
            List<FuelSaleTransaction> transactions;
            if (int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "releasetoken")?.Value, out int targetReleaseToken))
            {
                SqliteDbContext dbContext = new SqliteDbContext();
                transactions = await dbContext.PumpTransactionModels.Where(t => t.ReleaseToken == targetReleaseToken).ToListAsync();
            }
            else
            {
                if (!int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "pumpid")?.Value, out int targetPumpId)
                    || !int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "logicalnozzleid")?.Value, out int targetLogicalNozzleId))
                    throw new ArgumentException("Must provide valid parameter value for PumpId and LogicalNozzleId");

                var targetTransactionNumber = input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "transactionnumber")?.Value;
                SqliteDbContext dbContext = new SqliteDbContext();
                int maxReturnDataCount = 50;
                if (string.IsNullOrEmpty(targetTransactionNumber))
                    transactions = await dbContext.PumpTransactionModels.Where(t =>
                         t.PumpId == targetPumpId && t.LogicalNozzleId == targetLogicalNozzleId).OrderByDescending(t => t.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
                else
                    transactions = await dbContext.PumpTransactionModels.Where(t =>
                         t.PumpId == targetPumpId && t.LogicalNozzleId == targetLogicalNozzleId
                         && t.TransactionSeqNumberFromPhysicalPump == targetTransactionNumber).OrderByDescending(t => t.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
            }

            return transactions.Select(trx =>
            {
                //NOTE, the queried trx may from old times, but the this.fdcPumpControllers and this.nozzleProductConfig 
                //are always the latest, so they may mismatch and produce incorrect data.
                var pumpHandler = this.fdcPumpControllers.FirstOrDefault(fpc => fpc.PumpId == trx.PumpId);
                return new
                {
                    trx.ReleaseToken,
                    trx.PumpId,
                    trx.LogicalNozzleId,
                    SiteLevelNozzleId = this.nozzleExtraInfos.FirstOrDefault(c =>
                        c.PumpId == trx.PumpId && c.NozzleLogicalId == trx.LogicalNozzleId)?.SiteLevelNozzleId,
                    trx.TransactionSeqNumberFromPhysicalPump,
                    trx.ProductBarcode,
                    ProductName = this.nozzleExtraInfos.FirstOrDefault(c =>
                       c.PumpId == trx.PumpId && c.NozzleLogicalId == trx.LogicalNozzleId)?.ProductName,

                    RawAmount = trx.Amount,
                    Amount = pumpHandler == null ? (double?)null : (trx.Amount / Math.Pow(10, pumpHandler.AmountDecimalDigits)),

                    RawVolume = trx.Volumn,
                    Volume = pumpHandler == null ? (double?)null : trx.Volumn / Math.Pow(10, pumpHandler.VolumeDecimalDigits),

                    RawPrice = trx.UnitPrice,
                    Price = pumpHandler == null ? (double?)null : trx.UnitPrice / Math.Pow(10, pumpHandler.PriceDecimalDigits),

                    RawAmountTotalizer = trx.AmountTotalizer,
                    AmountTotalizer = pumpHandler == null ? (double?)null : trx.AmountTotalizer / Math.Pow(10, pumpHandler.AmountDecimalDigits),

                    RawVolumeTotalizer = trx.VolumeTotalizer,
                    VolumeTotalizer = pumpHandler == null ? (double?)null : trx.VolumeTotalizer / Math.Pow(10, pumpHandler.VolumeDecimalDigits),

                    trx.State,
                    trx.SaleStartTime,
                    trx.SaleEndTime,
                };
            });
        }

        [UniversalApi]
        public int PumpCount => this.fdcPumpControllers?.Count ?? 0;

        [UniversalApi]
        public async Task<IEnumerable<object>> GetDeviceState(ApiData input)
        {
            List<object> resultList = new List<object>();
            foreach (var controller in this.FdcPumpControllers)
            {
                var state = await controller.QueryStatusAsync();
                //var nozzles = controller.Nozzles.Select(n => new
                //{
                //    n.LogicalId,
                //    VolumeTotalizer = GetVolumeTotalizer(controller.PumpId, n.LogicalId),
                //    Amount = GetLatestTransactionAmount(controller.PumpId, n.LogicalId),
                //    n.LogicalState
                //});
                List<object> nozzles = new List<object>();
                foreach (var n in controller.Nozzles)
                {
                    if (n.VolumeTotalizer == null)
                    {
                        var result = await GetFuelPointTotalsAsync(controller.PumpId, n.LogicalId);
                        n.VolumeTotalizer = (int?)(result.Item2 * Math.Pow(10, controller.VolumeDecimalDigits));
                    }
                    var Amount = GetLatestTransactionAmount(controller.PumpId, n.LogicalId);
                    nozzles.Add(new { n.LogicalId, n.VolumeTotalizer, Amount });
                }
                resultList.Add(new { controller.PumpId, DeviceState = state, Nozzles = nozzles });
            }
            return resultList;
        }

        private int GetLatestTransactionAmount(int pumpId, int nozzleId)
        {
            var dueDate = DateTime.Now.Subtract(new TimeSpan(30, 0, 0, 0));
            SqliteDbContext dbContext = new SqliteDbContext();
            var trans = dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Cleared
                && t.PumpId == pumpId && t.LogicalNozzleId == nozzleId
                && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime).Take(1);
            return trans.Any() ? trans.FirstOrDefault().Amount : 0;
        }

        [UniversalApi(Description = "Get lastest 1000 count of Payable or Locked state fuel trx for nozzles." +
                                   "</br>input parameters are explained as below, " +
                                   "</br>para.Name==\"nozzleid\" nozzle logical id" +
                                   "</br>\"pumpid\" the target pump id" +
                                    "</br>Leave input as Null to get for all nozzles in all pumps")]
        public async Task<object> GetLastAvailableTransaction(ApiData input)
        {
            var nozzleId = -1;
            var pumpId = -1;
            if (input != null && !input.IsEmpty())//with no search condition specified, return latest 1000 records.
            {
                var temp = "";
                foreach (var p in input.Parameters)
                {
                    temp += p.Name + " " + p.Value + " ";
                }

                //fdcLogger.LogDebug("input is not null, parameters:{0}", temp);
                nozzleId = input.Get("nozzleid", -1);
                pumpId = input.Get("pumpid", -1);
            }

            fdcLogger.LogDebug("GetAvailableTransactions for nozzle:{0},pump:{1}", nozzleId, pumpId);
            List<object> resultList = new List<object>();
            var result = await GetAvailableFuelSaleTrxsWithDetailsAsync(pumpId, nozzleId, 1000);
            if (result != null && result.Any())
            {
                var all = result.GroupBy(r => new { r.PumpId, r.LogicalNozzleId });
                foreach (var a in all)
                {
                    var trans = a.Where(_ =>
                        _.State == FuelSaleTransactionState.Payable || _.State == FuelSaleTransactionState.Locked);
                    resultList.Add(new { a.Key.PumpId, a.Key.LogicalNozzleId, hasPayableTransactions = trans.Any(), trans.FirstOrDefault().VolumeTotalizer, trans.FirstOrDefault().Amount });
                }

                return resultList;
            }

            resultList.Add(new { LogicalNozzleId = nozzleId, PumpId = pumpId, hasPayableTransactions = false });
            return resultList;
        }

        private bool config_AutoAuthCallingPumps;
        private int config_MaxStackUnpaidTrxPerPump;
        private int config_ListeningPort;

        private FDCPOSInterfaceServer fdcServer;


        public string MetaConfigName { get; set; }
        private IEnumerable<NozzleExtraInfo> nozzleExtraInfos;

        private List<IFdcPumpController> fdcPumpControllers;
        private List<IFdcCommunicableController> fdcCommunicableControllers;
        private List<IAutoTankGaugeController> autoTankGaugeControllers = new List<IAutoTankGaugeController>();
        private IServiceProvider services;
        private Configurator configurator;
        private EventHandler onConfiguratorConfigFileChangedEventHandler;
        private System.Timers.Timer purgeDatabaseTimer;
        static ILogger fdcLogger;//= NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("FdcServer");
        static ILogger cloudLogger;// = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("CloudRestClient");

        private object syncObject = new object();
        enum FDCLogicalState
        {
            FDC_LOGICAL_ST_UNLOCKED = 0,
            FDC_LOGICAL_ST_LOCKED = 1,
        };

        #region App configs

        /// <summary>
        /// when doing major schema change of the config class, the class should not be modify directly.
        /// instead, for better fallback support, create a new class with a version suffix, and also
        /// update the version string in appCtorParamsJsonSchema.json file.
        /// </summary>
        public class AppConfigV1
        {
            public bool AutoAuthCallingPumps { get; set; }
            public int MaxStackUnpaidTrxPerPump { get; set; }
            public int FdcServerListeningPort { get; set; }
            public int? PurgePayableTrxOlderThanByMin { get; set; }
            public int? PurgeClearedTrxOlderThanByDay { get; set; }
            public List<AppProductConfigV1> ProductConfigs { get; set; }
            public List<AppPumpConfigV1> PumpConfigs { get; set; }
        }

        public class AppPumpConfigV1
        {
            public int PumpId { get; set; }
            public List<AppNozzleConfigV1> NozzleConfigs { get; set; }
        }

        public class AppProductConfigV1
        {
            public int ProductCode { get; set; }
            public string ProductName { get; set; }
        }

        public class AppNozzleConfigV1
        {
            public int NozzleLogicalId { get; set; }
            public int? SiteLevelNozzleId { get; set; }
            public int ProductCode { get; set; }

            /// <summary>
            /// Gets or sets the tank number of this nozzle undergroud linked.
            /// </summary>
            public int? TankNumber { get; set; }

            public string Description { get; set; }
        }

        #endregion

        /// <summary>
        /// A callback function that will be called on DefaultDispatcher(a framework util for instantiate App and DeviceHandler) 
        /// is trying to serialize param strings(most likely saved into a database) into params of this class's ctor, and got an error.
        /// 
        /// should handle the json str that cause the serialize error, resolve and reformat it to a compatible content.
        /// </summary>
        /// <param name="incompatibleCtorParamsJsonStr">sample: [{"UITemplateVersion":"1.1","AutoAuthCallingPumps":false,"MaxStackUnpaidTrxPerPump":3}, "2nd string parameter", 3]</param>
        /// <returns>need follow format of json array, sample: [{firstObjectParameter},"2nd string parameter",3]</returns>
        private static List<object> ResolveCtorMetaPartsConfigCompatibility(string incompatibleCtorParamsJsonStr)
        {
            var jsonParams = JsonDocument.Parse(incompatibleCtorParamsJsonStr).RootElement.EnumerateArray().ToArray();

            //sample:   "UITemplateVersion":"1.0"
            string uiTemplateVersionRegex = @"(?<=""UITemplateVersion""\:\"").+?(?="")";
            var match = Regex.Match(jsonParams.First().GetRawText(), uiTemplateVersionRegex, RegexOptions.IgnoreCase | RegexOptions.Multiline);
            if (match.Success)
            {
                // the curVersion string has been found, can handle it with proper fallback.
                var curVersion = match.Value;
                if (curVersion == "1.0")
                {
                    var existsAppConfigV1 = JsonSerializer.Deserialize(jsonParams.First().GetRawText(), typeof(AppConfigV1));
                    // AppConfigV2 appConfigV2 = new AppConfigV2();
                    // appConfigV2.aaa = existsAppConfigV1.aaaa;
                    // appConfigV2.bbb = existsAppConfigV1.bbbb;
                    // ...
                    // return new List<object>(){appConfigV2};
                }
                //else if(curVersion == "2.0")
                //{
                //      var existsAppConfigV2 = JsonSerializer.Deserialize(jsonParams.First().GetRawText(), typeof(AppConfigV2));
                //      AppConfigV3 appConfigV3 = new AppConfigV3();
                //      appConfigV3.aaa = existsAppConfigV2.aaaa;
                //      appConfigV3.bbb = existsAppConfigV2.bbbb;
                //      ...
                //      return new List<object>(){appConfigV3};
                //}
            }
            else
            {
                // try fallback as best as you can by reading the raw input string, 
                // if you have no idea to do any fallback, then return a most common config for let the user can go through easier.
                return new List<object>() {
                new AppConfigV1() {
                    AutoAuthCallingPumps = false,
                    FdcServerListeningPort = 4711,
                    MaxStackUnpaidTrxPerPump = 3 } };
            }

            return new List<object>() {
                new AppConfigV1() {
                    AutoAuthCallingPumps = false,
                    FdcServerListeningPort = 4711,
                    MaxStackUnpaidTrxPerPump = 3 } };
        }

        public Task Test(params object[] parameters)
        {
            if ((!this.fdcServer?.IsStarted) ?? false)
                throw new InvalidOperationException("Fdc server failed to start on a port");
            return Task.CompletedTask;
        }

        [ParamsJsonSchemas("appCtorParamsJsonSchema")]
        public FdcServerHostApp(AppConfigV1 appConfig, IServiceProvider services)
        {
            this.config_AutoAuthCallingPumps = appConfig.AutoAuthCallingPumps;
            this.config_MaxStackUnpaidTrxPerPump = appConfig.MaxStackUnpaidTrxPerPump;
            this.config_ListeningPort = appConfig.FdcServerListeningPort;
            if (appConfig.PurgeClearedTrxOlderThanByDay.HasValue || appConfig.PurgePayableTrxOlderThanByMin.HasValue)
            {
                this.purgeDatabaseTimer = new System.Timers.Timer();
                this.purgeDatabaseTimer.Interval = 1000 * 60;
                this.purgeDatabaseTimer.Elapsed += (s, a) =>
                {
                    if (appConfig.PurgeClearedTrxOlderThanByDay.HasValue)
                    {
                        try
                        {
                            using (var dbContext = new SqliteDbContext())
                            {
                                var due = DateTime.Now.Subtract(new TimeSpan(appConfig.PurgeClearedTrxOlderThanByDay.Value, 0, 0, 0));
                                var deleting = dbContext.PumpTransactionModels.Where(t => t.State == FuelSaleTransactionState.Cleared && t.PaidTime.HasValue && t.PaidTime <= due);
                                dbContext.RemoveRange(deleting);
                                var deletedRowCount = dbContext.SaveChanges();
                                if (deletedRowCount > 0)
                                    fdcLogger.LogDebug($"PurgeClearedTrxOlderThanByDay purged: {deletedRowCount} rows");
                            }
                        }
                        catch (Exception exxx)
                        {
                            fdcLogger.LogError("PurgeClearedTrxOlderThanByDay exceptioned: " + exxx.ToString());
                        }

                        try
                        {
                            if (appConfig.PurgePayableTrxOlderThanByMin.HasValue)
                            {
                                using (var dbContext = new SqliteDbContext())
                                {
                                    var due = DateTime.Now.Subtract(new TimeSpan(0, appConfig.PurgePayableTrxOlderThanByMin.Value, 0));
                                    var deleting = dbContext.PumpTransactionModels.Where(t => t.State == FuelSaleTransactionState.Payable && t.SaleStartTime.HasValue && t.SaleStartTime <= due);
                                    dbContext.RemoveRange(deleting);
                                    var deletedRowCount = dbContext.SaveChanges();
                                    if (deletedRowCount > 0)
                                        fdcLogger.LogDebug($"PurgePayableTrxOlderThanByMin purged: {deletedRowCount} rows");
                                }
                            }
                        }
                        catch (Exception exxx)
                        {
                            fdcLogger.LogError("PurgePayableTrxOlderThanByMin exceptioned: " + exxx.ToString());
                        }
                    }
                };
                this.purgeDatabaseTimer.Start();
            }

            this.services = services;
            var loggerFactory = services.GetRequiredService<ILoggerFactory>();
            fdcLogger = loggerFactory.CreateLogger("DynamicPrivate_FdcServer");
            cloudLogger = loggerFactory.CreateLogger("CloudRestClient");
            //this.configurator = services.GetService<Configurator>();
            this.nozzleExtraInfos = appConfig.PumpConfigs
                ?.SelectMany(p => p.NozzleConfigs,
                    (p, ns) =>
                        new NozzleExtraInfo()
                        {
                            PumpId = p.PumpId,
                            NozzleLogicalId = ns.NozzleLogicalId,
                            SiteLevelNozzleId = ns.SiteLevelNozzleId,
                            ProductBarcode = ns.ProductCode,
                            ProductName = appConfig.ProductConfigs?.FirstOrDefault(p => p.ProductCode == ns.ProductCode)?.ProductName,
                            TankNumber = ns.TankNumber,
                            Description = ns.Description,
                        });
            FdcResourceArbitrator.fdcLogger = fdcLogger;
            this.fdcServer = new FDCPOSInterfaceServer(services);
        }

        public FdcServerHostApp(string autoAuthCallingPumps, int maxStackUnpaidTrxPerPump, int listeningPort
            , IServiceProvider services)
        {
            //QRCoder.AsciiQRCode c = new QRCoder.AsciiQRCode();
            this.config_AutoAuthCallingPumps = autoAuthCallingPumps.ToLower() == "true" ? true : false;
            this.config_MaxStackUnpaidTrxPerPump = maxStackUnpaidTrxPerPump;
            this.config_ListeningPort = listeningPort;

            this.services = services;
            var loggerFactory = services.GetRequiredService<ILoggerFactory>();
            fdcLogger = loggerFactory.CreateLogger("DynamicPrivate_FdcServer");
            cloudLogger = loggerFactory.CreateLogger("CloudRestClient");
            this.configurator = services.GetService<Configurator>();
            this.nozzleExtraInfos = Configurator.Default.NozzleExtraInfoConfiguration.Mapping;
            FdcResourceArbitrator.fdcLogger = fdcLogger;
            this.fdcServer = new FDCPOSInterfaceServer(services);
            this.onConfiguratorConfigFileChangedEventHandler = (_, __) =>
            {
                try
                {
                    var c = Configurator.Default.DeviceProcessorConfiguration.Processor
                        .FirstOrDefault(p => p.Name == this.MetaConfigName);
                    if (c == null) return;

                    var newAutoAuthCallingPumps = c.Parameter.First(p => p.Name == "AutoAuthCallingPumps").Value.ToLower() == "true" ? true : false;
                    var newMaxStackUnpaidTrxPerPump = int.Parse(c.Parameter.First(p => p.Name == "MaxStackUnpaidTrxPerPump").Value);

                    if (this.config_AutoAuthCallingPumps != newAutoAuthCallingPumps)
                    {
                        fdcLogger.LogInformation("Configuration change detected on parameter: autoAuthCallingPumps, " +
                            "will update its value from: " + this.config_AutoAuthCallingPumps + " to: " + newAutoAuthCallingPumps);
                        this.config_AutoAuthCallingPumps = newAutoAuthCallingPumps;
                    }

                    if (this.config_MaxStackUnpaidTrxPerPump != newMaxStackUnpaidTrxPerPump)
                    {
                        fdcLogger.LogInformation("Configuration change detected on parameter: maxStackUnpaidTrxPerPump, " +
                            "will update its value from: " + this.config_MaxStackUnpaidTrxPerPump + " to: " + newMaxStackUnpaidTrxPerPump);
                        this.config_MaxStackUnpaidTrxPerPump = newMaxStackUnpaidTrxPerPump;
                    }
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("Configuration auto reload exceptioned: " + exxx);
                }
            };
            this.configurator.OnConfigFileChanged += this.onConfiguratorConfigFileChangedEventHandler;
        }

        public void Init(IEnumerable<IProcessor> processors)
        {
            this.fdcPumpControllers = new List<IFdcPumpController>();
            this.fdcPumpControllers.AddRange(
                processors.WithHandlerOrApp<IFdcPumpController>().SelectHandlerOrAppThenCast<IFdcPumpController>());
            foreach (var p in processors)
            {
                if (p.IsWithHandlerOrApp<IEnumerable<IFdcPumpController>>())
                {
                    var pumpGroupHandler = p.SelectHandlerOrAppThenCast<IEnumerable<IFdcPumpController>>();
                    this.fdcPumpControllers.AddRange(pumpGroupHandler);

                    // dynamic check does `OnFdcServerInit` defined 
                    if (pumpGroupHandler.GetType().GetMethod("OnFdcServerInit")?.GetParameters()
                            ?.FirstOrDefault()?.ParameterType?.IsAssignableFrom(typeof(Dictionary<string, object>)) ?? false)
                    {
                        pumpGroupHandler.GetType().GetMethod("OnFdcServerInit")?.Invoke(
                            pumpGroupHandler,
                            new[]{
                                new Dictionary<string, object>() {
                                    { "NozzleProductMapping", this.nozzleExtraInfos }
                            }
                        });
                    }
                }
            }

            if (this.fdcPumpControllers.GroupBy(p => p.PumpId).Any(g => g.Count() > 1))
                throw new ArgumentException("Duplicate PumpId in Fdc Pump Controllers, PumpId must be unique");

            this.fdcCommunicableControllers = new List<IFdcCommunicableController>();
            this.fdcCommunicableControllers.AddRange(processors.WithHandlerOrApp<IFdcCommunicableController>().SelectHandlerOrAppThenCast<IFdcCommunicableController>());

            this.autoTankGaugeControllers = new List<IAutoTankGaugeController>();
            this.autoTankGaugeControllers.AddRange(processors.WithHandlerOrApp<IAutoTankGaugeController>().SelectHandlerOrAppThenCast<IAutoTankGaugeController>());
        }

        public class Grouping<TKey, TElement> : List<TElement>, IGrouping<TKey, TElement>
        {
            public Grouping(TKey key) : base() => Key = key;
            public Grouping(TKey key, int capacity) : base(capacity) => Key = key;
            public Grouping(TKey key, IEnumerable<TElement> collection)
                : base(collection) => Key = key;
            public TKey Key { get; private set; }
        }

        public Task<bool> Start()
        {
            //var fdcServer = new FDCPOSInterfaceServer();

            #region OnGetCountrySettings, OnLogOnReq and OnLogOffReq

            fdcServer.OnGetCountrySettingsReq += (string workstationID, string applicationSender, int requestID) =>
            {
                fdcLogger.LogDebug("OnGetCountrySettingsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")");
                fdcServer.GetCountrySettings(workstationID, applicationSender, requestID, "Litre", "CNY", "Litre", "C", "3", ".", "CHN", "zh-cn", (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
            };
            fdcServer.OnStartForecourtReq += (string workstationID, string applicationSender, int requestID) =>
            {
                fdcLogger.LogDebug("OnStartForecourtReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")");
                fdcServer.StartForecourt(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
            };
            fdcServer.OnLogOnReq += (string workstationID, string applicationSender, int requestId, int responsePort, int unsolicitedPort, int version) =>
            {
                fdcLogger.LogInformation("OnLogOnReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", ver: " + version + ")");
                fdcServer.LogOn(workstationID, applicationSender, requestId, responsePort, unsolicitedPort, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
            };
            fdcServer.OnLogOffReq += (string workstationID, string applicationSender, int requestId) =>
            {
                fdcLogger.LogInformation("OnLogOffReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ")");
                fdcServer.LogOff(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
            };

            #endregion

            #region reserve related

            fdcServer.OnLockNozzleReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int NozzleNo) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnLockNozzleReq (wid: " + workstationID + ", appSender: "
                        + applicationSender + ", requestId: " + requestId
                        + ", deviceId: " + deviceId + ", NozzleNo: " + NozzleNo);
                    var result = await this.LockNozzleAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, NozzleNo);
                    if (!result)
                        fdcLogger.LogInformation("    LockNozzle failed");
                    fdcLogger.LogDebug("   OnLockNozzleReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnLockNozzleReq exceptioned: " + exxx.ToString());
                }
            };
            fdcServer.OnUnlockNozzleReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int NozzleNo) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnUnlockNozzleReq (wid: " + workstationID + ", appSender: "
                        + applicationSender + ", requestId: " + requestId
                        + ", deviceId: " + deviceId + ", NozzleNo: " + NozzleNo);
                    var result = await this.UnlockNozzleAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, NozzleNo);
                    //if (!result)
                    //    fdcServer.UnlockNozzle(workstationID, applicationSender, requestId, deviceId, deviceId, NozzleNo, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    //else
                    //    fdcServer.UnlockNozzle(workstationID, applicationSender, requestId, deviceId, deviceId, NozzleNo, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString());
                    fdcLogger.LogDebug("   OnUnlockNozzleReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnUnlockNozzleReq exceptioned: " + exxx.ToString());
                }
            };
            fdcServer.OnReserveFuelPointReq += async (string workstationID, string applicationSender, int requestId, int deviceId) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnReserveFuelPointReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId);
                    var result = await FdcResourceArbitrator.Default.TryReserveFuelPointAsync(int.Parse(workstationID), deviceId);
                    if (result)
                        fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    else
                    {
                        fdcLogger.LogInformation("    ReserveFuelPoint failed");
                        fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_FPLOCK, OverallResult.Failure.ToString());
                    }

                    fdcLogger.LogDebug("   OnReserveFuelPointReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnReserveFuelPointReq exceptioned: " + exxx.ToString());
                    fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString());
                }
            };
            fdcServer.OnFreeFuelPointReq += async (string workstationID, string applicationSender, int requestId, int deviceId) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnFreeFuelPointReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId);
                    var result = await FdcResourceArbitrator.Default.TryUnreserveFuelPointAsync(int.Parse(workstationID), deviceId);
                    if (result)
                        fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    else
                        fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString());
                    fdcLogger.LogDebug("   OnFreeFuelPointReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnFreeFuelPointReq exceptioned: " + exxx.ToString());
                    fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString());
                }
            };
            fdcServer.OnLockFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId,
                int transactionNo, string releaseToken) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnLockFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: "
                        + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")");
                    var result = await this.LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, transactionNo, int.Parse(releaseToken));
                    //var result = this.LockFuelSaleTrxAndNotifyAllFdcClients(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken));
                    //if (result != null)
                    //{
                    //    var targetController = fdcPumpControllers
                    //        .First(c => c.PumpId == deviceId) as IFdcPumpController;
                    //    fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                    //        transactionNo, releaseToken, (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString());
                    //    fdcServer.FuelSaleTrx(deviceId,
                    //       result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                    //       result.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                    //       result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                    //       result.LogicalNozzleId,
                    //       int.Parse(result.ProductBarcode),
                    //       "refer cloud" + result.ProductBarcode, "", 1,
                    //       int.Parse(result.TransactionSeqNumberFromPhysicalPump),
                    //       (int)FuelSaleTransactionState.Locked,
                    //       0, releaseToken,
                    //       result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                    //       result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                    //       workstationID, "", int.Parse(workstationID), 0);
                    //}
                    //else
                    //{
                    //    fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                    //              transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                    //}
                    if (result == null)
                        fdcLogger.LogDebug($"   OnLockFuelSaleTrxReq(releaseToken: {releaseToken}) failed");
                    fdcLogger.LogDebug($"   OnLockFuelSaleTrxReq(releaseToken: {releaseToken}) done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnLockFuelSaleTrxReq exceptioned: " + exxx.ToString());
                    try
                    {
                        fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                                  transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                    }
                    catch (Exception eeee)
                    {
                        fdcLogger.LogError("Fdc server is trying to respond a failed LocFuelSaleTrx response to a fdcclient that issue current LockFuleSaleTrx request, but still failed with reason: " + eeee);
                    }
                }
            };
            fdcServer.OnUnlockFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId,
                int transactionNo, string releaseToken) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnUnlockFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender
                        + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo
                        + ", releaseToken: " + releaseToken);
                    var result = await this.UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, transactionNo, int.Parse(releaseToken));
                    //var result = this.UnlockFuelSaleTrxAndNotifyAllFdcClients(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken));
                    //if (result != null)
                    //{
                    //    var targetController = fdcPumpControllers
                    //           .First(c => c.PumpId == deviceId) as IFdcPumpController;
                    //    fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                    //        transactionNo, releaseToken, (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString());
                    //    fdcServer.FuelSaleTrx(deviceId,
                    //        result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                    //        result.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                    //        result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                    //       result.LogicalNozzleId,
                    //        int.Parse(result.ProductBarcode),
                    //        "refer cloud" + result.ProductBarcode, "", 1,
                    //        int.Parse(result.TransactionSeqNumberFromPhysicalPump),
                    //        (int)FuelSaleTransactionState.Payable,
                    //        0, releaseToken,
                    //        result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                    //        result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                    //        "", "", int.Parse(workstationID), 0);
                    //}
                    //else
                    //{
                    //    fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                    //      transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                    //}
                    if (result == null)
                        fdcLogger.LogDebug("   OnUnlockFuelSaleTrxReq failed");
                    fdcLogger.LogDebug("   OnUnlockFuelSaleTrxReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnUnlockFuelSaleTrxReq exceptioned: " + exxx.ToString() + Environment.NewLine);
                    try
                    {
                        fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId,
                              transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                    }
                    catch (Exception eeee)
                    {
                        fdcLogger.LogError("Fdc server is trying to respond a failed UnlocFuelSaleTrx response to a fdcclient that issue current UnlockFuleSaleTrx request, but still failed with reason: " + eeee);
                    }
                }
            };

            #endregion

            fdcServer.OnGetTankDataReq += async (string workstationID, string applicationSender, int requestID, int deviceId, int tankNo) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnGetTankDataReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ", deviceId: " + deviceId + ", tankNo: " + tankNo + ")");
                    if (this.autoTankGaugeControllers == null || !this.autoTankGaugeControllers.Any())
                    {
                        fdcLogger.LogDebug($"   failed for OnGetTankDataReq since there's an empty AutoTankGaugeControllers list.");
                        fdcServer.GetTankDataSend(workstationID, applicationSender, requestID,
                           (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString());
                        return;
                    }

                    var atgController = this.autoTankGaugeControllers.FirstOrDefault();
                    if (!atgController.Tanks.Any())
                    {
                        fdcLogger.LogDebug($"   failed for OnGetTankDataReq since there's no Tanks at all in AutoTankGaugeController.");
                        fdcServer.GetTankDataSend(workstationID, applicationSender, requestID,
                           (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString());
                        return;
                    }

                    if (deviceId != -1
                        && !atgController.Tanks.Any(t => t.TankNumber == deviceId))
                    {
                        fdcLogger.LogDebug($"   failed for OnGetTankDataReq since there's no Tank with number: {deviceId} in AutoTankGaugeController.");
                        fdcServer.GetTankDataSend(workstationID, applicationSender, requestID,
                           (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString());
                        return;
                    }

                    //fdcLogger.LogDebug($"   atgController has {atgController.Tanks?.Count() ?? -1} tanks configed.");
                    if (deviceId == -1)
                    {
                        foreach (var tank in atgController.Tanks)
                        {
                            try
                            {
                                var tankReading = await atgController.GetTankReadingAsync(tank.TankNumber);
                                fdcLogger.LogDebug($"   GetTankReadingAsync for tankNumber: {tank.TankNumber} succeed, data-> { tankReading.ToString()}");
                                fdcServer.GetTankDataAdd(workstationID, applicationSender, requestID,
                                    tank.TankNumber,
                                    tank.TankNumber, 0, 0,
                                    tankReading.Height ?? 0,
                                    tankReading.Volume ?? 0,
                                    tankReading.TcVolume ?? 0,
                                    tankReading.Temperature ?? 0,
                                    tankReading.Water ?? 0,
                                    0,
                                    (int)ErrorCode.ERRCD_OK, (int)LogicalDeviceState.FDC_READY);
                            }
                            catch (Exception exx)
                            {
                                fdcLogger.LogInformation($"   failed for GetTankReadingAsync for tankNumber: {tank.TankNumber} by exception: {exx}" +
                                    $"{Environment.NewLine} will skip this tank and continue on next tank...");
                                continue;
                            }
                        }
                    }
                    else
                    {
                        var tankReading = await atgController.GetTankReadingAsync(deviceId);
                        fdcLogger.LogDebug($"       GetTankReadingAsync for tankNumber: " + deviceId + " succeed " +
                            Environment.NewLine + tankReading.ToString());
                        fdcServer.GetTankDataAdd(workstationID, applicationSender, requestID,
                            deviceId,
                            deviceId, 0, 0,
                            tankReading.Height ?? 0,
                            tankReading.Volume ?? 0,
                            tankReading.TcVolume ?? 0,
                            tankReading.Temperature ?? 0,
                            tankReading.Water ?? 0,
                            0,
                            (int)ErrorCode.ERRCD_OK, (int)LogicalDeviceState.FDC_READY);
                    }

                    fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());

                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnGetTankDataReq exceptioned: " + exxx.ToString());
                    fdcServer.GetTankDataSend(workstationID, applicationSender, requestID,
                        (int)ErrorCode.ERRCD_BADVAL, OverallResult.Failure.ToString());
                }
            };
            fdcServer.OnGetProductTableReq += (string workstationID, string applicationSender, int requestID) =>
            {
                fdcLogger.LogDebug("OnGetProductTableReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")");

                var allProductBarcodes = nozzleExtraInfos.GroupBy(p => p.ProductBarcode);
                foreach (var b in allProductBarcodes)
                    fdcServer.GetProductTableAdd(workstationID, applicationSender, requestID, b.Key, (b.FirstOrDefault()?.ProductName ?? ("refer cloud for name of " + b.Key)));
                fdcServer.GetProductTableSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
            };
            fdcServer.OnGetConfigurationReq += (string workstationID, string applicationSender, int requestId, string deviceType) =>
            {
                lock (this.syncObject)
                {
                    try
                    {
                        fdcLogger.LogDebug("OnGetConfigurationReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceType: " + deviceType);
                        if (deviceType == "TLG")
                        {
                            /* here based on local manual config to generate tanks and return to POS side, probly use the concrete AutoTankGaugeControl.Tanks 
                             * is a better choice but the known issue is the product code and label read from VR ATG console may not correct(may bug in VR console).
                             * so keep this way of local manual config with high priority.
                            */
                            var tankGroups = this.nozzleExtraInfos.Where(np => np.TankNumber.HasValue)
                                 .GroupBy(np => np.TankNumber)
                                 .Select(tg =>
                                 {
                                     /* if real ATG exists, will use ATG Tank info, otherwise create a logical Tank object but fill with info from nozzleExtraInfo*/
                                     var tank = this.autoTankGaugeControllers?.SelectMany(c => c.Tanks ?? new Tank[] { })?.FirstOrDefault(t => t.TankNumber == tg.Key)
                                        ?? new Tank()
                                        {
                                            TankNumber = (byte)tg.Key.Value,
                                            Label = "Logical tank",
                                            Product = new Product()
                                            {
                                                ProductCode = tg.First().ProductBarcode.ToString(),
                                                ProductLabel = tg.First().Description
                                            }
                                        };
                                     var pumpGroups = tg.GroupBy(np => np.PumpId).Select(pg => new Grouping<int, byte>(pg.Key, pg.Select(x => (byte)x.NozzleLogicalId)));
                                     return new Grouping<Tank, Grouping<int, byte>>(tank, pumpGroups);
                                 });
                            if (!tankGroups.Any())
                            {
                                fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId,
                                    deviceType, OverallResult.WrongConfiguration.ToString());
                                fdcLogger.LogDebug("   OnGetConfigurationReq with TLG done with WrongConfiguration as no tanks info generated.");
                                return;
                            }

                            fdcLogger.LogDebug("   OnGetConfigurationReq with TLG done with success, returned data-> " +
                                tankGroups.Select(tg => $"Tank with number: {tg.Key.TankNumber}, " +
                                    $"label: {tg.Key.Label ?? ""}, productCode: {tg.Key.Product?.ProductCode ?? ""} linked to PumpIds: " +
                                        $"{tg.Select(p => $"{p.Key}(nzlLogiIds: {p.Select(n => n.ToString()).Aggregate("", (acc, n) => acc + ", " + n)})").Aggregate("", (acc, n) => acc + ", " + n)}")
                                .Aggregate((acc, n) => acc + "; " + n));
                            fdcServer.GetConfigurationAddTLG(workstationID, applicationSender, requestId, tankGroups);
                        }
                        else
                        {
                            foreach (var fdcPumpController in fdcPumpControllers)
                            {
                                foreach (var nozzle in fdcPumpController.Nozzles)
                                {
                                    var bindProduct = nozzleExtraInfos.FirstOrDefault(n =>
                                        n.PumpId == fdcPumpController.PumpId
                                            && n.NozzleLogicalId == nozzle.LogicalId);
                                    var fuelPrice = nozzle.RealPriceOnPhysicalPump == null ? 0 :
                                        (nozzle.RealPriceOnPhysicalPump.Value / Math.Pow(10, fdcPumpController.PriceDecimalDigits));
                                    if (string.IsNullOrEmpty(deviceType) || deviceType == "DSP")
                                    {
                                        int bindProductNo = (bindProduct == null ? 0 : bindProduct.ProductBarcode);
                                        if (bindProduct == null)
                                            fdcLogger.LogInformation("Could not find bind product for pumpId: " + fdcPumpController.PumpId + ", nozzleId: " + nozzle.LogicalId + ", will use 0 instead");
                                        fdcLogger.LogDebug("       OnGetConfigurationReq pumpId: "
                                            + fdcPumpController.PumpId
                                            + ", nozzle logicalId: " + nozzle.LogicalId + ", nozzle phyId: " + nozzle.PhysicalId + ", productNo: " + bindProductNo
                                            + ", price: " + fuelPrice);
                                        fdcServer.GetConfigurationAddDSP(workstationID, applicationSender, requestId,
                                            fdcPumpController.PumpId,
                                            bindProductNo,
                                            bindProduct?.ProductName ?? ("refer cloud for name of " + bindProductNo),
                                            1,
                                            fuelPrice);
                                        fdcServer.GetConfigurationAddFP(workstationID, applicationSender, requestId,
                                            fdcPumpController.PumpId,
                                            fdcPumpController.PumpId, nozzle.LogicalId, (bindProduct == null ? 0 : bindProduct.ProductBarcode), 0, 100, 0);
                                    }
                                    //if (string.IsNullOrEmpty(deviceType) || deviceType == "PP")
                                    //    fdcServer.GetConfigurationAddPP(workstationID, applicationSender, requestId, fdcPumpController.PumpId);

                                }
                            }
                        }

                        fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId,
                                deviceType, OverallResult.Success.ToString());
                        fdcLogger.LogDebug("   OnGetConfigurationReq done");
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError("OnGetConfigurationReq exceptioned: " + exxx.ToString());
                        fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId, deviceType, OverallResult.Failure.ToString());
                    }
                }
            };
            fdcServer.OnGetDeviceStateReq += async (string workstationID, string applicationSender, int requestId, string deviceType, int deviceId) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnGetDeviceStateReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ")");

                    // -1 indicates query all pumps
                    if (deviceId == -1)
                    {
                        var controllers = fdcPumpControllers;
                        foreach (var c in controllers)
                        {
                            var s = await c.QueryStatusAsync();
                            fdcLogger.LogDebug("       Pump with pumpId: " + c.PumpId + " is in state: " + s.ToString()
                                + ", nozzles states are(LogicalId-State): "
                                + (c.Nozzles != null ?
                                    (c.Nozzles.Any() ?
                                        c.Nozzles.Select(n =>
                                            n.LogicalId.ToString() + "-" + (n.LogicalState?.ToString() ?? ""))
                                            .Aggregate((n, acc) => n + ", " + acc) : "") : ""));
                            byte nozzleLockedOrUnlockedBitMap = 0;
                            foreach (var nozzle in c.Nozzles)
                            {
                                if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED)
                                    nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1);
                                else
                                    nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0);
                            }

                            fdcServer.GetDeviceStateAdd(workstationID, applicationSender, requestId, deviceType, c.PumpId, (int)s,
                                -1, "", "", (int)FDCLogicalState.FDC_LOGICAL_ST_UNLOCKED, "",
                                c.Nozzles.Count(), 0, nozzleLockedOrUnlockedBitMap, 0);
                        }
                    }
                    else
                    {
                        var targetController = fdcPumpControllers.First(c => c.PumpId == deviceId) as IFdcPumpController;
                        byte nozzleLockedOrUnlockedBitMap = 0;
                        foreach (var nozzle in targetController.Nozzles)
                        {
                            if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED)
                                nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1);
                            else
                                nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0);
                        }

                        fdcServer.GetDeviceStateAdd(workstationID, applicationSender, requestId, deviceType, deviceId,
                            (int)targetController.QueryStatusAsync().Result,
                            -1, "", "", (int)FDCLogicalState.FDC_LOGICAL_ST_UNLOCKED, "",
                            targetController.Nozzles.Count(),
                            0, nozzleLockedOrUnlockedBitMap, 0);
                    }

                    fdcServer.GetDeviceStateSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    fdcLogger.LogDebug("   OnGetDeviceStateReq done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnGetDeviceStateReq exceptioned: " + exxx.ToString());
                    fdcServer.GetDeviceStateSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString());
                }
            };
            fdcServer.OnAuthoriseFuelPointReq += async
                (string workstationID, string applicationSender, int requestId, string releaseToken, int fuellingType,
                int deviceId, int reservingDeviceId, double maxTrxAmount, double maxTrxVolume,
                string products, int mode, bool lockFuelSaleTrx, string payType) =>
            {
                //enum DeviceStatus
                //{
                //    FDC_ST_NOTHING = -1,
                //    FDC_ST_CONFIGURE = 0,
                //    FDC_ST_DISABLED = 1,
                //    FDC_ST_ERRORSTATE = 2,
                //    FDC_ST_FUELLING = 3,
                //    FDC_ST_INVALIDSTATE = 4,
                //    FDC_ST_LOCKED = 5,
                //    FDC_ST_OFFLINE = 6,
                //    FDC_ST_OUTOFORDER = 7,
                //    FDC_ST_READY = 8,
                //    FDC_ST_REQUESTED = 9,
                //    FDC_ST_STARTED = 10,
                //    FDC_ST_SUSPENDED = 11,
                //    FDC_ST_CALLING = 12,
                //    FDC_ST_TEST = 13,
                //    FDC_ST_SUSPENDED_STARTING = 14, // Added in FDC version 00.05
                //    FDC_ST_SUSPENDED_FUELLING = 15,
                //    FDC_ST_CLOSED = 16,
                //    FDC_ST_AUTHORISED = 17, // Added in FDC version 00.07
                //};
                try
                {
                    fdcLogger.LogDebug("OnAuthoriseFuelPointReq(wid: " + workstationID + ", appSender: " + applicationSender
                        + ", requestId: " + requestId + ", releaseToken: " + releaseToken
                        + ", deviceId: " + deviceId + ", reservingDeviceId: " + reservingDeviceId + ", maxTrxAmount: "
                        + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume + ", products: " + products
                        + ", lockFuelSaleTrx: " + lockFuelSaleTrx + ", payType: " + payType + ")");

                    //if (targetController.QueryStatus() == LogicalDeviceState.FDC_FUELLING)
                    //{
                    //fdcLogger.LogDebug("       Pump: " + targetController.PumpId + " is in fueling so re-authorising request will be interpreted as RoundUpByAmount request");
                    //var result = targetController.FuelingRoundUpByAmount();
                    //fdcLogger.LogDebug("       Pump: " + targetController.PumpId + " RoundUpByAmount returned with: " + result);
                    //fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                    //    (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    //return;
                    //}

                    //if (!string.IsNullOrEmpty(maxStackUnpaidTrxConfig))
                    //{
                    //    SqliteDbContext dbContext = new SqliteDbContext();
                    //    var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId && t.State == FuelSaleTransactionState.Payable);
                    //    if (unpaidTrxCount >= int.Parse(maxStackUnpaidTrxConfig))
                    //    {
                    //        fdcLogger.LogInformation("    Authorizing failed due to pump: " + targetController.PumpId + " have: " + unpaidTrxCount + " unpaid trx");
                    //        fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                    //        (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_MAXSTACKLIMIT, OverallResult.Failure.ToString());
                    //        return;
                    //    }
                    //}


                    var succeed = await this.AuthorizePumpAsync(deviceId, maxTrxAmount, maxTrxVolume);
                    if (succeed)
                        fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                            (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    else
                    {
                        fdcLogger.LogError("Authorising FP(requestId: " + requestId + ", releaseToken: " + releaseToken + ", deviceId: " + deviceId
                            + ", reservingDeviceId: " + reservingDeviceId + ", maxTrxAmount: " + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume
                            + ", products: " + products + ", lockFuelSaleTrx: " + lockFuelSaleTrx + ", payType: " + payType + ") failed");
                        fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                                (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_NOPERM, OverallResult.Failure.ToString());
                    }

                    fdcLogger.LogDebug("   Authorising FP done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnAuthoriseFuelPointReq exceptioned: " + exxx.ToString());
                    fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                                (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_NOPERM, OverallResult.Failure.ToString());
                }
            };
            fdcServer.OnTerminateFuellingReq += async (string workstationID, string applicationSender, int requestId, int deviceId) =>
            {
                fdcLogger.LogInformation("OnTerminateFuellingReq(wid: " + workstationID + ", appSender: " + applicationSender
                               + ", requestId: " + requestId + ", deviceId: " + deviceId + ")");
                var targetController = fdcPumpControllers.FirstOrDefault(c => c.PumpId == deviceId);
                if (targetController == null)
                {
                    fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADDEVID, OverallResult.WrongDeviceNo.ToString());
                    return;
                }

                try
                {
                    /* interpreted as UnAuthorize has no special reason but for a rush request from customer */
                    fdcLogger.LogInformation("      OnTerminateFuellingReq will be interpreted as UnAuthorize to pump handler.");
                    var result = await targetController.UnAuthorizeAsync(1);
                    if (result)
                        fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                    else
                        fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_INOP, OverallResult.Failure.ToString());
                }
                catch (Exception exx)
                {
                    fdcLogger.LogError("OnTerminateFuellingReq exceptioned: " + exx);
                    fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString());
                }
            };

            fdcServer.OnGetAvailableFuelSaleTrxsReq += (string workstationID, string applicationSender, int requestId, int deviceId) =>
            {
                // for limit data size, only show latest N days' data.
                int maxReturnDays = 365;
                // for further limit data size, only show latest N count of data.
                int maxReturnDataCount = 1000;
                lock (this.syncObject)
                {
                    try
                    {
                        fdcLogger.LogDebug("OnGetAvailableFuelSaleTrxsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId);
                        var dueDate = DateTime.Now.Subtract(new TimeSpan(maxReturnDays, 0, 0, 0));
                        int totalReturnTrxCount = 0;
                        SqliteDbContext dbContext = new SqliteDbContext();
                        if (deviceId == -1)
                        {
                            var all = dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Paid
                                && t.State != FuelSaleTransactionState.Cleared
                                && t.SaleStartTime >= dueDate)
                                .Select(t => new { t.PumpId, t.TransactionSeqNumberFromPhysicalPump, t.State, t.SaleStartTime, t.ReleaseToken })
                                .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount);
                            foreach (var unpaidTrx in all)
                            {
                                // only show the trx in active pump list.
                                if (!fdcPumpControllers.Select(p => p.PumpId).Any(p => p == unpaidTrx.PumpId))
                                    continue;
                                totalReturnTrxCount++;
                                fdcServer.GetAvailableFuelSaleTrxsAdd(workstationID, applicationSender, requestId,
                                    unpaidTrx.PumpId,
                                    int.Parse(unpaidTrx.TransactionSeqNumberFromPhysicalPump),
                                    unpaidTrx.ReleaseToken.ToString(),
                                    (int)unpaidTrx.State);
                            }
                        }
                        else
                        {
                            var all = dbContext.PumpTransactionModels.Where(t => t.PumpId == deviceId
                                && t.State != FuelSaleTransactionState.Paid
                                && t.State != FuelSaleTransactionState.Cleared
                                && t.SaleStartTime >= dueDate)
                                .Select(t => new { t.PumpId, t.TransactionSeqNumberFromPhysicalPump, t.State, t.SaleStartTime, t.ReleaseToken })
                                .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount);
                            foreach (var unpaidTrx in all)
                            {
                                // only show the trx in active pump list.
                                if (!fdcPumpControllers.Select(p => p.PumpId).Any(p => p == unpaidTrx.PumpId))
                                    continue;
                                totalReturnTrxCount++;
                                fdcServer.GetAvailableFuelSaleTrxsAdd(workstationID, applicationSender, requestId,
                                    unpaidTrx.PumpId,
                                    int.Parse(unpaidTrx.TransactionSeqNumberFromPhysicalPump),
                                    unpaidTrx.ReleaseToken.ToString(),
                                    (int)unpaidTrx.State);
                            }
                        }

                        fdcServer.GetAvailableFuelSaleTrxsSend(workstationID, applicationSender, requestId,
                           (int)ErrorCode.ERRCD_OK, (totalReturnTrxCount > 0 ? OverallResult.Success.ToString() : OverallResult.NoData.ToString()));
                        fdcLogger.LogDebug("   OnGetAvailableFuelSaleTrxsReq done (total count: " + totalReturnTrxCount + ")");
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError("OnGetAvailableFuelSaleTrxsReq exceptioned: " + exxx.ToString());
                        fdcServer.GetAvailableFuelSaleTrxsSend(workstationID, applicationSender, requestId,
                           (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString());
                    }
                }
            };
            fdcServer.OnGetFuelSaleTrxDetailsReq += (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) =>
            {
                lock (this.syncObject)
                {
                    try
                    {
                        /* Fdc client may send wildchar `*` in transactionNo which will be interpreted as -1 here, that means query all trx */
                        fdcLogger.LogDebug("OnGetFuelSaleTrxDetailsReq (wid: " + workstationID
                            + ", appSender: " + applicationSender + ", requestId: " + requestId
                            + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken);
                        int databaseId = int.Parse(releaseToken);
                        SqliteDbContext dbContext = new SqliteDbContext();
                        List<Edge.Core.Database.Models.FuelSaleTransaction> target;
                        if (transactionNo == -1)
                            target = dbContext.PumpTransactionModels.Where(t => t.PumpId == deviceId).ToList();
                        else
                            target = dbContext.PumpTransactionModels.Where(t =>
                                t.ReleaseToken == databaseId
                                && t.PumpId == deviceId
                                && t.TransactionSeqNumberFromPhysicalPump == transactionNo.ToString()).ToList();

                        if (target.Any())
                        {
                            var targetController = fdcPumpControllers
                                .First(c => c.PumpId == deviceId) as IFdcPumpController;
                            foreach (var trx in target)
                            {
                                var reservedBy = string.IsNullOrEmpty(trx.LockedByFdcClientId) ?
                                    -1 : int.Parse(trx.LockedByFdcClientId);
                                fdcServer.GetFuelSaleTrxDetailsAdd(workstationID, applicationSender, requestId,
                                    deviceId,
                                    trx.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                                    trx.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                                    trx.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                                    trx.LogicalNozzleId,
                                    int.Parse(trx.ProductBarcode),
                                    "", "", 1,
                                    int.Parse(trx.TransactionSeqNumberFromPhysicalPump), releaseToken,
                                    (int)trx.State,
                                    trx.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                                    trx.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                                    trx.LockedByFdcClientId,
                                    "9999",
                                    reservedBy, 1);
                            }

                            fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId,
                                (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                        }
                        else
                        {
                            fdcLogger.LogInformation("       Trx with releaseToken: " + releaseToken + " is not found");
                            fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId,
                                (int)ErrorCode.ERRCD_OK, OverallResult.NoData.ToString());
                        }

                        fdcLogger.LogDebug("   OnGetFuelSaleTrxDetailsReq done");
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError("OnGetFuelSaleTrxDetailsReq exceptioned: " + exxx.ToString());
                        fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId,
                            (int)ErrorCode.ERRCD_BADVAL, OverallResult.Failure.ToString());
                    }
                }
            };
            fdcServer.OnClearFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnClearFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender
                        + ", requestId: " + requestId + ", deviceId: " + deviceId
                        + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")");
                    var target = await this.ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(deviceId, transactionNo.ToString(), int.Parse(releaseToken), workstationID, applicationSender, requestId);
                    if (target == null)
                        fdcLogger.LogError("OnClearFuelSaleTrxReq failed(wid: " + workstationID + ", appSender: " + applicationSender
                            + ", requestId: " + requestId + ", deviceId: " + deviceId
                            + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnClearFuelSaleTrxReq exceptioned: " + exxx.ToString());
                    try
                    {
                        fdcServer.ClearFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, transactionNo, releaseToken,
                                (int)ErrorCode.ERRCD_BADVAL, 1, OverallResult.Failure.ToString());
                    }
                    catch (Exception eeee)
                    {
                        fdcLogger.LogError("Fdc server is trying to respond a failed ClearFuelSaleTrx response to a fdcclient that issue current ClearFuelSaleTrx request, but still failed with reason: " + eeee);
                    }
                }
            };

            // FdcClient send ServiceRequestChangeFuelPrice will be routed into here
            fdcServer.OnChangeFuelPriceInStringReq += (string workstationID,
                string applicationSender, int requestId, string formattedValues) =>
            {
                try
                {
                    fdcLogger.LogInformation("OnChangeFuelPriceInStringReq (requestId:" +
                        requestId + ") with data-> (barcode; PriceNew; ModeNo; PriceOld; EffectiveDatetime;!): " + formattedValues);
                    //formattedValues is:   gradeId(barCode);PriceNew;ModeNo;EffectiveDatetime!       e.g.:   5;1980;1;!
                    //this function should return:   gradeId(barCode);PriceNew;ModeNo;PriceOld;EffectiveDatetime;!        e.g.:   2;1998;1;1981;!

                    var targetProductBarcode = int.Parse(formattedValues.Split(';')[0]);
                    // raw means the price is send from POS, and typically it contains decimal points, 
                    // need re-caculate with money digits since pump does not recognize decimal points.
                    var rawNewPriceWithHumanReadableFormat = double.Parse(formattedValues.Split(';')[1]);
                    var succeedNozzles = this.ChangeFuelPriceAsync(targetProductBarcode, rawNewPriceWithHumanReadableFormat).Result;
                    fdcLogger.LogInformation("   OnChangeFuelPriceInStringReq (requestId:" + requestId + ") is done");
                    // even only one succeed, return a success. need refine?
                    if (succeedNozzles != null && succeedNozzles.Any())
                    {
                        // notify all tcp FdcClients that price changed.
                        fdcServer.FuelPriceChange(targetProductBarcode, 1,
                rawNewPriceWithHumanReadableFormat,
                0,
                // always set older time since the new price is already activated on pump.
                DateTime.Now.Subtract(new TimeSpan(0, 5, 0)).ToString("yyyy-MM-dd HH:mm:ss"));
                        return targetProductBarcode.ToString() + ";" + rawNewPriceWithHumanReadableFormat.ToString() + ";1;" + 0 + ";!";
                    }
                    else return null;
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnChangeFuelPriceInStringReq exceptioned: " + exxx);
                    return null;
                }
            };
            //fdcServer.OnChangeFuelPriceAddReq += (string workstationID, string applicationSender,
            //    int requestId, int product, double price, int mode, string effectiveDateTime) =>
            // {


            // };
            //fdcServer.OnChangeFuelPriceEndReq += (string workstationID, string applicationSender, int requestId) =>
            //    {
            //        fdcServer.ChangeFuelPriceEndReq(workstationID, applicationSender, requestId);
            //        return "";
            //    };

            fdcServer.OnGetCurrentFuellingStatusReq += (string workstationID, string applicationSender,
                int requestId, int deviceId) =>
            {
                /* do nothing for now, the fuelling status will notify by unsolicited event. */
                var targetController = fdcPumpControllers.First(c => c.PumpId == deviceId) as IFdcPumpController;
                //if (targetController.QueryStatus() == LogicalDeviceState.FDC_FUELLING)
                //{
                //    fdcServer.GetCurrentFuellingStatusAdd(workstationID, applicationSender, requestId, deviceId)
                //}
            };
            fdcServer.OnGetFuelPointTotalsReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int nozzleId) =>
            {
                try
                {
                    fdcLogger.LogDebug("OnGetFuelPointTotalsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId
                        + ", nozzleId: " + nozzleId);
                    if (deviceId == -1)
                    {
                        /* query all pumps and all its nozzles */
                        var everReadSuccessfully = false;
                        foreach (var pumpController in fdcPumpControllers)
                        {
                            var pumpState = await pumpController.QueryStatusAsync();
                            if (pumpState != LogicalDeviceState.FDC_READY)
                            {
                                fdcLogger.LogDebug($"    OnGetFuelPointTotalsReq, pump: {pumpController.PumpId} is in state: {pumpState}, will skip to query its totalizer.");
                                continue;
                            }

                            foreach (var nozzle in pumpController.Nozzles)
                            {
                                var result = await this.GetFuelPointTotalsAsync(pumpController.PumpId, (byte)nozzle.LogicalId);
                                everReadSuccessfully = true;
                                fdcLogger.LogInformation("        OnGetFuelPointTotalsReq for pump: " + pumpController.PumpId
                                    + ", nozzle: " + nozzle.LogicalId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2);
                                fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, pumpController.PumpId, nozzle.LogicalId,
                                    nozzleExtraInfos.FirstOrDefault(n => n.PumpId == pumpController.PumpId && n.NozzleLogicalId == nozzle.LogicalId).ProductBarcode,
                                    result.Item2,
                                    result.Item1,
                                    (pumpController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, pumpController.PriceDecimalDigits));
                            }
                        }

                        if (everReadSuccessfully)
                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                        else
                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.DeviceUnavailable.ToString());
                    }
                    else if (nozzleId == -1)
                    {
                        /* query a specified pump and all its nozzles */
                        var pumpController = fdcPumpControllers
                               .First(c => c.PumpId == deviceId) as IFdcPumpController;
                        var pumpState = await pumpController.QueryStatusAsync();
                        if (pumpState != LogicalDeviceState.FDC_READY)
                        {
                            fdcLogger.LogDebug($"    OnGetFuelPointTotalsReq, pump: {pumpController.PumpId} is in state: {pumpState}, will skip to query its totalizer.");
                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.DeviceUnavailable.ToString());
                        }
                        else
                        {
                            foreach (var nozzle in pumpController.Nozzles)
                            {
                                var result = await this.GetFuelPointTotalsAsync(pumpController.PumpId, (byte)nozzle.LogicalId);
                                fdcLogger.LogInformation("        OnGetFuelPointTotalsReq for pump: " + pumpController.PumpId + ", nozzle: " + nozzle.LogicalId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2);
                                fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, pumpController.PumpId, nozzle.LogicalId,
                                    nozzleExtraInfos.FirstOrDefault(n => n.PumpId == pumpController.PumpId && n.NozzleLogicalId == nozzle.LogicalId).ProductBarcode,
                                    result.Item2,
                                    result.Item1,
                                    (pumpController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, pumpController.PriceDecimalDigits));
                            }

                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                        }
                    }
                    else
                    {
                        /* query a specified pump and a specified nozzle */
                        var targetController = fdcPumpControllers
                            .First(c => c.PumpId == deviceId) as IFdcPumpController;
                        var pumpState = await targetController.QueryStatusAsync();
                        if (pumpState != LogicalDeviceState.FDC_READY)
                        {
                            fdcLogger.LogDebug($"    OnGetFuelPointTotalsReq, pump: {targetController.PumpId} is in state: {pumpState}, will skip to query its totalizer.");
                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.DeviceUnavailable.ToString());
                        }
                        else
                        {
                            var result = await this.GetFuelPointTotalsAsync(targetController.PumpId, (byte)nozzleId);
                            fdcLogger.LogInformation("        OnGetFuelPointTotalsReq for pump: " + targetController.PumpId + ", nozzle: " + nozzleId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2);

                            fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, deviceId, nozzleId,
                                nozzleExtraInfos.FirstOrDefault(n => n.PumpId == targetController.PumpId && n.NozzleLogicalId == nozzleId).ProductBarcode,
                                result.Item2,
                                result.Item1,
                                (targetController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, targetController.PriceDecimalDigits));
                            fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                        }
                    }

                    fdcLogger.LogDebug("   OnGetFuelPointTotals done");
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("OnGetFuelPointTotalsReq exceptioned: " + exxx.ToString());
                    fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId,
                       (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString());
                }
            };

            #region setup Fdc unsolicited event

            foreach (var fdcPumpController in this.fdcPumpControllers)
            {
                fdcPumpController.OnStateChange += async (s, stateChangeArg) =>
                {
                    var pump = s as IFdcPumpController;
                    try
                    {
                        fdcLogger.LogDebug("Pump " + pump.PumpId
                            + " StateChanged to: " + stateChangeArg.NewPumpState.ToString() + ", nozzles states are(LogicalId-State): "
                            + (pump.Nozzles != null ?
                                (pump.Nozzles.Any() ?
                                    pump.Nozzles.Select(n =>
                                        n.LogicalId.ToString() + "-" + (n.LogicalState?.ToString() ?? ""))
                                    .Aggregate((n, acc) => n + ", " + acc) : "") : "")
                            + ", StateChangedNozzles are: "
                            + (stateChangeArg.StateChangedNozzles != null ?
                                (stateChangeArg.StateChangedNozzles.Any() ?
                                    stateChangeArg.StateChangedNozzles.Select(n => n.LogicalId.ToString())
                                    .Aggregate((n, acc) => n + ", " + acc) : "") : ""));

                        this.OnStateChange?.Invoke(s, stateChangeArg);
                        var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                        await universalApiHub.FireEvent(this, "OnFdcControllerStateChange",
                            new
                            {
                                pump.PumpId,
                                logicalId = (stateChangeArg.StateChangedNozzles != null && stateChangeArg.StateChangedNozzles.Any()) ?
                                    stateChangeArg.StateChangedNozzles.FirstOrDefault().LogicalId.ToString() : "",
                                logicalState = stateChangeArg.NewPumpState
                            });
                        fdcLogger.LogTrace("Pump " + pump.PumpId + " StateChanged event fired and back");

                        // this is used for send to fdc client.
                        byte nozzleUpOrDownBitMap = 0;
                        if (stateChangeArg.StateChangedNozzles != null)
                            foreach (var upNozzle in stateChangeArg.StateChangedNozzles)
                            {
                                nozzleUpOrDownBitMap = SetBit(nozzleUpOrDownBitMap, upNozzle.LogicalId - 1, upNozzle.LogicalId, 1);
                            }

                        byte nozzleLockedOrUnlockedBitMap = 0;
                        foreach (var nozzle in pump.Nozzles)
                        {
                            if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED)
                                nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1);
                            else
                                nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0);
                        }

                        fdcServer.DeviceStateChange(Wayne.FDCPOSLibrary.DeviceType.DT_FuellingPoint, pump.PumpId,
                            (int)(stateChangeArg.NewPumpState), (int)LogicalDeviceState.FDC_UNDEFINED, "", "", "",
                            pump.Nozzles.Count(), nozzleUpOrDownBitMap, nozzleLockedOrUnlockedBitMap, 0);
                        fdcLogger.LogTrace($"Pump {pump.PumpId } Fdc DeviceStateChange event fired and back");
                        if (stateChangeArg.NewPumpState == LogicalDeviceState.FDC_CALLING)
                        {
                            if (this.config_AutoAuthCallingPumps)
                            {
                                if (this.config_MaxStackUnpaidTrxPerPump > 0)
                                {
                                    SqliteDbContext dbContext = new SqliteDbContext();
                                    var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == pump.PumpId && t.State == FuelSaleTransactionState.Payable);
                                    if (unpaidTrxCount >= this.config_MaxStackUnpaidTrxPerPump)
                                    {
                                        fdcLogger.LogInformation("    Auto authorizing is not permit since pump " + pump.PumpId + " has: " + unpaidTrxCount + " unpaid trx");
                                        return;
                                    }
                                }

                                fdcLogger.LogDebug("Auto authorizing Pump: " + pump.PumpId);
                                int autoAuthDefaultAmount = 9999;
                                var result = await pump.AuthorizeWithAmountAsync((int)(autoAuthDefaultAmount * Math.Pow(10, pump.AmountDecimalDigits)), 1);
                                if (!result)
                                    fdcLogger.LogError("Auto auth Pump: " + pump.PumpId + " FAILED!");
                            }
                        }
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError($"PumpId: {pump.PumpId}, fdcPumpController.OnStateChange exceptioned: {exxx}");
                    }
                };

                fdcPumpController.OnCurrentFuellingStatusChange += async (s, a) =>
                {
                    var pump = s as IFdcPumpController;
                    try
                    {
                        fdcLogger.LogDebug($"Pump {pump.PumpId }, Nozzle: {a?.Transaction?.Nozzle?.LogicalId.ToString() ?? ""}, OnCurrentFuellingStatusChange");
                        var product = nozzleExtraInfos.FirstOrDefault(c => c.PumpId == pump.PumpId && c.NozzleLogicalId == a.Transaction.Nozzle.LogicalId);
                        if (product != null)
                            a.Transaction.Barcode = product.ProductBarcode;
                        else
                            a.Transaction.Barcode = 1;

                        if (a.Transaction.Finished)
                        {
                            fdcLogger.LogInformation("Pump " + pump.PumpId + ",        transaction is finished, vol: " + a.Transaction.Volumn + ", amount: " + a.Transaction.Amount
                                + ", price: " + a.Transaction.Price + ", nozzleNo: " + a.Transaction.Nozzle.LogicalId
                                + ", volumeTotalizer: " + (a.Transaction.VolumeTotalizer ?? -1)
                                + ", amountTotalizer: " + (a.Transaction.AmountTotalizer ?? -1)
                                + ", seqNumber: " + a.Transaction.SequenceNumberGeneratedOnPhysicalPump
                                + ", productBarcode: " + product?.ProductBarcode
                                + ", productName: " + (product?.ProductName ?? ""));
                            //enum SaleTrxStatus
                            //{
                            //    SALE_TRX_UNDEFINED = 0,
                            //    SALE_TRX_PAYABLE = 1,
                            //    SALE_TRX_LOCKED = 2,
                            //    SALE_TRX_PAID = 3,
                            //    SALE_TRX_CLEARED = 4,
                            //};
                            // will do a duplication search in a short time range(by days) since physical pump will reuse seq id.
                            var duplicationDectectTimeRange = 3;
                            var range = DateTime.Now.Subtract(new TimeSpan(duplicationDectectTimeRange, 0, 0, 0));
                            SqliteDbContext dbContext = new SqliteDbContext();
                            var existed = dbContext.PumpTransactionModels.Where(f =>
                                f.PumpId == pump.PumpId
                                && f.TransactionSeqNumberFromPhysicalPump == a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString()
                                && f.UnitPrice == a.Transaction.Price
                                && f.Amount == a.Transaction.Amount
                                && f.Volumn == a.Transaction.Volumn
                                && f.SaleEndTime > range).ToList();
                            if (existed.Any())
                            {
                                fdcLogger.LogWarning("A new trx duplicated with an existed trx in db which done in recent "
                                    + duplicationDectectTimeRange + " days, it was with " +
                                    "releaseToken:" + existed.First().ReleaseToken + ", pumpId: " + pump.PumpId + " and seqNo: "
                                    + a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString()
                                    + ", will do nothing and NOT notify any POS, while the existed one in database detail is-> "
                                    + "Vol: " + existed.First().Volumn
                                    + ", Amount: " + existed.First().Amount
                                    + ", State: " + existed.First().State.ToString()
                                    + ", SaleStartTime: " + (existed.First().SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "")
                                    + ", EndStartTime: " + (existed.First().SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "")
                                    + ", ProductBarcode: " + existed.First().ProductBarcode.ToString()
                                    + "");
                            }
                            else
                            {
                                var trx = new Edge.Core.Database.Models.FuelSaleTransaction()
                                {
                                    TransactionSeqNumberFromPhysicalPump = a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString(),
                                    PumpId = pump.PumpId,
                                    LogicalNozzleId = a.Transaction.Nozzle.LogicalId,
                                    Amount = a.Transaction.Amount,
                                    Volumn = a.Transaction.Volumn,
                                    UnitPrice = a.Transaction.Price,
                                    VolumeTotalizer = a.Transaction.VolumeTotalizer ?? -1,
                                    AmountTotalizer = a.Transaction.AmountTotalizer ?? -1,
                                    ProductBarcode = product?.ProductBarcode.ToString() ?? "9999",
                                    //ProductName = "refer cloud",
                                    State = FuelSaleTransactionState.Payable,
                                    LockedByFdcClientId = "",
                                    // hard code for now, need do it in PumpHandler
                                    SaleStartTime = a.Transaction.SaleStartTime ?? ((a.Transaction.SaleEndTime ?? DateTime.Now).Subtract(new TimeSpan(0, 3, 0))),
                                    SaleEndTime = a.Transaction.SaleEndTime ?? DateTime.Now,
                                };
                                dbContext.PumpTransactionModels.Add(trx);
                                dbContext.SaveChanges();
                                fdcLogger.LogDebug("        ######transaction is done saving to db with ReleaseToken(Id): " + trx.ReleaseToken);

                                var safe = this.OnFdcFuelSaleTransactinStateChange;
                                safe?.Invoke(this, new FdcFuelSaleTransactinStateChangeEventArg(trx, FuelSaleTransactionState.Payable));
                                var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                                await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(trx, FuelSaleTransactionState.Payable));

                                var startingTime = DateTime.Now;
                                fdcServer.FuelSaleTrx(pump.PumpId,
                                    trx.Volumn / Math.Pow(10, pump.VolumeDecimalDigits),
                                    trx.Amount / Math.Pow(10, pump.AmountDecimalDigits),
                                    trx.UnitPrice / Math.Pow(10, pump.PriceDecimalDigits),
                                    trx.LogicalNozzleId,
                                    product?.ProductBarcode ?? 0,
                                    product?.ProductName ?? ("refer cloud for name of " + (product?.ProductBarcode ?? -1)),
                                    "", 1,
                                    int.Parse(trx.TransactionSeqNumberFromPhysicalPump),
                                    (int)trx.State,
                                    0, trx.ReleaseToken.ToString(),
                                    trx.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                                    trx.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                                    "", "", 0, 0);
                                fdcLogger.LogDebug($"        ######transaction(releaseToken: {trx.ReleaseToken}) is done broadcasting to POSes, used: {DateTime.Now.Subtract(startingTime).TotalMilliseconds}");
                                this.OnCurrentFuellingStatusChange?.Invoke(s,
                                    new FdcServerTransactionDoneEventArg(a.Transaction)
                                    { ReleaseToken = trx.ReleaseToken });
                                fdcLogger.LogTrace("Pump " + pump.PumpId + " OnCurrentFuellingStatusChange event fired and back");
                                if (a.Transaction.VolumeTotalizer == null)
                                {
                                    var result = await GetFuelPointTotalsAsync(pump.PumpId, a.Transaction.Nozzle.LogicalId);
                                    a.Transaction.AmountTotalizer = (int?)(result.Item1 * Math.Pow(10, pump.AmountDecimalDigits));
                                    a.Transaction.VolumeTotalizer = (int?)(result.Item2 * Math.Pow(10, pump.VolumeDecimalDigits));
                                }

                                var targetNozzle = pump.Nozzles.First(n => n.LogicalId == a.Transaction.Nozzle.LogicalId);
                                if (targetNozzle != null) targetNozzle.VolumeTotalizer = a.Transaction.VolumeTotalizer;
                                await universalApiHub.FireEvent(this, "OnCurrentFuellingStatusChange", new FdcServerTransactionDoneEventArg(a.Transaction) { FuelingEndTime = trx.SaleEndTime });
                            }
                        }
                        else
                        {
                            fdcLogger.LogDebug("        transaction is ongoing, vol: " + a.Transaction.Volumn + ", amount: " + a.Transaction.Amount
                                + ", price: " + a.Transaction.Price + ", nozzleNo: " + a.Transaction.Nozzle.LogicalId
                                + ", seqNumber: " + a.Transaction.SequenceNumberGeneratedOnPhysicalPump ?? ""
                                + ", productBarcode: " + product.ProductBarcode
                                + ", productName: " + (product.ProductName ?? ""));
                            fdcServer.CurrentFuellingStatus(pump.PumpId,
                                a.Transaction.Volumn / Math.Pow(10, pump.VolumeDecimalDigits),
                                a.Transaction.Amount / Math.Pow(10, pump.AmountDecimalDigits),
                                a.Transaction.Price / Math.Pow(10, pump.PriceDecimalDigits),
                                a.Transaction.Nozzle.LogicalId,
                                a.Transaction.SequenceNumberGeneratedOnPhysicalPump, "9999", 1, a.Transaction.Nozzle.LogicalId);
                            this.OnCurrentFuellingStatusChange?.Invoke(s, new FdcServerTransactionDoneEventArg(a.Transaction));
                            fdcLogger.LogTrace("Pump " + pump.PumpId + " OnCurrentFuellingStatusChange event fired and back");
                            var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                            await universalApiHub.FireEvent(this, "OnCurrentFuellingStatusChange", new FdcServerTransactionDoneEventArg(a.Transaction));
                        }
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError($"PumpId: { pump?.PumpId ?? -1}, fdcPumpController.OnCurrentFuellingStatusChange exceptioned: " + exxx);
                    }
                };
            }

            #endregion

            #region handle BroadcastGenericTypelessMessage

            if (this.fdcCommunicableControllers != null)
            {
                this.fdcCommunicableControllers.ToList().ForEach(c =>
                {
                    c.BroadcastMessageViaFdc += (msg) =>
                    {
                        fdcServer.SendGenericTypelessMessageToFdcClient(null, null, msg);
                        return true;
                    };
                });
                this.fdcCommunicableControllers.ToList().ForEach(c =>
                {
                    c.SendMessageViaFdc += (workstationID, applicationSender, msg) =>
                    {
                        fdcServer.SendGenericTypelessMessageToFdcClient(workstationID, applicationSender, msg);
                        return true;
                    };
                });
            }

            fdcServer.OnGetGenericTypelessMessageReq += (string workstationID, string applicationSender, int requestId, string message) =>
            {
                this.fdcCommunicableControllers.ToList().ForEach(c =>
                {
                    if (c.OnMessageReceivedViaFdc != null)
                    {
                        var returnResult = c.OnMessageReceivedViaFdc(message);
                        fdcServer.GenericTypelessMessageSend(workstationID, applicationSender, requestId, returnResult.Item1, returnResult.Item2.ToString());
                    }
                });
            };

            #endregion

            fdcLogger.LogDebug("Start all FdcPumpController initing...(total: "
                + (fdcPumpControllers?.Count() ?? -1)
                + ", pump Ids are: " + ((fdcPumpControllers?.Any() ?? false) ?
                    (fdcPumpControllers.Select(c => c.PumpId.ToString()).Aggregate((acc, n) => acc + ", " + n) + ")") : ")"));
            foreach (var fdcPumpController in this.fdcPumpControllers)
            {
                var onFdcServerInitParams = new Dictionary<string, object>();
                try
                {

                    #region LastPriceChange for each nozzles

                    // nozzle logical id:rawPrice
                    var innerParams0 = new Dictionary<byte, int>();
                    foreach (var nozzle in fdcPumpController.Nozzles)
                    {
                        var dbContext = new SqliteDbContext();
                        var lastPriceChange = dbContext.FuelPriceChanges
                            .Where(t => t.PumpId == fdcPumpController.PumpId
                                && t.LogicalNozzleId == nozzle.LogicalId)
                            .OrderByDescending(b => b.Id).FirstOrDefault();
                        if (lastPriceChange != null)
                            innerParams0.Add(nozzle.LogicalId, lastPriceChange.NewPriceWithoutDecimal);
                    }

                    onFdcServerInitParams.Add("LastPriceChange", innerParams0);

                    #endregion

                    #region LastFuelSaleTrx for each nozzles

                    // nozzle logical id:LastSaleTrx
                    var innerParams1 = new Dictionary<byte, FuelSaleTransaction>();
                    foreach (var nozzle in fdcPumpController.Nozzles)
                    {
                        var dbContext = new SqliteDbContext();
                        var lastPaidTrx = dbContext.PumpTransactionModels
                            .Where(t => t.PumpId == fdcPumpController.PumpId
                                && t.LogicalNozzleId == nozzle.LogicalId)
                            .OrderByDescending(b => b.SaleStartTime).FirstOrDefault();
                        if (lastPaidTrx != null)
                            innerParams1.Add(nozzle.LogicalId, lastPaidTrx);
                    }

                    onFdcServerInitParams.Add("LastFuelSaleTrx", innerParams1);

                    #endregion

                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("Retrieve lastPriceChange for OnFdcServerInit from db for Pump with pumpid: "
                        + fdcPumpController.PumpId + " exceptioned: " + exxx.ToString()
                        + System.Environment.NewLine + "Will skip this pump and continue for next pump.");
                    continue;
                }

                try
                {
                    fdcPumpController.OnFdcServerInit(onFdcServerInitParams);
                }
                catch (Exception exxx)
                {
                    fdcLogger.LogError("FdcPumpController with pumpid: "
                        + fdcPumpController.PumpId + " exceptioned in OnFdcServerInit, detail: " + exxx.ToString()
                        + System.Environment.NewLine + "Will skip and continue for next FdcPumpController.");
                    continue;
                }
            }

            fdcLogger.LogDebug("Done all FdcPumpController init, start FdcServer tcp listening...");
            var _ = fdcServer.Start(this.config_ListeningPort, true, "WINCOR", 2, true, "NONE");
            fdcLogger.LogDebug("Done FdcServer tcp listening...");
            return Task.FromResult(_);
        }

        public Task<bool> Stop()
        {
            if (this.onConfiguratorConfigFileChangedEventHandler != null)
                this.configurator.OnConfigFileChanged -= this.onConfiguratorConfigFileChangedEventHandler;
            this.purgeDatabaseTimer?.Stop();
            // set fdcPumpControllers to an empty list, as it needs to be stopped from firing any event to any substriber.
            this.fdcPumpControllers = new List<IFdcPumpController>();
            var stopFdcServerResult = fdcServer.Stop();
            return Task.FromResult(stopFdcServerResult);
        }

        private static byte SetBit(byte target, int bitStartIndex, int bitEndIndex, int replacedValue)
        {
            if (bitStartIndex < 0 || bitEndIndex > 7 || bitEndIndex < bitStartIndex)
            {
                throw new ArgumentException("bitStartIndex or bitEndIndex value is not valid");
            }

            byte mask = 0;
            for (int i = 0; i < bitEndIndex - bitStartIndex + 1; i++)
            {
                mask += (byte)Math.Pow(2, i);
            }

            if (replacedValue > mask)
            {
                throw new ArgumentOutOfRangeException("Replaced value: " + replacedValue + " cannot fit the bits range");
            }

            byte maskedValue = (byte)(target & (255 - (mask << bitStartIndex)));
            return (byte)(maskedValue + (replacedValue << bitStartIndex));
        }

        public void Dispose()
        {
        }

        #region pump control interface opened for local call, used in other fc app.

        /// <summary>
        /// fired once PumpController state changed.
        /// </summary>
        public event EventHandler<FdcPumpControllerOnStateChangeEventArg> OnStateChange;

        /// <summary>
        /// fired once the fuelling trx from PumpController state changed.
        /// used to watch fuelling progress for a PumpController.
        /// </summary>
        public event EventHandler<FdcServerTransactionDoneEventArg> OnCurrentFuellingStatusChange;

        /// <summary>
        /// fired once the Fdc fuel sale trx state changed, like it's turn into Payable, Locked, Unlocked state etc.
        /// used for local app processors to watch the trx state, the state change mostly triggered by Fdc client request.
        /// </summary>
        public event EventHandler<FdcFuelSaleTransactinStateChangeEventArg> OnFdcFuelSaleTransactinStateChange;

        public IEnumerable<IFdcPumpController> FdcPumpControllers => this.fdcPumpControllers;

        /// <summary>
        /// maxTrxAmount has the high priority. 
        /// </summary>
        /// <param name="pumpId">the target pump, which for authorizing</param>
        /// <param name="maxTrxAmount">human readable number, with decimal point, like 5 RMB, should input 5.    leave 0 if set with unlimited amount.</param>
        /// <param name="maxTrxVolume">human readable number, with decimal point, like 6.5L, should input 6.5. leave 0 if set with unlimited vol</param>
        /// <returns></returns>
        public async Task<bool> AuthorizePumpAsync(int pumpId, double maxTrxAmount, double maxTrxVolume)
        {
            fdcLogger.LogDebug("Authorizing Pump: " + pumpId + ", maxTrxAmount: " + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume);

            var targetController = fdcPumpControllers
                .First(c => c.PumpId == pumpId) as IFdcPumpController;
            if (this.config_MaxStackUnpaidTrxPerPump > 0)
            {
                SqliteDbContext dbContext = new SqliteDbContext();
                var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId
                    && t.State == FuelSaleTransactionState.Payable);
                if (unpaidTrxCount >= this.config_MaxStackUnpaidTrxPerPump)
                {
                    fdcLogger.LogInformation("    Authorizing from FdcClient is not permit since pump " + targetController.PumpId + " has: " + unpaidTrxCount + " unpaid trx");
                    //fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId,
                    //    (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_MAXSTACKLIMIT, OverallResult.Failure.ToString());
                    return false;
                }
            }

            bool succeed = false;
            if (maxTrxAmount == 0 && maxTrxVolume == 0)
                // fuel with unlimited
                succeed = await targetController.AuthorizeAsync(1);
            else if (maxTrxAmount == 0 && maxTrxVolume != 0)
                succeed = await targetController.AuthorizeWithVolumeAsync((int)(maxTrxVolume * Math.Pow(10, targetController.VolumeDecimalDigits)), 1);
            else
                succeed = await targetController.AuthorizeWithAmountAsync((int)(maxTrxAmount * Math.Pow(10, targetController.AmountDecimalDigits)), 1);
            fdcLogger.LogDebug("   AuthorizePump: " + pumpId + " finished with: " + succeed.ToString());
            return succeed;
        }

        /// <summary>
        /// Clear an unpaid fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, 
        /// so it's a local in-process call.
        /// </summary>
        /// <param name="pumpId">pump id of the target trx belongs</param>
        /// <param name="transactionNo">trx Number of the target trx that is on clearing</param>
        /// <param name="trxDbUniqueId">database row unique id of the target trx that is on clearing</param>
        /// <param name="lockingClientId">the caller identity, the trx will be marked as paid by it, make sure it unique in process
        ///     , otherwise and the only impact, you lose the chance to know who really mark this trx with paid in db.</param>
        /// <returns>the trx successfully cleared(marked as state `Paid`) from db, otherwise failed to clear and return null value</returns>
        [UniversalApi(Description = "Clear an unpaid fuel sale trx from db and notify all tcp FdcClients.")]
        public async Task<FuelSaleTransaction> ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(int pumpId, string transactionNo, int trxDbUniqueId, string lockingClientId)
        {
            return await this.ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(pumpId, transactionNo, trxDbUniqueId, lockingClientId, null, -1);
        }


        /// <summary>
        /// Clear an unpaid fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for Tcp FdcClient caller, the caller most likely a POS which connected in via TCP.
        /// Will return a clearFuelSaleTrx response to tcp Fdc client caller.
        /// </summary>
        /// <param name="pumpId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <param name="workstationID">fdc client's workstation id</param>
        /// <param name="appSenderId">must specify the correct value that used for send respone back via tcp</param>
        /// <param name="fdcClientRequestId">must specify the correct value that used for send respone back via tcp, for pair with request</param>
        /// <returns></returns>
        public async Task<FuelSaleTransaction> ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(int pumpId, string transactionNo,
            int trxDbUniqueId, string workstationID, string appSenderId, int fdcClientRequestId)
        {
            try
            {
                fdcLogger.LogDebug("ClearFuelSaleTrxAndNotifyAllFdcClients (wid: " + workstationID + ", appSenderId: " + (appSenderId ?? "") + ", pumpId: " + pumpId
                            + ", transactionNo: " + transactionNo + ", releaseToken: " + trxDbUniqueId + ")");
                SqliteDbContext dbContext = new SqliteDbContext();
                int databaseId = trxDbUniqueId;
                var target = await dbContext.PumpTransactionModels.FirstOrDefaultAsync(t =>
                        t.ReleaseToken == databaseId
                        && t.PumpId == pumpId
                        && t.TransactionSeqNumberFromPhysicalPump == transactionNo);

                if (target != null)
                {
                    if (target.State == FuelSaleTransactionState.Paid)
                    {
                        fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo
                            + ", releaseToken: " + trxDbUniqueId
                            + " failed due to target trx is already a Paid trx");
                        // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                        // litefccCore internal app, then do not reply a response.
                        if (!string.IsNullOrEmpty(appSenderId))
                        {
                            int.TryParse(transactionNo, out int p_trxNo);
                            fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(),
                                      (int)ErrorCode.ERRCD_NOTPOSSIBLE, 1, OverallResult.Failure.ToString());
                        }

                        return null;
                    }
                    else
                    {
                        target.State = FuelSaleTransactionState.Paid;
                        target.PaidByFdcClientId = workstationID;
                        target.PaidTime = DateTime.Now;
                        await dbContext.SaveChangesAsync();
                        var targetController = fdcPumpControllers
                                .First(c => c.PumpId == pumpId) as IFdcPumpController;
                        int.TryParse(transactionNo, out int p_trxNo);
                        // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                        // litefccCore internal app, then do not reply a response.
                        if (!string.IsNullOrEmpty(appSenderId))
                            fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(),
                                (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString());

                        var safe = this.OnFdcFuelSaleTransactinStateChange;
                        safe?.Invoke(this,
                            new FdcFuelSaleTransactinStateChangeEventArg(target, FuelSaleTransactionState.Paid));
                        var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                        await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(target, FuelSaleTransactionState.Paid));

                        fdcServer.FuelSaleTrx(target.PumpId,
                            target.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                            target.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                            target.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                            target.LogicalNozzleId,
                            int.Parse(target.ProductBarcode),
                            "", "", 1,
                            int.Parse(target.TransactionSeqNumberFromPhysicalPump),
                            // looks like should return 'Paid', but from old code, we put 'Cleared'
                            (int)FuelSaleTransactionState.Cleared,
                            0, trxDbUniqueId.ToString(),
                            target.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                            target.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                            target.LockedByFdcClientId ?? "",
                            "", -1, 0);
                        return target;
                    }
                }
                else
                {
                    fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo
                            + ", releaseToken: " + trxDbUniqueId
                            + " failed due to target trx could not found");
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(appSenderId))
                    {
                        int.TryParse(transactionNo, out int p_trxNo);
                        fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(),
                                  (int)ErrorCode.ERRCD_BADVAL, 1, OverallResult.Failure.ToString());
                    }
                    return null;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo
                            + ", releaseToken: " + trxDbUniqueId
                            + " failed due to exception: " + exxx);
                return null;
            }
        }

        /// <summary>
        /// Lock an unlocked state fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps,
        /// so it's a local in-process call.
        /// </summary>
        /// <param name="lockingClientId">the caller identity, the trx will be marked as locked by it, make sure it unique in process
        ///     , otherwise, the locked trx may get unlocked unexpectly by other caller with same id.</param>
        /// <param name="pumpId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <returns></returns>
        [UniversalApi(Description = "Lock an unlocked state fuel sale trx from db and notify all tcp FdcClients.")]
        public async Task<FuelSaleTransaction> LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId,
            int pumpId,
            int transactionNo, int trxDbUniqueId)
        {
            return await this.LockFuelSaleTrxAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, transactionNo, trxDbUniqueId);
        }

        /// <summary>
        /// Lock an unlocked fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP.
        /// Will return a response to tcp Fdc client caller.
        /// </summary>
        /// <param name="lockingClientId">the caller identity, the trx will be marked as locked by it, make sure it unique in process
        ///     , otherwise, the locked trx may get unlocked unexpectly by other caller with same id.</param>
        /// <param name="fdcClientAppSender"></param>
        /// <param name="fdcClientRequestId"></param>
        /// <param name="pumpId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <returns></returns>
        public async Task<FuelSaleTransaction> LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId, string fdcClientAppSender, int fdcClientRequestId,
            int pumpId,
            int transactionNo, int trxDbUniqueId)
        {
            try
            {
                fdcLogger.LogDebug("LockFuelSaleTrxAndNotifyAllFdcClients from lockingClientId: " + lockingClientId
                    + ", fdcClientAppSender: " + (fdcClientAppSender ?? "")
                    + ", fdcClientRequestId: " + fdcClientRequestId
                    + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId);
                var result = await FdcResourceArbitrator.Default.TryLockFuelSaleTrxAsync(lockingClientId, pumpId, transactionNo, trxDbUniqueId);
                fdcLogger.LogDebug($"   TryLockFuelSaleTrx(trxDbUniqueId: {trxDbUniqueId}) with result: {!(result == null)}");
                //this.LockFuelSaleTrx(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken));
                if (result != null)
                {
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.LockFuelSaleTrx(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId,
                            transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString());

                    var safe = this.OnFdcFuelSaleTransactinStateChange;
                    safe?.Invoke(this,
                        new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Locked));
                    var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                    await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Locked));

                    var targetController = fdcPumpControllers
                        .First(c => c.PumpId == pumpId) as IFdcPumpController;
                    fdcServer.FuelSaleTrx(pumpId,
                       result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                       result.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                       result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                       result.LogicalNozzleId,
                       int.Parse(result.ProductBarcode),
                       this.nozzleExtraInfos
                            .FirstOrDefault(n => n.PumpId == pumpId && n.NozzleLogicalId == result.LogicalNozzleId)?.ProductName ??
                                ("refer cloud for name of " + result.ProductBarcode), "", 1,
                       int.Parse(result.TransactionSeqNumberFromPhysicalPump),
                       (int)FuelSaleTransactionState.Locked,
                       0, trxDbUniqueId.ToString(),
                       result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                       result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                       lockingClientId.ToString(), "", lockingClientId, 0);
                }
                else
                {
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.LockFuelSaleTrx(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId,
                              transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                }

                fdcLogger.LogTrace($"   LockFuelSaleTrxAndNotifyAllFdcClients(trxDbUniqueId: {trxDbUniqueId}) returned");
                return result;
            }
            catch (Exception exx)
            {
                fdcLogger.LogError("LockFuelSaleTrxAndNotifyAllFdcClients exceptioned for lockingClientId: "
                    + lockingClientId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId + ", exception detail: " + exx);
                throw;
            }
        }

        /// <summary>
        /// Unlock a locked state fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps,
        /// so it's a local in-process call.
        ///  </summary>
        /// <param name="lockingClientId">the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx</param>
        /// <param name="pumpId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <returns></returns>
        [UniversalApi(Description = "Unlock a locked state fuel sale trx from db and notify all tcp FdcClients.")]
        public async Task<FuelSaleTransaction> UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId,
            int pumpId,
            int transactionNo, int trxDbUniqueId)
        {
            return await this.UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, transactionNo, trxDbUniqueId);
        }

        public IEnumerable<NozzleExtraInfo> GetNozzleExtraInfos()
        {
            return this.nozzleExtraInfos;
        }

        /// <summary>
        /// Unlock a locked fuel sale trx from db and notify all tcp FdcClients.
        /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP.
        /// Will return a response to tcp Fdc client caller.
        ///  </summary>
        /// <param name="unlockingClientId">the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx</param>
        /// <param name="fdcClientAppSender"></param>
        /// <param name="fdcClientRequestId"></param>
        /// <param name="pumpId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <returns></returns>
        public async Task<FuelSaleTransaction> UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int unlockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId,
            int transactionNo, int trxDbUniqueId)
        {
            try
            {
                fdcLogger.LogDebug("UnlockFuelSaleTrxAndNotifyAllFdcClients from lockingClientId: " + unlockingClientId
                    + ", fdcClientAppSender: " + (fdcClientAppSender ?? "")
                    + ", fdcClientRequestId: " + fdcClientRequestId
                    + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId);
                var result = await FdcResourceArbitrator.Default.TryUnlockFuelSaleTrxAsync(unlockingClientId, pumpId, transactionNo, trxDbUniqueId);
                if (result != null)
                {
                    var targetController = fdcPumpControllers
                           .First(c => c.PumpId == pumpId) as IFdcPumpController;
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.UnlockFuelSaleTrx(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId,
                        transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString());

                    var safe = this.OnFdcFuelSaleTransactinStateChange;
                    safe?.Invoke(this,
                        new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Payable));
                    var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
                    await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Payable));

                    fdcServer.FuelSaleTrx(pumpId,
                        result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits),
                        result.Amount / Math.Pow(10, targetController.AmountDecimalDigits),
                        result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits),
                        result.LogicalNozzleId,
                        int.Parse(result.ProductBarcode),
                        this.nozzleExtraInfos
                            .FirstOrDefault(n => n.PumpId == pumpId && n.NozzleLogicalId == result.LogicalNozzleId)?.ProductName ??
                                ("refer cloud for name of " + result.ProductBarcode), "", 1,
                        int.Parse(result.TransactionSeqNumberFromPhysicalPump),
                        (int)FuelSaleTransactionState.Payable,
                        0, trxDbUniqueId.ToString(),
                        result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                        result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"),
                        "", "", unlockingClientId, 0);
                }
                else
                {
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.UnlockFuelSaleTrx(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId,
                            transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString());
                }

                return result;
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("UnlockFuelSaleTrxAndNotifyAllFdcClients exceptioned for unlockingClientId: "
                    + unlockingClientId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId + ", exception detail: " + exxx);
                throw;
            }
        }


        /// <summary>
        /// Lock a nozzle by call into a PumpHandler and then notify all tcp FdcClients.
        /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps,
        /// so it's a local in-process call.
        /// </summary>
        /// <param name="lockingClientId">the caller identity, the trx will be marked as locked by it, make sure it unique in process
        ///     , otherwise, the locked trx may get unlocked unexpectly by other caller with same id.</param>
        /// <param name="pumpId"></param>
        /// <param name="logicalNozzleId">logical nozzle in the pump for locking</param>
        /// <returns></returns>
        public async Task<bool> LockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId,
            int pumpId,
            int logicalNozzleId)
        {
            return await this.LockNozzleAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, logicalNozzleId);
        }

        /// <summary>
        /// Lock a nozzle by call into a PumpHandler and then notify all tcp FdcClients.
        /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP.
        /// Will return a response to tcp Fdc client caller.
        /// </summary>
        /// <param name="lockingClientId">the caller identity, the trx will be marked as locked by it, make sure it unique in process
        ///     , otherwise, the locked trx may get unlocked unexpectly by other caller with same id.</param>
        /// <param name="fdcClientAppSender"></param>
        /// <param name="fdcClientRequestId"></param>
        /// <param name="pumpId"></param>
        /// <param name="logicalNozzleId"></param>
        /// <returns></returns>
        public async Task<bool> LockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId, string fdcClientAppSender, int fdcClientRequestId,
            int pumpId,
            int logicalNozzleId)
        {
            try
            {
                fdcLogger.LogDebug("LockNozzleAndNotifyAllFdcClientsAsync from lockingClientId: " + lockingClientId
                    + ", fdcClientAppSender: " + (fdcClientAppSender ?? "")
                    + ", fdcClientRequestId: " + fdcClientRequestId
                    + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId);
                var targetController = fdcPumpControllers
                               .First(c => c.PumpId == pumpId) as IFdcPumpController;
                var result = await targetController.LockNozzleAsync((byte)logicalNozzleId);
                if (result)
                {
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                            pumpId, pumpId, logicalNozzleId,
                            (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                }
                else
                {
                    fdcLogger.LogDebug("LockNozzleAndNotifyAllFdcClientsAsync failed");
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                            pumpId, pumpId, logicalNozzleId,
                            (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString());
                }

                return result;
            }
            catch (Exception exx)
            {
                fdcLogger.LogError("LockNozzleAndNotifyAllFdcClientsAsync exceptioned for lockingClientId: "
                    + lockingClientId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId + ", exception detail: " + exx);
                fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                    pumpId, pumpId, logicalNozzleId,
                    (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.Failure.ToString());
                return false;
            }
        }

        /// <summary>
        /// Unlock a nozzle by call into a PumpHandler and then notify all tcp FdcClients
        /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps,
        /// so it's a local in-process call.
        ///  </summary>
        /// <param name="lockingClientId">the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx</param>
        /// <param name="pumpId"></param>
        /// <param name="logicalNozzleId"></param>
        /// <returns></returns>
        public async Task<bool> UnlockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId,
            int pumpId,
            int logicalNozzleId)
        {
            return await this.UnlockNozzleAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, logicalNozzleId);
        }

        /// <summary>
        /// Unlock a nozzle by call into a PumpHandler and then notify all tcp FdcClients
        /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP.
        /// Will return a response to tcp Fdc client caller.
        ///  </summary>
        /// <param name="unlockingClientId">the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx</param>
        /// <param name="fdcClientAppSender"></param>
        /// <param name="fdcClientRequestId"></param>
        /// <param name="pumpId"></param>
        /// <param name="logicalNozzleId"></param>
        /// <returns></returns>
        public async Task<bool> UnlockNozzleAndNotifyAllFdcClientsAsync(int unlockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId,
            int logicalNozzleId)
        {
            try
            {
                fdcLogger.LogDebug("UnlockNozzleAndNotifyAllFdcClientsAsync from lockingClientId: " + unlockingClientId
                    + ", fdcClientAppSender: " + (fdcClientAppSender ?? "")
                    + ", fdcClientRequestId: " + fdcClientRequestId
                    + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId);
                var targetController = fdcPumpControllers
                               .First(c => c.PumpId == pumpId) as IFdcPumpController;
                var result = await targetController.UnlockNozzleAsync((byte)logicalNozzleId);
                if (result)
                {
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                            pumpId, pumpId,
                            logicalNozzleId,
                            (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString());
                }
                else
                {
                    fdcLogger.LogDebug("UnlockNozzleAndNotifyAllFdcClientsAsync failed");
                    // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like
                    // litefccCore internal app, then do not reply a response.
                    if (!string.IsNullOrEmpty(fdcClientAppSender))
                        fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                            pumpId, pumpId,
                            logicalNozzleId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString());
                }

                return result;
            }
            catch (Exception exx)
            {
                fdcLogger.LogError("UnlockNozzleAndNotifyAllFdcClientsAsync exceptioned for lockingClientId: "
                    + unlockingClientId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId + ", exception detail: " + exx);
                fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId,
                            pumpId, pumpId,
                            logicalNozzleId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.Failure.ToString());
                return false;
            }
        }

        /// <summary>
        /// Query the totalizer value (money and volume) with decimal point, with default timeout 10 seconds.
        /// </summary>
        /// <param name="pumpId"></param>
        /// <param name="nozzleLogicalId">from 1 to N</param>
        /// <returns>With decimal points value, Money:Volume</returns>
        [UniversalApi(Description = "returned value are With decimal points value, format-> Money:Volume")]
        public async Task<Tuple<double, double>> GetFuelPointTotalsAsync(int pumpId, byte nozzleLogicalId)
        {
            try
            {
                if (fdcLogger.IsEnabled(LogLevel.Debug))
                    fdcLogger.LogDebug("GetFuelPointTotals for pumpId: " + pumpId + ", nozzleLogicalId: " + nozzleLogicalId);

                var targetController = fdcPumpControllers.First(c => c.PumpId == pumpId) as IFdcPumpController;
                var totalizerResultTask = targetController.QueryTotalizerAsync(nozzleLogicalId);
                await Task.WhenAny(totalizerResultTask, Task.Delay(10000));
                if (totalizerResultTask.IsCompleted)
                {
                    if (fdcLogger.IsEnabled(LogLevel.Debug))
                        fdcLogger.LogDebug("      GetFuelPointTotals for pumpId: " + pumpId
                        + ", nozzleLogicalId: " + nozzleLogicalId
                        + ", no decimal point Money<->Volume pair is: " + totalizerResultTask.Result.Item1 + "<->" + totalizerResultTask.Result.Item2);
                    return new Tuple<double, double>(
                         totalizerResultTask.Result.Item1 / Math.Pow(10, targetController.AmountDecimalDigits),
                         totalizerResultTask.Result.Item2 / Math.Pow(10, targetController.VolumeDecimalDigits));
                }
                else
                {
                    fdcLogger.LogInformation("      GetFuelPointTotals for pumpId: " + pumpId
                        + ", nozzleLogicalId: " + nozzleLogicalId + " timed out.");
                    return new Tuple<double, double>(-1, -1);
                }
            }
            catch (Exception ex)
            {
                fdcLogger.LogError("GetFuelPointTotals for pumpId:" + pumpId +
                    ", nozzleLogicalId: " + nozzleLogicalId + " exception:" + ex.Message);
                return new Tuple<double, double>(-1, -1);
            }

        }

        /// <summary>
        /// Query the IFdcPumpController
        /// </summary>
        /// <param name="pumpId"></param>
        /// <returns>IFdcPumpController</returns>
        public IFdcPumpController GetFdcPumpController(int pumpId)
        {
            if (fdcLogger.IsEnabled(LogLevel.Debug))
                fdcLogger.LogDebug("GetFdcPumpController for pumpId: " + pumpId);

            var targetController = fdcPumpControllers
                .First(c => c.PumpId == pumpId) as IFdcPumpController;
            return targetController;
        }

        /// <summary>
        /// Get the fuel sale trxs with details which are in state: Undefined, Payable, or Locked for target Pump.
        /// NOTE, for performance purpose, internal throttle is enabled to only return latest 365 days 
        ///     or latest 1000 rows of data.
        /// </summary>
        /// <param name="pumpId">the target pump id, or -1 which means the target is all pumps</param>
        /// <returns>list of FuelSaleTransaction, the most recent trx is in more front position</returns>
        public async Task<IEnumerable<FuelSaleTransaction>> GetAvailableFuelSaleTrxsWithDetailsAsync(int pumpId)
        {
            return await this.GetAvailableFuelSaleTrxsWithDetailsAsync(pumpId, -1, 100);
        }

        /// <summary>
        /// Get the fuel sale trxs with details which are in state: Undefined, Payable, or Locked for target Pump.
        /// </summary>
        /// <param name="pumpId">the target pump id, or -1 which means the target is all pumps</param>
        /// <param name="nozzleLogicalId">further specify the nozzle logical id, or -1 which means all nozzles in the pump.</param>
        /// <param name="rowCount">limit the returned row count</param>
        /// <returns>list of FuelSaleTransaction, the most recent trx is in more front position</returns>
        [UniversalApi(Description = "get the latest fuel transactions with state Payable or Locked.")]
        public async Task<IEnumerable<FuelSaleTransaction>> GetAvailableFuelSaleTrxsWithDetailsAsync(int pumpId, int nozzleLogicalId, int rowCount)
        {
            // for limit data size, only show latest N days' data.
            int maxReturnDays = 365;
            // for further limit data size, only show latest N count of data.
            int maxReturnDataCount = rowCount;
            try
            {
                var dueDate = DateTime.Now.Subtract(new TimeSpan(maxReturnDays, 0, 0, 0));
                SqliteDbContext dbContext = new SqliteDbContext();
                if (pumpId == -1)
                {
                    if (nozzleLogicalId == -1)
                    {
                        var all = await dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Paid
                                && t.State != FuelSaleTransactionState.Cleared
                                && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
                        return all;
                    }
                    else
                    {
                        var all = await dbContext.PumpTransactionModels.Where(t => t.LogicalNozzleId == nozzleLogicalId
                                && t.State != FuelSaleTransactionState.Paid
                                && t.State != FuelSaleTransactionState.Cleared
                                && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
                        return all;
                    }
                }

                if (nozzleLogicalId == -1)
                {
                    var some = await dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                        && t.State != FuelSaleTransactionState.Paid
                        && t.State != FuelSaleTransactionState.Cleared
                        && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
                    return some;
                }
                else
                {
                    var some = await dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                        && t.LogicalNozzleId == nozzleLogicalId
                        && t.State != FuelSaleTransactionState.Paid
                        && t.State != FuelSaleTransactionState.Cleared
                        && t.SaleStartTime >= dueDate)
                    .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount).ToListAsync();
                    return some;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("GetAvailableFuelSaleTrxsWithDetails for pumpId: " + pumpId
                    + ", nozzleLogicalId: " + nozzleLogicalId
                    + ", rowCount: " + rowCount + " exceptioned: "
                    + exxx.ToString());
                return null;
            }
        }

        /// <summary>
        /// Get the fuel sale trxes with details by varies of conditions.
        /// </summary>
        /// <param name="pumpId">the target pump id, or -1 for all pumps</param>
        /// <param name="nozzleLogicalId">further specify the nozzle logical id, or -1 for all nozzles in the pump.</param>
        /// <param name="pageRowCount">for paging the returned data, controls how many data rows in a single page</param>
        /// <param name="pageIndex">for paging the returned data, controls return which data page</param>
        /// <param name="filterTimestamp">controls trx that only with its SaleStartTime >= filterTimestamp will be returned, leave null to ignore this check, the results are ordered by SaleStartTime (with ascending).</param>
        /// <returns></returns>
        [UniversalApi(Description = "get the latest fuel transactions by conditions.</br>" +
            "&lt;param name=\"pumpId\"&gt;the target pump id, or null for all pumps&lt;/param&gt;</br>" +
            "&lt;param name=\"nozzleLogicalId\"&gt;further specify the nozzle logical id, or null for all nozzles in the pump.&lt;/param&gt;</br>" +
            "&lt;param name=\"pageRowCount\"&gt;for paging the returned data, controls how many data rows in a single page&lt;/param&gt;</br>" +
            "&lt;param name=\"pageIndex\"&gt;for paging the returned data, controls return which data page&lt;/param&gt;</br>" +
            "&lt;param name=\"filterTimestamp\"&gt;controls trx that only with its SaleStartTime &gt;= filterTimestamp will be returned, leave null to ignore this check, the results are ordered by SaleStartTime (with default ascending)&lt;/param&gt;")]
        public async Task<IEnumerable<FuelSaleTransaction>> GetFuelSaleTrxsWithDetailsAsync(int? pumpId, int? nozzleLogicalId,
            FuelSaleTransactionState transactionState,
            int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null, bool isAscending = true)
        {
            // for limit data size, only show latest N days' data.
            int maxReturnDays = 60;
            try
            {
                var dueDate = DateTime.Now.Subtract(new TimeSpan(maxReturnDays, 0, 0, 0));
                if (filterTimestamp != null)
                    dueDate = filterTimestamp.Value;
                SqliteDbContext dbContext = new SqliteDbContext();
                if (!pumpId.HasValue)
                {
                    IOrderedQueryable<FuelSaleTransaction> orderedResults = null;
                    if (isAscending)
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.State == transactionState
                            && t.SaleStartTime >= dueDate).OrderBy(r => r.SaleStartTime);
                    else
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.State == transactionState
                            && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime);
                    var all = await orderedResults.Skip(pageRowCount * pageIndex).Take(pageRowCount).ToListAsync();
                    return all;
                }

                if (!nozzleLogicalId.HasValue)
                {
                    IOrderedQueryable<FuelSaleTransaction> orderedResults = null;
                    if (isAscending)
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                            && t.State == transactionState && t.SaleStartTime >= dueDate).OrderBy(r => r.SaleStartTime);
                    else
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                            && t.State == transactionState && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime);
                    var some = await orderedResults.Skip(pageRowCount * pageIndex).Take(pageRowCount).ToListAsync();
                    return some;
                }
                else
                {
                    IOrderedQueryable<FuelSaleTransaction> orderedResults = null;
                    if (isAscending)
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                            && t.LogicalNozzleId == nozzleLogicalId
                            && t.State == transactionState && t.SaleStartTime >= dueDate).OrderBy(r => r.SaleStartTime);
                    else
                        orderedResults = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId
                            && t.LogicalNozzleId == nozzleLogicalId
                            && t.State == transactionState && t.SaleStartTime >= dueDate).OrderByDescending(r => r.SaleStartTime);
                    var some = await orderedResults.Skip(pageRowCount * pageIndex).Take(pageRowCount).ToListAsync();
                    return some;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError($"GetFuelSaleTrxsWithDetailsAsync for pumpId: { pumpId }, nozzleLogicalId: { nozzleLogicalId }, " +
                    $"pageRowCount: { pageRowCount }, pageIndex: {pageIndex}, filterTimestamp: {filterTimestamp ?? DateTime.MinValue}, isAscending: {isAscending}, exceptioned:{exxx}");
                return null;
            }
        }

        /// <summary>
        /// Change a price for pumps that have the target barcode product configurated for their nozzles.
        /// </summary>
        /// <param name="barcode">fuel product barcode</param>
        /// <param name="newPriceWithDecimalPoints">new price with decimal points</param>
        /// <returns>the successfully price changed nozzle will be returned</returns>
        [UniversalApi]
        public async Task<IEnumerable<LogicalNozzle>> ChangeFuelPriceAsync(int barcode, double newPriceWithDecimalPoints)
        {
            try
            {
                fdcLogger.LogInformation("ChangeFuelPrice for product with barcode: " + barcode
                    + " to new price(with decimal points): " + newPriceWithDecimalPoints);

                var targetNozzles = fdcPumpControllers.SelectMany(p => p.Nozzles).Join(
                    nozzleExtraInfos.Where(c => c.ProductBarcode == barcode),
                    n => n.PumpId + "," + n.LogicalId,
                    c => c.PumpId + "," + c.NozzleLogicalId,
                    (n, c) => n);
                if (!targetNozzles.Any())
                {
                    fdcLogger.LogError("ChangeFuelPrice, have NOT seen product with barcode: " + barcode
                        + " bound to any nozzles from all pumps, will quit.");
                    return null;
                }

                //var oldPriceWithNoHumanReadableFormat = targetNozzles.First().RealPriceOnPhysicalPump;
                var succeedNozzles = new List<LogicalNozzle>();
                foreach (var nozzle in targetNozzles)
                {
                    bool succeed = false;
                    var targetController = fdcPumpControllers.First(p => p.PumpId == nozzle.PumpId);
                    var pumpRawFormatPrice = (int)(
                        Math.Round(newPriceWithDecimalPoints * Math.Pow(10, targetController.PriceDecimalDigits),
                        MidpointRounding.AwayFromZero));
                    var previousPrice = targetController.Nozzles
                        .First(n => n.LogicalId == nozzle.LogicalId)?.RealPriceOnPhysicalPump;
                    //if (previousPrice.HasValue
                    //    && previousPrice.Value == pumpRawFormatPrice)
                    //{
                    //    succeed = true;
                    //    fdcLogger.LogInformation("        price change, Pump " + targetController.PumpId + ", NozzleLogicalId " + nozzle.LogicalId + ", new price EQUALS the previous price, will NOT perform ChangeFuelPrice request");
                    //}
                    //else
                    //{

                    FuelPriceChange dbFuelPriceChange = null;
                    try
                    {
                        dbFuelPriceChange = new FuelPriceChange()
                        {
                            PumpId = nozzle.PumpId,
                            LogicalNozzleId = nozzle.LogicalId,
                            NewPriceWithoutDecimal = pumpRawFormatPrice,
                            StartTime = DateTime.Now,
                            FinishTime = null
                        };
                        var dbContext = new SqliteDbContext();
                        dbContext.FuelPriceChanges.Add(dbFuelPriceChange);
                        await dbContext.SaveChangesAsync();
                        //fdcLogger.LogDebug("        ChangeFuelPrice, new price for pump: " + targetController.PumpId + " pre-save into database succeed");
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError("        ChangeFuelPrice, new price pre-save into database for pump: "
                            + targetController.PumpId + ", logical Nozzle: " + nozzle.LogicalId
                            + " exceptioned: " + exxx
                            + Environment.NewLine
                            + "Will ignore this nozzle and continue to next...");
                        continue;
                    }

                    try
                    {
                        succeed = await targetController.ChangeFuelPriceAsync(pumpRawFormatPrice, nozzle.LogicalId);
                        fdcLogger.LogInformation("       ChangeFuelPrice succeed in IFdcController side for pump with PumpId: " + targetController.PumpId
                            + ", LogicalNozzleId: " + nozzle.LogicalId);
                    }
                    catch (Exception exxx)
                    {
                        fdcLogger.LogError("        ChangeFuelPrice, pump " + targetController.PumpId
                            + ", logical nozzle " + nozzle.LogicalId + " exceptioned in pump side: "
                            + exxx
                            + Environment.NewLine
                            + "Will continue to next nozzle.");
                        continue;
                    }

                    if (succeed)
                    {
                        succeedNozzles.Add(nozzle);
                        //fdcLogger.LogDebug("ChangeFuelPrice for pump: " + targetController.PumpId
                        //    + ", logicalNozzle: " + nozzle.LogicalId + " succeed on pump side, will post-save to db");
                        try
                        {
                            dbFuelPriceChange.FinishTime = DateTime.Now;
                            var dbContext = new SqliteDbContext();
                            await dbContext.SaveChangesAsync();
                        }
                        catch (Exception exxx)
                        {
                            fdcLogger.LogError("        ChangeFuelPrice, new price post-save into database for pump: "
                                + targetController.PumpId
                                + ", logicalNozzle: " + nozzle.LogicalId + " exceptioned: "
                                + exxx
                                + Environment.NewLine
                                + "Will ignore and continue to next nozzle");
                        }
                    }
                    else
                    {
                        fdcLogger.LogError("        ChangeFuelPrice, Pump " + targetController.PumpId
                                + ", NozzleLogicalId " + nozzle.LogicalId + " apply new price failed on pump side");
                    }
                }

                return succeedNozzles;
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("ChangeFuelPrice, generic exception: " + exxx);
                return null;
            }
        }

        #endregion

    }
}