using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

using C4IT.FASD.Base;
using C4IT.Logging;
using C4IT.Security;

using Newtonsoft.Json.Linq;

using static C4IT.DataHistoryProvider.cDataHistoryCollectorTokenCache;
using static C4IT.Logging.cLogManager;

namespace C4IT.DataHistoryProvider
{
    public class cDataHistoryCollectorTokenCache
    {
        private const int timerCleanupInterval = 15 * 60 * 1000;

        private System.Threading.Timer cleanupTimer;

        private cDataHistoryCollector Collector;

        private Dictionary<Guid, Dictionary<cF4SDTokenRegistration.enumTokenType, cTokenInfo>> dicToken = null;

        public class cTokenInfo
        {
            public Guid id;
            public cF4SDTokenRegistration.enumTokenType type;
            public string name;
            public string name2;
            public string secret;
            public DateTime validUntil;
            public DateTime renewUntil;
            public string IdRemote;

            public bool IsValid()
            {
                if (id == Guid.Empty)
                    return false;
                if (validUntil < DateTime.UtcNow + TimeSpan.FromMinutes(cDataHistoryCollector.constMinutesUntilTokenIsInvalid))
                    return false;

                return true;
            }

            public cTokenInfo GetNewInstance()
            {
                var _sec = secret;
                if (HasTokenTypeLongLifetime(type))
                    _sec = cSecurePassword.Instance.Decode(secret);
                var _ret = new cTokenInfo()
                {
                    id = id,
                    type = type,
                    name = name,
                    name2 = name2,
                    secret = _sec,
                    validUntil = validUntil,
                    renewUntil = renewUntil,
                    IdRemote = IdRemote
                };

                return _ret;
            }
        }

        private static class TokenCacheCriticalSection
        {
            private static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

            public static async Task EnterAsync()
            {
                await Semaphore.WaitAsync();
            }

            public static void Leave()
            {
                Semaphore.Release();
            }
        }

        private static bool HasTokenTypeLongLifetime(cF4SDTokenRegistration.enumTokenType tokenType)
        {
            switch (tokenType)
            {
                case cF4SDTokenRegistration.enumTokenType.M42Api:
                    return true;
                default:
                    return false;
            }
        }

        public cDataHistoryCollectorTokenCache(cDataHistoryCollector Collector, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            this.Collector = Collector;
            cleanupTimer = new System.Threading.Timer(async (x) => await DoCleanup(requestInfo, 0), null, timerCleanupInterval, timerCleanupInterval);
        }

        private bool privInsert(cTokenInfo info, bool overwite)
        {
            if (dicToken == null)
                return false;

            if (!dicToken.TryGetValue(info.id, out var _entyId))
            {
                _entyId = new Dictionary<cF4SDTokenRegistration.enumTokenType, cTokenInfo>();
                dicToken.Add(info.id, _entyId);
            }

            if (!overwite && _entyId.ContainsKey(info.type))
                return false;

            _entyId[info.type] = info;

            return true;
        }

        public async Task LoadAsync(cDbConnection DbConn, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                if (dicToken == null)
                    dicToken = new Dictionary<Guid, Dictionary<cF4SDTokenRegistration.enumTokenType, cTokenInfo>>();

                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("GetAllExternalTokens", out var Query))
                    return;

                var sqlerror = 0;
                var sqlStartTime = DateTime.UtcNow;

