package imports

import (
	"sort"
	"strings"
	"time"

	"github.com/docker/go-units"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

const (
	estimatedPostProcessSizePerSecond = 500 * units.MiB
	maxPreProcessedPercent            = 50
)

func calculateTableCompletePercentForRemoteBackend(
	tblStatus *cloudv1.CurrentTableStatus, prevCollectTS, currentCollectTS int64) float64 {
	switch tblStatus.Phase {
	case cloudv1.TableImportPhaseUnknown, cloudv1.TableImportPhasePending:
		return 0.0
	case cloudv1.TableImportPhasePreProcessing:
		// encoding stage
		if tblStatus.Size > 0 {
			return float64(tblStatus.ProcessedSize*maxPreProcessedPercent) / float64(tblStatus.Size)
		} else {
			return float64(maxPreProcessedPercent)
		}
	case cloudv1.TableImportPhaseImporting, cloudv1.TableImportPhaseIndexBuilding:
		if tblStatus.CompletedPercent == 0 {
			return float64(maxPreProcessedPercent)
		}
		percentDelta := 0.0
		if prevCollectTS > 0 && currentCollectTS > 0 && currentCollectTS > prevCollectTS && tblStatus.Size > 0 {
			percentDelta = float64(100-maxPreProcessedPercent) * //nolint: gomnd // rest of the percent
				(float64(currentCollectTS-prevCollectTS) / // elapsed time since last collection
					float64(tblStatus.Size/getEstimatedImportSizePerSecond(tblStatus.Size))) // estimated total time
		}
		result := float64(tblStatus.CompletedPercent) + percentDelta
		if result >= 100.0 { //nolint: gomnd // no need
			return 99.0 //nolint: gomnd // no need
		}
		return result
	case cloudv1.TableImportPhaseFailed:
		// For failed state, set to a small value,
		// so that if there is an existing progress value,
		// it will be kept.
		return 0.0
	default:
		return 100.0 //nolint: gomnd // no need
	}
}

// getEstimatedImportSizePerSecond returns the estimated import size per second
// TODO this is a rough estimate, need to be improved.
func getEstimatedImportSizePerSecond(tableSize uint64) uint64 {
	switch {
	case tableSize > 500*units.GiB:
		return 1 * units.GiB
	case tableSize > 100*units.GiB:
		return 500 * units.MiB //nolint: gomnd // no need
	default:
		return 50 * units.MiB //nolint: gomnd // no need
	}
}

func estimateTableImportPhaseForRemoteBackend(
	overallPhase cloudv1.ImportConditionType,
	taskPhase cloudv1.ImportTaskPhaseType,
	tblProgress *TableProgress) cloudv1.TableImportPhaseType {
	if len(tblProgress.Message) > 0 {
		return cloudv1.TableImportPhaseFailed
	}
	switch tblProgress.Status {
	case taskStatusRunning:
		if tblProgress.TotalWritten >= tblProgress.TotalSize {
			return cloudv1.TableImportPhaseImporting
		} else {
			return cloudv1.TableImportPhasePreProcessing
		}
	case taskStatusCompleted:
		switch {
		case taskPhase == cloudv1.ImportTaskPhasePostProcessing:
			return cloudv1.TableImportPhasePostProcessing
		case overallPhase == cloudv1.ImportCompleted:
			return cloudv1.TableImportPhaseComplete
		default:
			return cloudv1.TableImportPhaseImported
		}
	default:
		return cloudv1.TableImportPhasePending
	}
}

func updateImportPhase(importStatus *cloudv1.ImportStatus, task *TaskProgress) {
	if task.Status == taskStatusRunning {
		importStatus.Phase = cloudv1.ImportRunning
		importStatus.IsTaskCreated = true
	}

	if task.Status == taskStatusCompleted {
		if task.Message == "" {
			importStatus.Phase = cloudv1.ImportCompleted
		} else {
			importStatus.Phase = cloudv1.ImportFailed
		}
	}
}

func collectTableStatistics(task *TaskProgress) (uint64, int64, int, int) {
	var totalSize uint64
	var totalProcessedSize int64
	var completedTables, pendingTables int

	for _, tableInfo := range task.Tables {
		totalSize += uint64(tableInfo.TotalSize)     // sum of total size
		totalProcessedSize += tableInfo.TotalWritten // sum of processed size
		switch tableInfo.Status {
		case taskStatusNotStarted:
			pendingTables++
		case taskStatusCompleted:
			completedTables++
		default:
			// do nothing
		}
	}

	return totalSize, totalProcessedSize, completedTables, pendingTables
}

func updateTaskPhase(importStatus *cloudv1.ImportStatus, totalTables, completedTables int) {
	if importStatus.Phase == cloudv1.ImportRunning {
		if totalTables > 0 {
			if totalTables == completedTables {
				importStatus.TaskPhase = cloudv1.ImportTaskPhasePostProcessing
			} else {
				importStatus.TaskPhase = cloudv1.ImportTaskPhaseImporting
			}
		} else {
			importStatus.TaskPhase = cloudv1.ImportTaskPhaseUnknown
		}
	} else {
		importStatus.TaskPhase = cloudv1.ImportTaskPhaseUnknown
	}
}

func processTableStatuses(
	tblDetailStatusMap map[string]cloudv1.CurrentTableStatus,
) ([]cloudv1.CurrentTableStatus, []cloudv1.CurrentTableStatus) {
	var currentTables []cloudv1.CurrentTableStatus
	var allTableStatuses []cloudv1.CurrentTableStatus

	for _, tblStatus := range tblDetailStatusMap {
		allTableStatuses = append(allTableStatuses, tblStatus)
		switch tblStatus.Phase {
		case cloudv1.TableImportPhasePreProcessing,
			cloudv1.TableImportPhaseImporting,
			cloudv1.TableImportPhaseIndexBuilding:
			currentTables = append(currentTables, tblStatus)
		default:
			// do nothing
		}
	}

	sort.Slice(currentTables, func(i, j int) bool { return currentTables[i].Name < currentTables[j].Name })

	if len(tblDetailStatusMap) < 1000 { //nolint: gomnd // no need
		sort.Slice(allTableStatuses, func(i, j int) bool {
			return allTableStatuses[i].Name < allTableStatuses[j].Name
		})
		return currentTables, allTableStatuses
	}

	return currentTables, nil
}

func updateCompletionPercentages(
	importStatus *cloudv1.ImportStatus, weightedPercentSum float64, prevCollectTime, collectTime int64,
) {
	// Set overall percent
	switch {
	case importStatus.TaskPhase == cloudv1.ImportTaskPhasePostProcessing:
		importStatus.CompletedPercent = 100
	case importStatus.Phase == cloudv1.ImportCompleted:
		importStatus.CompletedPercent = 100
	default:
		if uint32(weightedPercentSum) > importStatus.CompletedPercent {
			importStatus.CompletedPercent = uint32(weightedPercentSum)
		}
	}

	// Get the overall post-processing percent
	postProcessProgress := uint32(estimateOverallPostProcessingPercent(
		importStatus, prevCollectTime, collectTime))
	if postProcessProgress > importStatus.PostImportCompletedPercent {
		importStatus.PostImportCompletedPercent = postProcessProgress
	}
}

func saveImportStatus(importStatus *cloudv1.ImportStatus, task *TaskProgress) {
	collectTime := time.Now().Unix()
	prevCollectTime := importStatus.CollectTime

	// Update phase
	updateImportPhase(importStatus, task)

	// Collect statistics
	totalSize, totalProcessedSize, completedTables, pendingTables := collectTableStatistics(task)
	totalTables := len(task.Tables)

	// Update task phase
	updateTaskPhase(importStatus, totalTables, completedTables)

	// Get detailed table status
	files, weightedPercentSum, tblDetailStatusMap :=
		getTableStatus(importStatus, task, prevCollectTime, collectTime, totalSize, totalTables)

	// Update basic status fields
	importStatus.CollectTime = collectTime
	importStatus.TotalTables = uint32(totalTables)
	importStatus.CompletedTables = uint32(completedTables)
	importStatus.PendingTables = uint32(pendingTables)
	importStatus.TotalSize = totalSize
	importStatus.TotalSizeWithIndex = uint64(task.SizeWithIndex)
	importStatus.ProcessedDataSize = uint64(totalProcessedSize)
	importStatus.Message = importErrorMessageDigest(task.Message)

	// Process and sort table statuses
	currentTables, allTableDetails := processTableStatuses(tblDetailStatusMap)
	importStatus.CurrentTables = currentTables
	importStatus.AllTableDetails = allTableDetails

	// Update completion percentages
	updateCompletionPercentages(importStatus, weightedPercentSum, prevCollectTime, collectTime)

	// Update total files
	if importStatus.TotalFiles == nil || *importStatus.TotalFiles < files {
		importStatus.TotalFiles = &files
	}
}

func getTableStatus(
	importStatus *cloudv1.ImportStatus, task *TaskProgress, prevCollectTime int64, collectTime int64,
	totalSize uint64, totalTables int,
) (uint32, float64, map[string]cloudv1.CurrentTableStatus) {
	var files uint32
	var weightedPercentSum = 0.0
	tblDetailStatusMap := map[string]cloudv1.CurrentTableStatus{}
	for _, tblStatus := range importStatus.AllTableDetails {
		tblDetailStatusMap[tblStatus.Name] = tblStatus
	}
	if len(tblDetailStatusMap) == 0 {
		for _, tblStatus := range importStatus.CurrentTables {
			tblDetailStatusMap[tblStatus.Name] = tblStatus
		}
	}
	// construct current table status list and percent
	for name, tableInfo := range task.Tables {
		var (
			current cloudv1.CurrentTableStatus
			ok      bool
		)

		current, ok = tblDetailStatusMap[name]
		if !ok {
			current = cloudv1.CurrentTableStatus{
				Name:             name,
				Phase:            cloudv1.TableImportPhaseUnknown,
				Size:             uint64(tableInfo.TotalSize),
				ProcessedSize:    uint64(tableInfo.TotalWritten),
				CompletedPercent: 0,
				Message:          importErrorMessageDigest(tableInfo.Message),
			}
		} else {
			current.Size = uint64(tableInfo.TotalSize)
			current.ProcessedSize = uint64(tableInfo.TotalWritten)
			current.Message = importErrorMessageDigest(tableInfo.Message)
		}

		// determine the current table phase
		curTblPhase := estimateTableImportPhaseForRemoteBackend(importStatus.Phase, importStatus.TaskPhase, tableInfo)
		if curTblPhase.StepGte(current.Phase) {
			current.Phase = curTblPhase
		}

		// calculate the percent of a table
		percent := uint32(calculateTableCompletePercentForRemoteBackend(&current, prevCollectTime, collectTime))
		if percent > current.CompletedPercent {
			current.CompletedPercent = percent
		}
		tblDetailStatusMap[name] = current
		if totalSize > 0 {
			weightedPercentSum += float64(current.CompletedPercent) * float64(current.Size) / float64(totalSize)
		} else {
			weightedPercentSum += float64(current.CompletedPercent) / float64(totalTables)
		}
	}
	return files, weightedPercentSum, tblDetailStatusMap
}

func estimateOverallPostProcessingPercent(
	importStatus *cloudv1.ImportStatus,
	prevCollectTS,
	currentCollectTS int64,
) float64 {
	switch {
	case importStatus.Phase == cloudv1.ImportCompleted:
		return 100.0 //nolint: gomnd // no need
	case importStatus.Phase == cloudv1.ImportRunning &&
		importStatus.TaskPhase == cloudv1.ImportTaskPhasePostProcessing:
		if importStatus.PostImportCompletedPercent == 0 {
			return 1.0
		}
		percentDelta := 0.0
		if prevCollectTS > 0 && currentCollectTS > 0 && currentCollectTS > prevCollectTS &&
			importStatus.TotalSize > 0 {
			percentDelta = float64(100) * //nolint: gomnd // no need
				(float64(currentCollectTS-prevCollectTS) / // elapsed time since last collection
					float64(importStatus.TotalSize/estimatedPostProcessSizePerSecond)) // estimated total time
		}
		result := float64(importStatus.PostImportCompletedPercent) + percentDelta
		if result >= 100.0 { //nolint: gomnd // no need
			return 99.0 //nolint: gomnd // no need
		}
		return result
	default:
		// set to a small value,
		// so that if there is an existing progress value,
		// it will be kept.
		return 0.0
	}
}

func importErrorMessageDigest(message string) string {
	if message == "" {
		return ""
	}
	unwrapErrMsg := unwrapMultiErr(message)
	// remove the stack trace
	idx := strings.Index(unwrapErrMsg, "github.com")
	if idx != -1 {
		unwrapErrMsg = unwrapErrMsg[:idx]
	}
	return strings.TrimSpace(unwrapErrMsg)
}

// unwrapMultiErr returns the first error message from multiErr.
func unwrapMultiErr(message string) string {
	var (
		// https://github.com/uber-go/multierr/blob/de75ae527b39a27afcb50a84427ec7b84021d5f4/error.go#L157-L172
		_multilinePrefix    = "the following errors occurred:"
		_multilineSeparator = "\n -  "
	)

	if !strings.HasPrefix(message, _multilinePrefix) {
		return message
	}

	message = strings.TrimPrefix(message, _multilinePrefix)
	items := strings.Split(message, _multilineSeparator)
	if len(items) <= 1 {
		return message
	}
	return items[1]
}
