/*
 * 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.cassandra.service;

import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType;
import org.apache.cassandra.repair.autorepair.AutoRepairUtils;
import org.apache.cassandra.utils.MBeanWrapper;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implement all the MBeans for AutoRepair.
 */
public class AutoRepairService implements AutoRepairServiceMBean
{
    private static final Logger logger = LoggerFactory.getLogger(AutoRepairService.class);

    public static final String MBEAN_NAME = "org.apache.cassandra.db:type=AutoRepairService";

    @VisibleForTesting
    protected AutoRepairConfig config;

    public static final AutoRepairService instance = new AutoRepairService();

    @VisibleForTesting
    protected AutoRepairService()
    {
    }

    public static void setup()
    {
        if (!CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean())
        {
            logger.info("Auto-repair service is disabled via JVM property");
            return;
        }
        instance.config = DatabaseDescriptor.getAutoRepairConfig();
    }

    static
    {
        if (CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean())
        {
            MBeanWrapper.instance.registerMBean(instance, MBEAN_NAME);
        }
    }

    public void checkCanRun(String repairType)
    {
        checkCanRun(RepairType.parse(repairType));
    }

    public void checkCanRun(RepairType repairType)
    {
        if (!config.isAutoRepairSchedulingEnabled())
            throw new ConfigurationException("Auto-repair scheduler is disabled.");

        if (repairType != RepairType.INCREMENTAL)
            return;

        if (config.getMaterializedViewRepairEnabled(repairType) && DatabaseDescriptor.isMaterializedViewsOnRepairEnabled())
            throw new ConfigurationException("Cannot run incremental repair while materialized view replay is enabled. Set materialized_views_on_repair_enabled to false.");

        if (DatabaseDescriptor.isCDCEnabled() && DatabaseDescriptor.isCDCOnRepairEnabled())
            throw new ConfigurationException("Cannot run incremental repair while CDC replay is enabled. Set cdc_on_repair_enabled to false.");
    }

    public AutoRepairConfig getAutoRepairConfig()
    {
        return config;
    }

    @Override
    public boolean isAutoRepairDisabled()
    {
        return !CassandraRelevantProperties.AUTOREPAIR_ENABLE.getBoolean()
               || config == null
               || !config.isAutoRepairSchedulingEnabled();
    }

