package healthcheck

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/db"
)

const (
	healthCheckItemStatusSuccess = "success"
	healthCheckItemStatusPending = "pending"
	healthCheckItemStatusFailed  = "failed"
	healthCheckItemStatusSkipped = "skipped"
)

type healthCheckStatusSummary struct {
	status       string
	errorMsg     string
	successItems int
	pendingItems int
	failedItems  int
}

type healthCheckResultStatusUpdater interface {
	UpdateHealthCheckResultStatus(uuid, status, errorMsg string) error
}

func (s *HealthCheckService) buildHealthCheckItemReport(result *db.HealthCheckResult, item CheckItem, includeContent bool) (HealthCheckItemReport, bool) {
	itemReport := HealthCheckItemReport{
		Name:        item.Name,
		Description: item.Description,
		CheckType:   item.CheckType,
		Params:      parseCheckItemParams(item.Params),
		Status:      healthCheckItemStatusPending,
	}

	if item.Failed {
		itemReport.Status = healthCheckItemStatusFailed
		itemReport.ErrorMsg = item.ErrorMsg
		return itemReport, false
	}

	content, err := s.getReportFromS3(result.ClusterID, result.UUID, item.Name)
	if err != nil {
		itemReport.Status = healthCheckItemStatusPending
		itemReport.ErrorMsg = "result not ready"
		return itemReport, !isHealthCheckS3NotFoundError(err)
	}

	itemReport.Status, itemReport.ErrorMsg = healthCheckItemStatusFromContent(content)
	if includeContent && content != nil {
		contentJSON, _ := json.Marshal(content)
		itemReport.Content = contentJSON
	}
	return itemReport, false
}

func reconcileHealthCheckStatus(dbStatus string, items []HealthCheckItemReport) healthCheckStatusSummary {
	summary := healthCheckStatusSummary{status: dbStatus}
	totalItems := len(items)
	if totalItems == 0 {
		return summary
	}

	failedMessages := make([]string, 0)
	for _, item := range items {
		switch normalizeHealthCheckItemStatus(item.Status) {
		case healthCheckItemStatusSuccess, healthCheckItemStatusSkipped:
			summary.successItems++
		case healthCheckItemStatusFailed:
			summary.failedItems++
			if item.ErrorMsg != "" {
				failedMessages = append(failedMessages, fmt.Sprintf("%s: %s", item.Name, item.ErrorMsg))
			}
		default:
			summary.pendingItems++
		}
	}

	switch {
	case summary.failedItems == totalItems:
		summary.status = string(db.StatusFailed)
	case summary.pendingItems > 0:
		if summary.successItems == 0 && summary.failedItems == 0 {
			summary.status = nonReadyHealthCheckStatus(dbStatus)
		} else {
			summary.status = string(db.StatusPartial)
		}
	case summary.failedItems > 0:
		summary.status = string(db.StatusPartialFailed)
	default:
		summary.status = string(db.StatusSuccess)
	}

	if summary.failedItems > 0 {
		if len(failedMessages) > 0 {
			summary.errorMsg = strings.Join(failedMessages, "; ")
		} else if summary.status == string(db.StatusFailed) {
			summary.errorMsg = "all health check items failed"
		} else {
			summary.errorMsg = fmt.Sprintf("%d of %d health check items failed", summary.failedItems, totalItems)
		}
	}
	return summary
}

func nonReadyHealthCheckStatus(dbStatus string) string {
	switch db.StatusType(dbStatus) {
	case db.StatusPending, db.StatusRunning:
		return dbStatus
	default:
		return string(db.StatusPartial)
	}
}

func healthCheckItemStatusFromContent(content any) (string, string) {
	status := healthCheckItemStatusSuccess
	errorMsg := ""

	contentMap, ok := content.(map[string]any)
	if !ok {
		return status, errorMsg
	}

	if rawStatus, ok := contentMap["status"]; ok {
		status = normalizeHealthCheckItemStatus(fmt.Sprint(rawStatus))
	}
	errorMsg = firstStringField(contentMap, "errorMsg", "error_msg", "error", "message")
	return status, errorMsg
}

func normalizeHealthCheckItemStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "success", "succeeded", "ok", "pass", "passed":
		return healthCheckItemStatusSuccess
	case "skipped", "skip":
		return healthCheckItemStatusSkipped
	case "failed", "fail", "error", "partial_failed":
		return healthCheckItemStatusFailed
	case "pending", "running", "processing", "partial", "":
		return healthCheckItemStatusPending
	default:
		return healthCheckItemStatusSuccess
	}
}

func firstStringField(content map[string]any, keys ...string) string {
	for _, key := range keys {
		if value, ok := content[key]; ok && value != nil {
			text := strings.TrimSpace(fmt.Sprint(value))
			if text != "" && text != "<nil>" {
				return text
			}
		}
	}
	return ""
}

