using AssortGenerator.Common.Helpers;
using QuestValidator.Common;
using QuestValidator.Common.Helpers;
using QuestValidator.Common.Models;
using QuestValidator.Helpers;
using QuestValidator.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Quest = QuestValidator.Models.Quest;

namespace GenerateQuestFile
{
    class Program
    {
        /// <summary>
        /// Generate a quests.json file in /output/
        /// Uses every quest from the live quest dump file
        /// If any quests are missing, it will use the quests.json file to fill in the blanks
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            var inputPath = DiskHelpers.CreateWorkingFolders();
            InputFileHelper.SetInputFiles(inputPath);

            // Read in quest files
            var questBlacklist = QuestHelper.GetQuestBlacklist();
            var existingQuestData = QuestHelper.GetQuestData();
            var liveQuestData = QuestHelper.GetLiveQuestData();

            var mergedLiveData = QuestHelper.MergeLiveQuestFiles(liveQuestData, questBlacklist);

            OutputQuestRequirementsToConsole(mergedLiveData.data);

            JsonWriter.WriteJson(mergedLiveData, "output", Directory.GetCurrentDirectory(), "mergedlivejson");

            // Find the quests that are missing from the live file from existing quests.json
            var missingQuests = GetMissingQuestsNotInLiveFile(existingQuestData, mergedLiveData, questBlacklist);

            // Create a list of quests to output
            // Use all quests in live file
            // Use quests from quests.json to fill in missing quests
            // Add live quests to collection to return later
            var questsToOutputToFile = new Dictionary<string, Quest>();
            foreach (var liveQuest in mergedLiveData.data)
            {
                questsToOutputToFile.Add(liveQuest._id, liveQuest);
            }

            // Add missing quests from existing quest data to fill in blanks from live data
            foreach (var missingQuest in missingQuests)
            {
                // Going from a pre-12.7.x version has problems, it doesnt have the new quest data format
                //CheckAndFixMissingProperties(missingQuest);

                questsToOutputToFile.Add(missingQuest._id, missingQuest);
            }

            // Now old + new quests have been merged, check quest list to see if any quests are missing
            foreach (var missingQuest in QuestNames.GetQuests())
            {
                if (!questsToOutputToFile.Any(x => x.Key == missingQuest.Value))
                {
                    LoggingHelpers.LogWarning($" quest not found in new or old data: {missingQuest.Key}");
                }
            }

            if (!questsToOutputToFile.ContainsKey("5e383a6386f77465910ce1f3")) // TextileP1Bear
            {
                // add textileP1Bear
            }

            if (!questsToOutputToFile.ContainsKey("5e4d515e86f77438b2195244")) // TextileP2Bear
            {
                // add TextileP2Bear
            }

            foreach (var quest in questsToOutputToFile)
            {
                AddQuestName(quest);

                var originalQuest = existingQuestData.FirstOrDefault(x => x.Key == quest.Key).Value;

                if (originalQuest is null)
                {
                    LoggingHelpers.LogWarning($"Cant check for original start conditions. Unable to find original quest {quest.Key} {QuestHelper.GetQuestNameById(quest.Key)}, skipping.");
                    continue;
                }

                AddMissingFields(quest);

                // Quest has start conditions, check to ensure they're carried over
                //if (originalQuest.conditions.AvailableForStart.Count > 0)
                //{
                //    AddMissingAvailableForStartConditions(originalQuest, quest);
                //}

                if (originalQuest.rewards.Fail.Count > 0)
                {
                    AddMissingFailRewards(originalQuest, quest);
                }

                // To make diffs more sane, copy the random IDs from the existing quests.json if possible
                CopyExistingRandomIds(originalQuest, quest.Value);
            }

