/**
 *
 * @param {any} str
 */
function log(str) {
    console.log(JSON.stringify(str, undefined, 4))
}

/**
 * convert object to string
 * @param {any} obj object
 * @returns {Object | undefined}
 */
function obj2str(obj) {
    if (Array.isArray(obj)) {
        return JSON.stringify(
            obj.map(function (idx) {
                return obj2str(idx)
            })
        )
    } else if (typeof obj === "object" && obj !== null) {
        var elements = Object.keys(obj)
            .sort()
            .map(function (key) {
                var val = obj2str(obj[key])
                if (val) {
                    return key + ":" + val
                }
            })

        var elementsCleaned = []
        for (var i = 0; i < elements.length; i++) {
            if (elements[i]) elementsCleaned.push(elements[i])
        }

        return "{" + elementsCleaned.join("|") + "}"
    }

    if (obj) return obj
}
var { LIGHTHOUSE_TOKEN } = require("../config")

/**
 * clear SSR cache
 */
function clearSSRCache() {
    var info = context.db.deleteMany("ssr", {})
    context.response.header("X-SSR-Cleared", info.removed)
}

/**
 * @param {{ [x: string]: any; }[]} dbObjs
 */
function calculateAverageDynamically(dbObjs) {
    const sumObj = {}
    let count = 0

    dbObjs.forEach((/** @type {{ [x: string]: any; }} */ obj) => {
        accumulate(obj, sumObj)
        count++
    })

    /**
     * @param {{ [x: string]: any; }} sourceObj
     * @param {{ [x: string]: any; }} targetObj
     */
    function accumulate(sourceObj, targetObj) {
        for (const key in sourceObj) {
            if (typeof sourceObj[key] === "number") {
                targetObj[key] = (targetObj[key] || 0) + sourceObj[key]
            } else if (typeof sourceObj[key] === "object" && sourceObj[key] !== null) {
                targetObj[key] = targetObj[key] || {}
                accumulate(sourceObj[key], targetObj[key])
            }
        }
    }

    /**
     * @param {{ [x: string]: any; }} targetObj
     */
    function average(targetObj) {
        for (const key in targetObj) {
            if (typeof targetObj[key] === "number") {
                targetObj[key] = targetObj[key] / count
            } else if (typeof targetObj[key] === "object") {
                average(targetObj[key])
            }
        }
    }

    average(sumObj)
    return sumObj
}

/**
 * @param {string} url
 */
function run(url) {
    const response = context.http
        .fetch(url, {
            timeout: 300,
            method: "GET",
            headers: {
                "Content-Type": "application/json",
            },
        })
        .body.json()
    // needs enough traffic to be collected
    const cruxMetrics = {
        "First Contentful Paint": response?.loadingExperience?.metrics?.FIRST_CONTENTFUL_PAINT_MS?.category,
        "First Input Delay": response?.loadingExperience?.metrics?.FIRST_INPUT_DELAY_MS?.category,
    }
    const lighthouse = response.lighthouseResult
    const lighthouseMetrics = {
        FCPS: lighthouse.audits["first-contentful-paint"].score * 100,
        FCPV: lighthouse.audits["first-contentful-paint"].numericValue / 1000,
        FMPS: lighthouse.audits["first-meaningful-paint"].score * 100,
        FMPV: lighthouse.audits["first-meaningful-paint"].numericValue / 1000,

        SIS: lighthouse.audits["speed-index"].score * 100,
        SIV: lighthouse.audits["speed-index"].numericValue / 1000,
        TTIS: lighthouse.audits["interactive"].score * 100,
        TTIV: lighthouse.audits["interactive"].numericValue / 1000,

        FPIDS: lighthouse.audits["max-potential-fid"].score * 100,
        FPIDV: lighthouse.audits["max-potential-fid"].numericValue / 1000,
    }

    let dbObject = {
        cruxMetrics,
        lighthouseMetrics,
        performance: Math.round(lighthouse.categories.performance.score * 100),
        accessibility: Math.round(lighthouse.categories.accessibility.score * 100),
        bestPractices: Math.round(lighthouse.categories["best-practices"].score * 100),
        seo: Math.round(lighthouse.categories.seo.score * 100),
    }
    return dbObject
}
function setUpQuery(subPath = "/") {
    const api = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
    let params = `category=performance&category=accessibility&category=best-practices&category=seo`

    const parameters = {
        url: encodeURIComponent(`https://allkids-erfurt.de/${subPath}`),
        key: LIGHTHOUSE_TOKEN,
    }

    let query = `${api}?`
    for (let key in parameters) {
        // @ts-ignore
        query += `${key}=${parameters[key]}&`
    }
    query += params // Append other parameters without URL encoding
    return query
}

