package healthcheck

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"gorm.io/gorm"

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

var (
	ErrHealthCheckReportNotFound           = errors.New("health check report not found")
	ErrHealthCheckReportItemNotFound       = errors.New("health check report item not found")
	ErrHealthCheckReportStorageUnavailable = errors.New("health check report storage unavailable")
)

type HealthCheckReportListOptions struct {
	Page        int
	PageSize    int
	Statuses    []string
	CreatedFrom *time.Time
	CreatedTo   *time.Time
}

type HealthCheckReportCheckItem struct {
	Name         string `json:"name"`
	Description  string `json:"description,omitempty"`
	CheckType    string `json:"checkType,omitempty"`
	Status       string `json:"status,omitempty"`
	ErrorMessage string `json:"errorMessage,omitempty"`
}

type HealthCheckReportSummary struct {
	ReportID        string                       `json:"reportId"`
	StrategyID      string                       `json:"strategyId"`
	OrgID           string                       `json:"orgId"`
	PortalOrgID     string                       `json:"portalOrgId,omitempty"`
	ClusterID       string                       `json:"clusterId"`
	Status          string                       `json:"status"`
	ErrorMessage    string                       `json:"errorMessage,omitempty"`
	CreatedAt       time.Time                    `json:"createdAt"`
	UpdatedAt       time.Time                    `json:"updatedAt"`
	TimeRangeAStart *time.Time                   `json:"timeRangeAStart,omitempty"`
	TimeRangeAEnd   *time.Time                   `json:"timeRangeAEnd,omitempty"`
	TimeRangeBStart *time.Time                   `json:"timeRangeBStart,omitempty"`
	TimeRangeBEnd   *time.Time                   `json:"timeRangeBEnd,omitempty"`
	CheckItems      []HealthCheckReportCheckItem `json:"checkItems"`
}

type HealthCheckReportPage struct {
	Items    []HealthCheckReportSummary `json:"items"`
	Page     int                        `json:"page"`
	PageSize int                        `json:"pageSize"`
	Total    int64                      `json:"total"`
}

type HealthCheckReportItem struct {
	Name         string          `json:"name"`
	Description  string          `json:"description,omitempty"`
	CheckType    string          `json:"checkType,omitempty"`
	Params       json.RawMessage `json:"params,omitempty" swaggertype:"object"`
	Status       string          `json:"status"`
	ErrorMessage string          `json:"errorMessage,omitempty"`
	Content      json.RawMessage `json:"content,omitempty" swaggertype:"object"`
}

type HealthCheckReport struct {
	ReportID        string                  `json:"reportId"`
	StrategyID      string                  `json:"strategyId"`
	OrgID           string                  `json:"orgId"`
	PortalOrgID     string                  `json:"portalOrgId,omitempty"`
	ClusterID       string                  `json:"clusterId"`
	Status          string                  `json:"status"`
	ErrorMessage    string                  `json:"errorMessage,omitempty"`
	CreatedAt       time.Time               `json:"createdAt"`
	UpdatedAt       time.Time               `json:"updatedAt"`
	TimeRangeAStart *time.Time              `json:"timeRangeAStart,omitempty"`
	TimeRangeAEnd   *time.Time              `json:"timeRangeAEnd,omitempty"`
	TimeRangeBStart *time.Time              `json:"timeRangeBStart,omitempty"`
	TimeRangeBEnd   *time.Time              `json:"timeRangeBEnd,omitempty"`
	Items           []HealthCheckReportItem `json:"items"`
}

type healthCheckReportQueryRepository interface {
	ListHealthCheckResultsByScope(
		dbaasOrgID, clusterID string,
		options model.HealthCheckResultListOptions,
	) ([]db.HealthCheckResult, int64, error)
	GetHealthCheckResultByScope(dbaasOrgID, clusterID, reportID string) (*db.HealthCheckResult, error)
	UpdateHealthCheckResultStatus(uuid, status, errorMsg string) error
}

// HealthCheckReportQueryService exposes existing report rows and objects. It
// may repair stale report status fields using the same S3 evidence as the
// Portal UI, but it never creates tasks or changes report contents.
type HealthCheckReportQueryService struct {
	repository healthCheckReportQueryRepository
	s3Client   s3GetObjectAPI
}

func NewHealthCheckReportQueryService(cfg *config.Config) *HealthCheckReportQueryService {
	return &HealthCheckReportQueryService{
		repository: model.NewHealthCheckDB(),
		s3Client:   newHealthCheckS3Client(cfg),
	}
}

func newHealthCheckReportQueryService(
	repository healthCheckReportQueryRepository,
	s3Client s3GetObjectAPI,
) *HealthCheckReportQueryService {
	return &HealthCheckReportQueryService{repository: repository, s3Client: s3Client}
}

func (s *HealthCheckReportQueryService) ListReports(
	orgID, clusterID string,
	options HealthCheckReportListOptions,
) (*HealthCheckReportPage, error) {
	results, total, err := s.repository.ListHealthCheckResultsByScope(
		orgID,
		clusterID,
		model.HealthCheckResultListOptions{
			Page:        options.Page,
			PageSize:    options.PageSize,
			Statuses:    options.Statuses,
			CreatedFrom: options.CreatedFrom,
			CreatedTo:   options.CreatedTo,
		},
	)
	if err != nil {
		return nil, fmt.Errorf("list health check reports: %w", err)
	}

	items := make([]HealthCheckReportSummary, 0, len(results))
	reader := &HealthCheckService{s3Client: s.s3Client}
	for i := range results {
		result := &results[i]
		summary := healthCheckReportSummaryFromResult(result)
		if s.s3Client != nil && shouldReadRepairHealthCheckResult(result.Status) {
			itemReports, statusSummary, readErrorCount := reader.collectHealthCheckItemReports(
				result,
				parseItemsToRun(result.ItemsToRun),
				false,
			)
			summary.CheckItems = healthCheckReportCheckItemsFromReports(itemReports)
			if readErrorCount == 0 {
				summary.Status = statusSummary.status
				summary.ErrorMessage = statusSummary.errorMsg
			}
			readRepairHealthCheckResultStatus(
				s.repository,
				result,
				statusSummary,
				readErrorCount,
				"canonical_list_reports",
			)
		}
		items = append(items, summary)
	}
	return &HealthCheckReportPage{
		Items:    items,
		Page:     options.Page,
		PageSize: options.PageSize,
		Total:    total,
	}, nil
}

