package com.atlassian.stash.internal.backup;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import java.util.*;
import java.util.function.Predicate;

import static com.atlassian.stash.internal.backup.DatabaseTableAttribute.CROWD;
import static com.atlassian.stash.internal.backup.DatabaseTableAttribute.PREPOPULATED;
import static com.atlassian.bitbucket.util.Chainable.chain;

/**
 * Enumeration of database tables for the current schema. No foreign key constraints will be violated when the
 * contents of these tables are restored to an empty schema, provided they are restored in the order shown below.
 * The foreign key relationships of each table are indicated in inline comments below. Some tables are decorated with
 * additional {@link DatabaseTableAttribute attributes} which can be queried for by passing arguments to {@link #getTableNames}.
 * <p>
 * <strong>NOTE (schema changes):</strong> This enum <strong>must</strong> be kept up-to-date with any schema changes
 * that effect table names or foreign keys!
 * <p>
 * <strong>NOTE (self-referencing foreign keys):</strong> If any table is added a self-referencing foreign key, its
 * instance constructor should specify a sorting column (see {@link #STA_COMMENT} for an example). If present, that
 * column will be used to order the rows of the tables when a database backup is generated to ensure no foreign key
 * violations occur when restoring the database .
 * <p>
 * <strong>WARNING: As noted above, the order of the values in this enum is critical for performing restores without
 * violating referential integrity constraints. When updating, ensure that all tables declaring a foreign key reference
 * to another table are positioned <em>below</em> the referenced table. Do not reorder the enums without thoroughly
 * testing.</strong>
 */
public enum DatabaseTable {

    // Bootstrap tables

    APP_PROPERTY("app_property"),

    // Embedded Crowd tables

    CWD_APPLICATION("cwd_application", CROWD),
    CWD_DIRECTORY("cwd_directory", CROWD),
    CWD_APP_DIR_MAPPING("cwd_app_dir_mapping", CROWD), // FK -> cwd_directory, cwd_application
    CWD_APP_DIR_GROUP_MAPPING("cwd_app_dir_group_mapping", CROWD), // FK -> cwd_application, cwd_app_dir_mapping, cwd_directory
    CWD_APP_DIR_OPERATION("cwd_app_dir_operation", CROWD), // FK -> cwd_app_dir_mapping
    CWD_APPLICATION_ADDRESS("cwd_application_address", CROWD), // FK -> cwd_application
    CWD_APPLICATION_ALIAS("cwd_application_alias", CROWD), // FK -> cwd_application
    CWD_APPLICATION_ATTRIBUTE("cwd_application_attribute", CROWD), // FK -> cwd_application
    CWD_DIRECTORY_ATTRIBUTE("cwd_directory_attribute", CROWD), // FK -> cwd_directory
    CWD_DIRECTORY_OPERATION("cwd_directory_operation", CROWD), // FK -> cwd_directory
    CWD_GROUP("cwd_group", CROWD), // FK -> cwd_directory
    CWD_GROUP_ATTRIBUTE("cwd_group_attribute", CROWD), // FK -> cwd_directory, cwd_group
    CWD_MEMBERSHIP("cwd_membership", CROWD), // FK -> cwd_directory
    CWD_PROPERTY("cwd_property", CROWD),
    CWD_USER("cwd_user", CROWD), // FK -> cwd_directory
    CWD_USER_ATTRIBUTE("cwd_user_attribute", CROWD), // FK -> cwd_directory, cwd_user
    CWD_USER_CREDENTIAL_RECORD("cwd_user_credential_record", CROWD), // FK -> cwd_user
    CWD_WEBHOOK("cwd_webhook", CROWD),

    // core tables