    @Override
    public String getAutoRepairConfiguration()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("repair scheduler configuration:");
        appendConfig(sb, "repair_check_interval", config.getRepairCheckInterval());
        appendConfig(sb, "repair_task_min_duration", config.getRepairTaskMinDuration());
        appendConfig(sb, "history_clear_delete_hosts_buffer_interval", config.getAutoRepairHistoryClearDeleteHostsBufferInterval());
        appendConfig(sb, "mixed_major_version_repair_enabled", config.getMixedMajorVersionRepairEnabled());
        for (RepairType repairType : RepairType.values())
        {
            sb.append(formatRepairTypeConfig(repairType, config));
        }
        return sb.toString();
    }

    @Override
    public void setAutoRepairEnabled(String repairType, boolean enabled)
    {
        checkCanRun(repairType);
        config.setAutoRepairEnabled(RepairType.parse(repairType), enabled);
    }

    @Override
    public void setRepairThreads(String repairType, int repairThreads)
    {
        config.setRepairThreads(RepairType.parse(repairType), repairThreads);
    }

    @Override
    public void setRepairPriorityForHosts(String repairType, String commaSeparatedHostSet)
    {
        Set<InetAddressAndPort> hosts = InetAddressAndPort.parseHosts(commaSeparatedHostSet, false);
        if (!hosts.isEmpty())
        {
            AutoRepairUtils.addPriorityHosts(RepairType.parse(repairType), hosts);
        }
    }

    @Override
    public void setForceRepairForHosts(String repairType, String commaSeparatedHostSet)
    {
        Set<InetAddressAndPort> hosts = InetAddressAndPort.parseHosts(commaSeparatedHostSet, false);
        if (!hosts.isEmpty())
        {
            AutoRepairUtils.setForceRepair(RepairType.parse(repairType), hosts);
        }
    }

    @Override
    public void setRepairMinInterval(String repairType, String minRepairInterval)
    {
        config.setRepairMinInterval(RepairType.parse(repairType), minRepairInterval);
    }

    @Override
    public void startScheduler()
    {
        config.startScheduler();
    }

    @Override
    public void setAutoRepairHistoryClearDeleteHostsBufferDuration(String duration)
    {
        config.setAutoRepairHistoryClearDeleteHostsBufferInterval(duration);
    }

    @Override
    public void setAutoRepairMinRepairTaskDuration(String duration)
    {
        config.setRepairTaskMinDuration(duration);
    }

    @Override
    public void setRepairSSTableCountHigherThreshold(String repairType, int sstableHigherThreshold)
    {
        config.setRepairSSTableCountHigherThreshold(RepairType.parse(repairType), sstableHigherThreshold);
    }

    @Override
    public void setAutoRepairTableMaxRepairTime(String repairType, String autoRepairTableMaxRepairTime)
    {
        config.setAutoRepairTableMaxRepairTime(RepairType.parse(repairType), autoRepairTableMaxRepairTime);
    }

    @Override
    public void setIgnoreDCs(String repairType, Set<String> ignoreDCs)
    {
        config.setIgnoreDCs(RepairType.parse(repairType), ignoreDCs);
    }

    @Override
    public void setPrimaryTokenRangeOnly(String repairType, boolean primaryTokenRangeOnly)
    {
        config.setRepairPrimaryTokenRangeOnly(RepairType.parse(repairType), primaryTokenRangeOnly);
    }

    @Override
    public void setParallelRepairPercentage(String repairType, int percentage)
    {
        config.setParallelRepairPercentage(RepairType.parse(repairType), percentage);
    }

    @Override
    public void setParallelRepairCount(String repairType, int count)
    {
        config.setParallelRepairCount(RepairType.parse(repairType), count);
    }

    @Override
    public void setAllowParallelReplicaRepair(String repairType, boolean enabled)
    {
        config.setAllowParallelReplicaRepair(RepairType.parse(repairType), enabled);
    }

    @Override
    public void setAllowParallelReplicaRepairAcrossSchedules(String repairType, boolean enabled)
    {
        config.setAllowParallelReplicaRepairAcrossSchedules(RepairType.parse(repairType), enabled);
    }

    @Override
    public void setMVRepairEnabled(String repairType, boolean enabled)
    {
        config.setMaterializedViewRepairEnabled(RepairType.parse(repairType), enabled);
    }

    @Override
    public void setRepairSessionTimeout(String repairType, String timeout)
    {
        config.setRepairSessionTimeout(RepairType.parse(repairType), timeout);
    }

    @Override
    public Set<String> getOnGoingRepairHostIds(String repairType)
    {
        List<AutoRepairUtils.AutoRepairHistory> histories = AutoRepairUtils.getAutoRepairHistory(RepairType.parse(repairType));
        if (histories == null)
        {
            return Collections.emptySet();
        }
        Set<String> hostIds = new HashSet<>();
        AutoRepairUtils.CurrentRepairStatus currentRepairStatus = new AutoRepairUtils.CurrentRepairStatus(histories, AutoRepairUtils.getPriorityHostIds(RepairType.parse(repairType)), null);
        for (UUID id : currentRepairStatus.hostIdsWithOnGoingRepair)
        {
            hostIds.add(id.toString());
        }
        for (UUID id : currentRepairStatus.hostIdsWithOnGoingForceRepair)
        {
            hostIds.add(id.toString());
        }
        return Collections.unmodifiableSet(hostIds);
    }

    @Override
    public void setAutoRepairTokenRangeSplitterParameter(String repairType, String key, String value)
    {
        config.getTokenRangeSplitterInstance(RepairType.parse(repairType)).setParameter(key, value);
    }

    @Override
    public void setRepairByKeyspace(String repairType, boolean repairByKeyspace)
    {
        config.setRepairByKeyspace(RepairType.parse(repairType), repairByKeyspace);
    }

    @Override
    public void setAutoRepairMaxRetriesCount(String repairType, int retries)
    {
        config.setRepairMaxRetries(RepairType.parse(repairType), retries);
    }

    @Override
    public void setAutoRepairRetryBackoff(String repairType, String interval)
    {
        config.setRepairRetryBackoff(RepairType.parse(repairType), interval);
    }

    @Override
    public void setMixedMajorVersionRepairEnabled(boolean enabled)
    {
        config.setMixedMajorVersionRepairEnabled(enabled);
    }

    private String formatRepairTypeConfig(RepairType repairType, AutoRepairConfig config)
    {
        StringBuilder sb = new StringBuilder();
        sb.append("\nconfiguration for repair_type: ").append(repairType.getConfigName());
        sb.append("\n\tenabled: ").append(config.isAutoRepairEnabled(repairType));
        // Only show configuration if enabled
        if (config.isAutoRepairEnabled(repairType))
        {
            Set<InetAddressAndPort> priorityHosts = AutoRepairUtils.getPriorityHosts(repairType);
            if (!priorityHosts.isEmpty())
            {
                appendConfig(sb, "priority_hosts", Joiner.on(',').skipNulls().join(priorityHosts));
            }

            appendConfig(sb, "min_repair_interval", config.getRepairMinInterval(repairType));
            appendConfig(sb, "repair_by_keyspace", config.getRepairByKeyspace(repairType));
            appendConfig(sb, "number_of_repair_threads", config.getRepairThreads(repairType));
            appendConfig(sb, "sstable_upper_threshold", config.getRepairSSTableCountHigherThreshold(repairType));
            appendConfig(sb, "table_max_repair_time", config.getAutoRepairTableMaxRepairTime(repairType));
            appendConfig(sb, "ignore_dcs", config.getIgnoreDCs(repairType));
            appendConfig(sb, "repair_primary_token_range_only", config.getRepairPrimaryTokenRangeOnly(repairType));
            appendConfig(sb, "parallel_repair_count", config.getParallelRepairCount(repairType));
            appendConfig(sb, "parallel_repair_percentage", config.getParallelRepairPercentage(repairType));
            appendConfig(sb, "allow_parallel_replica_repair", config.getAllowParallelReplicaRepair(repairType));
            appendConfig(sb, "allow_parallel_replica_repair_across_schedules", config.getAllowParallelReplicaRepairAcrossSchedules(repairType));
            appendConfig(sb, "materialized_view_repair_enabled", config.getMaterializedViewRepairEnabled(repairType));
            appendConfig(sb, "initial_scheduler_delay", config.getInitialSchedulerDelay(repairType));
            appendConfig(sb, "repair_session_timeout", config.getRepairSessionTimeout(repairType));
            appendConfig(sb, "force_repair_new_node", config.getForceRepairNewNode(repairType));
            appendConfig(sb, "repair_max_retries", config.getRepairMaxRetries(repairType));
            appendConfig(sb, "repair_retry_backoff", config.getRepairRetryBackoff(repairType));

            final ParameterizedClass splitterClass = config.getTokenRangeSplitter(repairType);
            final String splitterClassName =  splitterClass.class_name != null ? splitterClass.class_name : AutoRepairConfig.DEFAULT_SPLITTER.getName();
            appendConfig(sb, "token_range_splitter", splitterClassName);
            Map<String, String> tokenRangeSplitterParameters = config.getTokenRangeSplitterInstance(repairType).getParameters();
            if (!tokenRangeSplitterParameters.isEmpty())
            {
                for (Map.Entry<String, String> param : tokenRangeSplitterParameters.entrySet())
                {
                    appendConfig(sb, String.format("token_range_splitter.%s", param.getKey()), param.getValue());
                }
            }
        }

        return sb.toString();
    }

    private <T> void appendConfig(StringBuilder sb, String config, T value)
    {
        sb.append(String.format("%s%s: %s", "\n\t", config, value));
    }
}