func (s *HealthCheckReportQueryService) GetReport(
	orgID, clusterID, reportID string,
	itemNames []string,
) (*HealthCheckReport, error) {
	result, err := s.repository.GetHealthCheckResultByScope(orgID, clusterID, reportID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, ErrHealthCheckReportNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get health check report: %w", err)
	}

	allItemsToRun := parseItemsToRun(result.ItemsToRun)
	itemsToRun, err := selectHealthCheckReportItems(allItemsToRun, itemNames)
	if err != nil {
		return nil, err
	}
	// Read all item statuses so report-level status has exactly the same meaning
	// as it does in Clinic Portal. The response still contains only the requested
	// item bodies.
	reader := &HealthCheckService{s3Client: s.s3Client}
	items, statusSummary, readErrorCount := reader.collectHealthCheckItemReports(result, allItemsToRun, true)
	if readErrorCount > 0 {
		return nil, fmt.Errorf("%w: %d report item reads failed", ErrHealthCheckReportStorageUnavailable, readErrorCount)
	}
	readRepairHealthCheckResultStatus(
		s.repository,
		result,
		statusSummary,
		readErrorCount,
		"canonical_get_report",
	)

	wanted := make(map[string]struct{}, len(itemsToRun))
	for _, item := range itemsToRun {
		wanted[item.Name] = struct{}{}
	}
	reportItems := make([]HealthCheckReportItem, 0, len(itemsToRun))
	for _, item := range items {
		if _, ok := wanted[item.Name]; !ok {
			continue
		}
		reportItems = append(reportItems, HealthCheckReportItem{
			Name:         item.Name,
			Description:  item.Description,
			CheckType:    item.CheckType,
			Params:       item.Params,
			Status:       item.Status,
			ErrorMessage: item.ErrorMsg,
			Content:      item.Content,
		})
	}
	return &HealthCheckReport{
		ReportID:        result.UUID,
		StrategyID:      result.StrategyUUID,
		OrgID:           result.DbaasOrgID,
		ClusterID:       result.ClusterID,
		Status:          statusSummary.status,
		ErrorMessage:    statusSummary.errorMsg,
		CreatedAt:       result.CreatedAt,
		UpdatedAt:       result.UpdatedAt,
		TimeRangeAStart: result.TimeRangeAStart,
		TimeRangeAEnd:   result.TimeRangeAEnd,
		TimeRangeBStart: result.TimeRangeBStart,
		TimeRangeBEnd:   result.TimeRangeBEnd,
		Items:           reportItems,
	}, nil
}

func healthCheckReportCheckItemsFromReports(items []HealthCheckItemReport) []HealthCheckReportCheckItem {
	checkItems := make([]HealthCheckReportCheckItem, 0, len(items))
	for _, item := range items {
		checkItems = append(checkItems, HealthCheckReportCheckItem{
			Name:         item.Name,
			Description:  item.Description,
			CheckType:    item.CheckType,
			Status:       item.Status,
			ErrorMessage: item.ErrorMsg,
		})
	}
	return checkItems
}

func healthCheckReportSummaryFromResult(result *db.HealthCheckResult) HealthCheckReportSummary {
	checkItems := make([]HealthCheckReportCheckItem, 0)
	for _, item := range parseItemsToRun(result.ItemsToRun) {
		status := ""
		if item.Failed {
			status = healthCheckItemStatusFailed
		}
		checkItems = append(checkItems, HealthCheckReportCheckItem{
			Name:         item.Name,
			Description:  item.Description,
			CheckType:    item.CheckType,
			Status:       status,
			ErrorMessage: item.ErrorMsg,
		})
	}
	return HealthCheckReportSummary{
		ReportID:        result.UUID,
		StrategyID:      result.StrategyUUID,
		OrgID:           result.DbaasOrgID,
		ClusterID:       result.ClusterID,
		Status:          result.Status,
		ErrorMessage:    result.ErrorMsg,
		CreatedAt:       result.CreatedAt,
		UpdatedAt:       result.UpdatedAt,
		TimeRangeAStart: result.TimeRangeAStart,
		TimeRangeAEnd:   result.TimeRangeAEnd,
		TimeRangeBStart: result.TimeRangeBStart,
		TimeRangeBEnd:   result.TimeRangeBEnd,
		CheckItems:      checkItems,
	}
}

func selectHealthCheckReportItems(items []CheckItem, requested []string) ([]CheckItem, error) {
	if len(requested) == 0 {
		return items, nil
	}
	wanted := make(map[string]struct{}, len(requested))
	for _, name := range requested {
		wanted[name] = struct{}{}
	}
	selected := make([]CheckItem, 0, len(wanted))
	for _, item := range items {
		if _, ok := wanted[item.Name]; !ok {
			continue
		}
		selected = append(selected, item)
		delete(wanted, item.Name)
	}
	if len(wanted) > 0 {
		return nil, ErrHealthCheckReportItemNotFound
	}
	return selected, nil
}
