/* SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2022-2024 Soren Stoutner <soren@stoutner.com>
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc/>.
 *
 * 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/>.
 */

// Application headers.
#include "CookiesDatabase.h"

// Define the private static schema constants.
const int CookiesDatabase::SCHEMA_VERSION = 0;

// Define the public static constants.
const QString CookiesDatabase::CONNECTION_NAME = QLatin1String("cookies_database");
const QString CookiesDatabase::COOKIES_TABLE = QLatin1String("cookies");
const QString CookiesDatabase::DOMAIN = QLatin1String("domain");
const QString CookiesDatabase::EXPIRATION_DATE = QLatin1String("expiration_date");
const QString CookiesDatabase::HTTP_ONLY = QLatin1String("http_only");
const QString CookiesDatabase::ID = QLatin1String("_id");
const QString CookiesDatabase::NAME = QLatin1String("name");
const QString CookiesDatabase::PATH = QLatin1String("path");
const QString CookiesDatabase::SECURE = QLatin1String("secure");
const QString CookiesDatabase::VALUE = QLatin1String("value");

// Construct the class.
CookiesDatabase::CookiesDatabase() {}

void CookiesDatabase::addDatabase()
{
    // Add the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), CONNECTION_NAME);

    // Set the database name.
    cookiesDatabase.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/cookies.db"));

    // Open the database.
    if (cookiesDatabase.open())  // Opening the database succeeded.
    {
        // Check to see if the cookies table already exists.
        if (cookiesDatabase.tables().contains(COOKIES_TABLE))  // The cookies table exists.
        {
            // Create a schema version query.
            QSqlQuery schemaVersionQuery(cookiesDatabase);

            // Query the database schema version.
            schemaVersionQuery.exec(QLatin1String("PRAGMA user_version"));

            // Move to the first record.
            schemaVersionQuery.first();

            // Get the current schema versin.
            int currentSchemaVersion = schemaVersionQuery.value(0).toInt();

            // Check to see if the schema has been updated.
            if (currentSchemaVersion < SCHEMA_VERSION)
            {
                // Run the schema update code.
                switch (currentSchemaVersion)
                {
                    // Upgrade code here.
                }

                // Create an update schema version version.
                QSqlQuery updateSchemaVersionQuery = QSqlQuery(cookiesDatabase);

                // Update the schema version.
                updateSchemaVersionQuery.exec(QLatin1String("PRAGMA user_version = ") + QString::number(SCHEMA_VERSION));
            }
        }
        else  // The cookies table does not exist.
        {
            // Instantiate a create table query.
            QSqlQuery createTableQuery(cookiesDatabase);

            // Prepare the create table query.
            createTableQuery.prepare(QLatin1String("CREATE TABLE ") + COOKIES_TABLE + QLatin1String("(") +
                                     ID + QLatin1String(" INTEGER PRIMARY KEY, ") +
                                     DOMAIN + QLatin1String(" TEXT NOT NULL, ") +
                                     NAME + QLatin1String(" TEXT NOT NULL, ") +
                                     PATH + QLatin1String(" TEXT NOT NULL, ") +
                                     EXPIRATION_DATE + QLatin1String(" TEXT, ") +
                                     HTTP_ONLY + QLatin1String(" INTEGER NOT NULL DEFAULT 0, ") +
                                     SECURE + QLatin1String(" INTEGER NOT NULL DEFAULT 0, ") +
                                     VALUE + QLatin1String(" TEXT NOT NULL)"));

            // Execute the query.
            if (!createTableQuery.exec())
            {
                // Log any errors.
                qDebug().noquote().nospace() << "Error creating table:  " << cookiesDatabase.lastError();
            }

            // Create an set schema version version.
            QSqlQuery setSchemaVersionQuery(cookiesDatabase);

            // Set the schema version.
            setSchemaVersionQuery.exec(QLatin1String("PRAGMA user_version = ") + QString::number(SCHEMA_VERSION));
        }
    }
    else  // Opening the database failed.
    {
        // Write the last database error message to the debug output.
        qDebug().noquote().nospace() << "Error opening database:  " << cookiesDatabase.lastError();
    }
}

