/*
 * Copyright 2022-2023 Soren Stoutner <soren@stoutner.com>.
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
 *
 * Privacy Browser PC 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.
 *
 * Privacy Browser PC 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 Privacy Browser PC.  If not, see <http://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 = "cookies_database";
const QString CookiesDatabase::COOKIES_TABLE = "cookies";
const QString CookiesDatabase::DOMAIN = "domain";
const QString CookiesDatabase::EXPIRATION_DATE = "expiration_date";
const QString CookiesDatabase::HTTP_ONLY = "http_only";
const QString CookiesDatabase::ID = "_id";
const QString CookiesDatabase::NAME = "name";
const QString CookiesDatabase::PATH = "path";
const QString CookiesDatabase::SECURE = "secure";
const QString CookiesDatabase::VALUE = "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) + "/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.
        {
            // Query the database schema version.
            QSqlQuery schemaVersionQuery = cookiesDatabase.exec(QStringLiteral("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.
                }

                // Update the schema version.
                cookiesDatabase.exec("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("CREATE TABLE " + COOKIES_TABLE + "(" +
                                     ID + " INTEGER PRIMARY KEY, " +
                                     DOMAIN + " TEXT NOT NULL, " +
                                     NAME + " TEXT NOT NULL, " +
                                     PATH + " TEXT NOT NULL, " +
                                     EXPIRATION_DATE + " TEXT, " +
                                     HTTP_ONLY + " INTEGER NOT NULL DEFAULT 0, " +
                                     SECURE + " INTEGER NOT NULL DEFAULT 0, " +
                                     VALUE + " TEXT NOT NULL)");

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

            // Set the schema version.
            cookiesDatabase.exec("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("INSERT INTO " + COOKIES_TABLE + " (" +
                                DOMAIN + ", " +
                                NAME + ", " +
                                PATH + ", " +
                                EXPIRATION_DATE + ", " +
                                HTTP_ONLY + ", " +
                                SECURE + ", " +
                                VALUE + ") "
                                "VALUES (:domain, :name, :path, :expiration_date, :http_only, :secure, :value)"
        );

        // Bind the values.
        addCookieQuery.bindValue(":domain", cookie.domain());
        addCookieQuery.bindValue(":name", QString(cookie.name()));
        addCookieQuery.bindValue(":path", cookie.path());
        addCookieQuery.bindValue(":expiration_date", cookie.expirationDate());
        addCookieQuery.bindValue(":http_only", cookie.isHttpOnly());
        addCookieQuery.bindValue(":secure", cookie.isSecure());
        addCookieQuery.bindValue(":value", QString(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("SELECT " + ID + " 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("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("DELETE FROM " + COOKIES_TABLE + " WHERE " +
                              DOMAIN + " = :domain AND " +
                              NAME + " = :name AND " +
                              PATH + " = :path");

    // Bind the values.
    deleteCookieQuery.bindValue(":domain", cookie.domain());
    deleteCookieQuery.bindValue(":name", QString(cookie.name()));
    deleteCookieQuery.bindValue(":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("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("SELECT * FROM " + COOKIES_TABLE + " WHERE " + ID + " = :id");

    // Bind the values.
    cookieQuery.bindValue(":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("SELECT " + ID + " FROM " + COOKIES_TABLE + " WHERE " +
                            DOMAIN + " = :domain AND " +
                            NAME + " = :name AND " +
                            PATH + " = :path");

    // Bind the values.
    isDurableQuery.bindValue(":domain", cookie.domain());
    isDurableQuery.bindValue(":name", QString(cookie.name()));
    isDurableQuery.bindValue(":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("SELECT " + EXPIRATION_DATE + " , " +
                          HTTP_ONLY + " , " +
                          SECURE + " , " +
                          VALUE +
                          " FROM " + COOKIES_TABLE +
                          " WHERE " + DOMAIN + " = :domain AND " +
                          NAME + " = :name AND " +
                          PATH + " = :path");

    // Bind the values.
    isUpdateQuery.bindValue(":domain", cookie.domain());
    isUpdateQuery.bindValue(":name", QString(cookie.name()));
    isUpdateQuery.bindValue(":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("UPDATE " + COOKIES_TABLE +
                              " SET " + EXPIRATION_DATE + " = :expiration_date , " +
                              HTTP_ONLY + " = :http_only , " +
                              SECURE + " = :secure , " +
                              VALUE + " = :value " +
                              "WHERE " + DOMAIN + " = :domain AND " +
                              NAME + " = :name AND " +
                              PATH + " = :path");

    // Bind the values.
    updateCookieQuery.bindValue(":domain", cookie.domain());
    updateCookieQuery.bindValue(":name", QString(cookie.name()));
    updateCookieQuery.bindValue(":path", cookie.path());
    updateCookieQuery.bindValue(":expiration_date", cookie.expirationDate());
    updateCookieQuery.bindValue(":http_only", cookie.isHttpOnly());
    updateCookieQuery.bindValue(":secure", cookie.isSecure());
    updateCookieQuery.bindValue(":value", QString(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("SELECT " + ID + " FROM " + COOKIES_TABLE +
                           " WHERE " + DOMAIN + " = :domain AND " +
                           NAME + " = :name AND " +
                           PATH + " = :path");

    // Bind the values.
    oldCookieQuery.bindValue(":domain", oldCookie.domain());
    oldCookieQuery.bindValue(":name", QString(oldCookie.name()));
    oldCookieQuery.bindValue(":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("UPDATE " + COOKIES_TABLE +
                              " SET " + DOMAIN + " = :domain , " +
                              NAME + " = :name , " +
                              PATH + " = :path , " +
                              EXPIRATION_DATE + " = :expiration_date , " +
                              HTTP_ONLY + " = :http_only , " +
                              SECURE + " = :secure , " +
                              VALUE + " = :value " +
                              "WHERE " + ID + " = :id");

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

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