    STASH_USER("stash_user"),
    STA_NORMAL_USER("sta_normal_user"), //FK -> stash_user
    STA_SERVICE_USER("sta_service_user"), //FK -> stash_user
    STA_SHARED_LOB("sta_shared_lob"),
    STA_USER_SETTINGS("sta_user_settings"), //FK -> stash_user, sta_shared_lob
    PROJECT("project"),
    STA_NORMAL_PROJECT("sta_normal_project"), //FK -> project
    STA_PERSONAL_PROJECT("sta_personal_project"), //FK -> project, stash_user
    REPOSITORY("repository"), // FK -> project
    STA_REPO_HOOK("sta_repo_hook"), // FK -> repository, sta_shared_lob
    STA_PERMISSION_TYPE("sta_permission_type", "perm_id", PREPOPULATED),
    STA_GLOBAL_PERMISSION("sta_global_permission"), // FK -> stash_user, sta_permission_type
    STA_PROJECT_PERMISSION("sta_project_permission"), // FK -> project, stash_user, sta_permission_type
    STA_REPO_PERMISSION("sta_repo_permission"), // FK -> repository, stash_user, sta_permission_type
    TRUSTED_APP("trusted_app"),
    TRUSTED_APP_RESTRICTION("trusted_app_restriction"), // FK -> trusted_app
    CURRENT_APP("current_app"),
    STA_REMEMBER_ME_TOKEN("sta_remember_me_token"), // FK -> stash_user
    HIBERNATE_UNIQUE_KEY("hibernate_unique_key", PREPOPULATED),
    ID_SEQUENCE("id_sequence"),
    PLUGIN_SETTING("plugin_setting"),
    PLUGIN_STATE("plugin_state"),
    CHANGESET("changeset"),
    CS_ATTRIBUTE("cs_attribute"), // FK -> changeset
    CS_REPO_MEMBERSHIP("cs_repo_membership"), // FK -> repository, changeset
    CS_INDEXER_STATE("cs_indexer_state"), // FK -> repository
    REPOSITORY_ACCESS("repository_access"), // FK -> stash_user, repository
    STA_REPOSITORY_SCOPED_ID("sta_repository_scoped_id"), // FK -> repository
    STA_PULL_REQUEST("sta_pull_request"), // FK -> repository
    STA_TASK("sta_task"), // FK -> stash_user, added here as sta_comment will have a foreign key relation to this table
    STA_COMMENT("sta_comment", "id"), // FK -> stash_user, sta_comment -- Ordered by ID due to parent_id -> id reference
    STA_DIFF_COMMENT_ANCHOR("sta_diff_comment_anchor"), // FK -> sta_comment
    STA_COMMIT_DISCUSSION("sta_cmt_discussion"), // FK -> repository
    STA_CMT_DISC_COMMENT_ANCHOR("sta_cmt_disc_comment_anchor"), // FK -> sta_cmt_discussion, sta_diff_comment_anchor, sta_pull_request
    STA_CMT_DISC_PARTICIPANT("sta_cmt_disc_participant"), // FK -> sta_cmt_discussion, stash_user
    BB_PR_PART_STATUS_WEIGHT("bb_pr_part_status_weight", PREPOPULATED),
    STA_PR_DIFF_COMMENT_ANCHOR("sta_pr_diff_comment_anchor"), // FK -> sta_diff_comment_anchor, sta_pull_request
    STA_PR_PARTICIPANT("sta_pr_participant"), // FK -> stash_user, sta_pull_request
    STA_DRIFT_REQUEST("sta_drift_request"), // FK -> sta_pull_request
    STA_ACTIVITY("sta_activity"), // FK -> stash_user
    STA_REPO_ACTIVITY("sta_repo_activity"), // FK -> repository, sta_activity
    STA_REPO_ORIGIN("sta_repo_origin"), // FK -> repository, repository
    STA_REPO_PUSH_ACTIVITY("sta_repo_push_activity"), // FK -> sta_repo_activity
    STA_REPO_PUSH_REF("sta_repo_push_ref"), // FK -> sta_repo_push_activity
    STA_CMT_DISC_ACTIVITY("sta_cmt_disc_activity"), // FK -> sta_cmt_discussion, sta_repo_activity
    STA_CMT_DISC_COMMENT_ACTIVITY("sta_cmt_disc_comment_activity"), // FK -> sta_comment, sta_cmt_disc_activity, sta_diff_comment_anchor
    STA_PR_ACTIVITY("sta_pr_activity"), // FK -> sta_activity, sta_pull_request
    STA_PR_COMMENT_ACTIVITY("sta_pr_comment_activity"), // FK -> sta_comment, sta_diff_comment_anchor, sta_pr_activity
    STA_PR_MERGE_ACTIVITY("sta_pr_merge_activity"), // FK -> sta_pr_activity
    STA_PR_RESCOPE_ACTIVITY("sta_pr_rescope_activity"), // FK -> sta_pr_activity
    STA_PR_RESCOPE_COMMITS("sta_pr_rescope_commit"), // FK -> sta_pr_rescope_activity
    STA_PR_RESCOPE_REQUEST("sta_pr_rescope_request"), // FK -> sta_repository, stash_user
    STA_PR_RESCOPE_REQUEST_CHANGE("sta_pr_rescope_request_change"), // FK -> sta_pr_rescope_request
    BB_PR_REVIEWERS_UPDATED_ACTIVITY("bb_pr_reviewer_upd_activity"), // FK -> sta_pr_activity
    BB_PR_REVIEWERS_ADDED("bb_pr_reviewer_added"), // FK -> bb_pr_reviewer_upd_activity, sta_user
    BB_PR_REVIEWERS_REMOVED("bb_pr_reviewer_removed"), // FK -> bb_pr_reviewer_upd_activity, sta_user
    STA_WATCHER("sta_watcher"), // FK -> stash_user, keyless references to sta_ann_changeset, sta_pull_request
    STA_DELETED_GROUP("sta_deleted_group"),
    BB_CLUSTEREDJOB("bb_clusteredjob"),
    BB_INTEGRITY_EVENT("bb_integrity_event"),
    BB_PROJ_MERGE_CONFIG("bb_proj_merge_config"), // FK -> project
    BB_PROJ_MERGE_STRATEGY("bb_proj_merge_strategy"), // FK -> bb_proj_merge_config
    BB_REPO_MERGE_CONFIG("bb_repo_merge_config"), // FK -> repository
    BB_REPO_MERGE_STRATEGY("bb_repo_merge_strategy"), // FK -> bb_repo_merge_config
    BB_SCM_MERGE_CONFIG("bb_scm_merge_config"),
    BB_SCM_MERGE_STRATEGY("bb_scm_merge_strategy"); // FK -> bb_scm_merge_config

