const ControllerMaster = require('./controller-master.js');
const Database = require('../database/database.js');
const Logger = require('../logger.js');

const PgFormat = require('pg-format');

// C

// R

async function SearchByTag(fuzzyTag) {
    await Database.Query('BEGIN TRANSACTION;');
    const dbres = await Database.Query(`
        SELECT lego_set.id, lego_set.name, tag.name AS "tag", inv.price, inv.new_price AS "discount", inv.stock
        FROM lego_set
            LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
            LEFT JOIN tag AS tag ON tags.tag = tag.id
            LEFT JOIN lego_set_inventory AS inv ON inv.set_id = lego_set.id
        WHERE tag.name ~* $1
    `, [fuzzyTag]).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'Sets not found',
            long: 'The sets you are looking for do not exist',
        };
    }

    let sets = dbres.rows;
    // combine tags into a single array
    for (const set of sets) {
        set.type = 'set';
        set.tags = [];
    }

    // combine (joined) rows into a single array
    sets = sets.reduce((arr, current) => {
        if (!arr.some(item => item.id === current.id)) {
            arr.push(current);
        }

        arr.find(item => item.id === current.id).tags.push(current.tag);
        return arr;
    }, []);

    return sets;
}

async function Search(fuzzyStrings) {
    await Database.Query('BEGIN TRANSACTION;');
    const dbres = await Database.Query(PgFormat(`
        SELECT lego_set.id, lego_set.name, tag.name AS "tag", inv.price, inv.new_price AS "discount", inv.stock
        FROM lego_set
            LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
            LEFT JOIN tag AS tag ON tags.tag = tag.id
            LEFT JOIN lego_set_inventory AS inv ON inv.set_id = lego_set.id
        WHERE lego_set.id ~* ANY(ARRAY[%L]) OR lego_set.name ~* ANY(ARRAY[%L]) OR tag.name ~* ANY(ARRAY[%L])
    `, fuzzyStrings, fuzzyStrings, fuzzyStrings), []).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'Set not found',
            long: 'The sets you are looking for do not exist',
        };
    }

    // order by levenshtine distance
    let sets = dbres.rows;
    sets.sort((a, b) => {
        const aName = a.name.toLowerCase();
        const bName = b.name.toLowerCase();
        const aTag = a.tag.toLowerCase();
        const bTag = b.tag.toLowerCase();
        const aFuzzy = fuzzyStrings[0].toLowerCase();
        const bFuzzy = fuzzyStrings[0].toLowerCase();

        const aDist = ControllerMaster.LevenshteinDistance(aName, aFuzzy);
        const bDist = ControllerMaster.LevenshteinDistance(bName, bFuzzy);
        const aTagDist = ControllerMaster.LevenshteinDistance(aTag, aFuzzy);
        const bTagDist = ControllerMaster.LevenshteinDistance(bTag, bFuzzy);

        if (aDist < bDist) {
            return -1;
        } else if (aDist > bDist) {
            return 1;
        } else {
            if (aTagDist < bTagDist) {
                return -1;
            } else if (aTagDist > bTagDist) {
                return 1;
            } else {
                return 0;
            }
        }
    });

    // combine tags into a single array
    for (const set of sets) {
        set.type = 'set';
        set.tags = [];
    }

    // combine (joined) rows into a single array
    sets = sets.reduce((arr, current) => {
        if (!arr.some(item => item.id === current.id)) {
            arr.push(current);
        }

        arr.find(item => item.id === current.id).tags.push(current.tag);
        return arr;
    }, []);

    return sets;
}

async function SumPrices(setsArray, quantityArray) {
    await Database.Query('BEGIN TRANSACTION;');
    const dbres = await Database.Query(PgFormat(`
        SELECT COALESCE(new_price, price) AS "price"
        FROM lego_set
            LEFT JOIN lego_set_inventory AS set_inventory ON set_inventory.set_id = lego_set.id
        WHERE lego_set.id IN (%L);
    `, setsArray), []).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'Bricks not found',
            long: 'The bricks you are looking for do not exist',
        };
    }

    let sum = 0;
    for (let i = 0; i < dbres.rows.length; i++) {
        sum += parseFloat(dbres.rows[i].price) * parseFloat(quantityArray[i]);
    }

    return sum;
}

