using System.Reflection;
using InABox.Clients;
using InABox.Core;
using InABox.Database;
using InABox.IPC;

namespace InABox.API
{
    public class RestService
    {
        public static bool CheckPasswordExpiration { get; set; } = true;

        public static bool IsHTTPS { get; set; } = false;

        protected static void GarbageCollection()
        {
            //DateTime now = DateTime.Now;
            //GC.Collect();
            //GC.WaitForPendingFinalizers();
            //GC.Collect();
            //Logger.Send(LogType.Information, "", String.Format("Garbage Collection takes {0:F2}ms", (DateTime.Now - now).TotalMilliseconds));
        }

        protected static Guid ValidateRequest(Request request, out string? userID)
        {
            var session = request.Credentials.Session;
            if (session != Guid.Empty)
            {
                var valid = CredentialsCache.Validate(session, out userID);
                if (valid != Guid.Empty)
                {
                    CredentialsCache.RefreshSessionExpiry(session);
                }
                return valid;
            }
            else if (request.Credentials.UserID is not null
                && request.Credentials.Password is not null
                && CredentialsCache.IsBypassed(request.Credentials.UserID, request.Credentials.Password))
            {
                userID = "";
                return CoreUtils.FullGuid;
            }
            userID = null;
            return Guid.Empty;
        }

        public static MultiQueryResponse QueryMultiple(MultiQueryRequest request, bool isSecure, Logger logger)
        {
            var start = DateTime.Now;

            var response = new MultiQueryResponse();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response;

            logger.Send(LogType.Information, userid, string.Format("[{0} {1}] QueryMultiple({2})",
                request.Credentials.Platform,
                request.Credentials.Version,
                request.Queries.Count));

            try
            {
                var queries = new Dictionary<string, IQueryDef>();
                foreach (var (key, query) in request.Queries)
                {
                    var type = CoreUtils.GetEntity(query.Type);
                    if (type.IsAssignableTo(typeof(ISecure)) && !isSecure)
                    {
                        logger.Send(LogType.Error, userid, $"{query.Type} is a secure entity. Request failed");
                    }
                    else
                    {
                        queries.Add(key, new QueryDef(type, query.Filter, query.Columns, query.Sort, null));
                    }
                }

                response.Tables = DbFactory.QueryMultiple(queries, userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
                response.Status = StatusCode.OK;

                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] QueryMultiple Complete", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] QueryMultiple Failed: {3} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, err.Message));
            }

            GarbageCollection();
            return response;
        }

        public static ValidateResponse Validate(ValidateRequest request, Logger logger)
        {
            var response = new ValidateResponse();

            User? user = null;
            bool reLogin = false;
            if (request.Credentials.Session != Guid.Empty)
            {
                var session = request.Credentials.Session;
                user = CredentialsCache.Validate(session);
                if (user != null)
                {
                    logger.Send(LogType.Information, "", $"{session} re-logged in!");
                    CredentialsCache.RefreshSessionExpiry(session);
                    reLogin = true;
                }
            }
            if (user == null)
            {
                if (request.UsePIN)
                {
                    logger.Send(LogType.Information, "", $"Login request with PIN {request.PIN}");
                    user = CredentialsCache.ValidateUser(request.PIN);
                }
                else
                {
                    var userID = request.UserID ?? request.Credentials.UserID;
                    var password = request.Password ?? request.Credentials.Password;
                    if (userID == null || password == null)
                        return response.Status(StatusCode.Error);
                    user = CredentialsCache.ValidateUser(userID, password);
                    if (user?.ID != CoreUtils.FullGuid)
                    {
                        logger.Send(LogType.Information, userID, $"Login request for {userID}");
                    }
                }
            }

            response.Status = StatusCode.OK;

            if (user == null)
            {
                logger.Send(LogType.Information, "", $"Login failed!");
                response.ValidationStatus = ValidationStatus.INVALID;
            }
            else if (CheckPasswordExpiration && !request.UsePIN && user.PasswordExpiration > DateTime.MinValue && user.PasswordExpiration < DateTime.Now)
            {
                logger.Send(LogType.Information, user.UserID, $"Password for ({user.UserID}) has expired!");

                response.ValidationStatus = ValidationStatus.PASSWORD_EXPIRED;
            }
            else if (reLogin)
            {
                logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");

                response.ValidationStatus = ValidationStatus.VALID;
                response.UserGuid = user.ID;
                response.UserID = user.UserID;
                response.SecurityID = user.SecurityGroup.ID;
                response.Session = request.Credentials.Session;
                response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
            }
            else if (user.ID == CoreUtils.FullGuid || !user.Use2FA)
            {
                logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!");

                response.ValidationStatus = ValidationStatus.VALID;
                response.UserGuid = user.ID;
                response.UserID = user.UserID;
                response.SecurityID = user.SecurityGroup.ID;
                response.Session = user.ID == CoreUtils.FullGuid ?
                    CredentialsCache.NewSession(user, true, DateTime.MaxValue) :
                    CredentialsCache.NewSession(user, true);
                response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
            }
            else
            {
                logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) requires 2FA. Sending code...");

                var session = CredentialsCache.SendCode(user.ID, out var recipient);
                if (session != null)
                {
                    response.ValidationStatus = ValidationStatus.REQUIRE_2FA;

                    response.UserGuid = user.ID;
                    response.UserID = user.UserID;
                    response.SecurityID = user.SecurityGroup.ID;
                    response.Session = session ?? Guid.Empty;
                    response.Recipient2FA = recipient;
                    response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue;
                }
                else
                {
                    response.Status = StatusCode.Error;
                    response.Messages.Add("Code failed to send");
                }
            }
            return response;
        }

