/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j 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 <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.database;

import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.neo4j.configuration.helpers.RemoteUri;
import org.neo4j.dbms.systemgraph.TopologyGraphDbmsModel;

/**
 * Implementations of this interface represent different kinds of Database reference.
 *
 * - {@link Internal} references point to databases which are present in this DBMS.
 * - {@link External} references point to databases which are not present in this DBMS.
 *
 * A database may have multiple references, each with a different alias.
 * The reference whose {@link #alias()} corresponds to the database's original name is known as the primary reference.
 */
public abstract class DatabaseReference implements Comparable<DatabaseReference> {
    private static final Comparator<DatabaseReference> referenceComparator =
            Comparator.comparing(a -> a.alias().name(), String::compareToIgnoreCase);
    private static final Comparator<DatabaseReference> nullSafeReferenceComparator =
            Comparator.nullsLast(referenceComparator);
    private static final NormalizedDatabaseName defaultNamespace =
            new NormalizedDatabaseName(TopologyGraphDbmsModel.DEFAULT_NAMESPACE);
    /**
     * @return the alias associated with this database reference
     */
    public abstract NormalizedDatabaseName alias();

    /**
     * @return the namespace that the alias is in, or empty if it is in the default namespace
     */
    public abstract Optional<NormalizedDatabaseName> namespace();

    /**
     * @return whether the alias associated with this reference is the database's original/true name
     */
    public abstract boolean isPrimary();

    /**
     * @return the unique identity for this reference
     */
    public abstract UUID id();

    @Override
    public int compareTo(DatabaseReference that) {
        return nullSafeReferenceComparator.compare(this, that);
    }

    public String toPrettyString() {
        var namespace = namespace().map(ns -> ns.name() + ".").orElse("");
        var name = alias().name();
        return namespace + name;
    }

    public boolean isComposite() {
        return false;
    }

    /**
     * External references point to databases which are not stored within this DBMS.
     */
    public static final class External extends DatabaseReference {
        private final NormalizedDatabaseName targetAlias;
        private final NormalizedDatabaseName alias;
        private final NormalizedDatabaseName namespace;
        private final RemoteUri externalUri;
        private final UUID uuid;

        /**
         * Creates an external database reference with no namespace (default namespace)
         */
        public External(
                NormalizedDatabaseName targetAlias, NormalizedDatabaseName alias, RemoteUri externalUri, UUID uuid) {
            this(targetAlias, alias, null, externalUri, uuid);
        }

        /**
         * Creates an external database reference
         */
        public External(
                NormalizedDatabaseName targetAlias,
                NormalizedDatabaseName alias,
                NormalizedDatabaseName namespace,
                RemoteUri externalUri,
                UUID uuid) {
            this.targetAlias = targetAlias;
            this.alias = alias;
            this.namespace = Objects.equals(namespace, defaultNamespace) ? null : namespace;
            this.externalUri = externalUri;
            this.uuid = uuid;
        }

        @Override
        public NormalizedDatabaseName alias() {
            return alias;
        }

        @Override
        public Optional<NormalizedDatabaseName> namespace() {
            return Optional.ofNullable(namespace);
        }

        @Override
        public boolean isPrimary() {
            return false;
        }

        public RemoteUri externalUri() {
            return externalUri;
        }

        public NormalizedDatabaseName targetAlias() {
            return targetAlias;
        }

        @Override
        public UUID id() {
            return uuid;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            External external = (External) o;
            return Objects.equals(targetAlias, external.targetAlias)
                    && Objects.equals(alias, external.alias)
                    && Objects.equals(namespace, external.namespace)
                    && Objects.equals(externalUri, external.externalUri)
                    && Objects.equals(uuid, external.uuid);
        }

        @Override
        public int hashCode() {
            return Objects.hash(targetAlias, alias, namespace, externalUri, uuid);
        }

        @Override
        public String toString() {
            return "External{"
                    + "alias=" + alias
                    + ", namespace=" + namespace
                    + ", remoteUri=" + externalUri
                    + ", remoteName=" + targetAlias
                    + ", uuid=" + uuid
                    + '}';
        }
    }

    /**
     * Local references point to databases which are stored within this DBMS.
     *
     * Note, however, that a local reference may point to databases not stored on this physical instance.
     */
    public static sealed class Internal extends DatabaseReference {
        protected final NormalizedDatabaseName alias;
        protected final NormalizedDatabaseName namespace;
        protected final NamedDatabaseId namedDatabaseId;
        protected final boolean primary;

        /**
         * Creates an internal database reference with no namespace (default namespace)
         */
        public Internal(NormalizedDatabaseName alias, NamedDatabaseId namedDatabaseId, boolean primary) {
            this(alias, null, namedDatabaseId, primary);
        }

        /**
         * Creates an internal database reference
         */
        public Internal(
                NormalizedDatabaseName alias,
                NormalizedDatabaseName namespace,
                NamedDatabaseId namedDatabaseId,
                boolean primary) {
            this.alias = alias;
            this.namespace = Objects.equals(namespace, defaultNamespace) ? null : namespace;
            this.namedDatabaseId = namedDatabaseId;
            this.primary = primary;
        }

        public NamedDatabaseId databaseId() {
            return namedDatabaseId;
        }

        @Override
        public NormalizedDatabaseName alias() {
            return alias;
        }

        @Override
        public Optional<NormalizedDatabaseName> namespace() {
            return Optional.ofNullable(namespace);
        }

        @Override
        public boolean isPrimary() {
            return primary;
        }

        @Override
        public UUID id() {
            return namedDatabaseId.databaseId().uuid();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Internal internal = (Internal) o;
            return primary == internal.primary
                    && Objects.equals(alias, internal.alias)
                    && Objects.equals(namespace, internal.namespace)
                    && Objects.equals(namedDatabaseId, internal.namedDatabaseId);
        }

        @Override
        public int hashCode() {
            return Objects.hash(alias, namespace, namedDatabaseId, primary);
        }

        @Override
        public String toString() {
            return "Internal{" + "alias="
                    + alias + ", namespace="
                    + namespace + ", namedDatabaseId="
                    + namedDatabaseId + ", primary="
                    + primary + '}';
        }
    }

    public static final class Composite extends DatabaseReference.Internal {
        private final List<DatabaseReference> constituents;

        /**
         * Creates a composite database reference
         */
        public Composite(
                NormalizedDatabaseName alias, NamedDatabaseId namedDatabaseId, Set<DatabaseReference> constituents) {
            super(alias, namedDatabaseId, true);
            this.constituents = constituents.stream().sorted().toList();
        }

        @Override
        public Optional<NormalizedDatabaseName> namespace() {
            return Optional.empty();
        }

        public List<DatabaseReference> constituents() {
            return constituents;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            if (!super.equals(o)) return false;
            Composite composite = (Composite) o;
            return Objects.equals(constituents, composite.constituents);
        }

        @Override
        public int hashCode() {
            return Objects.hash(super.hashCode(), constituents);
        }

        @Override
        public String toString() {
            return "Composite{" + "alias="
                    + alias + ", namespace="
                    + namespace + ", namedDatabaseId="
                    + namedDatabaseId + ", primary="
                    + primary + ", constituents="
                    + constituents + '}';
        }

        @Override
        public boolean isComposite() {
            return true;
        }
    }
}
