/*
 * Copyright (C) 2025-2026  Tim Zimmermann <tim@aglais.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.aglais.zms.db.core;

import org.aglais.zms.db.core.model.AncestryData;
import org.aglais.zms.db.core.model.Species;
import org.aglais.zms.db.core.model.User;
import org.aglais.zms.util.Log;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * Class representing a Database connection
 */
public abstract class Database {

    private static final String LOG_TAG = Database.class.getSimpleName();
    private static final Map<String, DatabaseProvider> PROVIDERS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private static final int BACKUP_VERSION = 3;
    private static final int MIN_BACKUP_VERSION = 1;

    static {
        ServiceLoader<DatabaseProvider> dbProviders = ServiceLoader.load(DatabaseProvider.class);
        for (DatabaseProvider provider : dbProviders) {
            PROVIDERS.put(provider.getName(), provider);
        }
    }

    /**
     * Get all currently available implementations of {@link Database}
     *
     * @return The names of all currently available implementations
     */
    public static Set<String> getAvailableImplementationNames() {
        return PROVIDERS.keySet();
    }

    /**
     * Create a new instance of a database implementation by name
     *
     * @param name     The database implementation name
     * @param host     The host to connect to
     * @param database The database to operate on
     * @param username The username to use for authentication
     * @param password The password to use for authentication
     * @return The newly created database implementation instance
     */
    public @NotNull
    static Database newInstance(String name, String host, String database, String username, String password) {
        DatabaseProvider provider = PROVIDERS.get(name);
        if (provider != null) {
            return provider.newInstance(host, database, username, password);
        }

        throw new IllegalArgumentException("Database implementation " + name + " not found");
    }

    /**
     * Create a backup of the database content
     *
     * @param out The {@link OutputStream} to write backup data to
     */
    public final void backup(OutputStream out) throws IOException, NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        DigestOutputStream digestOut = new DigestOutputStream(out, digest);
        ObjectOutputStream objOut = new ObjectOutputStream(digestOut);
        List<Species> speciesList = getAllSpecies();
        List<User> userList = getAllUsers();