void CookiesDatabase::addCookie(const QNetworkCookie &cookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Check to see if the cookie already exists in the database.
    if (isDurable(cookie))  // The cookie already exists.
    {
        // Update the existing cookie.
        updateCookie(cookie);
    }
    else  // The cookie doesn't already exist.
    {
        // Instantiate an add cookie query.
        QSqlQuery addCookieQuery(cookiesDatabase);

        // Prepare the add cookie query.
        addCookieQuery.prepare(QLatin1String("INSERT INTO ") + COOKIES_TABLE + QLatin1String(" (") +
                                DOMAIN + QLatin1String(", ") +
                                NAME + QLatin1String(", ") +
                                PATH + QLatin1String(", ") +
                                EXPIRATION_DATE + QLatin1String(", ") +
                                HTTP_ONLY + QLatin1String(", ") +
                                SECURE + QLatin1String(", ") +
                                VALUE + QLatin1String(") ") +
                                QLatin1String("VALUES (:domain, :name, :path, :expiration_date, :http_only, :secure, :value)"));

        // Bind the values.
        addCookieQuery.bindValue(QLatin1String(":domain"), cookie.domain());
        addCookieQuery.bindValue(QLatin1String(":name"), QLatin1String(cookie.name()));
        addCookieQuery.bindValue(QLatin1String(":path"), cookie.path());
        addCookieQuery.bindValue(QLatin1String(":expiration_date"), cookie.expirationDate());
        addCookieQuery.bindValue(QLatin1String(":http_only"), cookie.isHttpOnly());
        addCookieQuery.bindValue(QLatin1String(":secure"), cookie.isSecure());
        addCookieQuery.bindValue(QLatin1String(":value"), QLatin1String(cookie.value()));

        // Execute the query.
        addCookieQuery.exec();
    }
}

int CookiesDatabase::cookieCount()
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate a count cookies query.
    QSqlQuery countCookiesQuery(cookiesDatabase);

    // Set the query to be forward only.
    countCookiesQuery.setForwardOnly(true);

    // Prepare the query.
    countCookiesQuery.prepare(QLatin1String("SELECT ") + ID + QLatin1String(" FROM ") + COOKIES_TABLE);

    // Execute the query.
    countCookiesQuery.exec();

    // Move to the last row.
    countCookiesQuery.last();

    // Initialize a number of cookies variable.
    int numberOfCookies = 0;

    // Check to see if the query is valid (there is at least one cookie).
    if (countCookiesQuery.isValid())
    {
        // Get the number of rows (which is zero based) and add one to calculate the number of cookies.
        numberOfCookies = countCookiesQuery.at() + 1;
    }

    // Return the number of cookies.
    return numberOfCookies;
}

void CookiesDatabase::deleteAllCookies()
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate a delete all cookies query.
    QSqlQuery deleteAllCookiesQuery(cookiesDatabase);

    // Prepare the delete all cookies query.
    deleteAllCookiesQuery.prepare(QLatin1String("DELETE FROM ") + COOKIES_TABLE);

    // Execute the query.
    deleteAllCookiesQuery.exec();
}

void CookiesDatabase::deleteCookie(const QNetworkCookie &cookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate a delete cookie query.
    QSqlQuery deleteCookieQuery(cookiesDatabase);

    // Prepare the delete cookie query.
    deleteCookieQuery.prepare(QLatin1String("DELETE FROM ") + COOKIES_TABLE + QLatin1String(" WHERE ") +
                              DOMAIN + QLatin1String(" = :domain AND ") +
                              NAME + QLatin1String(" = :name AND ") +
                              PATH + QLatin1String(" = :path"));

    // Bind the values.
    deleteCookieQuery.bindValue(QLatin1String(":domain"), cookie.domain());
    deleteCookieQuery.bindValue(QLatin1String(":name"), QLatin1String(cookie.name()));
    deleteCookieQuery.bindValue(QLatin1String(":path"), cookie.path());

    // Execute the query.
    deleteCookieQuery.exec();
}

QList<QNetworkCookie*>* CookiesDatabase::getCookies()
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate a cookies query.
    QSqlQuery cookiesQuery(cookiesDatabase);

    // Set the query to be forward only, which is more performant.
    cookiesQuery.setForwardOnly(true);

    // Prepare the cookies query.
    cookiesQuery.prepare(QLatin1String("SELECT * FROM ") + COOKIES_TABLE);

    // Execute the query.
    cookiesQuery.exec();

    // Create a cookie list.
    QList<QNetworkCookie*> *cookieListPointer = new QList<QNetworkCookie*>;

    // Populate the cookie list.
    while (cookiesQuery.next())
    {
        // Create a cookie.
        QNetworkCookie *cookiePointer = new QNetworkCookie();

        // Populate the cookie.
        cookiePointer->setDomain(cookiesQuery.value(DOMAIN).toString());
        cookiePointer->setName(cookiesQuery.value(NAME).toString().toUtf8());
        cookiePointer->setPath(cookiesQuery.value(PATH).toString());
        cookiePointer->setExpirationDate(QDateTime::fromString(cookiesQuery.value(EXPIRATION_DATE).toString(), Qt::ISODate));
        cookiePointer->setHttpOnly(cookiesQuery.value(HTTP_ONLY).toBool());
        cookiePointer->setSecure(cookiesQuery.value(SECURE).toBool());
        cookiePointer->setValue(cookiesQuery.value(VALUE).toString().toUtf8());

        // Add the cookie to the list.
        cookieListPointer->append(cookiePointer);
    }

    // Return the cookie list.
    return cookieListPointer;
}

