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

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig.RepairType;
import org.apache.cassandra.repair.autorepair.AutoRepairUtils;
import org.apache.cassandra.repair.autorepair.AutoRepair;
import org.apache.cassandra.service.AutoRepairService;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;

/**
 * Metrics related to AutoRepair.
 */
public class AutoRepairMetrics
{
    public final Gauge<Integer> repairsInProgress;
    public final Gauge<Integer> nodeRepairTimeInSec;
    public final Gauge<Integer> clusterRepairTimeInSec;
    public final Gauge<Integer> longestUnrepairedSec;
    public final Gauge<Integer> repairStartLagSec;
    public final Gauge<Integer> succeededTokenRangesCount;
    public final Gauge<Integer> failedTokenRangesCount;
    public final Gauge<Integer> skippedTokenRangesCount;
    public final Gauge<Integer> skippedTablesCount;
    public final Gauge<Integer> totalMVTablesConsideredForRepair;
    public final Gauge<Integer> totalDisabledRepairTables;
    public final Gauge<Long> totalBytesToRepair;
    public final Gauge<Long> bytesAlreadyRepaired;
    public final Gauge<Integer> totalKeyspaceRepairPlansToRepair;
    public final Gauge<Integer> keyspaceRepairPlansAlreadyRepaired;

    public Counter repairTurnMyTurn;
    public Counter repairTurnMyTurnDueToPriority;
    public Counter repairTurnMyTurnForceRepair;
    public Counter repairDelayedByReplica;
    public Counter repairDelayedBySchedule;

    private final RepairType repairType;

    private volatile int repairStartLagSecVal;

    public AutoRepairMetrics(RepairType repairType)
    {
        this.repairType = repairType;
        AutoRepairMetricsFactory factory = new AutoRepairMetricsFactory(repairType);

        repairsInProgress = Metrics.register(factory.createMetricName("RepairsInProgress"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).isRepairInProgress() ? 1 : 0;
            }
        });

        nodeRepairTimeInSec = Metrics.register(factory.createMetricName("NodeRepairTimeInSec"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getNodeRepairTimeInSec();
            }
        });

        clusterRepairTimeInSec = Metrics.register(factory.createMetricName("ClusterRepairTimeInSec"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getClusterRepairTimeInSec();
            }
        });

        skippedTokenRangesCount = Metrics.register(factory.createMetricName("SkippedTokenRangesCount"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getSkippedTokenRangesCount();
            }
        });

        skippedTablesCount = Metrics.register(factory.createMetricName("SkippedTablesCount"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getSkippedTablesCount();
            }
        });

        longestUnrepairedSec = Metrics.register(factory.createMetricName("LongestUnrepairedSec"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getLongestUnrepairedSec();
            }
        });

        repairStartLagSec = Metrics.register(factory.createMetricName("RepairStartLagSec"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return repairStartLagSecVal;
            }
        });

        succeededTokenRangesCount = Metrics.register(factory.createMetricName("SucceededTokenRangesCount"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getSucceededTokenRangesCount();
            }
        });

        failedTokenRangesCount = Metrics.register(factory.createMetricName("FailedTokenRangesCount"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getFailedTokenRangesCount();
            }
        });

        repairTurnMyTurn = Metrics.counter(factory.createMetricName("RepairTurnMyTurn"));
        repairTurnMyTurnDueToPriority = Metrics.counter(factory.createMetricName("RepairTurnMyTurnDueToPriority"));
        repairTurnMyTurnForceRepair = Metrics.counter(factory.createMetricName("RepairTurnMyTurnForceRepair"));

        repairDelayedByReplica = Metrics.counter(factory.createMetricName("RepairDelayedByReplica"));
        repairDelayedBySchedule = Metrics.counter(factory.createMetricName("RepairDelayedBySchedule"));

        totalMVTablesConsideredForRepair = Metrics.register(factory.createMetricName("TotalMVTablesConsideredForRepair"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getTotalMVTablesConsideredForRepair();
            }
        });

        totalDisabledRepairTables = Metrics.register(factory.createMetricName("TotalDisabledRepairTables"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getTotalDisabledTablesRepairCount();
            }
        });
        totalBytesToRepair = Metrics.register(factory.createMetricName("TotalBytesToRepair"), new Gauge<Long>()
        {
            public Long getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getTotalBytesToRepair();
            }
        });
        bytesAlreadyRepaired = Metrics.register(factory.createMetricName("BytesAlreadyRepaired"), new Gauge<Long>()
        {
            public Long getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getBytesAlreadyRepaired();
            }
        });
        totalKeyspaceRepairPlansToRepair = Metrics.register(factory.createMetricName("TotalKeyspaceRepairPlansToRepair"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getTotalKeyspaceRepairPlansToRepair();
            }
        });
        keyspaceRepairPlansAlreadyRepaired = Metrics.register(factory.createMetricName("KeyspaceRepairPlansAlreadyRepaired"), new Gauge<Integer>()
        {
            public Integer getValue()
            {
                return AutoRepair.instance.getRepairState(repairType).getKeyspaceRepairPlansAlreadyRepaired();
            }
        });
    }

    public void recordTurn(AutoRepairUtils.RepairTurn turn)
    {
        switch (turn)
        {
            case MY_TURN:
                repairTurnMyTurn.inc();
                break;
            case MY_TURN_FORCE_REPAIR:
                repairTurnMyTurnForceRepair.inc();
                break;
            case MY_TURN_DUE_TO_PRIORITY:
                repairTurnMyTurnDueToPriority.inc();
                break;
            default:
                throw new RuntimeException(String.format("Unrecoginized turn: %s", turn.name()));
        }
        this.repairStartLagSecVal = 0;
    }

    /**
     * Record perceived lag in scheduling repair.
     * <p/>
     * Takes the current time and subtracts it from the given last repair finish time.  It then compares the difference
     * with the min repair interval for this repair type, and if that value is greater than 0, records it.
     */
    public void recordRepairStartLag(long lastFinishTimeInMs)
    {
        long now = AutoRepair.instance.currentTimeMs();
        long deltaFinish = now - lastFinishTimeInMs;
        long deltaMinRepairInterval = deltaFinish - AutoRepairService.instance
                                                    .getAutoRepairConfig().getRepairMinInterval(repairType)
                                                    .toMilliseconds();
        this.repairStartLagSecVal = deltaMinRepairInterval > 0 ? (int) MILLISECONDS.toSeconds(deltaMinRepairInterval) : 0;
    }

    @VisibleForTesting
    protected static class AutoRepairMetricsFactory implements MetricNameFactory
    {
        private static final String TYPE = "AutoRepair";
        @VisibleForTesting
        protected final String repairType;

        protected AutoRepairMetricsFactory(RepairType repairType)
        {
            this.repairType = repairType.toString().toLowerCase();
        }

        @Override
        public CassandraMetricsRegistry.MetricName createMetricName(String metricName)
        {
            StringBuilder mbeanName = new StringBuilder();
            mbeanName.append(DefaultNameFactory.GROUP_NAME).append(':');
            mbeanName.append("type=").append(TYPE);
            mbeanName.append(",name=").append(metricName);
            mbeanName.append(",repairType=").append(repairType);

            StringBuilder scope = new StringBuilder();
            scope.append("repairType=").append(repairType);

            return new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME, TYPE.toLowerCase(),
                                                           metricName, scope.toString(), mbeanName.toString());
        }
    }
}
