using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using DSharpPlus.Entities;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;

namespace SupportBoi.Interviews;

public enum StepType
{
    // TODO: Support multiselector as separate type
    ERROR,
    INTERVIEW_END = 1,
    BUTTONS,
    TEXT_SELECTOR,
    USER_SELECTOR,
    ROLE_SELECTOR,
    MENTIONABLE_SELECTOR, // User or role
    CHANNEL_SELECTOR,
    TEXT_INPUT,
    REFERENCE_END
}

public enum ButtonType
{
    PRIMARY,
    SECONDARY,
    SUCCESS,
    DANGER
}

public class ReferencedInterviewStep
{
    [JsonProperty("id")]
    public string id;

    // If this step is on a button, give it this style.
    [JsonConverter(typeof(StringEnumConverter))]
    [JsonProperty("button-style")]
    public ButtonType? buttonStyle;

    // If this step is in a selector, give it this description.
    [JsonProperty("selector-description")]
    public string selectorDescription;

    // Runs at the end of the reference
    [JsonProperty("after-reference-step")]
    public InterviewStep afterReferenceStep;

    public ReferencedInterviewStep() { }

    public ReferencedInterviewStep(ReferencedInterviewStep other)
    {
        id = other.id;
        buttonStyle = other.buttonStyle;
        selectorDescription = other.selectorDescription;

        if (other.afterReferenceStep != null)
        {
            afterReferenceStep = new InterviewStep(other.afterReferenceStep);
        }
    }

    public bool TryGetReferencedStep(Dictionary<string, InterviewStep> definitions, out InterviewStep step, bool ignoreReferenceParameters = false)
    {
        if (!definitions.TryGetValue(id, out InterviewStep tempStep))
        {
            Logger.Error("Could not find referenced step '" + id + "' in interview.");
            step = null;
            return false;
        }

        step = new InterviewStep(tempStep);
        if (!ignoreReferenceParameters)
        {
            step.buttonStyle = buttonStyle;
            step.selectorDescription = selectorDescription;
            step.afterReferenceStep = afterReferenceStep;
        }

        return true;
    }
}

// A tree of steps representing an interview.
// The tree is generated by the config file when a new ticket is opened or the restart interview command is used.
// Additional components not specified in the config file are populated as the interview progresses.
// The entire interview tree is serialized and stored in the database to record responses as they are made.
public class InterviewStep
{
    public static int DefaultMaxFieldLength => 1024;
    public static int DefaultMinFieldLength => 0;

    // Title of the message embed.
    [JsonProperty("heading")]
    public string heading;

    // Message contents sent to the user.
    [JsonProperty("message")]
    public string message;

    // The type of message.
    [JsonConverter(typeof(StringEnumConverter))]
    [JsonProperty("step-type")]
    public StepType stepType;

    // Colour of the message embed.
    [JsonProperty("color")]
    public string color = "CYAN";

    // Used as label for this answer in the post-interview summary.
    [JsonProperty("summary-field")]
    public string summaryField;

    // If this step is on a button, give it this style.
    [JsonConverter(typeof(StringEnumConverter))]
    [JsonProperty("button-style")]
    public ButtonType? buttonStyle;

    // If this step is a selector, give it this placeholder.
    [JsonProperty("selector-placeholder")]
    public string selectorPlaceholder;

    // If this step is in a selector, give it this description.
    [JsonProperty("selector-description")]
    public string selectorDescription;

    // The maximum length of a text input.
    [JsonProperty("max-length")]
    public int? maxLength;

    // The minimum length of a text input.
    [JsonProperty("min-length")]
    public int? minLength;

    // Adds a summary to the message.
    [JsonProperty("add-summary")]
    public bool? addSummary;

    // References to steps defined elsewhere in the template
    [JsonProperty("step-references")]
    public Dictionary<string, ReferencedInterviewStep> references = new();

    // If set will merge answers with the delimiter, otherwise will overwrite
    [JsonProperty("answer-delimiter")]
    public string answerDelimiter;

    // Possible questions to ask next, an error message, or the end of the interview.
    [JsonProperty("steps")]
    public Dictionary<string, InterviewStep> steps = new();

    // ////////////////////////////////////////////////////////////////////////////
    // The following parameters are populated by the bot, not the json template. //
    // ////////////////////////////////////////////////////////////////////////////

    // The ID of this message where the bot asked this question.
    [JsonProperty("message-id")]
    public ulong messageID;

    // The contents of the user's answer.
    [JsonProperty("answer")]
    public string answer;

    // The ID of the user's answer message if this is a TEXT_INPUT type.
    [JsonProperty("answer-id")]
    public ulong answerID;

    // Any extra messages generated by the bot that should be removed when the interview ends.
    [JsonProperty("related-message-ids")]
    public List<ulong> relatedMessageIDs;

    // This is only set when the user gets to a referenced step
    [JsonProperty("after-reference-step")]
    public InterviewStep afterReferenceStep = null;

    public InterviewStep() { }