                await TokenCacheCriticalSection.EnterAsync();
                try
                {
                    dicToken.Clear();

                    var _toDelete = new List<cTokenInfo>();
                    var _toUpdate = new List<cTokenInfo>();

                    using (var _reader = await DataHistorySqlHelper.GetDataReaderAsync(DbConn, Query, null, Token))
                    {
                        if (_reader != null)
                            try
                            {
                                if (_reader.HasRows)
                                {
                                    while (await _reader.ReadAsync())
                                    {
                                        try
                                        {
                                            var _entry = new cTokenInfo();
                                            _entry.id = _reader.GetGuid(0);
                                            _entry.type = (cF4SDTokenRegistration.enumTokenType)_reader.GetInt32(1);
                                            _entry.name = _reader.IsDBNull(2) ? null : _reader.GetString(2);
                                            _entry.name2 = _reader.IsDBNull(3) ? null : _reader.GetString(3);
                                            var _secret = _reader.IsDBNull(4) ? null : _reader.GetString(4);
                                            if (HasTokenTypeLongLifetime(_entry.type))
                                                _entry.secret = _secret;
                                            else
                                                _entry.secret = cSecurePassword.Instance.Decode(_secret);
                                            _entry.validUntil = _reader.IsDBNull(5) ? DateTime.MinValue : _reader.GetDateTime(5);
                                            _entry.renewUntil = _reader.IsDBNull(6) ? DateTime.MinValue : _reader.GetDateTime(6);
                                            _entry.IdRemote = _reader.IsDBNull(7) ? null : _reader.GetString(7);

                                            int valid = -1;
                                            if (_entry.IsValid())
                                                valid = await Collector.ValidateTokenInfo(_entry);

                                            if (valid < 0)
                                                _toDelete.Add(_entry);
                                            else
                                            {
                                                if (!privInsert(_entry, false))
                                                    _toDelete.Add(_entry);

                                                if (valid > 0)
                                                    _toUpdate.Add(_entry);
                                            }
                                        }
                                        catch (Exception E)
                                        {
                                            cLogManager.DefaultLogger.LogException(E);
                                        }

                                        if (Token.IsCancellationRequested)
                                            return;
                                    }
                                }
                            }
                            catch (Exception E)
                            {
                                cLogManager.DefaultLogger.LogException(E);
                            }
                    }

                    if (_toUpdate.Count > 0)
                    {
                        foreach (var entry in _toUpdate)
                        {
                            var _secEnc = entry.secret;
                            if (!HasTokenTypeLongLifetime(entry.type))
                                _secEnc = cSecurePassword.Instance.Encode(entry.secret);
                            await SaveToDbAsync(entry, _secEnc, requestInfo, LogDeep + 1, Token);
                        }
                    }
                }
                catch (Exception E)
                {
                    sqlerror = E.HResult;
                    cLogManager.DefaultLogger.LogException(E);
                }
                finally
                {
                    TokenCacheCriticalSection.Leave();
                    if (DataHistorySqlHelper.LogSql) DataHistorySqlHelper.SaveSqlTimingEntry(Query.Name, sqlStartTime, "", sqlerror);
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
        }

        private async Task SaveToDbAsync(cTokenInfo _tokenInfo, string secEnc, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }

            var sqlerror = 0;
            var sqlStartTime = DateTime.UtcNow;

            if (!DataHistorySqlHelper.GetWellKnownSqlStatement("InsertOrUpdateExternalToken", out var Query))
                return;

