import { storeActions } from "@/constants/store-actions.js";
import store from "@/store";
import { dynamicStrings } from "@/constants/dynamic-strings";

export function fetchCmsContentForPage(fmgPage) {
    return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
        const pageDataFromCms = {};

        response.data.Result.forEach((widget) => {
            let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Name);

            // If we already have this widget, push it on the collection
            if (widgetWithReplacements.Name in pageDataFromCms) {
                pageDataFromCms[widgetWithReplacements.Name].push(widgetWithReplacements.Model);
                return;
            }

            pageDataFromCms[widgetWithReplacements.Name] = [widgetWithReplacements.Model];
        });

        Object.keys(pageDataFromCms).forEach((key) => {
            if (pageDataFromCms[key].length === 1) {
                pageDataFromCms[key] = pageDataFromCms[key][0];
            }
        });

        return pageDataFromCms;
    });
}

// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
    // Pull all matches out of the string.
    const regexExp = new RegExp("{([^{}]*?):([^{}]*?)}", "g");
    const regexMatches = [...str.matchAll(regexExp)];
    const globalStateMatches = regexMatches.filter((match) => {
        return match[1] === dynamicStrings.GLOBAL_STATE;
    });

    // Our final string value that will be built from the matches.
    let stringBuilder = "";
    for (const match of globalStateMatches) {
        const valueFromStore = getStoreValueFromString(match[2]);
        if (!valueFromStore) {
            return ""; // if we can't map our string to state data, return an empty string.
        }

        const stringWithReplacement = str.replace(match[0], valueFromStore);

        // If we still have values we need to substitute, call this function again.
        if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
            return mapStringToState(stringWithReplacement);
        }

        // Concatenate the string.
        stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
    }
    return stringBuilder.trimStart();
}

// Parent function for processWidgetItemForReplacement. This will loop through the parent
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
    const objWithReplacements = {
        Name: widgetName,
        Model: {},
    };

    Object.keys(widgetModel).forEach((key) => {
        let modelWithReplacements = processWidgetItemForReplacement(widgetModel, key);

        objWithReplacements.Model[key] = modelWithReplacements;
    });

    return objWithReplacements;
}

// This function will process the widget item and replace any global state variables with their values.
// This is a recursive function, it will call itself until it runs out of items to iterate on given the object.
function processWidgetItemForReplacement(widgetModel, key) {
    // If we have a string, and it needs to be replaced.
    if (typeof widgetModel[key] === "string") {
        widgetModel[key] = processIfStatements(
            widgetModel[key],
            dynamicStrings.GLOBAL_STATE,
            getStoreValueFromString
        );
        if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
            widgetModel[key] = mapStringToState(widgetModel[key]);
        }
        return widgetModel[key];
    }

    // If we have an object. array, etc
    if (typeof widgetModel[key] === "object" && Object.keys(widgetModel[key]).length) {
        Object.keys(widgetModel[key]).forEach((item) => {
            processWidgetItemForReplacement(widgetModel[key], item);
        });

        return widgetModel[key];
    }

    // If we have something else like a number, boolean, etc. just return it
    return widgetModel[key];
}

function getStoreValueFromString(str) {
    let storeOrStateObject = str.includes("getters") ? store : store.state;
    for (const s of str.split(".")) {
        if (storeOrStateObject[s] != undefined) {
            storeOrStateObject = storeOrStateObject[s];
        } else {
            return ""; // if we can't map our string to state data, return an empty string.
        }
    }
    return storeOrStateObject;
}

///////////////////////////////////
// If Statement Processing Logic //
///////////////////////////////////

/**
 * Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
 * @param {*} str string - Input string to be processed
 * @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
 * @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
 * @returns The processed string
 */
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
    const containsRelevantIfStatement = new RegExp("{if:" + ifConditionKeyword + ":.+?}", "g").test(
        str
    );
    if (!containsRelevantIfStatement) {
        return str;
    } else {
        const ifStatementRegexExpression = getIfStatementRegexExpression();
        const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
        const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
            ifStatementRegexMatches,
            ifConditionKeyword
        );
        executeIfStatementAndSetProcessedStrings(
            completeIfStatementArray,
            replacePlaceholderCallback
        );
        const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
        return processIfStatements(
            reconstructedPostProcessedString,
            ifConditionKeyword,
            replacePlaceholderCallback
        );
    }
}

function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
    let index = 0;
    for (const match of matches) {
        if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
            let interiorIndex = 0;
            let nestedLevel = 0;
            let elseStatementIndex = null;
            for (const interiorMatch of matches.slice(index + 1)) {
                if (interiorMatch.groups.isIfStatement) {
                    if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
                        break;
                    } else {
                        nestedLevel++;
                    }
                } else if (interiorMatch.groups.isElseStatement) {
                    if (!nestedLevel) {
                        elseStatementIndex = interiorIndex + 1;
                    }
                } else if (interiorMatch.groups.isEndStatement) {
                    if (nestedLevel) {
                        nestedLevel--;
                    } else {
                        const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
                        flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
                        return ifStatementArray;
                    }
                }
                interiorIndex++;
            }
        }
        index++;
    }
}