var config = require("../config")
/**
 *
 * @param {HookContext} c
 * @param {string} filename
 * @param {string} locale
 * @returns {string}
 */
function tpl(c, filename, locale) {
    return c.tpl.execute(c.fs.readFile(filename), {
        context: c,
        config: config,
    })
}
const { operatorEmail, operatorName } = require("../config")
const { cryptchaSiteId } = require("../config-client")
const { postUserRegister } = require("./facebookRestAPI")

function sendOperatorRatingMail() {
    if (!context.data) context.data = {}
    let locale = context.request().query("locale")
    locale = locale ? locale : "de-DE"
    context.data.tibiLink = `project/${context.project().id}/collection/rating/edit/${context.data.id}` //projekt ID
    context.smtp.sendMail({
        to: operatorEmail,
        from: operatorEmail,
        fromName: operatorName,
        subject: tpl(context, "templates/operator_rating_subject.de-DE.txt", locale),
        html: tpl(context, "templates/operator_rating_body.de-DE.html", locale),
    })
}

/**@param {ProductRating} currentRating */
function validateAndModifyRating(currentRating) {
    // Fetch the rating object from the database

    /** @type {ProductRating} */ // @ts-ignore
    let oldRating = context.db.find("rating", {
        filter: { orderId: currentRating.bigcommerceOrderId, productId: currentRating.bigCommerceProductId },
    })[0]

    if (oldRating && (oldRating.status != "pending" || currentRating.status != "pending")) {
        //@ts-ignore
        oldRating.review_date = convertDateToUTCISO(oldRating.review_date)
        if (oldRating.status == "pending") {
            oldRating.status = currentRating.status
            if (currentRating.status == "approved") return currentRating
        }
        // if currentrating is approved but oldrating is not pending, disallow the change by returning the old rating
        return context?.user?.auth()?.id &&
            (currentRating?.status == "pending" || currentRating?.status == "rejected") &&
            (oldRating?.status == "approved" || oldRating?.status == "rejected")
            ? currentRating
            : oldRating
    }

    // If the status of the current rating is 'pending', or not provided
    if (currentRating.status == "pending" || !currentRating.status) {
        currentRating.status = "pending"
        if (!currentRating.review_date) {
            //@ts-ignore
            currentRating.review_date = new Date().toISOString()
        }
    }

    return currentRating
}

/** @param {ProductRating} [orderRating] */ // @ts-ignore
function productInsideOrder(orderRating) {
    /**@type {Order} */
    //@ts-ignore
    withAccount((login) => {
        let order = context.db.find("bigCommerceOrder", {
            filter: {
                bigCommerceId: orderRating?.bigcommerceOrderId,
                customerBigCommerceId: login.bigCommerceId,
            },
        })[0]
        if (!order) {
            throw {
                status: 404,
                data: {
                    message: "Order not found",
                },
            }
        }
    })
    let order = context.db.find("bigCommerceOrder", {
        filter: { bigCommerceId: orderRating?.bigcommerceOrderId },
    })[0]

    if (!order) throw { error: "No Order object with given ID.", status: 400 }
    const productIds = order.products?.map((product) => product.bigCommerceId)
    if (productIds.length == 0) throw { error: "No products inside the Order.", status: 400 }

    let productInRating = orderRating?.bigCommerceProductId
    let productInsideOrder = productIds.includes(productInRating)

    if (!productInsideOrder) throw { error: "Rated products are not inside the Order.", status: 400 }
}

/**
 *
 * @param {HookContext | string} c
 */
function getJwt(c, required = true) {
    let tokenString

    if (typeof c == "string") {
        tokenString = c
    } else {
        const authHeader = c.request().header("Authorization")

        if (!authHeader) {
            if (required) {
                throw {
                    status: 403,
                    error: "missing authorization header",
                    log: false,
                }
            } else {
                return null
            }
        }

        tokenString = authHeader.replace("Bearer ", "")
    }

    if (!tokenString) {
        if (required) {
            throw {
                status: 403,
                error: "token: is empty",
                log: false,
            }
        } else {
            return null
        }
    }

    const token = context.jwt.parse(tokenString, {
        secret: config.jwtSecret,
    })

    if (required && !token.valid) {
        throw {
            status: 403,
            error: "token: " + token.error,
            log: false,
        }
    }

    return token
}