        objOut.writeInt(BACKUP_VERSION);
        objOut.writeLong(System.currentTimeMillis());
        objOut.writeInt(speciesList.size());
        for (Species species : speciesList) {
            List<AncestryData> speciesAncestry = getSpeciesAncestry(species);

            objOut.writeObject(species);
            objOut.writeInt(speciesAncestry.size());
            for (AncestryData data : speciesAncestry) {
                objOut.writeObject(data);
            }
        }
        objOut.writeInt(userList.size());
        for (User user : userList) {
            objOut.writeObject(user);
        }
        objOut.write(digest.digest());
        objOut.flush();
    }

    /**
     * Restore a backup of the database content
     *
     * @param in       The {@link InputStream} to read backup data from
     * @param listener The {@link IBackupListener} to be notified once the backup was loaded successfully, can be null
     * @return {@link BackupRestoreResult}
     */
    public final BackupRestoreResult restore(InputStream in, IBackupListener listener) throws IOException,
            ClassNotFoundException, NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        DigestInputStream digestIn = new DigestInputStream(in, digest);
        ObjectInputStream objIn = new ObjectInputStream(digestIn);

        int backupVersion = objIn.readInt();
        if (backupVersion < MIN_BACKUP_VERSION || backupVersion > BACKUP_VERSION) {
            Log.e(LOG_TAG, "Backup version v" + backupVersion + " not supported");
            Log.e(LOG_TAG, "Minimum supported backup version: v" + MIN_BACKUP_VERSION);
            Log.e(LOG_TAG, "Maximum supported backup version: v" + BACKUP_VERSION);
            return BackupRestoreResult.ERROR_UNSUPPORTED_VERSION;
        }

        long timestamp = 0;
        if (backupVersion > 2)
            timestamp = objIn.readLong();

        List<Species> speciesList = new ArrayList<>();
        HashMap<Species, List<AncestryData>> ancestryMap = new HashMap<>();
        int speciesCount = objIn.readInt();
        for (int i = 0; i < speciesCount; i++) {
            Species species = (Species) objIn.readObject();

            List<AncestryData> speciesAncestry = new ArrayList<>();
            int ancestryCount = objIn.readInt();
            for (int j = 0; j < ancestryCount; j++) {
                AncestryData data = (AncestryData) objIn.readObject();

                speciesAncestry.add(data);
            }
            speciesList.add(species);
            ancestryMap.put(species, speciesAncestry);
        }

        List<User> userList = new ArrayList<>();
        if (backupVersion > 1) {
            int userCount = objIn.readInt();
            for (int i = 0; i < userCount; i++) {
                User user = (User) objIn.readObject();

                userList.add(user);
            }
        }

        if (backupVersion > 2) {
            byte[] computedHash = digest.digest();
            byte[] storedHash = new byte[digest.getDigestLength()];

            if (objIn.read(storedHash) != digest.getDigestLength() || !MessageDigest.isEqual(computedHash, storedHash)) {
                Log.e(LOG_TAG, "Backup hash mismatch");

                return BackupRestoreResult.ERROR_HASH_MISMATCH;
            }
        }

        IBackupListener.BackupInfo backupInfo = new IBackupListener.BackupInfo(backupVersion, timestamp, speciesList,
                ancestryMap, userList);
        if (listener != null && !listener.onBackupLoaded(backupInfo))
            return BackupRestoreResult.ERROR_CANCELLED;

        // First clear the current database
        for (Species species : getAllSpecies())
            deleteSpecies(species);

        if (backupVersion > 1)
            for (User user : getAllUsers())
                deleteUser(user);

        // Now import the loaded data
        for (Species species : speciesList) {
            addSpecies(species);
            updateSpeciesAncestry(species, ancestryMap.get(species));
        }

        for (User user : userList)
            addUser(user);

        return BackupRestoreResult.OK;
    }

    /**
     * Restore a backup of the database content
     *
     * @param in The {@link InputStream} to read backup data from
     * @return {@link BackupRestoreResult}
     */
    public final BackupRestoreResult restore(InputStream in) throws IOException, ClassNotFoundException,
            NoSuchAlgorithmException {
        return restore(in, null);
    }

    /**
     * Connect to the database
     *
     * @return True if the connection was successful
     */
    public abstract boolean connect();

    /**
     * Check if the database is connected
     *
     * @return True if the connection is working
     */
    public abstract boolean isConnected();

    /**
     * Disconnect from the database
     */
    public abstract void disconnect();

    /**
     * Get the currently installed database schema version
     *
     * @return The database version, 0 if not yet installed
     */
    public abstract int getVersion();

    /**
     * Get the database schema version currently required to run Aglais ZMS.
     * If this value doesn't match {@link #getVersion()}, {@link #upgrade(int)} must be used to upgrade the schema.
     *
     * @return The currently required database version
     */
    public abstract int getRequiredVersion();

    /**
     * Upgrade the database schema. The value specified can only be the value of {@link Database#getVersion()} + 1
     *
     * @param version The version to upgrade to
     * @return True if the database was successfully upgraded
     */
    public abstract boolean upgrade(int version);

    /**
     * Insert species data into the database
     *
     * @param species The species to add
     * @return The species object with initialized ID or null if adding failed
     */
    public abstract Species addSpecies(@NotNull Species species);

    /**
     * Get all species currently stored in the database
     *
     * @return All known species
     */
    public abstract List<Species> getAllSpecies();

    /**
     * Deletes the specified species from the database.
     *
     * @param species The species to be deleted
     * @return True if the species was successfully deleted, false otherwise
     */
    public abstract boolean deleteSpecies(Species species);

    /**
     * Updates the specified species record in the database.
     *
     * @param species The species object containing updated details. The species must have a valid ID.
     * @return True if the species was successfully updated, false otherwise.
     */
    public abstract boolean updateSpecies(Species species);

    /**
     * Get all the ancestry data associated with the given species
     *
     * @param species The species to load data for
     * @return All data found related to the given species
     */
    public abstract List<AncestryData> getSpeciesAncestry(Species species);

    /****
     * Updates the ancestry information for the given species.
     *
     * @param species The species whose ancestry is to be updated
     * @param ancestry The list of ancestry data to be associated with the species
     * @return true if the update was successful, false otherwise
     */
    public abstract boolean updateSpeciesAncestry(Species species, List<AncestryData> ancestry);

    /**
     * Insert user data into the database
     *
     * @param user The user to add
     * @return The user object with initialized ID or null if adding failed
     */
    public abstract User addUser(@NotNull User user);

    /**
     * Create a default admin user
     */
    protected final boolean createDefaultUser() {
        String userName = "Admin";
        String password = "AglaisZMS";

        User user = new User(-1, userName, null);

        user.setClearTextPassword(password);

        if (addUser(user) != null) {
            Log.i(LOG_TAG, "Created default user with name " + userName + " and password " + password);

            return true;
        }

        return false;
    }

    /**
     * Get all users currently stored in the database
     *
     * @return All known users
     */
    public abstract List<User> getAllUsers();

    /**
     * Deletes the specified user from the database.
     *
     * @param user The user to be deleted
     * @return True if the user was successfully deleted, false otherwise
     */
    public abstract boolean deleteUser(User user);

    /**
     * Updates the specified user record in the database.
     *
     * @param user The user object containing updated details. The user must have a valid ID.
     * @return True if the user was successfully updated, false otherwise.
     */
    public abstract boolean updateUser(User user);

    /**
     * Get a user by name (case-sensitive)
     *
     * @param name The username to search for
     * @return The user matching the name, null if not found
     */
    public abstract @Nullable User getUserByName(String name);

    /**
     * Get a user by ID
     *
     * @param id The user ID to search for
     * @return The user with the matching ID, null if not found
     */
    public abstract @Nullable User getUserByID(int id);
}
