using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Edge.Core.Database;
using Edge.Core.Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Applications.FDC
{
    public class FdcResourceArbitrator : IFdcResourceArbitrator
    {
        public static ILogger fdcLogger { get; set; }// = ServiceBuilder.Provider.GetRequiredService<ILoggerFactory>().CreateLogger("FdcServer");// NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("FdcServer");
        private static FdcResourceArbitrator instance = new FdcResourceArbitrator();

        private object syncObject = new object();

        public static FdcResourceArbitrator Default => instance;

        //private SqliteDbContext dbContext;
        private FdcResourceArbitrator()
        {
        }

        private int __TryReserveFuelPointAsyncGuard = 0;
        public async Task<bool> TryReserveFuelPointAsync(int posId, int fuelPointId)
        {
            if (0 != Interlocked.CompareExchange(ref this.__TryReserveFuelPointAsyncGuard, 1, 0))
                return false;
            try
            {
                var dbContext = new SqliteDbContext();
                var existed = dbContext.FuelPointReservationModels.Where(f => f.FuelPointId == fuelPointId);
                if (!existed.Any())
                {
                    dbContext.FuelPointReservationModels.Add(new FuelPointReservation()
                    {
                        FuelPointId = fuelPointId,
                        ReservingTime = DateTime.Now,
                        ReservedByFdcClientId = posId
                    });
                    await dbContext.SaveChangesAsync();
                    fdcLogger.LogDebug("    created reservation");
                    return true;
                }

                // add a protection for deadlock by someone, if locked by lasted over 5m, auto unlocked it.
                int maxHoldTime = 5;
                if (existed.Any())
                {
                    var lockDue = DateTime.Now.Subtract(existed.First().ReservingTime);
                    if (lockDue >= new TimeSpan(0, maxHoldTime, 0))
                    {
                        fdcLogger.LogInformation("    FuelPoint with id: " + fuelPointId + " have been hold by "
                            + existed.First().ReservedByFdcClientId
                            + " for " + lockDue.TotalMinutes + " minutes (since: "
                            + existed.First().ReservingTime.ToString("yyyy-MM-dd HH:mm:ss") + "), will auto unlock it for avoid long lock");
                        dbContext.FuelPointReservationModels.Remove(existed.First());
                        await dbContext.SaveChangesAsync();
                        existed = dbContext.FuelPointReservationModels.Where(f => f.FuelPointId == fuelPointId);
                    }
                }

                if (!existed.Any())
                {
                    dbContext.FuelPointReservationModels.Add(new FuelPointReservation()
                    {
                        FuelPointId = fuelPointId,
                        ReservingTime = DateTime.Now,
                        ReservedByFdcClientId = posId
                    });
                    await dbContext.SaveChangesAsync();
                    fdcLogger.LogDebug("    Created a FuelPoint Reservation for FdcClientId: " + posId);
                    return true;
                }
                else
                {
                    if (existed.First().ReservedByFdcClientId != posId)
                    {
                        fdcLogger.LogDebug("    ReserveFuelPoint failed due to Fp already locked by FdcClientId: " + existed.First().ReservedByFdcClientId);
                        return false;
                    }
                    else
                    {
                        // refresh the time
                        existed.First().ReservingTime = DateTime.Now;
                        await dbContext.SaveChangesAsync();
                        fdcLogger.LogDebug("    Updated FuelPoint ReservingTime since previous already reserved by caller FdcClientId: " + posId);
                        return true;
                    }
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("TryReserveFuelPoint exceptioned: " + exxx.ToString());
                return false;
            }
            finally
            {
                this.__TryReserveFuelPointAsyncGuard = 0;
            }
        }

        private int __TryUnreserveFuelPointAsyncGuard = 0;
        public async Task<bool> TryUnreserveFuelPointAsync(int posId, int fuelPointId)
        {
            if (0 != Interlocked.CompareExchange(ref this.__TryUnreserveFuelPointAsyncGuard, 1, 0))
                return false;
            try
            {
                var dbContext = new SqliteDbContext();
                var reservedBySelf = dbContext.FuelPointReservationModels.Where(f => f.FuelPointId == fuelPointId
                    && f.ReservedByFdcClientId == posId);
                if (!reservedBySelf.Any())
                {
                    fdcLogger.LogError("    failed on unreserve target pump with pump id:" + fuelPointId
                        + " since it didn't get reserved or others already reserved it");
                    return false;
                }
                else
                {
                    dbContext.FuelPointReservationModels.Remove(reservedBySelf.First());
                    await dbContext.SaveChangesAsync();
                    return true;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("TryUnreserveFuelPoint exceptioned: " + exxx.ToString());
                return false;
            }
            finally
            {
                this.__TryUnreserveFuelPointAsyncGuard = 0;
            }
        }

        private int __TryLockFuelSaleTrxAsyncGuard = 0;
        public async Task<FuelSaleTransaction> TryLockFuelSaleTrxAsync(int posId, int fuelPointId, int transactionNo, int trxDbUniqueId)
        {
            if (0 != Interlocked.CompareExchange(ref this.__TryLockFuelSaleTrxAsyncGuard, 1, 0))
                return null;
            try
            {
                var dbContext = new SqliteDbContext();
                var targetTrx = dbContext.PumpTransactionModels.Where(f =>
                    f.ReleaseToken == trxDbUniqueId
                    && f.PumpId == fuelPointId
                    && f.TransactionSeqNumberFromPhysicalPump == transactionNo.ToString()
                    && f.State == FuelSaleTransactionState.Payable).OrderByDescending(t => t.SaleStartTime).FirstOrDefault();
                byte maxRetryTimes = 3;
                byte retriedTimes = 0;
            retry:
                if (targetTrx != null)
                {
                    if (string.IsNullOrEmpty(targetTrx.LockedByFdcClientId))
                    {
                        targetTrx.LockedTime = DateTime.Now;
                        targetTrx.LockedByFdcClientId = posId.ToString();
                        targetTrx.State = FuelSaleTransactionState.Locked;
                        try
                        {
                            await dbContext.SaveChangesAsync();
                        }
                        catch (DbUpdateConcurrencyException ex)
                        {
                            fdcLogger.LogInformation("    lock FuelSaleTrx failed due to DbUpdateConcurrencyException, targetTrx in app memory is: "
                                + targetTrx.ToString());
                            try
                            {
                                // Update the values of the entity that failed to save from the store 
                                ex.Entries.Single().Reload();
                                fdcLogger.LogInformation("    while the actual in db is: "
                                    + targetTrx.ToString()
                                    + System.Environment.NewLine + "will retry saving...");
                                retriedTimes++;
                                if (retriedTimes > maxRetryTimes)
                                { fdcLogger.LogInformation("    DbUpdateConcurrency retried time reached its max, will quit."); return null; }
                                goto retry;
                            }
                            catch (Exception exxx)
                            {
                                fdcLogger.LogInformation("    exceptioned again for reload targetTrx from db: " + exxx);
                                return null;
                            }
                        }

                        return targetTrx;
                    }
                    else if (targetTrx.LockedByFdcClientId == posId.ToString())
                    {
                        fdcLogger.LogDebug("    lock FuelSaleTrx succeed since POS: " + posId + " already own it");
                        return targetTrx;
                    }
                    else
                    {
                        fdcLogger.LogInformation("    lock FuelSaleTrx failed due to it already locked by other POS: "
                            + targetTrx.LockedByFdcClientId + " at "
                            + (targetTrx.LockedTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""));
                        return null;
                    }
                }
                else
                {
                    fdcLogger.LogInformation("    lock FuelSaleTrx failed since it not existed or state is not Payable");
                    return null;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("TryLockFuelSaleTrx exceptioned: " + exxx.ToString());
                return null;
            }
            finally { this.__TryLockFuelSaleTrxAsyncGuard = 0; }
        }

        private int __TryUnlockFuelSaleTrxAsyncGuard = 0;
        /// <summary>
        /// Try set the SaleTrx state to Payable.
        /// </summary>
        /// <param name="posId"></param>
        /// <param name="fuelPointId"></param>
        /// <param name="transactionNo"></param>
        /// <param name="trxDbUniqueId"></param>
        /// <returns></returns>
        public async Task<FuelSaleTransaction> TryUnlockFuelSaleTrxAsync(int posId, int fuelPointId, int transactionNo, int trxDbUniqueId)
        {
            if (0 != Interlocked.CompareExchange(ref this.__TryUnlockFuelSaleTrxAsyncGuard, 1, 0))
                return null;
            try
            {
                var dbContext = new SqliteDbContext();
                var targetTrx = dbContext.PumpTransactionModels.Where(f =>
                    f.ReleaseToken == trxDbUniqueId
                    && f.PumpId == fuelPointId
                    && f.TransactionSeqNumberFromPhysicalPump == transactionNo.ToString()
                    && f.State == FuelSaleTransactionState.Locked
                    && f.LockedByFdcClientId == posId.ToString()).FirstOrDefault();
                if (targetTrx != null)
                {
                    fdcLogger.LogDebug("    unlock FuelSaleTrx succeed");
                    targetTrx.LockedByFdcClientId = "";
                    targetTrx.LockedTime = DateTime.MinValue;
                    targetTrx.State = FuelSaleTransactionState.Payable;
                    await dbContext.SaveChangesAsync();
                    return targetTrx;
                }
                else
                {
                    fdcLogger.LogInformation("    unlock FuelSaleTrx failed");
                    return null;
                }
            }
            catch (Exception exxx)
            {
                fdcLogger.LogError("TryUnlockFuelSaleTrx exceptioned: " + exxx.ToString());
                return null;
            }
            finally { this.__TryUnlockFuelSaleTrxAsyncGuard = 0; }
        }

        //private int __TryLockNozzleAsyncGuard = 0;
        ///// <summary>
        ///// 
        ///// </summary>
        ///// <param name="posId"></param>
        ///// <param name="fuelPointId"></param>
        ///// <param name="logicalNozzleId"></param>
        ///// <returns></returns>
        //public async Task<bool> TryLockNozzleAsync(int posId, int fuelPointId, int logicalNozzleId)
        //{
        //    if (0 != Interlocked.CompareExchange(ref this.__TryLockNozzleAsyncGuard, 1, 0))
        //        return false;
        //    try
        //    {
        //        var dbContext = new SqliteDbContext();
        //        var existsLockingNozzleRecord = dbContext.NozzleLocks.Where(f =>
        //            f.FuelPointId == fuelPointId
        //            && f.LogicalNozzleId == logicalNozzleId).FirstOrDefault();
        //        if (existsLockingNozzleRecord != null)
        //        {
        //            if (existsLockingNozzleRecord.LockedByFdcClientId != null
        //                && existsLockingNozzleRecord.LockedByFdcClientId == posId)
        //            {
        //                existsLockingNozzleRecord.LockingTime = DateTime.Now;
        //                try
        //                {
        //                    await dbContext.SaveChangesAsync();
        //                    return true;
        //                }
        //                catch (Exception ex)
        //                {
        //                    fdcLogger.LogInformation("    exceptioned for updating locking time for lock nozzle from db: " + ex);
        //                    return false;
        //                }
        //            }
        //            else if (existsLockingNozzleRecord.LockedByFdcClientId != null
        //                && existsLockingNozzleRecord.LockedByFdcClientId != posId)
        //            {
        //                fdcLogger.LogInformation("    lock nozzle failed due to already locked by POS: " + existsLockingNozzleRecord.LockedByFdcClientId);
        //                return false;
        //            }
        //        }

        //        try
        //        {
        //            var newNozzleLocking = new NozzleLock()
        //            {
        //                LockingTime = DateTime.Now,
        //                LockedByFdcClientId = posId,
        //                FuelPointId = fuelPointId,
        //                LogicalNozzleId = logicalNozzleId
        //            };
        //            dbContext.NozzleLocks.Add(newNozzleLocking);
        //            await dbContext.SaveChangesAsync();
        //            return true;
        //        }
        //        catch (Exception ex)
        //        {

        //            fdcLogger.LogInformation("    exceptioned for create a lock nozzle from db: " + ex);
        //            return false;
        //        }
        //    }
        //    catch { return false; }
        //    finally { this.__TryLockNozzleAsyncGuard = 0; }
        //}

        //private int __TryUnlockNozzleAsyncGuard = 0;
        ///// <summary>
        ///// 
        ///// </summary>
        ///// <param name="posId"></param>
        ///// <param name="fuelPointId"></param>
        ///// <param name="logicalNozzleId"></param>
        ///// <returns></returns>
        //public async Task<NozzleLock> TryUnlockNozzleAsync(int posId, int fuelPointId, int logicalNozzleId)
        //{
        //    if (0 != Interlocked.CompareExchange(ref this.__TryUnlockNozzleAsyncGuard, 1, 0))
        //        return null;
        //    try
        //    {
        //        var dbContext = new SqliteDbContext();
        //        var targetTrx = dbContext.NozzleLocks.Where(f =>
        //            f.FuelPointId == fuelPointId
        //            && f.LogicalNozzleId == logicalNozzleId
        //            && f.LockedByFdcClientId == posId).FirstOrDefault();
        //        if (targetTrx != null)
        //        {
        //            fdcLogger.LogDebug("    unlock NozzleAsync succeed");
        //            targetTrx.LockedByFdcClientId = null;
        //            targetTrx.LockingTime = null;
        //            await dbContext.SaveChangesAsync();
        //            return targetTrx;
        //        }
        //        else
        //        {
        //            fdcLogger.LogInformation("    unlock NozzleAsync failed");
        //            return null;
        //        }
        //    }
        //    catch (Exception exxx)
        //    {
        //        fdcLogger.LogError("TryUnlockNozzleAsync exceptioned: " + exxx.ToString());
        //        return null;
        //    }
        //    finally { this.__TryUnlockNozzleAsyncGuard = 0; }
        //}
    }
}