/**
 * call callback with login clains
 *
 * @param {(claims: JWTLoginClaims) => void} callback
 * @param {boolean} required
 * @param {string} apiTokenName
 * @returns {boolean} void if backendAuth
 */
function withAccount(callback, required = true, apiTokenName = null, allowViaRefreshToken = false) {
    if (apiTokenName) {
        const r = context.request()
        if (r.header(apiTokenName)) {
            // checked via api collection config, so ensure it is given
            return
        }
    }

    const backendAuth = context.user.auth()

    if (!backendAuth) {
        // require authorization header with jwt
        let token
        /** @type {JWTLoginClaims} */ // @ts-ignore
        let loginClaims
        try {
            token = getJwt(context, required)
            // @ts-ignore
            loginClaims = token && token.claims
        } catch (e) {
            if (allowViaRefreshToken) {
                const rT = getRefreshToken()
                if (rT?.valid) {
                    /** @type {JWTRefreshClaims} */ // @ts-ignore
                    const refreshClaims = rT.claims
                    const accountId = refreshClaims && refreshClaims.tibiId
                    /** @type {Customer} */ // @ts-ignore
                    const customer = context.db.find("bigCommerceCustomer", {
                        filter: { _id: accountId },
                    })[0]
                    loginClaims = {
                        bigCommerceId: customer.bigCommerceId,
                        email: customer.email,
                        tibiId: customer.id,
                    }
                } else {
                    throw {
                        status: 403,
                        error: "token: invalid refresh token",
                        log: false,
                    }
                }
            } else {
                throw e
            }
        }

        // log(loginClaims)

        if (!loginClaims || !loginClaims.tibiId || !loginClaims.bigCommerceId || !loginClaims.email) {
            if (required) {
                throw {
                    status: 403,
                    error: "token: invalid claims",
                    log: false,
                }
            }
            return false
        }

        callback(loginClaims)
        return true
    }
}
/**
 */
function getRefreshToken() {
    const refreshToken = context.cookie.get("bkdfRefreshToken")
    if (refreshToken) {
        console.log("REFRESHFOUND")
        return getJwt(refreshToken, false)
    }
    return null
}

/**
 * @param {BigCommerceCustomer} customer
 */
function createTibiCustomer(customer) {
    const username = customer.form_fields.find((field) => field.name === "username")?.value
    const internalCustomer = context.db.create("bigCommerceCustomer", {
        bigCommerceId: customer.id,
        email: customer.email.toLowerCase(),
        username: username.toLowerCase(),
    })
    const orders = context.db.find("bigCommerceOrder", {
        filter: {
            customerBigCommerceId: customer.id,
        },
    })
    if (orders && orders.length) {
        orders.forEach((order) => {
            context.db.update("bigCommerceOrder", order.id, {
                customerTibiId: internalCustomer.id,
            })
        })
    }
    /*
    not necessary for now, as bigcommerce itself takes care of it
    try {
        postUserRegister()
    } catch (e) {
        console.error(e)
    }*/
}
/**
 * @param {BigCommerceCustomer} customer
 * @param {Customer} internalCustomer
 */
function updateTibiCustomer(customer, internalCustomer) {
    const username = customer.form_fields.find((field) => field.name === "username")?.value
    context.db.update("bigCommerceCustomer", internalCustomer.id, {
        email: customer.email.toLowerCase(),
        username: username,
    })
}

/**
 *
 * @param {number} status
 */

function statusIsValid(status) {
    return !!status && status < 400
}

function cryptchaCheck() {
    const solutionId = context.data._sId
    const solution = context.data._s

    const body = JSON.stringify({ solution: solution })
    const resp = context.http.fetch(
        "https://cryptcha.webmakers.de/api/command?siteId=" +
            cryptchaSiteId +
            "&cmd=check&clear=1&solutionId=" +
            solutionId,
        {
            method: "POST",
            body,
            headers: { "Content-Type": "application/json" },
        }
    )
    const j = resp.body.json()

    const corrent = j.status == "ok" && resp.status == 200
    if (!corrent) {
        throw {
            status: 400,
            log: false,
            error: "incorrect data",
        }
    }
    return true
}

module.exports = {
    log,
    clearSSRCache,
    obj2str,
    run,
    setUpQuery,
    calculateAverageDynamically,
    sendOperatorRatingMail,
    validateAndModifyRating,
    productInsideOrder,
    getJwt,
    withAccount,
    getRefreshToken,
    statusIsValid,
    createTibiCustomer,
    updateTibiCustomer,
    cryptchaCheck,
}
