/*MT*

    MediaTomb - http://www.mediatomb.cc/

    sql_database.h - this file is part of MediaTomb.

    Copyright (C) 2005 Gena Batyan <bgeradz@mediatomb.cc>,
                       Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>

    Copyright (C) 2006-2010 Gena Batyan <bgeradz@mediatomb.cc>,
                            Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>,
                            Leonhard Wimmer <leo@mediatomb.cc>

    Copyright (C) 2016-2025 Gerbera Contributors

    MediaTomb is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 2
    as published by the Free Software Foundation.

    MediaTomb 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
    version 2 along with MediaTomb; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

    $Id$
*/

/// @file database/sql_database.h

#ifndef __SQL_STORAGE_H__
#define __SQL_STORAGE_H__

#include "config/config.h"
#include "config/config_val.h"
#include "database.h"
#include "sql_format.h"

#include <array>
#include <mutex>
#include <unordered_set>
#include <utility>

// forward declarations
template <class Item>
class AddUpdateTable;
class CdsContainer;
class CdsResource;
class SQLEmitter;
class SQLResult;
class SQLRow;
template <class Col>
class EnumColumnMapper;
enum class BrowseColumn;
enum class ResourceDataType;
enum class Operation;

#define DBVERSION 25
#define STRING_LIMIT "GRBMAX"

#define INTERNAL_SETTINGS_TABLE "mt_internal_setting"

class SQLDatabase : public Database {
public:
    /* methods to override in subclasses */
    virtual std::string quote(const std::string& value) const = 0;
    std::string quote(const std::string& value, std::size_t len) const;
    /// @brief ensure correct string quoting for SQL statement
    std::string quote(const char* str, std::size_t len = 0) const { return quote(std::string(str), len); }
    /* wrapper functions for different types */
    std::string quote(char val) const { return quote(fmt::to_string(val)); }
    static std::string quote(int val) { return fmt::to_string(val); }
    static std::string quote(unsigned int val) { return fmt::to_string(val); }
    static std::string quote(long val) { return fmt::to_string(val); }
    static std::string quote(unsigned long val) { return fmt::to_string(val); }
    static std::string quote(bool val) { return val ? "1" : "0"; }
    static std::string quote(long long val) { return fmt::to_string(val); }

    /// @brief returns a fmt-printable identifier name
    SQLIdentifier identifier(const std::string& name) const { return { name, table_quote_begin, table_quote_end }; }

    // hooks for transactions
    virtual void beginTransaction(std::string_view tName) { }
    virtual void rollback(std::string_view tName) { }
    virtual void commit(std::string_view tName) { }

    virtual void del(std::string_view tableName, const std::string& clause, const std::vector<int>& ids) = 0;
    virtual void execOnTable(std::string_view tableName, const std::string& query, int objId) = 0;
    virtual int exec(const std::string& query, const std::string& getLastInsertId = "") = 0;
    virtual void execOnly(const std::string& query) = 0;
    virtual std::shared_ptr<SQLResult> select(const std::string& query) = 0;

    void addObject(const std::shared_ptr<CdsObject>& obj, int* changedContainer) override;
    void updateObject(const std::shared_ptr<CdsObject>& obj, int* changedContainer) override;

    std::shared_ptr<CdsObject> loadObject(int objectID) override;
    std::shared_ptr<CdsObject> loadObject(const std::string& group, int objectID) override;
    int getChildCount(int contId, bool containers, bool items, bool hideFsRoot) override;
    std::map<int, int> getChildCounts(const std::vector<int>& contId, bool containers, bool items, bool hideFsRoot) override;

    std::size_t getObjects(int parentID, bool withoutContainer, std::unordered_set<int>& ret, bool full) override;
    std::vector<int> getRefObjects(int objectId) override;
    std::unordered_set<int> getUnreferencedObjects() override;

    std::unique_ptr<ChangedContainers> removeObject(int objectID, const fs::path& path, bool all) override;
    std::unique_ptr<ChangedContainers> removeObjects(const std::unordered_set<int>& list, bool all = false) override;

    std::shared_ptr<CdsObject> loadObjectByServiceID(const std::string& serviceID, const std::string& group) override;
    std::vector<int> getServiceObjectIDs(char servicePrefix) override;

    /* accounting methods */
    long long getFileStats(const StatsParam& stats) override;
    std::map<std::string, long long> getGroupStats(const StatsParam& stats) override;

    std::vector<std::shared_ptr<CdsObject>> browse(BrowseParam& param) override;
    std::vector<std::shared_ptr<CdsObject>> search(SearchParam& param) override;

    std::vector<std::string> getMimeTypes() override;
    std::map<std::string, std::shared_ptr<CdsContainer>> getShortcuts() override;

