/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.kafka.metadata.properties;

import org.apache.kafka.common.Uuid;

import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Properties;

/**
 * An immutable class which contains the per-log-directory information stored in an individual
 * meta.properties file.
 */
public final class MetaProperties {
    /**
     * The property that sets the version number.
     */
    static final String VERSION_PROP = "version";

    /**
     * The property that specifies the cluster id.
     */
    static final String CLUSTER_ID_PROP = "cluster.id";

    /**
     * The property that specifies the broker id. Only in V0.
     */
    static final String BROKER_ID_PROP = "broker.id";

    /**
     * The property that specifies the node id. Replaces broker.id in V1.
     */
    static final String NODE_ID_PROP = "node.id";

    /**
     * The property that specifies the directory id.
     */
    static final String DIRECTORY_ID_PROP = "directory.id";

    /**
     * The version of the MetaProperties file.
     */
    private final MetaPropertiesVersion version;

    /**
     * The cluster ID, which may be Optional.empty in V0.
     */
    private final Optional<String> clusterId;

    /**
     * The node ID, which may be OptionalInt.empty in V0.
     */
    private final OptionalInt nodeId;

    /**
     * The directory ID, or Optional.empty if none is specified.
     */
    private final Optional<Uuid> directoryId;

    /**
     * Creates a new MetaProperties object.
     */
    public static class Builder {
        private MetaPropertiesVersion version = MetaPropertiesVersion.V0;
        private Optional<String> clusterId = Optional.empty();
        private OptionalInt nodeId = OptionalInt.empty();
        private Optional<Uuid> directoryId = Optional.empty();

        public Builder() {
        }

        public Builder(MetaProperties metaProps) {
            this.version = metaProps.version();
            this.clusterId = metaProps.clusterId();
            this.nodeId = metaProps.nodeId();
            this.directoryId = metaProps.directoryId();
        }

        public Builder(Properties props) {
            this.version = MetaPropertiesVersion.fromNumberString(
                props.getProperty(VERSION_PROP,
                    MetaPropertiesVersion.V0.numberString()));
            if (version.hasBrokerId()) {
                if (props.containsKey(BROKER_ID_PROP)) {
                    this.nodeId = OptionalInt.of(PropertiesUtils.loadRequiredIntProp(props, BROKER_ID_PROP));
                }
            } else {
                this.nodeId = OptionalInt.of(PropertiesUtils.loadRequiredIntProp(props, NODE_ID_PROP));
            }
            this.clusterId = Optional.ofNullable(props.getProperty(CLUSTER_ID_PROP));
            if (props.containsKey(DIRECTORY_ID_PROP)) {
                try {
                    this.directoryId = Optional.of(Uuid.fromString(props.getProperty(DIRECTORY_ID_PROP)));
                } catch (Exception e) {
                    throw new RuntimeException("Unable to read " + DIRECTORY_ID_PROP + " as a Uuid: " +
                            e.getMessage(), e);
                }
            } else {
                this.directoryId = Optional.empty();
            }
        }

        public MetaPropertiesVersion version() {
            return version;
        }

        public Builder setVersion(MetaPropertiesVersion version) {
            this.version = version;
            return this;
        }

        public Optional<String> clusterId() {
            return clusterId;
        }

        public Builder setClusterId(String clusterId) {
            return setClusterId(Optional.of(clusterId));
        }

        public Builder setClusterId(Optional<String> clusterId) {
            this.clusterId = clusterId;
            return this;
        }

        public OptionalInt nodeId() {
            return nodeId;
        }

        public Builder setNodeId(OptionalInt nodeId) {
            this.nodeId = nodeId;
            return this;
        }

        public Builder setNodeId(int nodeId) {
            return setNodeId(OptionalInt.of(nodeId));
        }

        public Optional<Uuid> directoryId() {
            return directoryId;
        }

        public Builder setDirectoryId(Optional<Uuid> directoryId) {
            this.directoryId = directoryId;
            return this;
        }

        public Builder setDirectoryId(Uuid directoryId) {
            return setDirectoryId(Optional.of(directoryId));
        }

        public MetaProperties build() {
            if (!version.equals(MetaPropertiesVersion.V0)) {
                if (clusterId.isEmpty()) {
                    throw new RuntimeException("cluster.id was not found.");
                }
                if (nodeId.isEmpty()) {
                    throw new RuntimeException("node.id was not found.");
                }
            }
            return new MetaProperties(version,
                clusterId,
                nodeId,
                directoryId);
        }
    }

    private MetaProperties(
        MetaPropertiesVersion version,
        Optional<String> clusterId,
        OptionalInt nodeId,
        Optional<Uuid> directoryId
    ) {
        this.version = version;
        this.clusterId = clusterId;
        this.nodeId = nodeId;
        this.directoryId = directoryId;
    }

    public MetaPropertiesVersion version() {
        return version;
    }

    public Optional<String> clusterId() {
        return clusterId;
    }

    public OptionalInt nodeId() {
        return nodeId;
    }

    public Optional<Uuid> directoryId() {
        return directoryId;
    }

    @Override
    public int hashCode() {
        return Objects.hash(version,
            clusterId,
            nodeId,
            directoryId);
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || !(o.getClass().equals(MetaProperties.class))) return false;
        MetaProperties other = (MetaProperties) o;
        return version.equals(other.version) &&
            clusterId.equals(other.clusterId) &&
            nodeId.equals(other.nodeId) &&
            directoryId.equals(other.directoryId);
    }

    @Override
    public String toString() {
        StringBuilder bld = new StringBuilder();
        bld.append("MetaProperties");
        bld.append("(version=").append(version.number());
        clusterId.ifPresent(id -> bld.append(", clusterId=").append(id));
        if (nodeId.isPresent()) {
            bld.append(", nodeId=").append(nodeId.getAsInt());
        }
        directoryId.ifPresent(id -> bld.append(", directoryId=").append(id));
        bld.append(")");
        return bld.toString();
    }

    public Properties toProperties() {
        Properties props = new Properties();
        props.setProperty(VERSION_PROP, version.numberString());
        clusterId.ifPresent(id -> props.setProperty(CLUSTER_ID_PROP, id));
        if (version.hasBrokerId()) {
            if (nodeId.isPresent()) {
                props.setProperty(BROKER_ID_PROP, "" + nodeId.getAsInt());
            }
        } else {
            props.setProperty(NODE_ID_PROP, "" + nodeId.getAsInt());
        }
        directoryId.ifPresent(id -> props.setProperty(DIRECTORY_ID_PROP, id.toString()));
        return props;
    }
}
