using Dapper;
using EVCB_OCPP.Domain;
using EVCB_OCPP.Domain.Models.Database;
using EVCB_OCPP.WSServer.Helper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OCPPPackage.Profiles;
using System.Data;

namespace EVCB_OCPP.WSServer.Service;

public interface IMainDbService
{
    Task<string> GetMachineAuthorizationKey(string ChargeBoxId);
    Task<string> GetMachineConfiguration(string ChargeBoxId, string configName);
    Task<string> GetMachineHeartbeatInterval(string ChargeBoxId);
    Task<MachineAndCustomerInfo> GetMachineIdAndCustomerInfo(string ChargeBoxId);
    Task<string> GetMachineSecurityProfile(string ChargeBoxId);
    Task UpdateMachineBasicInfo(string ChargeBoxId, Machine machine);
    Task AddOCMF(OCMF oCMF);
    ValueTask<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId);
    Task UpdateConnectorStatus(string Id, ConnectorStatus connectorStatus);
    ValueTask AddConnectorStatus(string ChargeBoxId, byte ConnectorId, DateTime CreatedOn, int Status,
        int ChargePointErrorCodeId, string ErrorInfo, string VendorId, string VendorErrorCode);
    Task AddServerMessage(ServerMessage message);
    Task AddServerMessage(string ChargeBoxId, string OutAction, object OutRequest, string CreatedBy = "", DateTime? CreatedOn = null, string SerialNo = "", string InMessage = "");
    ValueTask AddMachineError(byte ConnectorId, DateTime CreatedOn, int Status, string ChargeBoxId, int ErrorCodeId, string ErrorInfo, int PreStatus, string VendorErrorCode, string VendorId);
    ValueTask<Customer> GetCustomer(string id);
    ValueTask<Customer> GetCustomer(Guid id);
    Task<Guid> GetCustomerIdByChargeBoxId(string chargeboxId);
    Task<int?> TryGetDuplicatedTransactionId(string chargeBoxId, Guid customerId, int connectorId, DateTime timestamp);
    Task<int> AddNewTransactionRecord(TransactionRecord newTransaction);
    Task<TransactionRecord> GetTransactionForStopTransaction(int transactionId, string chargeBoxId);
    Task UpdateTransaction(int transactionId, int meterStop, DateTime stopTime, int stopReasonId, string stopReason, string stopIdTag, string receipt, int cost);
    Task<bool> UpdateHeartBeats(IEnumerable<Machine> heartBeatsData);
}

public class MainDbService : IMainDbService
{
    public MainDbService(
        IDbContextFactory<MainDBContext> contextFactory,
        SqlConnectionFactory<MainDBContext> sqlConnectionFactory,
        IMemoryCache memoryCache,
        IConfiguration configuration,
        ILoggerFactory loggerFactory,
        ILogger<MainDbService> logger)
    {
        this.contextFactory = contextFactory;
        this.sqlConnectionFactory = sqlConnectionFactory;
        this.memoryCache = memoryCache;
        this.loggerFactory = loggerFactory;
        this.logger = logger;
        var startupLimit = GetStartupLimit(configuration);
        //this.connectionString = configuration.GetConnectionString("MainDBContext");
        this.startupSemaphore = new(startupLimit);

        var opLimit = GetOpLimit(configuration);
        this.opSemaphore = new SemaphoreSlim(opLimit);

        InitUpdateConnectorStatusHandler();
        InitUpdateMachineBasicInfoHandler();
        InitAddServerMessageHandler();
    }

    private const string CustomerMemCacheKeyFromat = "Customer_{0}";
    private const string ChargeBoxConnectorIdMemCacheKeyFromat = "Connector_{0}{1}";

    private readonly IDbContextFactory<MainDBContext> contextFactory;
    private readonly SqlConnectionFactory<MainDBContext> sqlConnectionFactory;
    private readonly IMemoryCache memoryCache;
    private readonly ILoggerFactory loggerFactory;
    private readonly ILogger<MainDbService> logger;

    //private string connectionString;
    private readonly QueueSemaphore startupSemaphore;
    private readonly SemaphoreSlim opSemaphore;
    private GroupSingleHandler<StatusNotificationParam> statusNotificationHandler;
    private GroupSingleHandler<UpdateMachineBasicInfoParam> updateMachineBasicInfoHandler;
    private GroupSingleHandler<ServerMessage> addServerMessageHandler;

    public async Task<MachineAndCustomerInfo> GetMachineIdAndCustomerInfo(string ChargeBoxId)
    {
        using var semaphoreWrapper = await startupSemaphore.GetToken();
        using var db = await contextFactory.CreateDbContextAsync();

        var machine = await db.Machine.Where(x => x.ChargeBoxId == ChargeBoxId && x.IsDelete == false).Select(x => new { x.CustomerId, x.Id }).AsNoTracking().FirstOrDefaultAsync();
        if (machine == null)
        {
            return new MachineAndCustomerInfo(string.Empty, Guid.Empty, "Unknown");
        }
        //var customerName = await db.Customer.Where(x => x.Id == machine.CustomerId).Select(x => x.Name).FirstOrDefaultAsync();
        var customer = await GetCustomer(machine.CustomerId);
        var customerName = customer?.Name;
        return new MachineAndCustomerInfo(machine.Id, machine.CustomerId, customerName);
    }

