// Copyright 2024 PingCAP, Inc.
//
// Licensed 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 priorityqueue

import (
	"time"

	"github.com/pingcap/tidb/pkg/sessionctx"
	"github.com/pingcap/tidb/pkg/statistics/handle/util"
)

// NoRecord is used to indicate that there is no related record in mysql.analyze_jobs.
const NoRecord = -1

// justFailed is used to indicate that the last analysis has just failed.
const justFailed = 0

const avgDurationQueryForTable = `
	SELECT AVG(TIMESTAMPDIFF(SECOND, start_time, end_time)) AS avg_duration
	FROM (
		SELECT start_time, end_time
		FROM mysql.analyze_jobs
		WHERE table_schema = %? AND table_name = %? AND state = 'finished' AND fail_reason IS NULL AND partition_name = ''
		ORDER BY id DESC
		LIMIT 5
	) AS recent_analyses;
`

// For multiple partitions, we only need to return the average duration of the most recent 5 successful analyses
// from all partitions.
const avgDurationQueryForPartition = `
	SELECT AVG(TIMESTAMPDIFF(SECOND, start_time, end_time)) AS avg_duration
	FROM (
		SELECT start_time, end_time
		FROM mysql.analyze_jobs
		WHERE table_schema = %? AND table_name = %? AND state = 'finished' AND fail_reason IS NULL AND partition_name in (%?)
		ORDER BY id DESC
		LIMIT 5
	) AS recent_analyses;
`

const lastFailedDurationQueryForTable = `
	SELECT TIMESTAMPDIFF(SECOND, start_time, CURRENT_TIMESTAMP)
	FROM mysql.analyze_jobs
	WHERE table_schema = %? AND table_name = %? AND state = 'failed' AND partition_name = ''
	ORDER BY id DESC
	LIMIT 1;
`

// LastFailedDurationQueryForPartition is used to get the duration of the last failed analysis for each specified partition.
// Exported for testing.
// For multiple partitions, we only need to return the duration of the most recent failed analysis.
// We pick the minimum duration of all failed analyses because we want to be conservative.
const LastFailedDurationQueryForPartition = `
	SELECT
		MIN(TIMESTAMPDIFF(SECOND, aj.start_time, CURRENT_TIMESTAMP)) AS min_duration
	FROM (
		SELECT
			MAX(id) AS max_id
		FROM
			mysql.analyze_jobs
		WHERE
			table_schema = %?
			AND table_name = %?
			AND state = 'failed'
			AND partition_name IN (%?)
		GROUP BY
			partition_name
	) AS latest_failures
	JOIN mysql.analyze_jobs aj ON aj.id = latest_failures.max_id;
`

// GetAverageAnalysisDuration returns the average duration of the last 5 successful analyses for each specified partition.
// If there are no successful analyses, it returns 0.
func GetAverageAnalysisDuration(
	sctx sessionctx.Context,
	schema, tableName string,
	partitionNames ...string,
) (time.Duration, error) {
	query := ""
	params := make([]any, 0, len(partitionNames)+3)
	if len(partitionNames) == 0 {
		query = avgDurationQueryForTable
		params = append(params, schema, tableName)
	} else {
		query = avgDurationQueryForPartition
		params = append(params, schema, tableName, partitionNames)
	}

	rows, _, err := util.ExecRows(sctx, query, params...)
	if err != nil {
		return NoRecord, err
	}

	// NOTE: if there are no successful analyses, we return 0.
	if len(rows) == 0 || rows[0].IsNull(0) {
		return NoRecord, nil
	}
	avgDuration := rows[0].GetMyDecimal(0)
	duration, err := avgDuration.ToFloat64()
	if err != nil {
		return NoRecord, err
	}

	return time.Duration(duration) * time.Second, nil
}

// GetLastFailedAnalysisDuration returns the duration since the last failed analysis.
// If there is no failed analysis, it returns 0.
func GetLastFailedAnalysisDuration(
	sctx sessionctx.Context,
	schema, tableName string,
	partitionNames ...string,
) (time.Duration, error) {
	query := ""
	params := make([]any, 0, len(partitionNames)+3)
	if len(partitionNames) == 0 {
		query = lastFailedDurationQueryForTable
		params = append(params, schema, tableName)
	} else {
		query = LastFailedDurationQueryForPartition
		params = append(params, schema, tableName, partitionNames)
	}

	rows, _, err := util.ExecRows(sctx, query, params...)
	if err != nil {
		return NoRecord, err
	}

	// NOTE: if there are no failed analyses, we return 0.
	if len(rows) == 0 || rows[0].IsNull(0) {
		return NoRecord, nil
	}
	lastFailedDuration := rows[0].GetUint64(0)
	if lastFailedDuration == 0 {
		return justFailed, nil
	}

	return time.Duration(lastFailedDuration) * time.Second, nil
}