    std::vector<std::shared_ptr<CdsObject>> findObjectByContentClass(
        const std::string& contentClass,
        int startIndex,
        int count,
        const std::string& group) override;
    std::shared_ptr<CdsObject> findObjectByPath(const fs::path& fullpath, const std::string& group, DbFileType fileType = DbFileType::Auto) override;
    int findObjectIDByPath(const fs::path& fullpath, DbFileType fileType = DbFileType::Auto) override;
    std::string incrementUpdateIDs(const std::unordered_set<int>& ids) override;

    fs::path buildContainerPath(int parentID, const std::string& title) override;
    bool addContainer(int parentContainerId, std::string virtualPath, const std::shared_ptr<CdsContainer>& cont, int* containerID) override;
    std::string getInternalSetting(const std::string& key) override;
    void storeInternalSetting(const std::string& key, const std::string& value) override = 0;

    std::shared_ptr<AutoscanList> getAutoscanList(AutoscanScanMode scanmode) override;
    void updateAutoscanList(AutoscanScanMode scanmode, const std::shared_ptr<AutoscanList>& list) override;

    std::shared_ptr<AutoscanDirectory> getAutoscanDirectory(int objectID) override;
    void addAutoscanDirectory(const std::shared_ptr<AutoscanDirectory>& adir) override;
    void updateAutoscanDirectory(const std::shared_ptr<AutoscanDirectory>& adir) override;
    void removeAutoscanDirectory(const std::shared_ptr<AutoscanDirectory>& adir) override;
    void checkOverlappingAutoscans(const std::shared_ptr<AutoscanDirectory>& adir) override;

    /* config methods */
    std::vector<ConfigValue> getConfigValues() override;
    void removeConfigValue(const std::string& item) override;
    void updateConfigValue(const std::string& key, const std::string& item, const std::string& value, const std::string& status = "unchanged") override;

    /* clients methods */
    std::vector<ClientObservation> getClients() override;
    void saveClients(const std::vector<ClientObservation>& cache) override;
    std::shared_ptr<ClientStatusDetail> getPlayStatus(const std::string& group, int objectId) override;
    std::vector<std::shared_ptr<ClientStatusDetail>> getPlayStatusList(int objectId) override;
    void savePlayStatus(const std::shared_ptr<ClientStatusDetail>& detail) override;
    std::vector<std::map<std::string, std::string>> getClientGroupStats() override;

    std::vector<int> getPathIDs(int objectID) override;

    void shutdown() override;
    virtual void shutdownDriver() = 0;

    int ensurePathExistence(const fs::path& path, int* changedContainer) override;

    static std::string getSortCapabilities();
    static std::string getSearchCapabilities();

    unsigned int getHash(int index) const { return index == -1 ? hashies.at(DBVERSION) : (index < DBVERSION ? hashies.at(index) : 0); }

    void deleteAll(std::string_view tableName);
    template <typename T>
    void deleteRow(const std::string& tableName, const std::string& key, const T& value);
    void deleteRows(std::string_view tableName, const std::string& key, const std::vector<int>& values);

protected:
    std::shared_ptr<Mime> mime;
    std::shared_ptr<ConverterManager> converterManager;

    char table_quote_begin { '\0' };
    char table_quote_end { '\0' };
    std::array<unsigned int, DBVERSION + 1> hashies;

    /// @brief Initial db version with gerbera
    std::size_t firstDBVersion = 1;
    /// @brief Maximum length designated as GRBMAX in ddl statement
    unsigned int stringLimit;
    /// @brief lock for special sql commands
    mutable std::recursive_mutex sqlMutex;
    using SqlAutoLock = std::scoped_lock<decltype(sqlMutex)>;
    std::map<int, std::shared_ptr<CdsContainer>> dynamicContainers;

    std::shared_ptr<EnumColumnMapper<BrowseColumn>> browseColumnMapper;

    /// @brief constructor for derived classes
    explicit SQLDatabase(const std::shared_ptr<Config>& config, std::shared_ptr<Mime> mime, std::shared_ptr<ConverterManager> converterManager);

    void init() override;

    /// @brief build up runtime data for dynamic (search based) containers
    void initDynContainers(const std::shared_ptr<CdsObject>& sParent = {});
    /// @brief create core entries in fresh database
    void fillDatabase();

    /// @brief migrate metadata from mt_cds_objects to mt_metadata before removing the column (DBVERSION 12)
    bool doMetadataMigration();
    /// @brief migrate metadata for cdsObject
    void migrateMetadata(int objectId, const std::string& metadataStr);

    /// @brief Add a column to resource table for each defined resource attribute
    void prepareResourceTable(const std::map<ResourceDataType, std::string_view>& addResourceColumnCmd);
    /// @brief migrate resources from mt_cds_objects to grb_resource before removing the column (DBVERSION 13)
    bool doResourceMigration();
    /// @brief migrate resource data for cdsObject
    void migrateResources(int objectId, const std::string& resourcesStr);