func shouldReadRepairHealthCheckResult(status string) bool {
	switch db.StatusType(status) {
	case db.StatusRunning, db.StatusPartial:
		return true
	default:
		return false
	}
}

func (s *HealthCheckService) collectHealthCheckItemReports(result *db.HealthCheckResult, itemsToRun []CheckItem, includeContent bool) ([]HealthCheckItemReport, healthCheckStatusSummary, int) {
	items := make([]HealthCheckItemReport, 0, len(itemsToRun))
	readErrorCount := 0
	for _, item := range itemsToRun {
		itemReport, readErr := s.buildHealthCheckItemReport(result, item, includeContent)
		if readErr {
			readErrorCount++
		}
		items = append(items, itemReport)
	}
	summary := reconcileHealthCheckStatus(result.Status, items)
	return items, summary, readErrorCount
}

func (s *HealthCheckService) readRepairHealthCheckResultStatus(result *db.HealthCheckResult, summary healthCheckStatusSummary, readErrorCount int, source string) {
	readRepairHealthCheckResultStatus(s.repository, result, summary, readErrorCount, source)
}

func readRepairHealthCheckResultStatus(repository healthCheckResultStatusUpdater, result *db.HealthCheckResult, summary healthCheckStatusSummary, readErrorCount int, source string) {
	if result == nil || summary.status == "" || summary.status == result.Status {
		return
	}
	if readErrorCount > 0 {
		log.Warn("skip health check result status read repair because s3 read had errors",
			zap.String("source", source),
			zap.String("uuid", result.UUID),
			zap.String("cluster_id", result.ClusterID),
			zap.String("dbaas_org_id", result.DbaasOrgID),
			zap.String("db_status", result.Status),
			zap.String("computed_status", summary.status),
			zap.Int("read_error_count", readErrorCount),
		)
		return
	}

	oldStatus := result.Status
	if repository == nil {
		log.Warn("skip health check result status read repair because repository is unavailable",
			zap.String("source", source),
			zap.String("uuid", result.UUID),
			zap.String("cluster_id", result.ClusterID),
			zap.String("dbaas_org_id", result.DbaasOrgID),
			zap.String("db_status", oldStatus),
			zap.String("computed_status", summary.status),
		)
		return
	}
	if err := repository.UpdateHealthCheckResultStatus(result.UUID, summary.status, summary.errorMsg); err != nil {
		log.Warn("read repair health check result status failed",
			zap.Error(err),
			zap.String("source", source),
			zap.String("uuid", result.UUID),
			zap.String("cluster_id", result.ClusterID),
			zap.String("dbaas_org_id", result.DbaasOrgID),
			zap.String("db_status", oldStatus),
			zap.String("computed_status", summary.status),
		)
		return
	}

	result.Status = summary.status
	result.ErrorMsg = summary.errorMsg
	log.Info("read repaired health check result status",
		zap.String("source", source),
		zap.String("uuid", result.UUID),
		zap.String("cluster_id", result.ClusterID),
		zap.String("dbaas_org_id", result.DbaasOrgID),
		zap.String("old_status", oldStatus),
		zap.String("new_status", summary.status),
		zap.Int("success_items", summary.successItems),
		zap.Int("pending_items", summary.pendingItems),
		zap.Int("failed_items", summary.failedItems),
	)
}

func logHealthCheckStatusSummary(source string, result *db.HealthCheckResult, summary healthCheckStatusSummary, readErrorCount int) {
	log.Info("health check result status summary",
		zap.String("source", source),
		zap.String("uuid", result.UUID),
		zap.String("cluster_id", result.ClusterID),
		zap.String("dbaas_org_id", result.DbaasOrgID),
		zap.String("db_status", result.Status),
		zap.String("computed_status", summary.status),
		zap.Int("success_items", summary.successItems),
		zap.Int("pending_items", summary.pendingItems),
		zap.Int("failed_items", summary.failedItems),
		zap.Int("read_error_count", readErrorCount),
	)
}

func isHealthCheckS3NotFoundError(err error) bool {
	if err == nil {
		return false
	}

	var reqErr awserr.RequestFailure
	if errors.As(err, &reqErr) && reqErr.StatusCode() == http.StatusNotFound {
		return true
	}

	var awsErr awserr.Error
	if errors.As(err, &awsErr) {
		switch awsErr.Code() {
		case s3.ErrCodeNoSuchKey, "NotFound", "404":
			return true
		}
	}

	text := strings.ToLower(err.Error())
	return strings.Contains(text, "status code: 404") ||
		strings.Contains(text, "nosuchkey") ||
		strings.Contains(text, "notfound") ||
		strings.Contains(text, "not found")
}