            // Iterate over quest objects a final time and add hard coded quest requirements if they dont already exist
            foreach (var quest in questsToOutputToFile)
            {
                var questRequirements = QuestRequirements.GetQuestRequirements(quest.Key);
                if (questRequirements is null || questRequirements.Count == 0)
                {
                    LoggingHelpers.LogWarning($"Quest requirement not found for : {quest.Value.QuestName}, skipping.");

                    continue;
                }

                foreach (var requirement in questRequirements)
                {
                    if (requirement.PreReqType == PreRequisiteType.Quest)
                    {
                        // Does quest have requirement
                        if (!quest.Value.conditions.AvailableForStart.Any(x => x.conditionType == "Quest"
                                && x.target.ToString() == requirement.Quest.Id))
                        {
                            LoggingHelpers.LogSuccess($"{quest.Value.QuestName} needs a prereq of quest {requirement.Quest.Name}, adding.");

                            string hashData = quest.Value._id + requirement.Quest.Id;
                            quest.Value.conditions.AvailableForStart.Add(new AvailableFor
                            {
                                conditionType = "Quest",
                                id = Sha256(hashData),
                                index = GetNextIndex(quest.Value.conditions.AvailableForStart.LastOrDefault()?.index),
                                parentId = "",
                                status = GetQuestStatus(requirement.QuestStatus),
                                target = requirement.Quest.Id,
                                visibilityConditions = new List<object>(),
                                availableAfter = 0
                            }
                        );
                        }
                        else
                        {
                            if (questRequirements != null)
                            {
                                LoggingHelpers.LogInfo($"{quest.Value.QuestName} already has prereq of quest {requirement.Quest.Name}, skipping.");
                            }
                        }
                    }

                    if (requirement.PreReqType == PreRequisiteType.RemoveQuest)
                    {
                        if (quest.Value.conditions.AvailableForStart.RemoveAll(x =>
                                x.conditionType == "Quest" && x.target.ToString() == requirement.Quest.Id) > 0)
                        {
                            LoggingHelpers.LogSuccess($"{quest.Value.QuestName} required {requirement.Quest.Name}, removing.");
                        }
                    }

                    if (requirement.PreReqType == PreRequisiteType.Level)
                    {
                        if (!quest.Value.conditions.AvailableForStart.Any(x => x.conditionType == "Level"
                                && int.Parse(x.value.ToString()) == requirement.Level))
                        {
                            LoggingHelpers.LogSuccess($"{quest.Value.QuestName} needs a prereq of level {requirement.Level}, adding.");

                            string hashData = quest.Value._id + "Level";
                            quest.Value.conditions.AvailableForStart.Add(new AvailableFor
                            {
                                conditionType = "Level",
                                id = Sha256(hashData),
                                index = GetNextIndex(quest.Value.conditions.AvailableForStart.LastOrDefault()?.index),
                                parentId = "",
                                dynamicLocale = false,
                                value = requirement.Level,
                                compareMethod = ">=",
                                visibilityConditions = new List<object>()
                            }
                            );
                        }
                    }

                    if (requirement.PreReqType == PreRequisiteType.RemoveLevel)
                    {
                        if (quest.Value.conditions.AvailableForStart.RemoveAll(x => x.conditionType == "Level") > 0)
                        {
                            LoggingHelpers.LogSuccess($"{quest.Value.QuestName} required level {requirement.Level}, removing.");
                        }
                    }
                }

                // To make diffs more sane, copy the random IDs from the existing quests.json if possible
                var originalQuest = existingQuestData.FirstOrDefault(x => x.Key == quest.Key).Value;
                if (originalQuest != null)
                {
                    CopyExistingRandomIds(originalQuest, quest.Value);
                }
            }
            OutputQuestRequirementsToConsole2(questsToOutputToFile);
            JsonWriter.WriteJson(questsToOutputToFile, "output", Directory.GetCurrentDirectory(), "quests");
        }

        private static void OutputQuestRequirementsToConsole(List<Quest> quests)
        {
            var output = new List<string>();
            foreach (var quest in quests)
            {
                var questConditions = quest.conditions.AvailableForStart.Where(x => x.conditionType == "Quest");
                if (questConditions != null)
                {
                    foreach (var questCondition in questConditions)
                    {
                        var x = questCondition.target.ToString();
                        Console.WriteLine($"{QuestHelper.GetQuestNameById(quest._id)} needs {QuestHelper.GetQuestNameById(x)}");
                    }
                }
            }

            // JsonWriter.WriteJson<QuestRoot>(output, "output", Directory.GetCurrentDirectory(), "questRequirements");
        }

        private static void OutputQuestRequirementsToConsole2(Dictionary<string, Quest> quests)
        {
            var output = new List<string>();
            foreach (var quest in quests)
            {
                var questConditions = quest.Value.conditions.AvailableForStart.Where(x => x.conditionType == "Quest");
                if (questConditions != null)
                {
                    foreach (var questCondition in questConditions)
                    {
                        var x = questCondition.target.ToString();
                        Console.WriteLine($"{QuestHelper.GetQuestNameById(quest.Value._id)} needs {QuestHelper.GetQuestNameById(x)}");
                    }
                }
            }

            // JsonWriter.WriteJson<QuestRoot>(output, "output", Directory.GetCurrentDirectory(), "questRequirements");
        }

        private static int[] GetQuestStatus(QuestStatus status)
        {
            switch (status)
            {
                case QuestStatus.Started:
                case QuestStatus.Success:
                case QuestStatus.Fail:
                    return new int[] { (int)status };
                case QuestStatus.StartedSuccess:
                    return new int[] { (int)QuestStatus.Started, (int)QuestStatus.Success };
                case QuestStatus.SuccessFail:
                    return new int[] { (int)QuestStatus.Success, (int)QuestStatus.Fail };
            }

            throw new Exception($"Unable to process quest status {status}");
        }