    public async Task<string> GetMachineConfiguration(string ChargeBoxId, string configName)
    {
        using var semaphoreWrapper = await startupSemaphore.GetToken();
        using var db = await contextFactory.CreateDbContextAsync();
        return await db.MachineConfigurations
            .Where(x => x.ChargeBoxId == ChargeBoxId && x.ConfigureName == configName)
            .Select(x => x.ConfigureSetting).FirstOrDefaultAsync();
    }

    public Task<string> GetMachineSecurityProfile(string ChargeBoxId)
    {
        return GetMachineConfiguration(ChargeBoxId, StandardConfiguration.SecurityProfile);
    }

    public Task<string> GetMachineAuthorizationKey(string ChargeBoxId)
    {
        return GetMachineConfiguration(ChargeBoxId, StandardConfiguration.AuthorizationKey);
    }

    public Task<string> GetMachineHeartbeatInterval(string ChargeBoxId)
    {
        return GetMachineConfiguration(ChargeBoxId, StandardConfiguration.HeartbeatInterval);
    }

    public Task UpdateMachineBasicInfo(string ChargeBoxId, Machine machine)
    {
        //return UpdateMachineBasicInfoEF(ChargeBoxId, machine);
        return updateMachineBasicInfoHandler.HandleAsync(new UpdateMachineBasicInfoParam(ChargeBoxId, machine));
    }