    /// @brief upgrade database version by applying migration commands
    void upgradeDatabase(
        unsigned int dbVersion,
        const std::array<unsigned int, DBVERSION + 1>& hashies,
        ConfigVal upgradeOption,
        std::string_view updateVersionCommand,
        const std::map<ResourceDataType, std::string_view>& addResourceColumnCmd);

    /// @brief internal version of sql exec command
    virtual void _exec(const std::string& query) = 0;

    /// @brief Get query for unreferenced objects depending on database
    virtual std::string getUnreferencedQuery(const std::string& table);

private:
    std::string sql_browse_columns;
    std::string sql_browse_query;
    std::string sql_search_columns;
    std::string sql_search_container_query_format;
    std::string sql_search_query;
    std::string sql_meta_query;
    std::string sql_autoscan_query;
    std::string sql_resource_query;
    std::map<ResourceDataType, std::string_view> addResourceColumnCmd;

    /// @brief Configuration content for dynamic folders
    std::shared_ptr<DynamicContentList> dynamicContentList;
    /// @brief Are dynamic search folders enabled in config
    bool dynamicContentEnabled;
    /// @brief Is sorting by sort_key enabled in config
    bool sortKeyEnabled;

    std::shared_ptr<CdsObject> createObjectFromRow(const std::string& group, const std::unique_ptr<SQLRow>& row);
    std::shared_ptr<CdsObject> createObjectFromSearchRow(const std::string& group, const std::unique_ptr<SQLRow>& row);
    std::vector<std::pair<std::string, std::string>> retrieveMetaDataForObject(int objectId);
    std::vector<std::shared_ptr<CdsResource>> retrieveResourcesForObject(int objectId);

    std::vector<std::shared_ptr<AddUpdateTable<CdsObject>>> _addUpdateObject(
        const std::shared_ptr<CdsObject>& obj,
        Operation op,
        int* changedContainer);

    void generateMetaDataDBOperations(
        const std::shared_ptr<CdsObject>& obj,
        Operation op,
        std::vector<std::shared_ptr<AddUpdateTable<CdsObject>>>& operations) const;
    void generateResourceDBOperations(
        const std::shared_ptr<CdsObject>& obj,
        Operation op,
        std::vector<std::shared_ptr<AddUpdateTable<CdsObject>>>& operations);

    /* helper for removeObject(s) */
    void _removeObjects(const std::vector<std::int32_t>& objectIDs);

    ChangedContainers _recursiveRemove(
        const std::vector<std::int32_t>& items,
        const std::vector<std::int32_t>& containers, bool all);

    std::unique_ptr<ChangedContainers> _purgeEmptyContainers(const ChangedContainers& maybeEmpty);

    /* helpers for autoscan */
    void _removeAutoscanDirectory(int autoscanID);
    int _getAutoscanObjectID(int autoscanID);
    void _autoscanChangePersistentFlag(int objectID, bool persistent);
    static std::shared_ptr<AutoscanDirectory> _fillAutoscanDirectory(const std::unique_ptr<SQLRow>& row);
    std::vector<int> _checkOverlappingAutoscans(const std::shared_ptr<AutoscanDirectory>& adir);

    /* location helper: filesystem path or virtual path to db location*/
    static std::string addLocationPrefix(
        char prefix,
        const fs::path& path,
        std::string_view suffix = "");
    /* location helpers: db location to filesystem path */
    static std::pair<fs::path, char> stripLocationPrefix(std::string_view dbLocation);

    std::shared_ptr<CdsObject> checkRefID(const std::shared_ptr<CdsObject>& obj);
    int createContainer(int parentID, const std::string& name, const std::string& virtualPath, int flags, bool isVirtual, const std::string& upnpClass, int refID,
        const std::vector<std::pair<std::string, std::string>>& itemMetadata, const std::vector<std::shared_ptr<CdsResource>>& itemResources = {});

    static bool remapBool(const std::string& field) { return field == "1"; }
    static bool remapBool(int field) { return field == 1; }

    std::shared_ptr<SQLEmitter> sqlEmitter;

    using AutoLock = std::scoped_lock<std::mutex>;
};

template <typename T>
void SQLDatabase::deleteRow(const std::string& tableName, const std::string& key, const T& value)
{
    exec(fmt::format("DELETE FROM {} WHERE {} = {}", identifier(tableName), identifier(key), quote(value)));
}

class SqlWithTransactions {
protected:
    explicit SqlWithTransactions(const std::shared_ptr<Config>& config)
        : use_transaction(config->getBoolOption(ConfigVal::SERVER_STORAGE_USE_TRANSACTIONS))
    {
    }

    bool use_transaction;
    bool inTransaction {};
};

#endif // __SQL_STORAGE_H__