QNetworkCookie* CookiesDatabase::getCookieById(const int &id)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate a cookie query.
    QSqlQuery cookieQuery(cookiesDatabase);

    // Set the query to be forward only.
    cookieQuery.setForwardOnly(true);

    // Prepare the cookies query.
    cookieQuery.prepare(QLatin1String("SELECT * FROM ") + COOKIES_TABLE + QLatin1String(" WHERE ") + ID + QLatin1String(" = :id"));

    // Bind the values.
    cookieQuery.bindValue(QLatin1String(":id"), id);

    // Execute the query.
    cookieQuery.exec();

    // Move to the first entry.
    cookieQuery.first();

    // Create a cookie.
    QNetworkCookie *cookiePointer = new QNetworkCookie();

    // Populate the cookie.
    cookiePointer->setDomain(cookieQuery.value(DOMAIN).toString());
    cookiePointer->setName(cookieQuery.value(NAME).toString().toUtf8());
    cookiePointer->setPath(cookieQuery.value(PATH).toString());
    cookiePointer->setExpirationDate(QDateTime::fromString(cookieQuery.value(EXPIRATION_DATE).toString(), Qt::ISODate));
    cookiePointer->setHttpOnly(cookieQuery.value(HTTP_ONLY).toBool());
    cookiePointer->setSecure(cookieQuery.value(SECURE).toBool());
    cookiePointer->setValue(cookieQuery.value(VALUE).toString().toUtf8());

    // Return the cookie.
    return cookiePointer;
}

bool CookiesDatabase::isDurable(const QNetworkCookie &cookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate an is durable query.
    QSqlQuery isDurableQuery(cookiesDatabase);

    // Set the query to be forward only.
    isDurableQuery.setForwardOnly(true);

    // Prepare the is durable query.
    isDurableQuery.prepare(QLatin1String("SELECT ") + ID + QLatin1String(" FROM ") + COOKIES_TABLE + QLatin1String(" WHERE ") +
                            DOMAIN + QLatin1String(" = :domain AND ") +
                            NAME + QLatin1String(" = :name AND ") +
                            PATH + QLatin1String(" = :path"));

    // Bind the values.
    isDurableQuery.bindValue(QLatin1String(":domain"), cookie.domain());
    isDurableQuery.bindValue(QLatin1String(":name"), QLatin1String(cookie.name()));
    isDurableQuery.bindValue(QLatin1String(":path"), cookie.path());

    // Execute the query.
    isDurableQuery.exec();

    // Move to the first entry.
    isDurableQuery.first();

    // Return the status of the cookie in the database.
    return (isDurableQuery.isValid());
}

bool CookiesDatabase::isUpdate(const QNetworkCookie &cookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate an is update query.
    QSqlQuery isUpdateQuery(cookiesDatabase);

    // Prepare the is update query.
    isUpdateQuery.prepare(QLatin1String("SELECT ") + EXPIRATION_DATE + QLatin1String(" , ") +
                          HTTP_ONLY + QLatin1String(" , ") +
                          SECURE + QLatin1String(" , ") +
                          VALUE +
                          QLatin1String(" FROM ") + COOKIES_TABLE +
                          QLatin1String(" WHERE ") + DOMAIN + QLatin1String(" = :domain AND ") +
                          NAME + QLatin1String(" = :name AND ") +
                          PATH + QLatin1String(" = :path"));

    // Bind the values.
    isUpdateQuery.bindValue(QLatin1String(":domain"), cookie.domain());
    isUpdateQuery.bindValue(QLatin1String(":name"), QLatin1String(cookie.name()));
    isUpdateQuery.bindValue(QLatin1String(":path"), cookie.path());

    // Execute the query.
    isUpdateQuery.exec();

    // Move to the first entry.
    isUpdateQuery.first();

    // Check to see if the cookie exists.
    if (isUpdateQuery.isValid())  // The cookie exists in the database.
    {
        // Check to see if the cookie data has changed.
        if ((QDateTime::fromString(isUpdateQuery.value(EXPIRATION_DATE).toString(), Qt::ISODate) != cookie.expirationDate()) ||
            (isUpdateQuery.value(HTTP_ONLY).toBool() != cookie.isHttpOnly()) ||
            (isUpdateQuery.value(SECURE).toBool() != cookie.isSecure()) ||
            (isUpdateQuery.value(VALUE).toString().toUtf8() != cookie.value()))  // The cookies data has changed.
        {
            //qDebug() << "The durable cookie data has changed.";

            // Return true.
            return true;
        }
        else  // The cookie data has not changed.
        {
            //qDebug() << "The durable cookie data is unchanged.";

            // Return false.
            return false;
        }
    }
    else  // The cookie does not exist in the database.
    {
        // Return false.
        return false;
    }
}