    public InterviewStep(InterviewStep other)
    {
        heading = other.heading;
        message = other.message;
        stepType = other.stepType;
        color = other.color;
        summaryField = other.summaryField;
        buttonStyle = other.buttonStyle;
        selectorPlaceholder = other.selectorPlaceholder;
        selectorDescription = other.selectorDescription;
        maxLength = other.maxLength;
        minLength = other.minLength;
        addSummary = other.addSummary;
        answerDelimiter = other.answerDelimiter;
        messageID = other.messageID;
        answer = other.answer;
        answerID = other.answerID;
        relatedMessageIDs = other.relatedMessageIDs;
        afterReferenceStep = other.afterReferenceStep;

        foreach (KeyValuePair<string,InterviewStep> childStep in other.steps ?? [])
        {
            steps.Add(childStep.Key, new InterviewStep(childStep.Value));
        }

        foreach (KeyValuePair<string,ReferencedInterviewStep> reference in other.references ?? [])
        {
            references.Add(reference.Key, new ReferencedInterviewStep(reference.Value));
        }
    }

    public bool TryGetCurrentStep(out InterviewStep currentStep)
    {
        bool result = TryGetTakenSteps(out List<InterviewStep> previousSteps);
        currentStep = previousSteps.FirstOrDefault();
        return result;
    }

    public bool TryGetTakenSteps(out List<InterviewStep> previousSteps)
    {
        // This object has not been initialized, we have checked too deep.
        if (messageID == 0)
        {
            previousSteps = null;
            return false;
        }

        // Check children.
        foreach (KeyValuePair<string,InterviewStep> childStep in steps)
        {
            // This child either is the one we are looking for or contains the one we are looking for.
            if (childStep.Value.TryGetTakenSteps(out previousSteps))
            {
                previousSteps.Add(this);
                return true;
            }
        }

        // This object is the deepest object with a message ID set, meaning it is the latest asked question.
        previousSteps = [this];
        return true;
    }

    public void GetSummary(ref OrderedDictionary summary)
    {
        if (messageID == 0)
        {
            return;
        }

        if (!string.IsNullOrWhiteSpace(summaryField) && !string.IsNullOrWhiteSpace(answer))
        {
            if (answerDelimiter != null && summary.Contains(summaryField))
            {
                if ((summary[summaryField] + answerDelimiter + answer).Length < 1024)
                {
                    summary[summaryField] += answerDelimiter + answer;
                }
                else
                {
                   Logger.Error("Tried to add answer '" + answer + "' to summary field '" + summaryField + "' but it was too long.");
                }
            }
            else
            {
                summary[summaryField] = answer;
            }
        }

        // This will always contain exactly one or zero children.
        foreach (KeyValuePair<string, InterviewStep> step in steps)
        {
            step.Value.GetSummary(ref summary);
        }
    }

    public void GetMessageIDs(ref List<ulong> messageIDs)
    {
        if (messageID != 0)
        {
            messageIDs.Add(messageID);
        }

        if (answerID != 0)
        {
            messageIDs.Add(answerID);
        }

        if (relatedMessageIDs != null)
        {
            messageIDs.AddRange(relatedMessageIDs);
        }

        // This will always contain exactly one or zero children.
        foreach (KeyValuePair<string, InterviewStep> step in steps)
        {
            step.Value.GetMessageIDs(ref messageIDs);
        }
    }

    public void AddRelatedMessageIDs(params ulong[] messageIDs)
    {
        if (relatedMessageIDs == null)
        {
            relatedMessageIDs = messageIDs.ToList();
        }
        else
        {
            relatedMessageIDs.AddRange(messageIDs);
        }
    }

    // Gets all steps in the interview tree, including after-reference-steps but not referenced steps
    public void GetAllSteps(ref List<InterviewStep> allSteps)
    {
        allSteps.Add(this);
        foreach (KeyValuePair<string, InterviewStep> step in steps)
        {
            step.Value.GetAllSteps(ref allSteps);
        }

        foreach (KeyValuePair<string,ReferencedInterviewStep> reference in references)
        {
            reference.Value.afterReferenceStep?.GetAllSteps(ref allSteps);
        }
    }