    public async Task AddOCMF(OCMF oCMF)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        await db.OCMF.AddAsync(oCMF);
        await db.SaveChangesAsync();
    }

    public async ValueTask AddConnectorStatus(
        string ChargeBoxId, byte ConnectorId, DateTime CreatedOn, int Status,
        int ChargePointErrorCodeId, string ErrorInfo, string VendorId, string VendorErrorCode)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        var _currentStatus = new Domain.Models.Database.ConnectorStatus()
        {
            ChargeBoxId = ChargeBoxId,
            ConnectorId = ConnectorId,
            CreatedOn = CreatedOn,
            Status = Status,
            ChargePointErrorCodeId = ChargePointErrorCodeId,
            ErrorInfo = ErrorInfo,
            VendorId = VendorId,
            VendorErrorCode = VendorErrorCode,
            Id = Guid.NewGuid().ToString()
        };
        await db.ConnectorStatus.AddAsync(_currentStatus);

        await db.SaveChangesAsync();

        memoryCache.Set(
            string.Format(ChargeBoxConnectorIdMemCacheKeyFromat, ChargeBoxId, ConnectorId)
            , _currentStatus, TimeSpan.FromHours(12));
    }

    public async ValueTask<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId)
    {
        var key = string.Format(ChargeBoxConnectorIdMemCacheKeyFromat, ChargeBoxId, ConnectorId);
        if (memoryCache.TryGetValue<ConnectorStatus>(key, out var status))
        {
            return status;
        }

        using var db = await contextFactory.CreateDbContextAsync();
        var statusFromDb = await db.ConnectorStatus.Where(x => x.ChargeBoxId == ChargeBoxId
                            && x.ConnectorId == ConnectorId).AsNoTracking().FirstOrDefaultAsync();

        memoryCache.Set(key, statusFromDb, TimeSpan.FromHours(12));
        return statusFromDb;
    }

    public async Task UpdateConnectorStatus(string Id, ConnectorStatus Status)
    {
        //await statusNotificationHandler.HandleAsync(new StatusNotificationParam(Id, Status));
        //await UpdateConnectorStatusEF(Id, Status);
        await UpdateConnectorStatusDapper(Id, Status);

        var key = string.Format(ChargeBoxConnectorIdMemCacheKeyFromat, Status.ChargeBoxId, Status.ConnectorId);
        memoryCache.Set(key, Status, TimeSpan.FromHours(12));
        return;
    }

    public Task<Guid> GetCustomerIdByChargeBoxId(string chargeboxId)
    {
        //return GetCustomerIdByChargeBoxIdEF(chargeboxId);
        return GetCustomerIdByChargeBoxIdDapper(chargeboxId);
    }

    public Task<int?> TryGetDuplicatedTransactionId(string chargeBoxId, Guid customerId, int connectorId, DateTime timestamp)
    {
        //return TryGetDuplicatedTransactionIdEF(chargeBoxId, customerId, connectorId, timestamp);
        return TryGetDuplicatedTransactionIdDapper(chargeBoxId, customerId, connectorId, timestamp);
    }


    public ValueTask AddMachineError(byte ConnectorId, DateTime CreatedOn, int Status, string ChargeBoxId,
        int ErrorCodeId, string ErrorInfo, int PreStatus, string VendorErrorCode, string VendorId)
    {
        //return AddMachineErrorEF(ConnectorId, CreatedOn, Status, ChargeBoxId, ErrorCodeId, ErrorInfo, PreStatus, VendorErrorCode, VendorId);
        return AddMachineErrorDapper(ConnectorId, CreatedOn, Status, ChargeBoxId, ErrorCodeId, ErrorInfo, PreStatus, VendorErrorCode, VendorId);
    }

    public Task AddServerMessage(string ChargeBoxId, string OutAction, object OutRequest, string CreatedBy, DateTime? CreatedOn = null, string SerialNo = "", string InMessage = "")
    {
        if (string.IsNullOrEmpty(CreatedBy))
        {
            CreatedBy = "Server";
        }

        if (string.IsNullOrEmpty(SerialNo))
        {
            SerialNo = Guid.NewGuid().ToString();
        }
        var _CreatedOn = CreatedOn ?? DateTime.UtcNow;

        string _OutRequest = "";
        if (OutRequest is not null)
        {
            _OutRequest = JsonConvert.SerializeObject(
                OutRequest,
                new JsonSerializerSettings()
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    Formatting = Formatting.None
                });
        }

        var data = new ServerMessage()
        {
            ChargeBoxId = ChargeBoxId,
            CreatedBy = CreatedBy,
            CreatedOn = _CreatedOn,
            OutAction = OutAction,
            OutRequest = _OutRequest,
            SerialNo = SerialNo,
            InMessage = InMessage
        };

        return AddServerMessage(data);
    }

    public Task AddServerMessage(ServerMessage message)
    {
        //return AddServerMessageEF(message);
        //return addServerMessageHandler.HandleAsync(message);
        return AddServerMessageDapper(message);
    }

    public ValueTask<Customer> GetCustomer(string id)
        => GetCustomer(new Guid(id));

    public async ValueTask<Customer> GetCustomer(Guid id)
    {
        var key = string.Format(CustomerMemCacheKeyFromat, id);
        if (memoryCache.TryGetValue<Customer>(key, out var customer))
        {
            return customer;
        }

        Customer toReturn = null;
        using (var db = await contextFactory.CreateDbContextAsync())
        {
            toReturn = await db.Customer.FirstOrDefaultAsync(x => x.Id == id);
        }

        if (toReturn is not null)
        {
            memoryCache.Set(key, toReturn, TimeSpan.FromSeconds(15));
        }

        return toReturn;
    }

    public Task<int> AddNewTransactionRecord(TransactionRecord newTransaction)
    {
        //return AddNewTransactionRecordEF(newTransaction);
        return AddNewTransactionRecordDapper(newTransaction);
    }

    public Task<TransactionRecord> GetTransactionForStopTransaction(int transactionId, string chargeBoxId)
    {
        //return GetTransactionForStopTransactionEF(transactionId, chargeBoxId);
        return GetTransactionForStopTransactionDapper(transactionId, chargeBoxId);
    }

    public Task UpdateTransaction(int transactionId, int meterStop, DateTime stopTime, int stopReasonId, string stopReason, string stopIdTag, string receipt, int cost)
    {
        //return UpdateTransactionEF(transactionId, meterStop, stopTime, stopReasonId, stopReason, stopIdTag, receipt, cost);
        return UpdateTransactionDapper(transactionId, meterStop, stopTime, stopReasonId, stopReason, stopIdTag, receipt, cost);
    }

    private async Task UpdateTransactionEF(int transactionId, int meterStop, DateTime stopTime, int stopReasonId, string stopReason, string stopIdTag, string receipt, int cost)
    {
        using var db = await contextFactory.CreateDbContextAsync();

        var _transaction = db.TransactionRecord.Where(x => x.Id == transactionId //&& x.ChargeBoxId == session.ChargeBoxId
            ).FirstOrDefault();

        _transaction.MeterStop = meterStop;
        _transaction.StopTime = stopTime;
        _transaction.StopReasonId = stopReasonId;
        _transaction.StopReason = stopReason;
        _transaction.StopIdTag = stopIdTag;
        _transaction.Receipt = receipt;
        _transaction.Cost = cost;

        //await db.SaveChangesAsync();
        await db.SaveChangesAsync();
    }

    private async Task UpdateTransactionDapper(int transactionId, int meterStop, DateTime stopTime, int stopReasonId, string stopReason, string stopIdTag, string receipt, int cost)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@TransactionId", transactionId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@MeterStop", meterStop, DbType.Decimal, ParameterDirection.Input, precision: 18, scale: 2);
        parameters.Add("@StopTime", stopTime, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@StopReasonId", stopReasonId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@StopReason", stopReason, DbType.String, ParameterDirection.Input, 60);
        parameters.Add("@StopIdTag", stopIdTag, DbType.String, ParameterDirection.Input, 20);
        parameters.Add("@Receipt", receipt, DbType.String, ParameterDirection.Input, 3000);
        parameters.Add("@Cost", cost, DbType.Decimal, ParameterDirection.Input, precision: 18, scale: 2);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var resultCnt = await conn.ExecuteAsync("""
            UPDATE TransactionRecord
            SET MeterStop = @MeterStop, StopTime = @StopTime, StopReasonId = @StopReasonId,
            StopReason = @StopReason, StopIdTag = @StopIdTag, Receipt = @Receipt, Cost = @Cost
            WHERE Id = @TransactionId
            """, parameters);
        if (resultCnt != 1)
        {
            throw new Exception("Update over one columes");
        }
        return;
    }

    public Task<bool> UpdateHeartBeats(IEnumerable<Machine> heartBeatsData)
    {
        //return UpdateHeartBeatsEF(heartBeatsData);
        return UpdateHeartBeatsDapper(heartBeatsData);
    }
    private void InitUpdateConnectorStatusHandler()
    {
        if (statusNotificationHandler is not null)
        {
            throw new Exception($"{nameof(InitUpdateConnectorStatusHandler)} should only called once");
        }

        statusNotificationHandler = new GroupSingleHandler<StatusNotificationParam>(
            handleFunc: BundleUpdateConnectorStatusDapper,
            logger: loggerFactory.CreateLogger("StatusNotificationHandler"),
            workerCnt: 1);
    }
    private void InitAddServerMessageHandler()
    {
        if (addServerMessageHandler is not null)
        {
            throw new Exception($"{nameof(InitAddServerMessageHandler)} should only called once");
        }

        addServerMessageHandler = new GroupSingleHandler<ServerMessage>(
            handleFunc: BundleAddServerMessage,
            logger: loggerFactory.CreateLogger("AddServerMessageHandler"));
    }

    private void InitUpdateMachineBasicInfoHandler()
    {
        if (updateMachineBasicInfoHandler is not null)
        {
            throw new Exception($"{nameof(InitUpdateMachineBasicInfoHandler)} should only called once");
        }

        updateMachineBasicInfoHandler = new GroupSingleHandler<UpdateMachineBasicInfoParam>(
            handleFunc: BundelUpdateMachineBasicInfo,
            logger: loggerFactory.CreateLogger("UpdateMachineBasicInfoHandler"),
            workerCnt: 10);
    }

    private async Task UpdateMachineBasicInfoEF(string chargeBoxId, Machine machine)
    {
        using var semaphoreWrapper = await startupSemaphore.GetToken();
        using var db = await contextFactory.CreateDbContextAsync();

        var _machine = await db.Machine.FirstOrDefaultAsync(x => x.ChargeBoxId == chargeBoxId);
        _machine.ChargeBoxSerialNumber = machine.ChargeBoxSerialNumber;
        _machine.ChargePointSerialNumber = machine.ChargePointSerialNumber;
        _machine.ChargePointModel = machine.ChargePointModel;
        _machine.ChargePointVendor = machine.ChargePointVendor;
        _machine.FW_CurrentVersion = machine.FW_CurrentVersion;
        _machine.Iccid = DateTime.UtcNow.ToString("yy-MM-dd HH:mm");
        _machine.Imsi = machine.Imsi;
        _machine.MeterSerialNumber = machine.MeterSerialNumber;
        _machine.MeterType = machine.MeterType;

        await db.SaveChangesAsync();

        //using var semaphoreWrapper = await startupSemaphore.GetToken();
    }

    private async Task BundelUpdateMachineBasicInfo(IEnumerable<UpdateMachineBasicInfoParam> pams)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        using var trans = await db.Database.BeginTransactionAsync();

        pams = pams.DistinctBy(x => x.ChargeBoxId);

        foreach (var pam in pams)
        {
            var _machine = db.Machine.FirstOrDefault(x => x.ChargeBoxId == pam.ChargeBoxId);
            _machine.ChargeBoxSerialNumber = pam.machine.ChargeBoxSerialNumber;
            _machine.ChargePointSerialNumber = pam.machine.ChargePointSerialNumber;
            _machine.ChargePointModel = pam.machine.ChargePointModel;
            _machine.ChargePointVendor = pam.machine.ChargePointVendor;
            _machine.FW_CurrentVersion = pam.machine.FW_CurrentVersion;
            _machine.Iccid = DateTime.UtcNow.ToString("yy-MM-dd HH:mm");
            _machine.Imsi = pam.machine.Imsi;
            _machine.MeterSerialNumber = pam.machine.MeterSerialNumber;
            _machine.MeterType = pam.machine.MeterType;
        }

        await db.SaveChangesAsync();
        await trans.CommitAsync();
    }

    private async Task UpdateConnectorStatusEF(string Id, ConnectorStatus Status)
    {
        using var db = await contextFactory.CreateDbContextAsync();

        ConnectorStatus status = new() { Id = Id };

        db.ChangeTracker.AutoDetectChangesEnabled = false;
        db.ConnectorStatus.Attach(status);


        status.CreatedOn = Status.CreatedOn;
        status.Status = Status.Status;
        status.ChargePointErrorCodeId = Status.ChargePointErrorCodeId;
        status.ErrorInfo = Status.ErrorInfo;
        status.VendorId = Status.VendorId;
        status.VendorErrorCode = Status.VendorErrorCode;


        db.Entry(status).Property(x => x.CreatedOn).IsModified = true;
        db.Entry(status).Property(x => x.Status).IsModified = true;
        db.Entry(status).Property(x => x.ChargePointErrorCodeId).IsModified = true;
        db.Entry(status).Property(x => x.ErrorInfo).IsModified = true;
        db.Entry(status).Property(x => x.VendorId).IsModified = true;
        db.Entry(status).Property(x => x.VendorErrorCode).IsModified = true;

        await db.SaveChangesAsync();
    }

    private async Task UpdateConnectorStatusDapper(string Id, ConnectorStatus Status)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@Id", Id, DbType.String, ParameterDirection.Input, 36);
        parameters.Add("@CreatedOn", Status.CreatedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@Status", Status.Status, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@ChargePointErrorCodeId", Status.ChargePointErrorCodeId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@ErrorInfo", Status.ErrorInfo, DbType.String, ParameterDirection.Input, 50);
        parameters.Add("@VendorId", Status.VendorId, DbType.String, ParameterDirection.Input, 255);
        parameters.Add("@VendorErrorCode", Status.VendorErrorCode, DbType.String, ParameterDirection.Input, 100);

        using var conn = await sqlConnectionFactory.CreateAsync();
        await conn.ExecuteAsync("""
            update ConnectorStatus
            set
            CreatedOn = @CreatedOn,
            Status = @Status,
            ChargePointErrorCodeId = @ChargePointErrorCodeId,
            ErrorInfo = @ErrorInfo,
            VendorId = @VendorId,
            VendorErrorCode = @VendorErrorCode
            where Id = @Id
            """, parameters);
    }

    private async Task<Guid> GetCustomerIdByChargeBoxIdEF(string chargeboxId)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        var _CustomerId = await db.Machine.Where(x => x.ChargeBoxId == chargeboxId).Select(x => x.CustomerId).FirstOrDefaultAsync();
        return _CustomerId;
    }

    private async Task<Guid> GetCustomerIdByChargeBoxIdDapper(string chargeboxId)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@ChargeBoxId", chargeboxId, DbType.String, ParameterDirection.Input, 50);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var _existedTx = await conn.QueryFirstOrDefaultAsync<Guid>("""
            select CustomerId
            from dbo.Machine
            where
            ChargeBoxId = @ChargeBoxId
            """, parameters);

        return _existedTx;
    }

    private async Task<int?> TryGetDuplicatedTransactionIdEF(string chargeBoxId, Guid customerId, int connectorId, DateTime timestamp)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        var _existedTx = await db.TransactionRecord.Where(x => x.CustomerId == customerId && x.ChargeBoxId == chargeBoxId
                               && x.ConnectorId == connectorId && x.StartTime == timestamp).Select(x => x.Id).FirstOrDefaultAsync();
        return _existedTx;
    }

    private async Task<int?> TryGetDuplicatedTransactionIdDapper(string chargeBoxId, Guid customerId, int connectorId, DateTime timestamp)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@ChargeBoxId", chargeBoxId, DbType.String, ParameterDirection.Input, 50);
        parameters.Add("@CustomerId", customerId, DbType.Guid, ParameterDirection.Input);
        parameters.Add("@ConnectorId", connectorId, DbType.Int16, ParameterDirection.Input);
        parameters.Add("@TimeStamp", timestamp, DbType.DateTime, ParameterDirection.Input);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var _existedTx = await conn.QueryFirstOrDefaultAsync<int?>("""
            SELECT Id
            FROM dbo.TransactionRecord
            WHERE
            ChargeBoxId = @ChargeBoxId and
            CustomerId = @CustomerId and
            ConnectorId = @ConnectorId and
            StartTime = @TimeStamp
            """, parameters);

        return _existedTx;
    }

    private async ValueTask AddMachineErrorEF(byte connectorId, DateTime createdOn, int status, string chargeBoxId, int errorCodeId, string errorInfo, int preStatus, string vendorErrorCode, string vendorId)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        await db.MachineError.AddAsync(new MachineError()
        {
            ConnectorId = connectorId,
            CreatedOn = createdOn,
            Status = status,
            ChargeBoxId = chargeBoxId,
            ErrorCodeId = errorCodeId,
            ErrorInfo = errorInfo,
            PreStatus = preStatus,
            VendorErrorCode = vendorErrorCode,
            VendorId = vendorId
        });

        await db.SaveChangesAsync();
    }

    private async ValueTask AddMachineErrorDapper(byte connectorId, DateTime createdOn, int status, string chargeBoxId, int errorCodeId, string errorInfo, int preStatus, string vendorErrorCode, string vendorId)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@ConnectorId", connectorId, DbType.Int16, ParameterDirection.Input);
        parameters.Add("@PreStatus", preStatus, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@Status", status, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@ErrorInfo", errorInfo, DbType.String, ParameterDirection.Input, 50);
        parameters.Add("@VendorId", vendorId, DbType.String, ParameterDirection.Input, 255);
        parameters.Add("@CreatedOn", createdOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@ErrorCodeId", errorCodeId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@VendorErrorCode", vendorErrorCode, DbType.String, ParameterDirection.Input, 100);
        parameters.Add("@ChargeBoxId", chargeBoxId, DbType.String, ParameterDirection.Input, 50);

        using var conn = await sqlConnectionFactory.CreateAsync();
        await conn.ExecuteAsync("""
            INSERT INTO MachineError
            (ConnectorId, PreStatus, Status, ErrorInfo, VendorId, CreatedOn, ErrorCodeId, VendorErrorCode, ChargeBoxId)
            VALUES (@ConnectorId, @PreStatus, @Status, @ErrorInfo, @VendorId, @CreatedOn, @ErrorCodeId, @VendorErrorCode, @ChargeBoxId)
            """, parameters);
    }

    private async Task BundleUpdateConnectorStatus(IEnumerable<StatusNotificationParam> statusNotifications)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        using var trans = await db.Database.BeginTransactionAsync();

        statusNotifications = statusNotifications.OrderBy(x => x.Status.CreatedOn).DistinctBy(x => x.Id);

        foreach (var param in statusNotifications)
        {
            ConnectorStatus status = new() { Id = param.Id };

            //db.ChangeTracker.AutoDetectChangesEnabled = false;
            db.ConnectorStatus.Attach(status);


            status.CreatedOn = param.Status.CreatedOn;
            status.Status = param.Status.Status;
            status.ChargePointErrorCodeId = param.Status.ChargePointErrorCodeId;
            status.ErrorInfo = param.Status.ErrorInfo;
            status.VendorId = param.Status.VendorId;
            status.VendorErrorCode = param.Status.VendorErrorCode;


            db.Entry(status).Property(x => x.CreatedOn).IsModified = true;
            db.Entry(status).Property(x => x.Status).IsModified = true;
            db.Entry(status).Property(x => x.ChargePointErrorCodeId).IsModified = true;
            db.Entry(status).Property(x => x.ErrorInfo).IsModified = true;
            db.Entry(status).Property(x => x.VendorId).IsModified = true;
            db.Entry(status).Property(x => x.VendorErrorCode).IsModified = true;

            //await db.SaveChangesAsync();
        }

        await db.SaveChangesAsync();
        await trans.CommitAsync();
        //db.ChangeTracker.Clear();
    }



    private Task BundleUpdateConnectorStatusDapper(IEnumerable<StatusNotificationParam> statusNotifications)
    {
        using var conn = sqlConnectionFactory.Create();

        foreach (var status in statusNotifications)
        {

            var parameters = new DynamicParameters();
            parameters.Add("@Id", status.Id, DbType.String, ParameterDirection.Input, 36);
            parameters.Add("@CreatedOn", status.Status.CreatedOn, DbType.DateTime, ParameterDirection.Input);
            parameters.Add("@Status", status.Status.Status, DbType.Int32, ParameterDirection.Input);
            parameters.Add("@ChargePointErrorCodeId", status.Status.ChargePointErrorCodeId, DbType.Int32, ParameterDirection.Input);
            parameters.Add("@ErrorInfo", status.Status.ErrorInfo, DbType.String, ParameterDirection.Input, 50);
            parameters.Add("@VendorId", status.Status.VendorId, DbType.String, ParameterDirection.Input, 255);
            parameters.Add("@VendorErrorCode", status.Status.VendorErrorCode, DbType.String, ParameterDirection.Input, 100);

            conn.Execute("""
                update ConnectorStatus
                set
                CreatedOn = @CreatedOn,
                Status = @Status,
                ChargePointErrorCodeId = @ChargePointErrorCodeId,
                ErrorInfo = @ErrorInfo,
                VendorId = @VendorId,
                VendorErrorCode = @VendorErrorCode
                where Id = @Id
                """, parameters);
        }
        return Task.CompletedTask;
    }

    private async Task BundleAddServerMessage(IEnumerable<ServerMessage> messages)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        using var trans = await db.Database.BeginTransactionAsync();

        foreach (var message in messages)
        {
            await db.ServerMessage.AddAsync(message);
        }

        await db.SaveChangesAsync();
        await trans.CommitAsync();
        //db.ChangeTracker.Clear();
    }

    private async Task AddServerMessageEF(ServerMessage message)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        using var trans = await db.Database.BeginTransactionAsync();

        await db.ServerMessage.AddAsync(message);

        await db.SaveChangesAsync();
        await trans.CommitAsync();
        //db.ChangeTracker.Clear();
    }

    private async Task AddServerMessageDapper(ServerMessage message)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@SerialNo", message.SerialNo, DbType.String, ParameterDirection.Input, 36);
        parameters.Add("@OutAction", message.OutAction, DbType.String, ParameterDirection.Input, 30);
        parameters.Add("@OutRequest", message.OutRequest, DbType.String, ParameterDirection.Input);
        parameters.Add("@InMessage", message.InMessage, DbType.String, ParameterDirection.Input);
        parameters.Add("@CreatedOn", message.CreatedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@CreatedBy", message.CreatedBy, DbType.String, ParameterDirection.Input, 36);
        parameters.Add("@ReceivedOn", message.ReceivedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@ChargeBoxId", message.ChargeBoxId, DbType.String, ParameterDirection.Input, 30);
        parameters.Add("@UpdatedOn", message.UpdatedOn, DbType.DateTime, ParameterDirection.Input);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var resultCnt = await conn.ExecuteAsync("""
            INSERT INTO ServerMessage
            (SerialNo, OutAction, OutRequest, InMessage, CreatedOn, CreatedBy, ReceivedOn, ChargeBoxId, UpdatedOn)
            VALUES (@SerialNo, @OutAction, @OutRequest, @InMessage, @CreatedOn, @CreatedBy, @ReceivedOn, @ChargeBoxId, @UpdatedOn)
            """, parameters);
        if (resultCnt != 1)
        {
            throw new Exception("Insert failed");
        }
        return;
    }

    private async Task<int> AddNewTransactionRecordEF(TransactionRecord newTransaction)
    {
        using var db = await contextFactory.CreateDbContextAsync();

        await db.TransactionRecord.AddAsync(newTransaction);

        await db.SaveChangesAsync();

        return newTransaction.Id;
    }

    private async Task<int> AddNewTransactionRecordDapper(TransactionRecord newTransaction)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@ChargeBoxId", newTransaction.ChargeBoxId, DbType.String, ParameterDirection.Input, 50);
        parameters.Add("@ConnectorId", newTransaction.ConnectorId, DbType.Int16, ParameterDirection.Input);
        parameters.Add("@CreatedOn", newTransaction.CreatedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@UpdatedOn", newTransaction.UpdatedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@StartTransactionReportedOn", newTransaction.StartTransactionReportedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@StopTransactionReportedOn", newTransaction.StopTransactionReportedOn, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@StartIdTag", newTransaction.StartIdTag, DbType.String, ParameterDirection.Input, 20);
        parameters.Add("@MeterStart", newTransaction.MeterStart, DbType.Decimal, ParameterDirection.Input, precision: 18, scale: 2);
        parameters.Add("@MeterStop", newTransaction.MeterStop, DbType.Decimal, ParameterDirection.Input, precision: 18, scale: 2);
        parameters.Add("@CustomerId", newTransaction.CustomerId, DbType.Guid, ParameterDirection.Input);
        parameters.Add("@StartTime", newTransaction.StartTime, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@StopTime", newTransaction.StopTime, DbType.DateTime, ParameterDirection.Input);
        parameters.Add("@ReservationId", newTransaction.ReservationId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@RetryStartTransactionTimes", newTransaction.RetryStartTransactionTimes, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@RetryStopTransactionTimes", newTransaction.RetryStopTransactionTimes, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@Fee", newTransaction.Fee, DbType.String, ParameterDirection.Input, 1500);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var id = await conn.QuerySingleAsync<int>("""
            INSERT INTO TransactionRecord
            (ChargeBoxId, ConnectorId, CreatedOn, UpdatedOn, StartTransactionReportedOn, StopTransactionReportedOn,
            StartIdTag, MeterStart, MeterStop, CustomerId, StartTime, StopTime, ReservationId, RetryStartTransactionTimes, RetryStopTransactionTimes, Fee)
            OUTPUT INSERTED.Id
            VALUES (@ChargeBoxId, @ConnectorId, @CreatedOn, @UpdatedOn, @StartTransactionReportedOn, @StopTransactionReportedOn,
            @StartIdTag, @MeterStart, @MeterStop, @CustomerId, @StartTime, @StopTime, @ReservationId, @RetryStartTransactionTimes, @RetryStopTransactionTimes, @Fee)
            """, parameters);
        return id;
    }

    private async Task<TransactionRecord> GetTransactionForStopTransactionEF(int transactionId, string chargeBoxId)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        return await db.TransactionRecord.Where(x => x.Id == transactionId
             && x.ChargeBoxId == chargeBoxId).FirstOrDefaultAsync();
    }

    private async Task<TransactionRecord> GetTransactionForStopTransactionDapper(int transactionId, string chargeBoxId)
    {
        var parameters = new DynamicParameters();
        parameters.Add("@TransactionId", transactionId, DbType.Int32, ParameterDirection.Input);
        parameters.Add("@ChargeBoxId", chargeBoxId, DbType.String, ParameterDirection.Input, 50);

        using var conn = await sqlConnectionFactory.CreateAsync();
        var record = await conn.QuerySingleAsync<TransactionRecord>("""
            SELECT ConnectorId, MeterStop, MeterStart, StopTime FROM TransactionRecord
            WHERE Id = @TransactionId and ChargeBoxId = @ChargeBoxId 
            """, parameters);
        return record;
    }

    private Task BulkInsertServerMessage(IEnumerable<ServerMessage> messages)
    {
        var table = new DataTable();
        table.Columns.Add("ChargeBoxId");
        table.Columns.Add("SerialNo");
        table.Columns.Add("OutAction");
        table.Columns.Add("OutRequest");
        table.Columns.Add("InMessage");
        table.Columns.Add("CreatedOn");
        table.Columns.Add("CreatedBy");
        table.Columns.Add("UpdatedOn");
        table.Columns.Add("ReceivedOn");

        foreach (var param in messages)
        {
            var row = table.NewRow();
            row["ChargeBoxId"] = param.ChargeBoxId;
            row["SerialNo"] = param.SerialNo;
            row["OutAction"] = param.OutAction;
            row["OutRequest"] = param.OutRequest;
            row["InMessage"] = param.InMessage;
            row["CreatedOn"] = param.CreatedOn;
            row["CreatedBy"] = param.CreatedBy;
            row["UpdatedOn"] = param.UpdatedOn;
            row["ReceivedOn"] = param.ReceivedOn;

            table.Rows.Add(row);
        }

        using SqlConnection sqlConnection = sqlConnectionFactory.Create();
        using SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(sqlConnection);

        sqlBulkCopy.BatchSize = messages.Count();
        sqlBulkCopy.DestinationTableName = "ServerMessage";

        sqlBulkCopy.ColumnMappings.Add("ChargeBoxId", "ChargeBoxId");
        sqlBulkCopy.ColumnMappings.Add("SerialNo", "SerialNo");
        sqlBulkCopy.ColumnMappings.Add("OutAction", "OutAction");
        sqlBulkCopy.ColumnMappings.Add("OutRequest", "OutRequest");
        sqlBulkCopy.ColumnMappings.Add("InMessage", "InMessage");
        sqlBulkCopy.ColumnMappings.Add("CreatedOn", "CreatedOn");
        sqlBulkCopy.ColumnMappings.Add("CreatedBy", "CreatedBy");
        sqlBulkCopy.ColumnMappings.Add("UpdatedOn", "UpdatedOn");
        sqlBulkCopy.ColumnMappings.Add("ReceivedOn", "ReceivedOn");

        return sqlBulkCopy.WriteToServerAsync(table);
    }

    private int GetStartupLimit(IConfiguration configuration)
    {
        var limitConfig = configuration["MainDbStartupLimit"];
        int limit = 5;
        if (limitConfig != default)
        {
            int.TryParse(limitConfig, out limit);
        }
        return limit;
    }

    private int GetOpLimit(IConfiguration configuration)
    {
        var limitConfig = configuration["MainDbOpLimit"];
        int limit = 500;
        if (limitConfig != default)
        {
            int.TryParse(limitConfig, out limit);
        }
        return limit;
    }

    private async Task<bool> UpdateHeartBeatsDapper(IEnumerable<Machine> heartBeatsData)
    {
        using var conn = await sqlConnectionFactory.CreateAsync();
        using var trans = await conn.BeginTransactionAsync();

        try
        {
            foreach (var data in heartBeatsData)
            {
                var parameters = new DynamicParameters();
                parameters.Add("@Id", data.Id, DbType.String, ParameterDirection.Input, 36);
                parameters.Add("@HeartbeatUpdatedOn", data.HeartbeatUpdatedOn, DbType.DateTime, ParameterDirection.Input);
                parameters.Add("@ConnectionType", data.ConnectionType, DbType.Int32, ParameterDirection.Input);

                var resultCnt = await conn.ExecuteAsync("""
                    UPDATE Machine
                    SET HeartbeatUpdatedOn = @HeartbeatUpdatedOn, ConnectionType = @ConnectionType
                    WHERE Id = @Id
                    """, parameters, trans);
                if (resultCnt != 1)
                {
                    throw new Exception("Update over one columes");
                }
            }
            await trans.CommitAsync();
        }
        catch
        {
            logger.LogCritical("HeartBeatCheckTrigger update fail, roll back");
            await trans.RollbackAsync();
            return false;
        }

        return true;
    }

    private async Task<bool> UpdateHeartBeatsEF(IEnumerable<Machine> heartBeatsData)
    {
        using var db = await contextFactory.CreateDbContextAsync();
        using var transaction = await db.Database.BeginTransactionAsync();

        try
        {
            foreach (var data in heartBeatsData)
            {
                var machine = new Machine() { Id = data.Id };
                if (machine != null)
                {
                    db.Machine.Attach(machine);
                    machine.HeartbeatUpdatedOn = DateTime.UtcNow;
                    machine.ConnectionType = data.ConnectionType;
                    db.Entry(machine).Property(x => x.HeartbeatUpdatedOn).IsModified = true;
                    db.Entry(machine).Property(x => x.ConnectionType).IsModified = true;
                }
            }

            await db.SaveChangesAsync();
            await transaction.CommitAsync();
            db.ChangeTracker.Clear();
        }
        catch (Exception ex)
        {
            logger.LogCritical(ex, "HeartBeatCheckTrigger update fail, roll back");
            transaction.Rollback();
            return false;
        }

        return true;
    }
}

public record MachineAndCustomerInfo (string MachineId, Guid CustomerId, string CustomerName);
public record StatusNotificationParam(string Id, ConnectorStatus Status);
public record UpdateMachineBasicInfoParam(string ChargeBoxId, Machine machine);