void CookiesDatabase::updateCookie(const QNetworkCookie &cookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Create the update cookie query.
    QSqlQuery updateCookieQuery(cookiesDatabase);

    // Prepare the update cookie query.
    updateCookieQuery.prepare(QLatin1String("UPDATE ") + COOKIES_TABLE +
                              QLatin1String(" SET ") + EXPIRATION_DATE + QLatin1String(" = :expiration_date , ") +
                              HTTP_ONLY + QLatin1String(" = :http_only , ") +
                              SECURE + QLatin1String(" = :secure , ") +
                              VALUE + QLatin1String(" = :value ") +
                              QLatin1String("WHERE ") + DOMAIN + QLatin1String(" = :domain AND ") +
                              NAME + QLatin1String(" = :name AND ") +
                              PATH + QLatin1String(" = :path"));

    // Bind the values.
    updateCookieQuery.bindValue(QLatin1String(":domain"), cookie.domain());
    updateCookieQuery.bindValue(QLatin1String(":name"), QLatin1String(cookie.name()));
    updateCookieQuery.bindValue(QLatin1String(":path"), cookie.path());
    updateCookieQuery.bindValue(QLatin1String(":expiration_date"), cookie.expirationDate());
    updateCookieQuery.bindValue(QLatin1String(":http_only"), cookie.isHttpOnly());
    updateCookieQuery.bindValue(QLatin1String(":secure"), cookie.isSecure());
    updateCookieQuery.bindValue(QLatin1String(":value"), QLatin1String(cookie.value()));

    // Execute the query.
    updateCookieQuery.exec();
}

void CookiesDatabase::updateCookie(const QNetworkCookie &oldCookie, const QNetworkCookie &newCookie)
{
    // Get a handle for the cookies database.
    QSqlDatabase cookiesDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Create the old cookie query.
    QSqlQuery oldCookieQuery(cookiesDatabase);

    // Set the query to be forward only.
    oldCookieQuery.setForwardOnly(true);

    // Prepare the old cookie query.
    oldCookieQuery.prepare(QLatin1String("SELECT ") + ID + QLatin1String(" FROM ") + COOKIES_TABLE +
                           QLatin1String(" WHERE ") + DOMAIN + QLatin1String(" = :domain AND ") +
                           NAME + QLatin1String(" = :name AND ") +
                           PATH + QLatin1String(" = :path"));

    // Bind the values.
    oldCookieQuery.bindValue(QLatin1String(":domain"), oldCookie.domain());
    oldCookieQuery.bindValue(QLatin1String(":name"), QLatin1String(oldCookie.name()));
    oldCookieQuery.bindValue(QLatin1String(":path"), oldCookie.path());

    // Execute the query.
    oldCookieQuery.exec();

    // Move to the first entry.
    oldCookieQuery.first();

    // Create the update cookie query.
    QSqlQuery updateCookieQuery(cookiesDatabase);

    // Prepare the update cookie query.
    updateCookieQuery.prepare(QLatin1String("UPDATE ") + COOKIES_TABLE +
                              QLatin1String(" SET ") + DOMAIN + QLatin1String(" = :domain , ") +
                              NAME + QLatin1String(" = :name , ") +
                              PATH + QLatin1String(" = :path , ") +
                              EXPIRATION_DATE + QLatin1String(" = :expiration_date , ") +
                              HTTP_ONLY + QLatin1String(" = :http_only , ") +
                              SECURE + QLatin1String(" = :secure , ") +
                              VALUE + QLatin1String(" = :value ") +
                              QLatin1String("WHERE ") + ID + QLatin1String(" = :id"));

    // Bind the values.
    updateCookieQuery.bindValue(QLatin1String(":id"), oldCookieQuery.value(0).toLongLong());
    updateCookieQuery.bindValue(QLatin1String(":domain"), newCookie.domain());
    updateCookieQuery.bindValue(QLatin1String(":name"), QLatin1String(newCookie.name()));
    updateCookieQuery.bindValue(QLatin1String(":path"), newCookie.path());
    updateCookieQuery.bindValue(QLatin1String(":expiration_date"), newCookie.expirationDate());
    updateCookieQuery.bindValue(QLatin1String(":http_only"), newCookie.isHttpOnly());
    updateCookieQuery.bindValue(QLatin1String(":secure"), newCookie.isSecure());
    updateCookieQuery.bindValue(QLatin1String(":value"), QLatin1String(newCookie.value()));

    // Execute the query.
    updateCookieQuery.exec();
}