function flagMatchesForProcessing(matches, elseStatementIndex) {
    matches[0].isFlaggedForProcessing = true;
    matches[matches.length - 1].isFlaggedForProcessing = true;
    if (elseStatementIndex) {
        matches[elseStatementIndex].isFlaggedForProcessing = true;
    }
}

function joinProcessedRegexArray(regexMatches) {
    let processedString = "";
    regexMatches.forEach((match) => {
        const rawString = match[0];
        processedString += match.groups.processedString ?? rawString;
    });
    return processedString;
}

function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
    const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
    let isInsideDesiredBlock = ifCondition;

    ifStatementArray.forEach((entry) => {
        if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
            isInsideDesiredBlock = !ifCondition;
        } else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
            isInsideDesiredBlock = true;
        }
        setProcessedStringOnEntry(entry, isInsideDesiredBlock);
    });
}

function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
    if (!isInsideDesiredBlock) {
        entry.groups.processedString = "";
    } else {
        if (entry.groups.isIfStatement) {
            entry.groups.processedString = entry.isFlaggedForProcessing
                ? entry.groups.ifTrailingString
                : entry[0];
        } else if (entry.groups.isElseStatement) {
            entry.groups.processedString = entry.isFlaggedForProcessing
                ? entry.groups.elseTrailingString
                : entry[0];
        } else {
            entry.groups.processedString = entry.isFlaggedForProcessing
                ? entry.groups.endTrailingString
                : entry[0];
        }
    }
}

function getIfStatementRegexExpression() {
    // Matches but does not capture:
    // {if:...} or {else} or {end}
    const anyLogicOperatorNonCapture = "(?:{(?:end|else|if:.*?)})";
    // NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
    const matchStartOfString =
        "(?<processedString>^.+?)" + // Match and Capture all characters (lazy), cannot be empty
        "(?=(?:{if))"; // Looks ahead but does not capture {if
    const matchIfOperator =
        "(?<isIfStatement>{if:)" + // Match & Capture {if:
        "(?<ifConditionType>.*?):" + // Match all chars up to and including next ":" - Capture all chars up to ":"
        "(?<ifCondition>.*?)}" + // Match all chars up to and including next "}" - Capture all chars up to "}"
        "(?<ifTrailingString>.*?)" + // Match and Capture all characters (lazy), can be empty
        "(?=" +
        anyLogicOperatorNonCapture +
        ")"; // Looks ahead but does not capture the next logic operator
    const matchElseOperator =
        "(?<isElseStatement>{else})" + // Match & Capture {else}
        "(?<elseTrailingString>.*?)" + // Match & Capture all characters (lazy), can be empty
        "(?=" +
        anyLogicOperatorNonCapture +
        ")"; // Looks ahead but does not capture the next logic operator
    const matchEndOperator =
        "(?<isEndStatement>{end})" + // Match & Capture {end}
        "(?<endTrailingString>.*?)" + // Match & Capture all chracters (lazy), can be empty
        "(?=" +
        anyLogicOperatorNonCapture +
        "|$)"; // Looks ahead but does not capture the next logic operator
    // Combine all matching patterns, separated by "or" pipes
    return new RegExp(
        matchStartOfString +
            "|" +
            matchIfOperator +
            "|" +
            matchElseOperator +
            "|" +
            matchEndOperator,
        "g"
    );
}

//////////////////////////////////////////
// End of If Statement Processing Logic //
//////////////////////////////////////////

export function doesCopyContainRouterLink(copy) {
    return copy.includes(dynamicStrings.ROUTER_LINK);
}

export function doesCopyContainTextLink(copy) {
    return copy.includes(dynamicStrings.TEXT_LINK);
}

/**
 * splits copy on { ... } such as {routerlink: ...}
 * @returns array of strings
 */
export function splitCopyOnCMSPlaceHolder(copy) {
    return copy.split(/{(.*?)}/g).filter((str) => str.length > 0);
}

/**
 * Returns string2 of input following this pattern: {string1:string2,string3}
 * @returns string
 */
export function getRouterLinkRouteFromCopy(copy) {
    // sample input:      {routerLink:estimate,provide your VIN}
    // first split would return      'estimate,provide your VIN'
    // second split would return     'estimate'
    return copy.split(":")[1].split(",")[0];
}

export function getExternalLink(copy) {
    let url = copy.split(":")[1].split(",")[0];
    if (url.includes("https-")) {
        const prefixAdded = url.replace("https-", "https://");
        return prefixAdded;
    }

    return "#!";
}

/**
 * Returns string3 of input following this pattern: {string1:string2,string3}
 * @returns string
 */
export function getRouterLinkDisplayTextFromCopy(copy) {
    // sample input:     {routerLink:estimate,provide your VIN}
    // first split would return     'estimate,provide your VIN'
    // second split would return             'provide your VIN'
    return copy.split(":")[1].split(",")[1];
}

// Copy returned from the CMS that has newlines will return blocks wrapped in
//   <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
//   attributes present
export function splitCMSCopyOnParagraphTag(copy) {
    // filter removes empty strings that are a result of string.split with regex
    return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
}
