import {
    ServerQueryParam,
    ServerQueryParamsJoin,
    ServerQueryQueryObject,
} from "../../../types";

type Param<T extends { [key: string]: any } = { [key: string]: any }> = {
    genObject?: ServerQueryParam<T>;
    tableName: string;
    dbFullName?: string;
};

type Return =
    | {
          string: string;
          values: string[];
      }
    | undefined;

/**
 * # SQL Query Generator
 * @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
 */
export default function sqlGenerator<
    T extends { [key: string]: any } = { [key: string]: any }
>({ tableName, genObject, dbFullName }: Param<T>): Return {
    if (!genObject) return undefined;

    const finalQuery = genObject.query ? genObject.query : undefined;

    const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined;

    const sqlSearhValues: string[] = [];

    const finalDbName = dbFullName ? `${dbFullName}.` : "";

    /**
     * # Generate Query
     */
    function genSqlSrchStr({
        queryObj,
        join,
        field,
    }: {
        queryObj: ServerQueryQueryObject[string];
        join?: ServerQueryParamsJoin[];
        field?: string;
    }) {
        const finalFieldName = (() => {
            if (queryObj?.tableName) {
                return `${finalDbName}${queryObj.tableName}.${field}`;
            }
            if (join) {
                return `${finalDbName}${tableName}.${field}`;
            }
            return field;
        })();

        let str = `${finalFieldName}=?`;

        if (
            typeof queryObj.value == "string" ||
            typeof queryObj.value == "number"
        ) {
            const valueParsed = String(queryObj.value);

            if (queryObj.equality == "LIKE") {
                str = `LOWER(${finalFieldName}) LIKE LOWER('%${valueParsed}%')`;
            } else if (queryObj.equality == "NOT EQUAL") {
                str = `${finalFieldName} != ?`;
                sqlSearhValues.push(valueParsed);
            } else {
                sqlSearhValues.push(valueParsed);
            }
        } else if (Array.isArray(queryObj.value)) {
            const strArray: string[] = [];
            queryObj.value.forEach((val) => {
                const valueParsed = val;
                if (queryObj.equality == "LIKE") {
                    strArray.push(
                        `LOWER(${finalFieldName}) LIKE LOWER('%${valueParsed}%')`
                    );
                } else if (queryObj.equality == "NOT EQUAL") {
                    strArray.push(`${finalFieldName} != ?`);
                    sqlSearhValues.push(valueParsed);
                } else {
                    strArray.push(`${finalFieldName} = ?`);
                    sqlSearhValues.push(valueParsed);
                }
            });

            str = "(" + strArray.join(` ${queryObj.operator || "AND"} `) + ")";
        }

        return str;
    }

    const sqlSearhString = queryKeys?.map((field) => {
        const queryObj = finalQuery?.[field];
        if (!queryObj) return;

        if (queryObj.__query) {
            const subQueryGroup = queryObj.__query;

            const subSearchKeys = Object.keys(subQueryGroup);
            const subSearchString = subSearchKeys.map((_field) => {
                const newSubQueryObj = subQueryGroup?.[_field];

                return genSqlSrchStr({
                    queryObj: newSubQueryObj,
                    field: _field,
                    join: genObject.join,
                });
            });

            return (
                "(" +
                subSearchString.join(` ${queryObj.operator || "AND"} `) +
                ")"
            );
        }

        return genSqlSrchStr({ queryObj, field, join: genObject.join });
    });

    function generateJoinStr(
        /** @type {import("../../../types").ServerQueryParamsJoinMatchObject} */ mtch: import("../../../types").ServerQueryParamsJoinMatchObject,
        /** @type {import("../../../types").ServerQueryParamsJoin} */ join: import("../../../types").ServerQueryParamsJoin
    ) {
        return `${finalDbName}${
            typeof mtch.source == "object" ? mtch.source.tableName : tableName
        }.${
            typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source
        }=${(() => {
            if (mtch.targetLiteral) {
                return `'${mtch.targetLiteral}'`;
            }

            if (join.alias) {
                return `${finalDbName}${
                    typeof mtch.target == "object"
                        ? mtch.target.tableName
                        : join.alias
                }.${
                    typeof mtch.target == "object"
                        ? mtch.target.fieldName
                        : mtch.target
                }`;
            }

            return `${finalDbName}${
                typeof mtch.target == "object"
                    ? mtch.target.tableName
                    : join.tableName
            }.${
                typeof mtch.target == "object"
                    ? mtch.target.fieldName
                    : mtch.target
            }`;
        })()}`;
    }

    let queryString = (() => {
        let str = "SELECT";
        if (genObject.selectFields?.[0]) {
            if (genObject.join) {
                str += ` ${genObject.selectFields
                    ?.map((fld) => `${finalDbName}${tableName}.${fld}`)
                    .join(",")}`;
            } else {
                str += ` ${genObject.selectFields?.join(",")}`;
            }
        } else {
            if (genObject.join) {
                str += ` ${finalDbName}${tableName}.*`;
            } else {
                str += " *";
            }
        }

        if (genObject.join) {
            /** @type {string[]} */
            const existingJoinTableNames: string[] = [tableName];

            str +=
                "," +
                genObject.join
                    .map((joinObj) => {
                        const joinTableName = joinObj.alias
                            ? joinObj.alias
                            : joinObj.tableName;

                        if (existingJoinTableNames.includes(joinTableName))
                            return null;
                        existingJoinTableNames.push(joinTableName);

                        if (joinObj.selectFields) {
                            return joinObj.selectFields
                                .map((selectField) => {
                                    if (typeof selectField == "string") {
                                        return `${finalDbName}${joinTableName}.${selectField}`;
                                    } else if (typeof selectField == "object") {
                                        let aliasSelectField = selectField.count
                                            ? `COUNT(${finalDbName}${joinTableName}.${selectField.field})`
                                            : `${finalDbName}${joinTableName}.${selectField.field}`;
                                        if (selectField.alias)
                                            aliasSelectField += ` AS ${selectField.alias}`;
                                        return aliasSelectField;
                                    }
                                })
                                .join(",");
                        } else {
                            return `${finalDbName}${joinTableName}.*`;
                        }
                    })
                    .filter((_) => Boolean(_))
                    .join(",");
        }

        str += ` FROM ${finalDbName}${tableName}`;

        if (genObject.join) {
            str +=
                " " +
                genObject.join
                    .map((join) => {
                        return (
                            join.joinType +
                            " " +
                            (join.alias
                                ? `${finalDbName}${join.tableName}` +
                                  " " +
                                  join.alias
                                : `${finalDbName}${join.tableName}`) +
                            " ON " +
                            (() => {
                                if (Array.isArray(join.match)) {
                                    return (
                                        "(" +
                                        join.match
                                            .map((mtch) =>
                                                generateJoinStr(mtch, join)
                                            )
                                            .join(
                                                join.operator
                                                    ? ` ${join.operator} `
                                                    : " AND "
                                            ) +
                                        ")"
                                    );
                                } else if (typeof join.match == "object") {
                                    return generateJoinStr(join.match, join);
                                }
                            })()
                        );
                    })
                    .join(" ");
        }

        return str;
    })();

    if (sqlSearhString?.[0] && sqlSearhString.find((str) => str)) {
        const stringOperator = genObject?.searchOperator || "AND";
        queryString += ` WHERE ${sqlSearhString.join(` ${stringOperator} `)} `;
    }

    if (genObject.order)
        queryString += ` ORDER BY ${
            genObject.join
                ? `${finalDbName}${tableName}.${String(genObject.order.field)}`
                : String(genObject.order.field)
        } ${genObject.order.strategy}`;

    if (genObject.limit) queryString += ` LIMIT ${genObject.limit}`;
    if (genObject.offset) queryString += ` OFFSET ${genObject.offset}`;

    return {
        string: queryString,
        values: sqlSearhValues,
    };
}