    private static Map<String, DatabaseTable> NAME_TO_TABLE_MAP = buildNameToTableMap();

    private static Map<String, DatabaseTable> buildNameToTableMap() {
        ImmutableMap.Builder<String, DatabaseTable> builder = ImmutableMap.builder();
        for (DatabaseTable table : values()) {
            builder.put(table.getTableName(), table);
        }
        return builder.build();
    }

    public static boolean isKnownTable(String tableName) {
        return NAME_TO_TABLE_MAP.containsKey(tableName);
    }

    private final String tableName;
    /** Ordering column for the Liquibase changeset in a backup. */
    private final String orderingColumn;
    private final Set<DatabaseTableAttribute> attributes;

    DatabaseTable(String tableName, DatabaseTableAttribute... attributes) {
        this(tableName, null, attributes);
    }

    DatabaseTable(String tableName, String orderingColumn, DatabaseTableAttribute... attributes) {
        this.tableName = tableName.toLowerCase(Locale.US);
        this.orderingColumn = orderingColumn;
        this.attributes = attributes.length > 0 ? EnumSet.copyOf(ImmutableSet.copyOf(attributes)) : Collections.emptySet();
    }

    /**
     * @return the lower-cased name of the table in the database
     */
    public String getTableName() {
        return tableName;
    }

    /**
     * @return an optional column name to order the table's rows in a backup
     */
    public String getOrderingColumn() {
        return orderingColumn;
    }

    /**
     * @return the names of all tables
     */
    public static List<String> getTableNames() {
        return chain(getTables()).transform(DatabaseTable::getTableName).toList();
    }

    /**
     * @param first an attribute that each returned table must have
     * @param rest further attributes that each returned table must have (thank you, Java generics)
     * @return the names of all tables having the supplied attributes, or all tables if no attributes are specified
     */
    public static List<String> getTableNames(DatabaseTableAttribute first, DatabaseTableAttribute... rest) {
        return chain(getTables()).filter(with(first, rest)).transform(DatabaseTable::getTableName).toList();
    }

    /**
     * @return all database tables
     */
    public static List<DatabaseTable> getTables() {
        return ImmutableList.copyOf(DatabaseTable.values());
    }

    private static Predicate<DatabaseTable> with(final DatabaseTableAttribute first, final DatabaseTableAttribute... rest) {
        final EnumSet<DatabaseTableAttribute> desired = EnumSet.of(first, rest);
        return input -> input.attributes.containsAll(desired);
    }

}