async function GetSet(setId) {
    await Database.Query('BEGIN TRANSACTION;');
    const dbres = await Database.Query(`
        SELECT lego_set.id, lego_set.name, description, tag.name AS "tag",
            set_contents.brick_id, set_contents.amount, inv.price,
            date_released, weight, dimensions_x, dimensions_y, dimensions_z,
            new_price AS "discount", inv.stock, inv.last_updated AS "last_stock_update"
        FROM lego_set
            LEFT JOIN lego_set_inventory AS inv ON inv.set_id = lego_set.id
            LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
            LEFT JOIN tag AS tag ON tags.tag = tag.id
            LEFT JOIN set_descriptor AS set_contents ON set_contents.set_id = lego_set.id
        WHERE lego_set.id = $1;
    `, [setId]).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'Set not found',
            long: 'The set you are looking for does not exist',
        };
    }

    const tags = dbres.rows.reduce((acc, cur) => {
        acc.add(cur.tag);
        return acc;
    }, new Set());

    const bricks = dbres.rows.reduce((acc, cur) => {
        acc[cur.brick_id] = cur.amount;
        return acc;
    }, {});

    const set = dbres.rows[0];
    set.includedBricks = bricks;
    set.tags = Array.from(tags);
    set.type = 'set';

    return set;
}

async function GetSets(page, resPerPage) {
    await Database.Query('BEGIN TRANSACTION;');
    const countRes = await Database.Query('SELECT COUNT (*) FROM lego_set;');
    const dbres = await Database.Query(`
        SELECT
            lego_set.id, lego_set.name, price, new_price AS "discount", tag.name AS "tag", inv.stock
        FROM lego_set
            LEFT JOIN lego_set_inventory as inv ON lego_set.id = inv.set_id
            LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
            LEFT JOIN tag AS tag ON tags.tag = tag.id
        ORDER BY id ASC
        LIMIT $1
        OFFSET $2;`,
    [resPerPage, page * resPerPage]).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    const total = parseInt(countRes.rows[0].count);

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'No sets found',
            long: 'There are no sets to display',
        };
    }

    let sets = dbres.rows;

    for (const set of sets) {
        set.type = 'set';
        set.tags = [];
    }


    // combine (joined) rows into a single array
    sets = sets.reduce((arr, current) => {
        if (!arr.some(item => item.id === current.id)) {
            arr.push(current);
        }

        arr.find(item => item.id === current.id).tags.push(current.tag);
        return arr;
    }, []);

    return { total, sets };
}

async function GetSetsByDate(page, resPerPage) {
    await Database.Query('BEGIN TRANSACTION;');
    const countRes = await Database.Query('SELECT COUNT (*) FROM lego_set;');
    const dbres = await Database.Query(`
        SELECT
            lego_set.id, lego_set.name, price, new_price AS "discount", tag.name AS "tag", date_released, inv.stock
        FROM lego_set
            LEFT JOIN lego_set_inventory as inv ON lego_set.id = inv.set_id
            LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
            LEFT JOIN tag AS tag ON tags.tag = tag.id
        ORDER BY date_released::int DESC
        LIMIT $1
        OFFSET $2;`,
    [resPerPage, page * resPerPage]).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');

    const total = parseInt(countRes.rows[0].count);

    // validate database response
    if (dbres.rows.length === 0) {
        return {
            error: 'No sets found',
            long: 'There are no sets to display',
        };
    }

    let sets = dbres.rows;

    for (const set of sets) {
        set.type = 'set';
        set.tags = [];
    }

    // combine (joined) rows into a single array
    sets = sets.reduce((arr, current) => {
        if (!arr.some(item => item.id === current.id)) {
            arr.push(current);
        }

        arr.find(item => item.id === current.id).tags.push(current.tag);
        return arr;
    }, []);

    return { total, sets };
}

// U

async function RemoveSetStock(setIdList, quantityList) {
    await Database.Query('BEGIN TRANSACTION;');
    for (let i = 0; i < setIdList.length; i++) {
        const dbres = await Database.Query(`
            UPDATE lego_set_inventory
            SET stock = stock - $1
            WHERE set_id = $2;
        `, [quantityList[i], setIdList[i]]).catch(() => {
            return {
                error: 'Database error',
            };
        });
        if (dbres.error) {
            Database.Query('ROLLBACK TRANSACTION;');
            Logger.Error(dbres.error);
            return dbres;
        }
    }
    Database.Query('COMMIT TRANSACTION;');
    return true;
}

async function UpdateStock(setId, newStock) {
    await Database.Query('BEGIN TRANSACTION;');
    const dbres = await Database.Query(`
        UPDATE lego_set_inventory
        SET stock = $1
        WHERE set_id = $2;
    `, [newStock, setId]).catch(() => {
        return {
            error: 'Database error',
        };
    });
    if (dbres.error) {
        Database.Query('ROLLBACK TRANSACTION;');
        Logger.Error(dbres.error);
        return dbres;
    }
    Database.Query('COMMIT TRANSACTION;');
    return true;
}

// D

module.exports = {
    SearchByTag,
    Search,
    SumPrices,
    GetSet,
    GetSets,
    GetSetsByDate,
    RemoveSetStock,
    UpdateStock,
};