            try
            {

                var Params = new Dictionary<string, object>()
                        {
                            { "Id", _tokenInfo.id },
                            { "Type", (int)_tokenInfo.type },
                            { "Name", _tokenInfo.name },
                            { "Name2", _tokenInfo.name2 },
                            { "Secret", secEnc },
                            { "ValidUntil", _tokenInfo.validUntil },
                            { "RenewUntil", _tokenInfo.renewUntil },
                            { "IdRemote", _tokenInfo.IdRemote },
                        };

                using (var Conn = new cDbConnection(Collector.mainDbConnection))
                {
                    if (!Conn.IsOpen)
                    {
                        LogEntry($"Could not open main database '{Collector.mainDbConnection.Database}', aborting  query'", LogLevels.Error);
                        return;
                    }

                    await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, Token);
                }
            }
            catch (Exception E)
            {
                sqlerror = E.HResult;
                LogException(E);
            }
            finally
            {
                if (CM != null) LogMethodEnd(CM);
            }
        }


        public async Task SetAsync(cF4SDTokenRegistration _tokenRegistration, cF4SDTokenValidityPeriod validityPeriod, CancellationToken Token, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                // encryption of the secret for non-temporary tokens
                string _sec = _tokenRegistration.Secret;
                string _secEnc = cSecurePassword.Instance.Encode(_tokenRegistration.Secret);
                if (HasTokenTypeLongLifetime(_tokenRegistration.TokenType))
                    _sec = _secEnc;

                var _tokenInfo = new cTokenInfo()
                {
                    id = _tokenRegistration.UserId,
                    type = _tokenRegistration.TokenType,
                    name = _tokenRegistration.Name,
                    name2 = _tokenRegistration.Name2,
                    secret = _sec,
                    validUntil = validityPeriod.validUntil,
                    renewUntil = validityPeriod.renewUntil
                };

                await TokenCacheCriticalSection.EnterAsync();


                try
                {
                    await SaveToDbAsync(_tokenInfo, _secEnc, requestInfo, LogDeep + 1, Token);
                    privInsert(_tokenInfo, true);
                }
                catch (Exception E)
                {
                    LogException(E);
                }
                finally
                {
                    TokenCacheCriticalSection.Leave();
                }

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }
        }

        public async Task<cTokenInfo> GetAsync(Guid UserId, cF4SDTokenRegistration.enumTokenType TokenType, cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (dicToken == null)
                return null;

            var doCleanup = false;
            try
            {
                await TokenCacheCriticalSection.EnterAsync();
                if (dicToken.TryGetValue(UserId, out var _entry))
                    if (_entry.TryGetValue(TokenType, out var _tokenInfo))
                    {
                        if (_tokenInfo.validUntil < DateTime.UtcNow)
                            return null;

                        return _tokenInfo.GetNewInstance();
                    }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                TokenCacheCriticalSection.Leave();
                if (doCleanup)
                    await DoCleanup(requestInfo, LogDeep + 1);

                if (CM != null) LogMethodEnd(CM);
            }

            return null;
        }

        public async Task DoCleanup(cF4sdWebRequestInfo requestInfo, int LogDeep)
        {
            if (dicToken == null)
                return;

            MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
            if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); }
            var _startTime = DateTime.UtcNow;

            try
            {
                var _current = DateTime.UtcNow;
                // remove all expiered tokens in the cache
                await TokenCacheCriticalSection.EnterAsync();
                cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
                var _toRemove1 = new List<Guid>();
                foreach (var _entry in dicToken)
                {
                    var _toRemove2 = new List<cF4SDTokenRegistration.enumTokenType>();
                    foreach (var _tokenInfoPair in _entry.Value)
                    {
                        if (_tokenInfoPair.Value.validUntil < _current)
                            _toRemove2.Add(_tokenInfoPair.Key);
                    }

                    foreach (var token in _toRemove2)
                        _entry.Value.Remove(token);

                    if (_entry.Value.Count == 0)
                        _toRemove1.Add(_entry.Key);
                }
                foreach (var _entry in _toRemove1)
                    dicToken.Remove(_entry);

                // synchronize the sql table with the cache
                if (!DataHistorySqlHelper.GetWellKnownSqlStatement("RemoveExpieredExternalTokens", out var Query))
                    return;

                // remove the expired tokens
                var Params = new Dictionary<string, object>()
                {
                    { "ValidUntil", _current }
                };

                var sqlStartTime = DateTime.UtcNow;

                try
                {

                    using (var Conn = new cDbConnection(Collector.mainDbConnection))
                    {
                        if (!Conn.IsOpen)
                        {
                            LogEntry($"Could not open main database '{Collector.mainDbConnection.Database}', aborting  query'", LogLevels.Error);
                            return;
                        }


                        var _n = await DataHistorySqlHelper.ExecuteAsync(Conn, Query, Params, requestInfo, CancellationToken.None);
                    }
                }
                catch (Exception E)
                {
                    LogException(E);
                }

            }
            catch (Exception E)
            {
                LogException(E);
            }
            finally
            {
                TokenCacheCriticalSection.Leave();
                cleanupTimer.Change(timerCleanupInterval, timerCleanupInterval);
                if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }
                if (CM != null) LogMethodEnd(CM);
            }


        }
    }
}
