using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;

using Microsoft.Extensions.DependencyInjection;

namespace AyaNova.Biz
{

    internal class LocaleBiz : BizObject, IImportAyaNova7Object
    {
        public bool SeedOrImportRelaxedRulesMode { get; set; }

        internal LocaleBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles userRoles)
        {
            ct = dbcontext;
            UserId = currentUserId;
            UserLocaleId = userLocaleId;
            CurrentUserRoles = userRoles;
            BizType = AyaType.Locale;
            SeedOrImportRelaxedRulesMode = false;//default
        }

        internal static LocaleBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext)
        {
            return new LocaleBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
        }

        //Version for internal use
        internal static LocaleBiz GetBizInternal(AyContext ct, long currentUserId, AuthorizationRoles roles)
        {
            return new LocaleBiz(ct, currentUserId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
        }



        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DUPLICATE - only way to create a new locale
        //        
        internal async Task<Locale> DuplicateAsync(NameIdItem inObj)
        {

            //make sure sourceid exists
            if (!LocaleExists(inObj.Id))
                AddError(ValidationErrorType.InvalidValue, "Id", "Source locale id does not exist");

            //Ensure name is unique and not too long and not empty
            Validate(inObj.Name, true);

            if (HasErrors)
                return null;

            //fetch the existing locale for duplication
            var SourceLocale = await ct.Locale.Include(x => x.LocaleItems).SingleOrDefaultAsync(m => m.Id == inObj.Id);

            //replicate the source to a new dest and save                        
            Locale NewLocale = new Locale();
            NewLocale.Name = inObj.Name;
            NewLocale.OwnerId = UserId;
            NewLocale.Stock = false;
            NewLocale.CjkIndex = false;
            foreach (LocaleItem i in SourceLocale.LocaleItems)
            {
                NewLocale.LocaleItems.Add(new LocaleItem() { Key = i.Key, Display = i.Display });
            }

            //Add it to the context so the controller can save it
            ct.Locale.Add(NewLocale);
            await ct.SaveChangesAsync();
            //Log
            EventLogProcessor.LogEventToDatabase(new Event(UserId, NewLocale.Id, AyaType.Locale, AyaEvent.Created), ct);
            return NewLocale;

        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        /// GET

        //Get entire locale
        internal async Task<Locale> GetAsync(long fetchId)
        {
            //This is simple so nothing more here, but often will be copying to a different output object or some other ops
            return await ct.Locale.Include(x => x.LocaleItems).SingleOrDefaultAsync(m => m.Id == fetchId);
        }



        //get picklist (simple non-paged)       
        internal async Task<List<NameIdItem>> GetPickListAsync()
        {
            List<NameIdItem> l = new List<NameIdItem>();
            l = await ct.Locale
                   .AsNoTracking()
                   .OrderBy(m => m.Name)
                   .Select(m => new NameIdItem()
                   {
                       Id = m.Id,
                       Name = m.Name
                   }).ToListAsync();

            return l;

        }


#if (DEBUG)
        internal AyaNova.Api.Controllers.LocaleController.LocaleCoverageInfo LocaleKeyCoverage()
        {
            AyaNova.Api.Controllers.LocaleController.LocaleCoverageInfo L = new AyaNova.Api.Controllers.LocaleController.LocaleCoverageInfo();
            L.RequestedKeys = ServerBootConfig.LocaleKeysRequested;
            L.RequestedKeys.Sort();
            var AllKeys = GetKeyList();
            foreach (string StockKey in AllKeys)
            {
                if (!L.RequestedKeys.Contains(StockKey))
                {
                    L.NotRequestedKeys.Add(StockKey);
                }
            }
            L.NotRequestedKeys.Sort();
            L.RequestedKeyCount = L.RequestedKeys.Count;
            L.NotRequestedKeyCount = L.NotRequestedKeys.Count;

            return L;
        }

        //Track requests for keys so we can determine which are being used and which are not
        //TODO: Ideally this should be paired with tests that either directly request each key that are def. being used
        //or the UI needs to be tested in a way that triggers every key to be used even errors etc
        internal static void TrackRequestedKey(string key)
        {
            if (!ServerBootConfig.LocaleKeysRequested.Contains(key))
                ServerBootConfig.LocaleKeysRequested.Add(key);
        }

        internal static void TrackRequestedKey(List<string> keys)
        {
            foreach (string Key in keys)
            {
                if (!ServerBootConfig.LocaleKeysRequested.Contains(Key))
                    ServerBootConfig.LocaleKeysRequested.Add(Key);
            }
        }

#endif

        //Get the keys for a list of keys provided
        internal async Task<List<KeyValuePair<string, string>>> GetSubset(List<string> param)
        {

#if (DEBUG)
            TrackRequestedKey(param);
#endif
            var ret = await ct.LocaleItem.Where(x => x.LocaleId == UserLocaleId && param.Contains(x.Key)).ToDictionaryAsync(x => x.Key, x => x.Display);
            return ret.ToList();
        }

        //Get the keys for a list of keys provided, static format for calling from other internal classes
        internal static async Task<Dictionary<string,string>> GetSubsetStatic(List<string> param, long localeId)
        {
#if (DEBUG)
            TrackRequestedKey(param);
#endif
            AyContext ct = ServiceProviderProvider.DBContext;
            if (!LocaleExistsStatic(localeId, ct))
                localeId = ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;
            var ret = await ct.LocaleItem.Where(x => x.LocaleId == localeId && param.Contains(x.Key)).ToDictionaryAsync(x => x.Key, x => x.Display);
            return ret;
        }


        //Get the CJKIndex value for the locale specified
        internal static async Task<bool> GetCJKIndex(long localeId, AyContext ct = null)
        {
            if (ct == null)
                ct = ServiceProviderProvider.DBContext;
            var ret = await ct.Locale.Where(x => x.Id == localeId).Select(m => m.CjkIndex).SingleOrDefaultAsync();
            return ret;
        }


        /// <summary>
        /// Get the value of the key provided in the default locale chosen
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        internal static async Task<string> GetDefaultLocalizedText(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
                return "ERROR: GetDefaultLocalizedText NO KEY VALUE SPECIFIED";
#if (DEBUG)
            TrackRequestedKey(key);
#endif
            AyContext ct = ServiceProviderProvider.DBContext;
            return await ct.LocaleItem.Where(m => m.LocaleId == ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID && m.Key == key).Select(m => m.Display).FirstOrDefaultAsync();
        }

        //Get all stock keys that are valid (used for import)
        internal static List<string> GetKeyList()
        {
            AyContext ct = ServiceProviderProvider.DBContext;
            return ct.LocaleItem.Where(m => m.LocaleId == 1).OrderBy(m => m.Key).Select(m => m.Key).ToList();
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UPDATE
        //


        internal bool PutLocaleItemDisplayText(LocaleItem dbObj, NewTextIdConcurrencyTokenItem inObj, Locale dbParent)
        {

            if (dbParent.Stock == true)
            {
                AddError(ValidationErrorType.InvalidOperation, "object", "LocaleItem is from a Stock locale and cannot be modified");
                return false;
            }

            //Replace the db object with the PUT object
            //CopyObject.Copy(inObj, dbObj, "Id");
            dbObj.Display = inObj.NewText;
            //Set "original" value of concurrency token to input token
            //this will allow EF to check it out
            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;

            //Only thing to validate is if it has data at all in it
            if (string.IsNullOrWhiteSpace(inObj.NewText))
                AddError(ValidationErrorType.RequiredPropertyEmpty, "Display (NewText)");

            if (HasErrors)
                return false;
            ct.SaveChanges();

            //Log
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbParent.Id, AyaType.Locale, AyaEvent.Modified), ct);

            return true;
        }


        internal bool PutLocaleName(Locale dbObj, NewTextIdConcurrencyTokenItem inObj)
        {
            if (dbObj.Stock == true)
            {
                AddError(ValidationErrorType.InvalidOperation, "object", "Locale is a Stock locale and cannot be modified");
                return false;
            }

            dbObj.Name = inObj.NewText;

            //Set "original" value of concurrency token to input token
            //this will allow EF to check it out
            ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;

            Validate(dbObj.Name, false);

            if (HasErrors)
                return false;

            ct.SaveChanges();
            //Log
            EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, AyaType.Locale, AyaEvent.Modified), ct);

            return true;
        }




        ////////////////////////////////////////////////////////////////////////////////////////////////
        //DELETE
        //

        internal bool Delete(Locale dbObj)
        {
            //Determine if the object can be deleted, do the deletion tentatively
            ValidateCanDelete(dbObj);
            if (HasErrors)
                return false;
            ct.Locale.Remove(dbObj);
            return true;
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //VALIDATION
        //

        //Can save or update?
        private void Validate(string inObjName, bool isNew)
        {
            //run validation and biz rules

            //Name required
            if (string.IsNullOrWhiteSpace(inObjName))
                AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");

            //Name must be less than 255 characters
            if (inObjName.Length > 255)
                AddError(ValidationErrorType.LengthExceeded, "Name", "255 char max");

            //Name must be unique
            if (ct.Locale.Where(m => m.Name == inObjName).FirstOrDefault() != null)
                AddError(ValidationErrorType.NotUnique, "Name");

            return;
        }


        //Can delete?
        private void ValidateCanDelete(Locale inObj)
        {
            //Decided to short circuit these; if there is one issue then return immediately (fail fast rule)

            //Ensure it's not a stock locale
            if (inObj.Stock == true)
            {
                AddError(ValidationErrorType.InvalidOperation, "object", "Locale is a Stock locale and cannot be deleted");
                return;
            }

            if (inObj.Id == ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID)
            {
                AddError(ValidationErrorType.InvalidOperation, "object", "Locale is set as the default server locale (AYANOVA_DEFAULT_LANGUAGE_ID) and can not be deleted");
                return;
            }

            //See if any users exist with this locale selected in which case it's not deleteable
            if (ct.User.Any(e => e.LocaleId == inObj.Id))
            {
                AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted in use by one or more Users");
                return;
            }
        }


        ////////////////////////////////////////////////////////////////////////////////////////////////
        //UTILITIES
        //

        public long LocaleNameToId(string localeName)
        {
            var v = ct.Locale.Where(c => c.Name == localeName).Select(x => x.Id);
            if (v.Count() < 1) return 0;
            return v.First();
        }

        public static long LocaleNameToIdStatic(string localeName, AyContext ct = null)
        {
            if (ct == null)
            {
                ct = ServiceProviderProvider.DBContext;
            }
            var v = ct.Locale.Where(c => c.Name == localeName).Select(x => x.Id);
            if (v.Count() < 1) return ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;
            return v.First();
        }

        public bool LocaleExists(string localeName)
        {
            return LocaleNameToId(localeName) != 0;
        }

        public bool LocaleExists(long id)
        {
            return ct.Locale.Any(e => e.Id == id);
        }


        public static bool LocaleExistsStatic(long id, AyContext ct)
        {
            return ct.Locale.Any(e => e.Id == id);
        }


        public static long EnsuredLocaleIdStatic(long id, AyContext ct)
        {
            if (!ct.Locale.Any(e => e.Id == id))
                return ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;
            return id;
        }

        public bool LocaleItemExists(long id)
        {
            return ct.LocaleItem.Any(e => e.Id == id);
        }


        /// <summary>
        /// Used by import, translate the old v7 locale key name into the new shorter version
        /// </summary>
        /// <param name="oldKey"></param>
        /// <returns></returns>
        public string Translatev7LocaleKey(string oldKey)
        {
            string s = oldKey.Replace(".Label.", ".", StringComparison.InvariantCultureIgnoreCase);
            if (s.StartsWith("O.", StringComparison.InvariantCultureIgnoreCase))
                s = s.Replace("O.", "", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".ToolBar.", ".", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".Go.", ".", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".Command.", ".", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".Error.", ".", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".Object.", ".", StringComparison.InvariantCultureIgnoreCase);
            if (s.StartsWith("UI.", StringComparison.InvariantCultureIgnoreCase))
                s = s.Replace("UI.", "", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace(".", "", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("AddressAddress", "Address", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("ContactPhoneContactPhone", "ContactPhone", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("ContactPhonePhone", "ContactPhone", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("PurchaseOrderPurchaseOrder", "PurchaseOrder", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("WorkorderItemMiscExpenseExpense", "WorkorderItemMiscExpense", StringComparison.InvariantCultureIgnoreCase);
            s = s.Replace("WorkorderItemTravelTravel", "WorkorderItemTravel", StringComparison.InvariantCultureIgnoreCase);
            return s;
        }

        //ocaleBiz::ImportV7 - old Key "Locale.Label.UI.DestLocale" translates to new Key "LocaleDestLocale" which is not valid!
        //LocaleUIDestLocale


        /// <summary>
        /// Ensure stock locales and setup defaults
        /// Called by boot preflight check code AFTER it has already ensured the locale is a two letter code if stock one was chosen
        /// </summary>
        public void ValidateLocales()
        {
            //Ensure default locales are present and that there is a server default locale that exists  

            if (!LocaleExists("en"))
            {
                throw new System.Exception($"E1015: stock locale English (en) not found in database!");
            }
            if (!LocaleExists("es"))
            {
                throw new System.Exception($"E1015: stock locale Spanish (es) not found in database!");
            }
            if (!LocaleExists("de"))
            {
                throw new System.Exception($"E1015: stock locale German (de) not found in database!");
            }
            if (!LocaleExists("fr"))
            {
                throw new System.Exception($"E1015: stock locale French (fr) not found in database!");
            }

            //Ensure chosen default locale exists
            switch (ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE)
            {
                case "en":
                case "es":
                case "de":
                case "fr":
                    break;
                default:
                    if (!LocaleExists(ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE))
                    {
                        throw new System.Exception($"E1015: stock locale {ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE} not found in database!");
                    }
                    break;

            }

            //Put the default locale ID number into the ServerBootConfig for later use
            ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID = LocaleNameToId(ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE);

        }


        /////////////////////////////////////////////////////////////////////
        /// IMPORT v7 implementation
        public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId, Dictionary<string, Dictionary<Guid, string>> tagLists)
        {
            //some types need to import from more than one source hence the seemingly redundant switch statement for futureproofing
            switch (j["IMPORT_TASK"].Value<string>())
            {
                case "main":
                    {
                        //Get source locale name from filename using regex
                        var SourceLocaleFileName = j["V7_SOURCE_FILE_NAME"].Value<string>();
                        Regex RxExtractLocaleName = new Regex(@"locale\.(.*)\.json");
                        var v = RxExtractLocaleName.Match(SourceLocaleFileName);
                        var SourceLocaleName = v.Groups[1].ToString();

                        //ENsure doesn't already exist
                        if (LocaleExists(SourceLocaleName))
                        {
                            //If there are any validation errors, log in joblog and move on
                            JobsBiz.LogJob(jobId, $"LocaleBiz::ImportV7Async ->  - Locale \"{SourceLocaleName}\" already exists in database, can not import over an existing locale", ct);
                            return false;
                        }

                        //keys to skip importing
                        List<string> SkipKeys = new List<string>();
                        SkipKeys.Add("UI.Label.CurrentUserName");
                        SkipKeys.Add("V7_SOURCE_FILE_NAME");
                        SkipKeys.Add("V7_TYPE");
                        SkipKeys.Add("IMPORT_TASK");

                        List<string> ValidKeys = GetKeyList();
                        Dictionary<string, string> NewLocaleDict = new Dictionary<string, string>();
                        foreach (var Pair in j.Children())
                        {
                            var V7Value = Pair.First.Value<string>();
                            var V7KeyName = ((JProperty)Pair).Name;

                            if (!SkipKeys.Contains(V7KeyName))
                            {
                                var RavenKeyName = Translatev7LocaleKey(V7KeyName);
                                if (!ValidKeys.Contains(RavenKeyName))
                                {
                                    throw new System.ArgumentOutOfRangeException($"LocaleBiz::ImportV7 - old Key \"{V7KeyName}\" translates to new Key \"{RavenKeyName}\" which is not valid!");
                                }

                                if (!NewLocaleDict.ContainsKey(RavenKeyName))
                                {
                                    NewLocaleDict.Add(RavenKeyName, V7Value);
                                }
                                else
                                {
                                    //Use the shortest V7Value string in the case of dupes
                                    if (NewLocaleDict[RavenKeyName].Length > V7Value.Length)
                                    {
                                        NewLocaleDict[RavenKeyName] = V7Value;
                                    }
                                }
                            }
                        }

                        //Now add keys that were added after v7 for RAVEN using default locale values
                        foreach (string s in ValidKeys)
                        {
                            if (!NewLocaleDict.ContainsKey(s))
                            {
                                NewLocaleDict.Add(s, GetDefaultLocalizedText(s).Result);
                            }
                        }

                        //Validate it's the correct number of keys expected
                        if (NewLocaleDict.Count != ValidKeys.Count)
                        {
                            throw new System.ArgumentOutOfRangeException($"LocaleBiz::ImportV7 - Import locale \"{SourceLocaleName}\" has an unexpected number of keys: {NewLocaleDict.Count}, expected {ValidKeys.Count} ");
                        }

                        //have file name, have all localized text
                        Locale l = new Locale();
                        l.Name = SourceLocaleName;
                        l.OwnerId = 1;
                        l.Stock = false;

                        foreach (KeyValuePair<string, string> K in NewLocaleDict)
                        {
                            l.LocaleItems.Add(new LocaleItem() { Key = K.Key, Display = K.Value });
                        }

                        ct.Locale.Add(l);
                        ct.SaveChanges();

                        //Log now that we have the Id, note that there is no source created / modified for this so just attributing to current userId 
                        EventLogProcessor.LogEventToDatabase(new Event(UserId, l.Id, AyaType.Locale, AyaEvent.Created), ct);

                    }
                    break;
            }

            //just to hide compiler warning for now
            await Task.CompletedTask;
            //this is the equivalent of returning void for a Task signature with nothing to return
            return true;
        }


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

    }//eoc

    //
}//eons

