/* ----------------------------------------------------------------------------

 Question Server
 GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.

 ------------------------------------------------------------------------- */

// https://www.sqlitetutorial.net/sqlite-nodejs/
// https://github.com/JoshuaWise/better-sqlite3/blob/HEAD/docs/api.md

export default {
    GetDB: GetDB,
    AddColumn: AddColumn,
    TableInfo: TableInfo,
    Update: Update,
    Delete: Delete,
    CreateTable: CreateTable,
    SelectAll: SelectAll,
    Select: Select,
    Insert: Insert,
    CloseDB: CloseDB,
    runStatement: runStatement,
    sanitizeQuery: sanitizeQuery,
}

import Sqlite, { Database, RunResult } from 'better-sqlite3'
import logger from '../utils/logger'
import utils from '../utils/utils'

const debugLog = process.env.NS_SQL_DEBUG_LOG

function sanitizeQuery(val: string | number): string | number {
    if (typeof val === 'string') {
        return val.replace(/'/g, '').replace(/;/g, '')
    }
    return val
}

// { asd: 'asd', basd: 4 } => asd = 'asd', basd = 4
function GetSqlQuerry(
    conditions: { [key: string]: string | number },
    type?: string,
    joiner?: string
) {
    const res = Object.keys(conditions).reduce((acc, key) => {
        const item = conditions[key]
        const conditionKey = sanitizeQuery(key)
        const condition = sanitizeQuery(conditions[key])

        if (typeof item === 'string') {
            acc.push(`${conditionKey} = '${condition}'`)
        } else {
            acc.push(`${conditionKey} = ${condition}`)
        }
        return acc
    }, [])
    if (type === 'where') {
        if (joiner) {
            return res.join(` ${joiner} `)
        } else {
            return res.join(' AND ')
        }
    } else {
        return res.join(', ')
    }
}

// -------------------------------------------------------------------------

function GetDB(path: string): Database {
    utils.CreatePath(path)
    const res = new Sqlite(path)
    res.pragma('synchronous = OFF')
    return res
}

function DebugLog(msg: string) {
    if (debugLog) {
        logger.DebugLog(msg, 'sql', 0)
    }
}

function AddColumn(
    db: Database,
    table: string,
    colName: string,
    colType: string
): RunResult {
    try {
        const command = `ALTER TABLE ${table} ADD COLUMN ${colName} ${colType}`
        const stmt = PrepareStatement(db, command)

        return stmt.run()
    } catch (err) {
        console.error(err)
        return null
    }
}

function TableInfo(
    db: Database,
    table: string
): {
    columns: any[]
    dataCount: number
} {
    try {
        const command = `PRAGMA table_info(${table})`
        const stmt = PrepareStatement(db, command)

        const infoRes = stmt.all()

        const s2 = `SELECT COUNT(*) FROM ${table}`
        const stmt2 = PrepareStatement(db, s2)

        const countRes = stmt2.get()

        return {
            columns: infoRes,
            dataCount: countRes[Object.keys(countRes)[0]],
        }
    } catch (err) {
        console.error(err)
        return null
    }
}

function Update(
    db: Database,
    table: string,
    newData: { [key: string]: string | number },
    conditions: { [key: string]: string | number }
): RunResult {
    try {
        const command = `UPDATE ${table} SET ${GetSqlQuerry(
            newData,
            'set'
        )} WHERE ${GetSqlQuerry(conditions, 'where')}`
        const stmt = PrepareStatement(db, command)

        return stmt.run()
    } catch (err) {
        console.error(err)
        return null
    }
}

function Delete(
    db: Database,
    table: string,
    conditions: { [key: string]: string | number }
): RunResult {
    try {
        const command = `DELETE FROM ${table} WHERE ${GetSqlQuerry(
            conditions,
            'where'
        )}`
        const stmt = PrepareStatement(db, command)

        return stmt.run()
    } catch (err) {
        console.error(err)
        return null
    }
}

interface DbColumnDescription {
    [key: string]: {
        type: string
        primary?: boolean
        autoIncrement?: boolean
        notNull?: boolean
        defaultZero?: boolean
        [key: string]: any
    }
}

function CreateTable(
    db: Database,
    name: string,
    columns: DbColumnDescription,
    foreignKeys: {
        keysFrom: string[]
        table: string
        keysTo: string[]
    }[]
): RunResult {
    // CREATE TABLE users(pw text PRIMARY KEY NOT NULL, id number, lastIP text, notes text, loginCount
    // number, lastLogin text, lastAccess text
    //
    // FOREIGN KEY(songartist, songalbum) REFERENCES album(albumartist, albumname) )

    try {
        const cols = Object.keys(columns)
            .reduce((acc, key) => {
                const item = columns[key]
                const flags: string[] = []
                const toCheck = {
                    primary: 'PRIMARY KEY',
                    notNull: 'NOT NULL',
                    unique: 'UNIQUE',
                    autoIncrement: 'AUTOINCREMENT',
                    defaultZero: 'DEFAULT 0',
                }
                Object.keys(toCheck).forEach((key) => {
                    if (item[key]) {
                        flags.push(toCheck[key])
                    }
                })

                acc.push(`${key} ${item.type} ${flags.join(' ')}`)
                return acc
            }, [])
            .join(', ')

        const fKeys: string[] = []
        if (foreignKeys) {
            foreignKeys.forEach((foreignKey) => {
                const { keysFrom, table, keysTo } = foreignKey
                fKeys.push(
                    `, FOREIGN KEY(${keysFrom.join(
                        ', '
                    )}) REFERENCES ${table}(${keysTo.join(', ')})`
                )
            })
        }

        // IF NOT EXISTS
        const command = `CREATE TABLE ${name}(${cols}${fKeys.join(' ')})`
        const stmt = PrepareStatement(db, command)
        return stmt.run()
    } catch (err) {
        console.error(err)
        return null
    }
}

function SelectAll(db: Database, from: string): any[] {
    try {
        const command = `SELECT * from ${from}`

        const stmt = PrepareStatement(db, command)
        return stmt.all()
    } catch (err) {
        console.error(err)
        return null
    }
}

// SELECT * FROM MyTable WHERE SomeColumn > LastValue ORDER BY SomeColumn LIMIT 100;
function Select(
    db: Database,
    from: string,
    conditions: { [key: string]: string | number },
    options: { joiner?: string; limit?: number } = {}
): any[] {
    const { joiner, limit } = options

    try {
        let command = `SELECT * from ${from} WHERE ${GetSqlQuerry(
            conditions,
            'where',
            joiner
        )}`

        if (!isNaN(limit)) {
            command += ` LIMIT ${limit}`
        }

        const stmt = PrepareStatement(db, command)
        return stmt.all()
    } catch (err) {
        console.error(err)
        return null
    }
}

function Insert(
    db: Database,
    table: string,
    data: { [key: string]: number | string }
): RunResult {
    try {
        const cols = Object.keys(data)
            .reduce((acc, key) => {
                acc.push(`${key}`)
                return acc
            }, [])
            .join(', ')

        const values = Object.keys(data)
            .map((key) => {
                const item = data[key]
                if (typeof item === 'string') {
                    return `'${item}'`
                } else {
                    return `${item}`
                }
            })
            .join(', ')

        const command = `INSERT INTO ${table} (${cols}) VALUES (${values})`
        const stmt = PrepareStatement(db, command)

        return stmt.run()
    } catch (err) {
        console.error(err)
        return null
    }
}

function runStatement(db: Database, command: string, runType?: string): any {
    const stmt = PrepareStatement(db, command)
    if (!runType) {
        return stmt.all()
    } else if (runType === 'run') {
        return stmt.run()
    }
    return null
}

function CloseDB(db: Database): void {
    db.close()
}

// -------------------------------------------------------------------------

function PrepareStatement(db: Database, command: string) {
    if (!db) {
        throw new Error(
            'DB is undefined in prepare statement! DB action called with undefined db'
        )
    }
    DebugLog(command)
    return db.prepare(command)
}