    public void Validate(ref List<string> errors,
                         ref List<string> warnings,
                         string stepID,
                         Dictionary<string, InterviewStep> definitions,
                         InterviewStep parent = null)
    {
        if (answerDelimiter != null && string.IsNullOrWhiteSpace(summaryField))
        {
            warnings.Add("An answer-delimiter has no effect without a summary-field.\n\n> " + stepID + ".answer-delimiter");
        }

        // TODO: Add url button here when implemented
        if (stepType is StepType.REFERENCE_END)
        {
            if (!string.IsNullOrWhiteSpace(message))
            {
                warnings.Add("The message parameter on '" + stepType + "' steps have no effect.\n\n> " + stepID + ".message");
            }
        }
        else
        {
            if (string.IsNullOrWhiteSpace(message))
            {
                errors.Add("'" + stepType + "' steps must have a message parameter.\n\n> " + stepID + ".message");
            }
        }

        if (stepType is StepType.ERROR or StepType.INTERVIEW_END or StepType.REFERENCE_END)
        {
            if (steps.Count > 0 || references.Count > 0)
            {
                warnings.Add("Steps of the type '" + stepType + "' cannot have child steps.\n\n> " + stepID + ".step-type");
            }

            if (!string.IsNullOrWhiteSpace(summaryField))
            {
                warnings.Add("Steps of the type '" + stepType + "' cannot have summary field names.\n\n> " + stepID + ".summary-field");
            }
        }
        else if (steps.Count == 0 && references.Count == 0)
        {
            errors.Add("Steps of the type '" + stepType + "' must have at least one child step.\n\n> " + stepID + ".step-type");
        }

        foreach (KeyValuePair<string, ReferencedInterviewStep> reference in references)
        {
            if (!reference.Value.TryGetReferencedStep(definitions, out InterviewStep referencedStep, true))
            {
                errors.Add("'" + reference.Value.id + "' does not exist in the step definitions.\n\n> " + FormatJSONKey(stepID + ".step-references", reference.Key));
            }
            else if (reference.Value.afterReferenceStep == null)
            {
                List<InterviewStep> allChildSteps = [];
                referencedStep.GetAllSteps(ref allChildSteps);
                if (allChildSteps.Any(s => s.stepType == StepType.REFERENCE_END))
                {
                    errors.Add("The '" + FormatJSONKey(stepID + ".step-references", reference.Key) + "' reference needs an after-reference-step as the '" + reference.Value.id + "' definition contains a REFERENCE_END step.");
                }
            }
        }

        if (parent?.stepType is not StepType.BUTTONS && buttonStyle != null)
        {
            warnings.Add("Button styles have no effect on child steps of a '" + parent?.stepType + "' step.\n\n> " + stepID + ".button-style");
        }

        if (parent?.stepType is not StepType.TEXT_SELECTOR && selectorDescription != null)
        {
            warnings.Add("Selector descriptions have no effect on child steps of a '" + parent?.stepType + "' step.\n\n> " + stepID + ".selector-description");
        }

        if (stepType is not StepType.TEXT_SELECTOR && selectorPlaceholder != null)
        {
            warnings.Add("Selector placeholders have no effect on steps of the type '" + stepType + "'.\n\n> " + stepID + ".selector-placeholder");
        }

        if (stepType is not StepType.TEXT_INPUT && maxLength != null)
        {
            warnings.Add("Max length has no effect on steps of the type '" + stepType + "'.\n\n> " + stepID + ".max-length");
        }

        if (stepType is not StepType.TEXT_INPUT && minLength != null)
        {
            warnings.Add("Min length has no effect on steps of the type '" + stepType + "'.\n\n> " + stepID + ".min-length");
        }

        foreach (KeyValuePair<string,InterviewStep> step in steps)
        {
            step.Value.Validate(ref errors, ref warnings, FormatJSONKey(stepID + ".steps", step.Key), definitions, this);
        }
    }

    private string FormatJSONKey(string parentPath, string key)
    {
        // The JSON schema error messages use this format for the JSON path, so we use it in the validation too.
        return parentPath + (key.ContainsAny('.', ' ', '[', ']', '(', ')', '/', '\\')
            ? "['" + key + "']"
            : "." + key);
    }

    public DiscordButtonStyle GetButtonStyle()
    {
        return GetButtonStyle(buttonStyle);
    }

    public static DiscordButtonStyle GetButtonStyle(ButtonType? buttonStyle)
    {
        return buttonStyle switch
        {
            ButtonType.PRIMARY   => DiscordButtonStyle.Primary,
            ButtonType.SECONDARY => DiscordButtonStyle.Secondary,
            ButtonType.SUCCESS   => DiscordButtonStyle.Success,
            ButtonType.DANGER    => DiscordButtonStyle.Danger,
            _                    => DiscordButtonStyle.Secondary
        };
    }

    public class StripInternalPropertiesResolver : DefaultContractResolver
    {
        private static readonly HashSet<string> ignoreProps =
        [
            "message-id",
            "answer",
            "answer-id",
            "related-message-ids"
        ];

        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(member, memberSerialization);
            if (ignoreProps.Contains(property.PropertyName))
            {
                property.ShouldSerialize = _ => false;
            }
            return property;
        }
    }
}

public class Interview(ulong channelID, InterviewStep interviewRoot, Dictionary<string, InterviewStep> definitions)
{
    public ulong channelID = channelID;
    public InterviewStep interviewRoot = interviewRoot;
    public Dictionary<string, InterviewStep> definitions = definitions;
}

public class Template(ulong categoryID, InterviewStep interview, Dictionary<string, InterviewStep> definitions)
{
    [JsonProperty("category-id", Required = Required.Always)]
    public ulong categoryID = categoryID;

    [JsonProperty("interview", Required = Required.Always)]
    public InterviewStep interview = interview;

    [JsonProperty("definitions", Required = Required.Default)]
    public Dictionary<string, InterviewStep> definitions = definitions;
}