using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using Sockeye.Util;
using Sockeye.Api.ControllerHelpers;
using Sockeye.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Sockeye.Biz
{
    internal class LicenseBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject
    {
        internal LicenseBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserTranslationId = userTranslationId;
            CurrentUserRoles = UserRoles;
            BizType = SockType.License;
        }

        internal static LicenseBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
        {
            if (httpContext != null)
                return new LicenseBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
            else
                return new LicenseBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //EXISTS 
        internal async Task<bool> ExistsAsync(long id)
        {
            return await ct.License.AnyAsync(z => z.Id == id);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //CREATE   
        //
        internal async Task<License> CreateAsync(License newObject, bool importedWithKeyDoNotGenerate = false)
        {
            await ValidateAsync(newObject, null);
            if (HasErrors)
                return null;
            else
            {
                if (!importedWithKeyDoNotGenerate)//do not generate is used for initial import only from rockfish, could be removed after the initial import
                {
                    await GenerateKey(newObject);

                }
                if (HasErrors) return null;
                newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
                await ct.License.AddAsync(newObject);
                await ct.SaveChangesAsync();
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct);
                await SearchIndexAsync(newObject, true);
                await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
                await HandlePotentialNotificationEvent(SockEvent.Created, newObject);

                //NOTIFY?
                if (newObject.Active
                      && !newObject.NotificationSent
                      && newObject.RegTo != RavenKeyFactory.REVOKED_TOKEN)
                {//this means it should notify customer / trialer immediately
                    await NotifyEndUserKeyIsAvailableAsync(newObject);
                }
                return newObject;
            }
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //GET 
        //      
        internal async Task<License> GetAsync(long id, bool logTheGetEvent = true)
        {
            var ret = await ct.License.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
            if (logTheGetEvent && ret != null)
                await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);

            return ParseKeySetDTOFields(ret);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //
        internal async Task<License> PutAsync(License putObject)
        {
            var dbObject = await GetAsync(putObject.Id, false);
            if (dbObject == null)
            {
                AddError(ApiErrorCode.NOT_FOUND, "id");
                return null;
            }
            if (dbObject.Concurrency != putObject.Concurrency)
            {
                AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return null;
            }

            putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
            await ValidateAsync(putObject, dbObject);
            if (HasErrors) return null;
            await GenerateKey(putObject);
            if (HasErrors) return null;

            ct.Replace(dbObject, putObject);
            try
            {
                await ct.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await ExistsAsync(putObject.Id))
                    AddError(ApiErrorCode.NOT_FOUND);
                else
                    AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
                return null;
            }
            await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, BizType, SockEvent.Modified), ct);
            await SearchIndexAsync(putObject, false);
            await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
            await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject);

            //NOTIFY?
            if (putObject.Active
                  && !putObject.NotificationSent
                  && putObject.RegTo != RavenKeyFactory.REVOKED_TOKEN)
            {//this means it should notify customer / trialer immediately
                await NotifyEndUserKeyIsAvailableAsync(putObject);
            }
            return putObject;
        }



        /////////////////////////////////////////////////////////////////////////////////
        //NOTIFY USER
        //
        internal async Task NotifyEndUserKeyIsAvailableAsync(License l)
        {

            if (!l.NotificationSent && !string.IsNullOrWhiteSpace(l.FetchEmail))
            {
                //## ------------------ DEFAULT NOTIFICATIONS TO CUSTOMER ON LICENSE AVAILABLE ----------------

                string body = string.Empty;

                switch (l.PGroup)
                {
                    case ProductGroup.AyaNova7:
                        if (l.TrialMode)
                        {
                            body = ServerGlobalBizSettings.Cache.V7TemporaryTrial;
                        }
                        else
                        {
                            if (l.Renewal)
                            {
                                body = ServerGlobalBizSettings.Cache.V7AddOnKey;
                            }
                            else
                            {
                                body = ServerGlobalBizSettings.Cache.V7NewKey;
                            }
                        }
                        break;
                    case ProductGroup.RavenPerpetual:
                        if (l.TrialMode)
                            return;//TRIAL REQUEST HAS *ALREADY* NOTIFIED USER NO NEED TO DO ANYTHING HERE                        
                        else
                        {//{newLicense}
                            /*
                            Your AyaNova license key is ready to be installed.
    AyaNova will fetch it automatically within 24 hours or you can force it to fetch immediately from the License page in AyaNova now.
    ---
    Registered to: ACCS
    Database id: 7ktPA+Eaq+LM4vNMkQdbwBkUMDzzFYXmH+m21n3w5Rk=
    Type: Perpetual
    Support and updates available until: Sunday, 14 January 2024
    ActiveInternalUsers: 3
                            */

                            var licenseInfo = $"Registered to: {l.RegTo}\nDatabase Id: {l.DbId}\nType: Perpetual\nSupport and updates until: {l.MaintenanceExpire.ToLongDateString()}\nUsers:{l.Users}";
                            body = ServerGlobalBizSettings.Cache.RavenNewKeyAvailable.Replace("{newLicense}", licenseInfo);
                        }
                        break;
                    case ProductGroup.RavenSubscription:
                        if (l.TrialMode)
                            return;//TRIAL REQUEST HAS *ALREADY* NOTIFIED USER NO NEED TO DO ANYTHING HERE                        
                        else
                        {//{newLicense}
                            /*
                           Your AyaNova activation key is ready to be installed.
AyaNova will fetch it automatically within 24 hours or you can force it to fetch immediately from the License page in AyaNova now.
---
Registered to: Sportseffect
Database id: ZAuW3p73DIvtIWk7VxI4fuy5MccqzGw/ZBqtkRKdpss=
Type: Subscription
Available for use until: Tuesday, 14 February 2023
ActiveInternalUsers: 2
ActiveCustomerUsers: 250
MaximumDataGB: 20
                            */

                            var licenseInfo = $"Registered to: {l.RegTo}\nDatabase Id: {l.DbId}\nType: Subscription\nAvailable for use until: {((DateTime)l.LicenseExpire).ToLongDateString()}\nUsers:{l.Users}\nCustomer login Users:{l.CustomerUsers}\nMaximum data GB:{l.MaxDataGB}";
                            body = ServerGlobalBizSettings.Cache.RavenNewKeyAvailable.Replace("{newLicense}", licenseInfo);
                        }
                        break;
                    default:
                        throw new System.NotSupportedException($"Product group {l.PGroup} not supported for license notification to {l.RegTo}");

                }



                var notifyDirectSMTP = new Sockeye.Api.Controllers.NotifyController.NotifyDirectSMTP()
                {
                    ToAddress = l.FetchEmail,
                    Subject = "AyaNova license key",
                    TextBody = body
                };


                IMailer m = Sockeye.Util.ServiceProviderProvider.Mailer;
                try
                {
                    await m.SendEmailAsync(notifyDirectSMTP.ToAddress, notifyDirectSMTP.Subject, notifyDirectSMTP.TextBody, ServerGlobalOpsSettingsCache.Notify, null, null, null);
                    await EventLogProcessor.LogEventToDatabaseAsync(new Event(1, notifyDirectSMTP.ObjectId, notifyDirectSMTP.SockType, SockEvent.DirectSMTP, $"\"{notifyDirectSMTP.Subject}\"->{notifyDirectSMTP.ToAddress}"), ct);
                }
                catch (Exception ex)
                {
                    var err = "License email to customer failure: SMTP direct message failed";
                    await NotifyEventHelper.AddOpsProblemEvent(err, ex);
                    AddError(ApiErrorCode.API_SERVER_ERROR, null, err + ExceptionUtil.ExtractAllExceptionMessages(ex));
                    return;
                }
            }
        }

           ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //
        internal async Task<bool> DeleteAsync(long id)
        {
            using (var transaction = await ct.Database.BeginTransactionAsync())
            {

                License dbObject = await GetAsync(id, false);
                if (dbObject == null)
                {
                    AddError(ApiErrorCode.NOT_FOUND);
                    return false;
                }
                ValidateCanDeleteAsync(dbObject);
                if (HasErrors)
                    return false;

                {
                    var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.License && x.ObjectId == id).Select(x => x.Id).ToListAsync();
                    if (IDList.Count() > 0)
                    {
                        ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
                        foreach (long ItemId in IDList)
                            if (!await b.DeleteAsync(ItemId, transaction))
                            {
                                AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
                                return false;
                            }
                    }
                }

                ct.License.Remove(dbObject);
                await ct.SaveChangesAsync();

                //Log event 
                await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct);
                await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct);
                await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
                await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
                await transaction.CommitAsync();
                await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject);

                return true;
            }
        }



        #region PARSE KEY SET DTO FIELDS
        //Read the license key text, parse it and set the dto fields accordingly
        public static License ParseKeySetDTOFields(License l)
        {

            if (l == null || string.IsNullOrWhiteSpace(l.Key))
                return l;


            if (l.Key.Contains("AyaNovaLicenseKey"))
            {
                //v7 key
                /*   
                                [KEY
                                {
                                "AyaNovaLicenseKey": {
                                "SchemaVersion": "7",
                                "Id": "1517418112",
                                "Created": "2018-01-31T09:01:52.1878494-08:00",
                                "Sub": "true",
                                "RegisteredTo": "Direct Telecom Services",
                                "EmailAddress": "chrisw@dts.solutions",
                                "FetchCode": "AgYuDnjDyQ",
                                "Source": "5246494432",
                                "InstallableUntil": "2019-01-31T09:01:52.089767-08:00",
                                "TotalScheduleableUsers": "15",
                                "Expires": "2019-01-31T00:00:00",
                                "RequestedTrial": "False",
                                "Plugins": {
                                "Plugin": [
                                {
                                "Item": "MBI - Minimal browser interface",
                                "SubscriptionExpires": "2018-06-13T00:00:00"
                                },
                                {
                                "Item": "WBI - Web browser interface",
                                "SubscriptionExpires": "2018-06-13T00:00:00"
                                },
                                {
                                "Item": "OutlookSchedule",
                                "SubscriptionExpires": "2018-06-13T00:00:00"
                                },
                                {
                                "Item": "AyaNovaOLI",
                                "SubscriptionExpires": "2018-06-13T00:00:00"
                                },
                                {
                                "Item": "RI - Responsive Interface",
                                "SubscriptionExpires": "2018-06-13T00:00:00"
                                }
                                ]
                                }
                                }
                                }
                                KEY]
                                [SIGNATURE
                                uBjnooIDd6MOiqT/z4tDQfeafkQiWDBtxDHXOxhZ7av1oWS72yPoe8BrAnDZiYbxE4+cHR3C0sPCgEVva5miV1foyi7P6YKkxkKQMxTUR5IssgWVHM59KnO1lR2ndCHWaqH3gHgSsb/sdvYfuHg8luTl1RgjNDZRdQqbPl4NLMcGGW86LoXjpLjsRRxImckBEJFnntd+aXCRmQjXEZWmfxDVW84qa6h+ZCOwL3KYJHuPQDcCmhcpp3MIR3OHoeYhmNG7TWuELsJ4hrsROcqSbEC/CdZD8hoZwtrysu/ZvNZOKchwFsiBaN47+DxK0K/fL/X8CDcG+w3iqgH/x5ipIw==
                                SIGNATURE]
 "Plugin": [
        {
          "Item": "MBI - Minimal browser interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "WBI - Web browser interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "QBI - QuickBooks interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "QBOI - QuickBooks Online interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "PTI - US Sage 50/Peachtree interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "QuickNotification",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "ExportToXls",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "OutlookSchedule",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "AyaNovaOLI",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "ImportExportCSVDuplicate",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        },
        {
          "Item": "RI - Responsive Interface",
          "SubscriptionExpires": "2024-01-08T00:00:00"
        }
                                */
                string keyNoWS = System.Text.RegularExpressions.Regex.Replace(StringUtil.Extract(l.Key, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
                var jKey = JObject.Parse(keyNoWS);
                l.Users = jKey["AyaNovaLicenseKey"]["TotalScheduleableUsers"].Value<int>();
                //plugins
                //var jaPlugins = jKey["AyaNovaLicenseKey"]["TotalScheduleableUsers"].Value<int>();
                var jaPlugins = (JArray)jKey.SelectToken("AyaNovaLicenseKey.Plugins.Plugin");
                for (int x = 0; x < jaPlugins.Count; x++)
                {
                    var jPlugin = (JObject)jaPlugins[x];
                    var plugItem = jPlugin["Item"].Value<string>();
                    var plugExpires = jPlugin["SubscriptionExpires"].Value<DateTime>().ToUniversalTime();

                    switch (plugItem)
                    {
                        case "MBI - Minimal browser interface":
                            {
                                l.MBI = true;
                                l.MBIExpires = plugExpires;
                            }
                            break;
                        case "WBI - Web browser interface":
                            {
                                l.WBI = true;
                                l.WBIExpires = plugExpires;
                            }
                            break;
                        case "QBI - QuickBooks interface":
                            {
                                l.QBI = true;
                                l.QBIExpires = plugExpires;
                            }
                            break;
                        case "QBOI - QuickBooks Online interface":
                            {
                                l.QBOI = true;
                                l.QBOIExpires = plugExpires;
                            }
                            break;
                        case "PTI - US Sage 50/Peachtree interface":
                            {
                                l.PTI = true;
                                l.PTIExpires = plugExpires;
                            }
                            break;
                        case "QuickNotification":
                            {
                                l.QuickNotification = true;
                                l.QuickNotificationExpires = plugExpires;
                            }
                            break;
                        case "ExportToXls":
                            {
                                l.ExportToXLS = true;
                                l.ExportToXLSExpires = plugExpires;
                            }
                            break;
                        case "OutlookSchedule":
                            {
                                l.OutlookSchedule = true;
                                l.OutlookScheduleExpires = plugExpires;
                            }
                            break;
                        case "AyaNovaOLI":
                            {
                                l.OLI = true;
                                l.OLIExpires = plugExpires;
                            }
                            break;
                        case "ImportExportCSVDuplicate":
                            {
                                l.ImportExportCSVDuplicate = true;
                                l.ImportExportCSVDuplicateExpires = plugExpires;
                            }
                            break;
                        case "RI - Responsive Interface":
                            {
                                l.RI = true;
                                l.RIExpires = plugExpires;
                            }
                            break;

                    }
                    // dr["Plugin"] = (string)p[x].SelectToken("Item");
                    // dr["SubscriptionExpires"] = (DateTime)p[x].SelectToken("SubscriptionExpires");
                    // dtPlugins.Rows.Add(dr);
                }

            }
            else if (l.Key.Contains("AyaNovaLiteLicenseKey"))
            {
                //v7 LITE key
                /*
                           "[KEY\n{\n  \"AyaNovaLiteLicenseKey\": {\n    \"SchemaVersion\": \"7\",\n    \"Id\": \"1648506791\",\n    \"Created\": \"2022-03-28T15:33:11.6010225-07:00\",\n    
                           \"Sub\": \"true\",\n    \"RegisteredTo\": \"Duncan Electric\",\n    \"EmailAddress\": \"sandrajod@att.net\",\n    
                           \"FetchCode\": \"hGAmScqYcU\",\n    \"Source\": \"5246494431\",\n    \"InstallableUntil\": \"2023-03-28T15:33:11.6009851-07:00\",\n    
                           \"TotalScheduleableUsers\": \"1\",\n    \"Expires\": \"2023-03-29T00:00:00\",\n    \"RequestedTrial\": \"False\",\n    \"Plugins\": {\n      
                           \"Plugin\": []\n    }\n  }\n}\nKEY]\n
                           [SIGNATURE\nKuOF/SpBL1d8AFebvm2lipmKeGdbR6WzbhN68fun+ffVGRjXNX1jWI3rbf9P/shs2/M8gHqW/B7T0vVovGqosmNsGtvaYo30TKlZj9Eicr2+zDf7ojwZiBCeEnFzXr9+7aZrsZSvN20Pqof0xf/J4BVp1T66ecuZywMzH0NGsXXZtXhWYhGvLAZAR1X5/j5gqysSdysmV9j8Euz91zs/BRyfdU0uwwrdQzrJzI4V1MFl+/mIkhMUNcJ5bzjisWS2xeyNYCYnGpMF5oaGPaIcEtmTAdf5fPNNvw3sNhPaZgwlzN8FjfK6T0VgS19PcHCMOA1bTAiLLFNk6wkcjGp2Cw==\nSIGNATURE]\n"
                           */
                string keyNoWS = System.Text.RegularExpressions.Regex.Replace(StringUtil.Extract(l.Key, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
                var jKey = JObject.Parse(keyNoWS);
                l.Users = 1;
                var jaPlugins = (JArray)jKey.SelectToken("AyaNovaLiteLicenseKey.Plugins.Plugin");
                for (int x = 0; x < jaPlugins.Count; x++)
                {
                    var jPlugin = (JObject)jaPlugins[x];
                    var plugItem = jPlugin["Item"].Value<string>();
                    var plugExpires = jPlugin["SubscriptionExpires"].Value<DateTime>().ToUniversalTime();

                    switch (plugItem)
                    {
                        case "MBI - Minimal browser interface":
                            {
                                l.MBI = true;
                                l.MBIExpires = plugExpires;
                            }
                            break;
                        case "WBI - Web browser interface":
                            {
                                l.WBI = true;
                                l.WBIExpires = plugExpires;
                            }
                            break;
                        case "QBI - QuickBooks interface":
                            {
                                l.QBI = true;
                                l.QBIExpires = plugExpires;
                            }
                            break;
                        case "QBOI - QuickBooks Online interface":
                            {
                                l.QBOI = true;
                                l.QBOIExpires = plugExpires;
                            }
                            break;
                        case "PTI - US Sage 50/Peachtree interface":
                            {
                                l.PTI = true;
                                l.PTIExpires = plugExpires;
                            }
                            break;
                        case "QuickNotification":
                            {
                                l.QuickNotification = true;
                                l.QuickNotificationExpires = plugExpires;
                            }
                            break;
                        case "ExportToXls":
                            {
                                l.ExportToXLS = true;
                                l.ExportToXLSExpires = plugExpires;
                            }
                            break;
                        case "OutlookSchedule":
                            {
                                l.OutlookSchedule = true;
                                l.OutlookScheduleExpires = plugExpires;
                            }
                            break;
                        case "AyaNovaOLI":
                            {
                                l.OLI = true;
                                l.OLIExpires = plugExpires;
                            }
                            break;
                        case "ImportExportCSVDuplicate":
                            {
                                l.ImportExportCSVDuplicate = true;
                                l.ImportExportCSVDuplicateExpires = plugExpires;
                            }
                            break;
                        case "RI - Responsive Interface":
                            {
                                l.RI = true;
                                l.RIExpires = plugExpires;
                            }
                            break;

                    }
                }

            }
            else
            {
                //V8 RAVEN key
                /*
                            {{
 "Key": {
   "LicenseFormat": "8",
   "Id": "1672261044",
   "RegisteredTo": "PDI Technologies",
   "DBID": "R6U37uNUN2hSQideG6Gg+MqoQY8vuUeyHFI6Kv7VDsE=",
   "Perpetual": true,
   "LicenseExpiration": "5555-01-01T00:00:00",
   "MaintenanceExpiration": "2023-12-28T00:00:00",
   "Features": [
     {
       "Name": "ActiveInternalUsers",
       "Count": 5
     }
   ]
 }
}}

"Features": [
      {
        "Name": "TrialMode"
      },
      {
        "Name": "ActiveInternalUsers",
        "Count": 5000
      },
      {
        "Name": "ActiveCustomerUsers",
        "Count": 20000
      },
      {
        "Name": "MaximumDataGB",
        "Count": 20
      }
    ]



    "Key": {
    "LicenseFormat": "8",
    "Id": "00-1593712003",
    "RegisteredTo": "Test corporation",
    "DBID": "mRntGkdwvYCDOAOroCQpB5Elbct09iNIS7lcU7QgRCY=",
    "LicenseExpiration": "2020-08-16T17:46:43.6261717Z",
    "MaintenanceExpiration": "2020-08-16T17:46:43.6261717Z",
    "Features": [
        {
            "Name": "TrialMode"
        },
        {
            "Name": "ServiceMode"
        },
        {
            "Name": "Accounting"
        },
        {
            "Name": "ServiceTechs",
            "Count": 1000
        }
    ]
}
}
                           */
                string keyNoWS = System.Text.RegularExpressions.Regex.Replace(StringUtil.Extract(l.Key, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
                var jKey = JObject.Parse(keyNoWS);

                // #if (DEBUG)
                //                 if (jKey["Key"]["DBID"].Value<string>() == "mRntGkdwvYCDOAOroCQpB5Elbct09iNIS7lcU7QgRCY=")
                //                     System.Diagnostics.Debugger.Break();
                // #endif


                var jaFeatures = (JArray)jKey.SelectToken("Key.Features");
                for (int x = 0; x < jaFeatures.Count; x++)
                {
                    var jFeature = (JObject)jaFeatures[x];
                    var feature = jFeature["Name"].Value<string>();
                    int count = 0;

                    if (feature == "TrialMode")
                        l.TrialMode = true;

                    if (jFeature["Count"] != null)
                        count = jFeature["Count"].Value<int>();

                    if (feature == "ActiveInternalUsers" || feature == "ServiceTechs")//at one point early in testing was ServiceTechs kept for import from rockfish
                        l.Users = count;

                    if (feature == "ActiveCustomerUsers")
                        l.CustomerUsers = count;

                    if (feature == "MaximumDataGB")
                        l.MaxDataGB = count;

                }

            }
#if (DEBUG)          
            if (l.Users == 0)
                System.Diagnostics.Debugger.Break();
#endif
            return l;

        }

        #endregion parsekeysetdtofields

        #region KEY GEN
        ////////////////////////////////////////////////////////////////////////////////////////////////
        //GENERATE 
        //
        internal async Task GenerateKey(License l)
        {

            if (l.CustomerId != null)
            {
                l.CustomerViz = await ct.Customer.AsNoTracking().Where(x => x.Id == l.CustomerId).Select(x => x.Name).FirstOrDefaultAsync();
            }

            switch (l.PGroup)
            {
                case ProductGroup.RavenPerpetual:
                case ProductGroup.RavenSubscription:
                    {
                        RavenKeyFactory.GenerateAndSetRavenKey(l);
                    }
                    break;
                case ProductGroup.AyaNova7:
                    {
                        KeyFactory.GenerateAndSetV7Key(l);
                    }
                    break;
            }

        }
        #endregion key gen

     
        ////////////////////////////////////////////////////////////////////////////////////////////////
        //SEARCH
        //
        private async Task SearchIndexAsync(License obj, bool isNew)
        {
            var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
            DigestSearchText(obj, SearchParams);
            if (isNew)
                await Search.ProcessNewObjectKeywordsAsync(SearchParams);
            else
                await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
        }

        public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, SockType specificType)
        {
            var obj = await GetAsync(id, false);
            var SearchParams = new Search.SearchIndexProcessObjectParameters();
            DigestSearchText(obj, SearchParams);
            return SearchParams;
        }

        public void DigestSearchText(License obj, Search.SearchIndexProcessObjectParameters searchParams)
        {
            if (obj != null)
                searchParams.AddText(obj.DbId)
                            .AddText(obj.FetchCode)
                            .AddText(obj.Wiki)
                            .AddText(obj.Tags)
                            .AddText(obj.FetchEmail)
                            .AddText(obj.Key)
                            .AddText(obj.RegTo);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        //VALIDATION
        //
        private async Task ValidateAsync(License proposedObj, License currentObj)
        {
            bool isNew = currentObj == null;

            //fetched keys are never editable, must be duped if re-issue
            //I'LL PROBABLY NEED TO CHANGE THIS LATER FOR SOME REASON BUT FOR NOW IT'S DEFENSIVE
            if (!isNew && currentObj.FetchedOn != null)
            {
                AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", "Fetched, not changeable, duplicate instead");
                return;
            }

            //MISC / NOTSET product group are not valid for keys
            if (proposedObj.PGroup == ProductGroup.Misc || proposedObj.PGroup == ProductGroup.NotSet)
            {
                AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "pGroup");
                return;
            }

            //If Active and fetchable (and were here as it's not yet fetched)
            //then more rules apply than if just saving while working on it...
            if (proposedObj.Active)
            {
                //It's being offered for fetch so make sure it's ready here
                if (string.IsNullOrWhiteSpace(proposedObj.RegTo))
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "RegTo");
                    return;
                }

                //It's ready to go, MUST have an email address to send notification to
                //for v8 keys even though there is no fetchemail required like v7
                //it is *still* used for notification and will be pre-filled with customer address if not provided so
                //this is a backstop to ensure the notification message can be sent
                //(revoked keys are *not* notified for obvious reasons so thats not a requirement here)
                if (string.IsNullOrWhiteSpace(proposedObj.FetchEmail) && proposedObj.RegTo != RavenKeyFactory.REVOKED_TOKEN)
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "FetchEmail");
                    return;
                }

                if (proposedObj.Users < 1)//both require Users to be set
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "Users");
                    return;
                }

                //all non trial keys require a customer id matched
                if (proposedObj.CustomerId == null && !proposedObj.TrialMode)
                {
                    AddError(ApiErrorCode.VALIDATION_REQUIRED, "CustomerId");
                    return;
                }




                //Active state rules for each product group
                switch (proposedObj.PGroup)
                {

                    case ProductGroup.RavenPerpetual:
                        {
                            if (string.IsNullOrWhiteSpace(proposedObj.DbId))
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "DbID");
                                return;
                            }


                        }
                        break;
                    case ProductGroup.RavenSubscription:
                        {
                            if (string.IsNullOrWhiteSpace(proposedObj.DbId))
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "DbID");
                                return;
                            }


                            //sub keys *always* expire
                            if (proposedObj.LicenseExpire == null)
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "LicenseExpire");
                                return;
                            }



                            if (proposedObj.CustomerUsers < 250)
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "CustomerUsers");
                                return;
                            }
                            if (proposedObj.MaxDataGB == null || proposedObj.MaxDataGB < 20)
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "MaxDataGB");
                                return;
                            }

                        }
                        break;
                    case ProductGroup.AyaNova7:
                        {
                            if (string.IsNullOrWhiteSpace(proposedObj.FetchEmail))
                            {
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "FetchEmail");
                                return;
                            }

                            if (proposedObj.WBI && proposedObj.WBIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "WBIExpires");
                            if (proposedObj.MBI && proposedObj.MBIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "MBIExpires");
                            if (proposedObj.RI && proposedObj.RIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "RIExpires");
                            if (proposedObj.QBI && proposedObj.QBIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "QBIExpires");
                            if (proposedObj.QBOI && proposedObj.QBOIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "QBOIExpires");
                            if (proposedObj.PTI && proposedObj.PTIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "PTIExpires");
                            if (proposedObj.QuickNotification && proposedObj.QuickNotificationExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuickNotificationExpires");
                            if (proposedObj.ExportToXLS && proposedObj.ExportToXLSExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "ExportToXLSExpires");
                            if (proposedObj.OutlookSchedule && proposedObj.OutlookScheduleExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "OutlookScheduleExpires");
                            if (proposedObj.OLI && proposedObj.OLIExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "OLIExpires");
                            if (proposedObj.ImportExportCSVDuplicate && proposedObj.ImportExportCSVDuplicateExpires == null)
                                AddError(ApiErrorCode.VALIDATION_REQUIRED, "ImportExportCSVDuplicateExpires");

                        }
                        break;
                }

            }

            await Task.CompletedTask;
        }


        private void ValidateCanDeleteAsync(License inObj)
        {

            if (inObj.FetchedOn != null)
                AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", "Fetched, not deletable");

        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //REPORTING
        //
        public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
        {
            var idList = dataListSelectedRequest.SelectedRowIds;
            JArray ReportData = new JArray();
            while (idList.Any())
            {
                var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
                idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();

                //query for this batch, comes back in db natural order unfortunately
                var batchResults = await ct.License.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();

                //order the results back into original
                //What is happening here:
                //for performance the query is batching a bunch at once by fetching a block of items from the sql server
                //however it's returning in db order which is often not the order the id list is in
                //so it needs to be sorted back into the same order as the ide list
                //This would not be necessary if just fetching each one at a time individually (like in workorder get report data)

                var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
                batchResults = null;

                foreach (License w in orderedList)
                {
                    if (!ReportRenderManager.KeepGoing(jobId)) return null;
                    await PopulateVizFields(w);
                    var jo = JObject.FromObject(w);
                    if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
                        jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
                    ReportData.Add(jo);
                }
                orderedList = null;
            }
            vc.Clear();
            return ReportData;
        }
        private VizCache vc = new VizCache();


        //populate viz fields from provided object
        private async Task PopulateVizFields(License o)
        {
            if (!vc.Has("customer", o.CustomerId))
            {
                vc.Add(await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(), "customer", o.CustomerId);
            }
            o.CustomerViz = vc.Get("customer", o.CustomerId);
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////       
        // IMPORT EXPORT
        //

        public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
        {
            return await GetReportData(dataListSelectedRequest, jobId);
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //JOB / OPERATIONS
        //
        public async Task HandleJobAsync(OpsJob job)
        {
            //Hand off the particular job to the corresponding processing code
            //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so 
            //basically any error condition during job processing should throw up an exception if it can't be handled
            switch (job.JobType)
            {
                case JobType.BatchCoreObjectOperation:
                    await ProcessBatchJobAsync(job);
                    break;
                default:
                    throw new System.ArgumentOutOfRangeException($"LicenseBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
            }
        }



        private async Task ProcessBatchJobAsync(OpsJob job)
        {
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
            await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
            List<long> idList = new List<long>();
            long FailedObjectCount = 0;
            JObject jobData = JObject.Parse(job.JobInfo);
            if (jobData.ContainsKey("idList"))
                idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
            else
                idList = await ct.License.AsNoTracking().Select(z => z.Id).ToListAsync();
            bool SaveIt = false;

            //---------------------------------
            //case 4192
            TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS);
            DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1));
            var TotalRecords = idList.LongCount();
            long CurrentRecord = -1;
            //---------------------------------

            foreach (long id in idList)
            {
                try
                {
                    //--------------------------------
                    //case 4192
                    //Update progress / cancel requested?                    
                    CurrentRecord++;
                    if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan))
                    {
                        await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}");
                        if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested)
                            break;
                        LastProgressCheck = DateTime.UtcNow;
                    }
                    //---------------------------------

                    SaveIt = false;
                    ClearErrors();
                    License o = null;
                    //save a fetch if it's a delete
                    if (job.SubType != JobSubType.Delete)
                        o = await GetAsync(id, false);
                    switch (job.SubType)
                    {
                        case JobSubType.TagAddAny:
                        case JobSubType.TagAdd:
                        case JobSubType.TagRemoveAny:
                        case JobSubType.TagRemove:
                        case JobSubType.TagReplaceAny:
                        case JobSubType.TagReplace:
                            SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
                            break;
                        case JobSubType.Delete:
                            if (!await DeleteAsync(id))
                            {
                                await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
                                FailedObjectCount++;
                            }
                            break;
                        default:
                            throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
                    }
                    if (SaveIt)
                    {
                        o = await PutAsync(o);
                        if (o == null)
                        {
                            await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
                            FailedObjectCount++;
                        }
                    }

                    //delay so we're not tying up all the resources in a tight loop
                    await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY);
                }
                catch (Exception ex)
                {
                    await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
                    await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
                }
            }

            //---------------------------------
            //case 4192
            await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}");
            //---------------------------------

            await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
            await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
        }






        ////////////////////////////////////////////////////////////////////////////////////////////////
        // NOTIFICATION PROCESSING
        //
        public async Task HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
        {
            if (ServerBootConfig.MIGRATING) return;
            ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger<LicenseBiz>();

            log.LogDebug($"HandlePotentialNotificationEvent processing:  [SockType:{this.BizType}, AyaEvent:{ayaEvent}]");

            bool isNew = currentObj == null;

            proposedObj.Name = "LICENSE ID" + proposedObj.Id.ToString();
            //STANDARD EVENTS FOR ALL OBJECTS
            await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);

            //SPECIFIC EVENTS FOR THIS OBJECT
            License o = (License)proposedObj;

            //## DELETED EVENTS
            //any event added below needs to be removed, so
            //just blanket remove any event for this object of eventtype that would be added below here
            //do it regardless any time there's an update and then
            //let this code below handle the refreshing addition that could have changes           
            //await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.License, o.Id, NotifyEventType.ContractExpiring);


            //## CREATED / MODIFIED EVENTS
            if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified)
            {


            }

        }//end of process notifications


        /////////////////////////////////////////////////////////////////////

    }//eoc


}//eons