        /// <summary>
        /// Latest version of eft has changed the quest json structure, this method adds missing fields
        /// Mega hack as we dont have a full dump as of 30/06/2022
        /// </summary>
        /// <param name="quest">quest to add missing fields to</param>
        private static void AddMissingFields(KeyValuePair<string, Quest> quest)
        {
            //side
            if (String.IsNullOrEmpty(quest.Value.side))
            {
                quest.Value.side = "Pmc";
                LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a side of 'pmc'");
            }

            //changeQuestMessageText
            if (String.IsNullOrEmpty(quest.Value.changeQuestMessageText))
            {
                quest.Value.changeQuestMessageText = $"{quest.Value._id} changeQuestMessageText";
                LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a changeQuestMessageText value");
            }

            // findInRaid
            foreach (var success in quest.Value.rewards.Success)
            {
                if (string.Equals(success.type, "item", StringComparison.OrdinalIgnoreCase)
                    && success.findInRaid == null)
                {
                    success.findInRaid = true;
                    LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a success item reward findInRaid value of 'true'");
                }
            }
        }

        private static void AddMissingFailRewards(Quest originalQuest, KeyValuePair<string, Quest> quest)
        {
            foreach (var originalFailReward in originalQuest.rewards.Fail)
            {
                // already has a fail reward of same type and target, skip
                if (quest.Value.rewards.Fail.Any(x => x.type == originalFailReward.type && x.target == originalFailReward.target))
                {
                    continue;
                }

                quest.Value.rewards.Fail.Add(originalFailReward);
            }
        }

        private static void CopyExistingRandomIds(Quest originalQuest, Quest quest)
        {
            CopyRewardRandomIds(originalQuest.rewards.Started, quest.rewards.Started);
            CopyRewardRandomIds(originalQuest.rewards.Success, quest.rewards.Success);
            CopyRewardRandomIds(originalQuest.rewards.Fail, quest.rewards.Fail);

            CopyConditionRandomIds(originalQuest.conditions.AvailableForStart, quest.conditions.AvailableForStart);
        }

        private static void CopyRewardRandomIds(List<RewardStatus> originalRewards, List<RewardStatus> rewards)
        {
            foreach (var reward in rewards)
            {
                var originalReward = originalRewards.FirstOrDefault(x => x.id == reward.id);
                if (originalReward == null)
                {
                    LoggingHelpers.LogWarning($"Unable to find matching original reward for {reward.id}. Skipping.");
                    continue;
                }

                reward.target = originalReward.target;

                if (reward.items != null)
                {
                    foreach (var item in reward.items)
                    {
                        QuestRewardItem originalItem = originalReward.items.FirstOrDefault(x => x._tpl == item._tpl && x.slotId == item.slotId);
                        if (originalItem == null)
                        {
                            LoggingHelpers.LogWarning($"Unable to find matching original reward item for {reward.id}-{item._tpl}. Skipping");
                            continue;
                        }

                        item._id = originalItem._id;
                        item.parentId = originalItem.parentId;
                    }

                    // Above changes can cause the target and first items id to become mismatched
                    if (reward.items.FirstOrDefault()._id != reward.target)
                    {
                        reward.target = reward.items.FirstOrDefault()._id;
                    }
                }
            }
        }

        // Allow stripping all whitespace in a string, used for comparing _props.target, which may have differing whitespace but still match
        private static readonly Regex whitespace = new Regex(@"\s+");
        private static string StripAllWhitespace(string input)
        {
            if (input == null)
            {
                return "";
            }

            return whitespace.Replace(input, "");
        }

        private static void CopyConditionRandomIds(List<AvailableFor> originalConditions, List<AvailableFor> conditions)
        {
            foreach (var condition in conditions)
            {
                var originalCondition = originalConditions.FirstOrDefault(
                    x => x.conditionType == condition.conditionType &&
                        x.index == condition.index &&
                        StripAllWhitespace(x.target?.ToString()) == StripAllWhitespace(condition.target?.ToString()) &&
                        x.counter?.id == condition.counter?.id
                );

                if (originalCondition == null)
                {
                    LoggingHelpers.LogWarning($"Unable to find matching original condition for {condition.conditionType}-{StripAllWhitespace(condition.target?.ToString())}. Skipping.");
                    continue;
                }

                condition.id = originalCondition.id;
            }
        }