        public static Check2FAResponse Check2FA(Check2FARequest request, Logger logger)
        {
            var response = new Check2FAResponse();

            logger.Send(LogType.Information, "", $"2FA check for session {request.Credentials.Session} and code {request.Code}");

            response.Valid = CredentialsCache.ValidateCode(request.Credentials.Session, request.Code);
            response.Status = StatusCode.OK;
            return response;
        }

        public static InfoResponse Info(InfoRequest request, Logger logger)
        {
            var response = new InfoResponse(new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), IsHTTPS, DbFactory.RestPort, DbFactory.RPCPort, DbFactory.ID));
            response.Status = StatusCode.OK;
            return response;
        }
    }

    public class RestService<TEntity> : RestService where TEntity : Entity, new()
    {
        private static string SimpleName(Type t)
        {
            return t.Name.Split('.').Last();
        }

        public static QueryResponse<TEntity> List(QueryRequest<TEntity> request, Logger logger)
        {
            var start = DateTime.Now;

            var response = new QueryResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] Query{2}: Filter=[{3}] Sort=[{4}]", request.Credentials.Platform, request.Credentials.Version,
                    SimpleName(typeof(TEntity)), request.Filter != null ? request.Filter.ToString() : "",
                    request.Sort != null ? request.Sort.AsOData() : ""));

            try
            {
                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
                response.Items = store.Query(request.Filter, request.Columns, request.Sort, request.Range);
                response.Status = StatusCode.OK;
                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] Query{3} Complete: {4} records / {5} columns returned", request.Credentials.Platform,
                        request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)),
                        response.Items.Rows.Count, response.Items.Columns.Count));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] Query{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
            }

            GarbageCollection();
            return response;
        }

        /*public static LoadResponse<TEntity> Load(LoadRequest<TEntity> request)
        {
            var start = DateTime.Now;

            var response = new LoadResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            Logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] Load{2}: Filter=[{3}]", request.Credentials.Platform, request.Credentials.Version,
                    SimpleName(typeof(TEntity)),
                    request.Filter != null ? request.Filter.AsOData() : ""));

            try
            {
                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
                response.Items = store.Load(request.Filter, request.Sort);
                response.Status = StatusCode.OK;
                Logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] Load{3} Complete: {4} records returned", request.Credentials.Platform,
                        request.Credentials.Version,
                        (uint)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Count()));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                Logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] Load{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
            }

            GarbageCollection();
            return response;
        }*/

        public static SaveResponse<TEntity> Save(SaveRequest<TEntity> request, Logger logger)
        {
            var start = DateTime.Now;

            var response = new SaveResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] Save{2}: Data=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)),
                    request.Item != null ? request.Item.ToString() : request + " (null)"));
            try
            {
                var e = request.Item;

                var preSaveChanges = BaseObjectExtensions.GetOriginaValues(e);

                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);

                store.Save(e, request.AuditNote);

                if (request.ReturnOnlyChanged)
                {
                    var currentChanges = BaseObjectExtensions.GetOriginaValues(e);

                    foreach (var (key, value) in currentChanges)
                    {
                        if (preSaveChanges.TryGetValue(key, out var oldValue))
                        {
                            if (!Equals(value, oldValue))
                            {
                                response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
                            }                          
                        }
                        else
                        {
                            response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
                        }
                    }
                }
                else
                {
                    response.Item = e;
                }

                response.Status = StatusCode.OK;
                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Item));

                CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] Save{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
            }

            GarbageCollection();
            return response;
        }

        public static MultiSaveResponse<TEntity> MultiSave(MultiSaveRequest<TEntity> request, Logger logger)
        {
            var start = DateTime.Now;

            var response = new MultiSaveResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] MultiSave{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
                    SimpleName(typeof(TEntity)), request.Items.Length,
                    request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
            try
            {
                var originals = new Dictionary<TEntity, Dictionary<string, object>>();

                var es = request.Items;

                foreach (var e in es)
                    originals[e] = BaseObjectExtensions.GetOriginaValues(e);

                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);

                store.Save(es, request.AuditNote);

                if (request.ReturnOnlyChanged)
                {                  
                    for (int i = 0; i < es.Length; ++i)
                    {
                        var result = new Dictionary<string, object>();

                        var e = es[i];

                        var currentChanges = BaseObjectExtensions.GetOriginaValues(e);

                        var preSaveChanges = originals[e];

                        foreach (var (key, value) in currentChanges)
                        {
                            if (preSaveChanges.TryGetValue(key, out var oldValue))
                            {
                                if (!Equals(value, oldValue))
                                {
                                    result[key] = CoreUtils.GetPropertyValue(e, key);
                                }
                            }
                            else
                            {
                                result[key] = CoreUtils.GetPropertyValue(e, key);
                            }
                        }
                        response.ChangedValues.Add(result);
                    }
                }
                else
                {
                    response.Items = es;
                }

                response.Status = StatusCode.OK;
                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), es.Length));

                CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
            }

            GarbageCollection();
            return response;
        }

        public static DeleteResponse<TEntity> Delete(DeleteRequest<TEntity> request, Logger logger)
        {
            var start = DateTime.Now;

            var response = new DeleteResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] Delete{2} Data=[{3}]", request.Credentials.Platform, request.Credentials.Version,
                    SimpleName(typeof(TEntity)),
                    request.Item));
            try
            {
                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
                store.Delete(request.Item, request.AuditNote);
                response.Status = StatusCode.OK;
                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] Delete{3} Complete", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity))));

                CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] Delete{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message));
            }

            GarbageCollection();
            return response;
        }

        public static MultiDeleteResponse<TEntity> MultiDelete(MultiDeleteRequest<TEntity> request, Logger logger)
        {
            var start = DateTime.Now;

            var response = new MultiDeleteResponse<TEntity>();

            var userguid = ValidateRequest(request, out var userid);
            if (userguid == Guid.Empty)
                return response.Status(StatusCode.Unauthenticated);

            logger.Send(LogType.Information, userid,
                string.Format("[{0} {1}] MultiDelete{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version,
                    SimpleName(typeof(TEntity)), request.Items.Length,
                    request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
            try
            {
                var es = request.Items;
                var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version, logger);
                store.Delete(es, request.AuditNote);
                response.Status = StatusCode.OK;
                logger.Send(LogType.Information, userid,
                    string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Items.Length));

                CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
            }
            catch (Exception err)
            {
                response.Status = StatusCode.Error;
                response.Messages.Add(err.Message);
                logger.Send(LogType.Error, userid,
                    string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version,
                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace));
            }

            GarbageCollection();
            return response;
        }
    }
}