        /// <summary>
        /// Check original quest for start conditions and if missing from new quest, add them
        /// </summary>
        /// <param name="originalQuest"></param>
        /// <param name="questToUpdate">quest to add to output json</param>
        private static void AddMissingAvailableForStartConditions(Quest originalQuest, KeyValuePair<string, Quest> questToUpdate)
        {
            // Iterate over quest requirements in existing quest file
            foreach (var questRequirementToAdd in originalQuest.conditions.AvailableForStart.ToList())
            {
                //Exists already, skip
                //if (questToUpdate.Value.conditions.AvailableForStart.Any(
                //    x => x._parent == questRequirementToAdd._parent
                //    && x._props.target?.ToString() == questRequirementToAdd._props.target?.ToString())
                //    )
                //{
                //    continue;
                //}

                if (questToUpdate.Value.conditions.AvailableForStart.Any(x => string.Equals(x.conditionType, "quest", StringComparison.CurrentCultureIgnoreCase)))
                {
                    continue;
                }

                if (questRequirementToAdd.conditionType == "Quest")
                {
                    LoggingHelpers.LogInfo($"Quest {questToUpdate.Value.QuestName} missing AvailableForStart quest requirement, adding prereq of {questRequirementToAdd.target} {QuestHelper.GetQuestNameById(questRequirementToAdd.target?.ToString())}");

                    if (!questRequirementToAdd.availableAfter.HasValue)
                    {
                        questRequirementToAdd.availableAfter = 0;
                    }

                    if (questRequirementToAdd.visibilityConditions == null || !questRequirementToAdd.visibilityConditions.Any())
                    {
                        questRequirementToAdd.visibilityConditions = new List<object>();
                    }

                    questRequirementToAdd.index = GetNextIndex(questToUpdate.Value.conditions.AvailableForStart.LastOrDefault()?.index);

                }

                // Already exists, skip
                if (questToUpdate.Value.conditions.AvailableForStart
                    .Any(x => x.target?.ToString() == questRequirementToAdd.target?.ToString()
                    && x.conditionType == questRequirementToAdd.conditionType))
                {
                    continue;
                }

                questToUpdate.Value.conditions.AvailableForStart.Add(questRequirementToAdd);
            }

            if (questToUpdate.Value.conditions.AvailableForStart.Count(x => x.conditionType == "Quest") > 1)
            {
                LoggingHelpers.LogWarning($"Quest {questToUpdate.Value.QuestName} has {questToUpdate.Value.conditions.AvailableForStart.Count(x => x.conditionType == "Quest")} quest prereqs, is this correct?");
            }
        }

        /// <summary>
        /// Get a bsg happy guid, must be 24 chars long
        /// </summary>
        /// <param name="randomSalt"></param>
        /// <returns></returns>
        static string Sha256(string randomSalt)
        {
            var crypt = new System.Security.Cryptography.SHA256Managed();
            var hash = new System.Text.StringBuilder();
            byte[] crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(randomSalt));
            foreach (byte theByte in crypto)
            {
                hash.Append(theByte.ToString("x2"));
            }
            return hash.ToString().Substring(0, 24);
        }

        /// <summary>
        /// Look up the quests name by guid and add human readable string to quest object
        /// </summary>
        /// <param name="quest"></param>
        private static void AddQuestName(KeyValuePair<string, Quest> quest)
        {
            var questName = QuestHelper.GetQuestNameById(quest.Value._id); // special characters like ", brake the client when it parses it, gotta remove
            var rgx = new Regex("[^a-zA-Z0-9 -]");
            quest.Value.QuestName = rgx.Replace(questName, "");
        }

        /// <summary>
        /// Loop over live quests and use if it exists, otherwise use existing data
        /// </summary>
        private static List<Quest> GetMissingQuestsNotInLiveFile(Dictionary<string, Quest> existingQuests, QuestRoot liveQuestData, List<string> blacklistedQuests)
        {
            var missingQuestsToReturn = new List<Quest>();
            foreach (var quest in existingQuests.Values)
            {
                var liveQuest = liveQuestData.data.Find(x => x._id == quest._id);
                if (liveQuest is null)
                {
                    if (blacklistedQuests.Contains(quest._id))
                    {
                        LoggingHelpers.LogInfo($"Skipping quest: {quest.QuestName}");
                        continue;
                    }

                    missingQuestsToReturn.Add(quest);
                    LoggingHelpers.LogError($"ERROR Quest {quest._id} {QuestHelper.GetQuestNameById(quest._id)} missing in live file. Will use fallback quests.json");
                }
                else
                {
                    LoggingHelpers.LogSuccess($"SUCCESS Quest {quest._id} {QuestHelper.GetQuestNameById(quest._id)} found in live file.");
                }
            }

            return missingQuestsToReturn;
        }

        private static int GetNextIndex(int? previousIndex)
        {
            if (previousIndex == null)
            {
                return 0;
            }

            return previousIndex.Value + 1;
        }
    }
}