package model

import (
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/gorm"

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

// HealthCheck Strategy Operations

func InsertHealthCheckStrategy(strategy *db.HealthCheckStrategy) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckStrategy{}).Create(strategy).Error
	})
}

func GetHealthCheckStrategyByUUID(uuid string) (*db.HealthCheckStrategy, error) {
	var strategy db.HealthCheckStrategy
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckStrategy{}).Where("uuid = ?", uuid).First(&strategy).Error
	})
	return &strategy, err
}

func ListHealthCheckStrategies(clusterID, dbaasOrgID string, page, perPage int) ([]db.HealthCheckStrategy, int64, error) {
	var (
		strategies []db.HealthCheckStrategy
		total      int64
		err        error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		trans := tx.Model(&db.HealthCheckStrategy{}).Where("cluster_id = ? AND dbaas_org_id = ?", clusterID, dbaasOrgID)

		if perPage > 0 {
			if err = trans.Count(&total).Error; err != nil {
				return errors.Annotate(err, "count healthcheck strategies error")
			}

			startIndex := pagination.GetStartIndex(page, perPage)
			trans = trans.Limit(perPage).Offset(startIndex)
		}

		if err := trans.Order("created_at DESC").Find(&strategies).Error; err != nil {
			return err
		}
		if perPage <= 0 {
			total = int64(len(strategies))
		}

		return nil
	})
	if err != nil {
		return nil, 0, err
	}

	return strategies, total, nil
}

func UpdateHealthCheckStrategy(strategy *db.HealthCheckStrategy) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckStrategy{}).Where("uuid = ?", strategy.UUID).Updates(strategy).Error
	})
}

func DeleteHealthCheckStrategy(uuid string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Where("uuid = ?", uuid).Delete(&db.HealthCheckStrategy{}).Error
	})
}

func ListPendingCronHealthCheckStrategies() ([]db.HealthCheckStrategy, error) {
	var strategies []db.HealthCheckStrategy
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckStrategy{}).
			Where("schedule_type = ? AND status = ? AND next_run_at <= ?", "cron", "active", time.Now()).
			Find(&strategies).Error
	})
	return strategies, err
}

func UpdateHealthCheckStrategyLastRun(uuid string, lastRunAt, nextRunAt *time.Time) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updates := map[string]any{
			"last_run_at": lastRunAt,
		}
		if nextRunAt != nil {
			updates["next_run_at"] = nextRunAt
		}
		return tx.Model(&db.HealthCheckStrategy{}).Where("uuid = ?", uuid).Updates(updates).Error
	})
}

func UpdateHealthCheckStrategyStatus(uuid, status, errorMsg string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updates := map[string]any{
			"status": status,
		}
		return tx.Model(&db.HealthCheckStrategy{}).Where("uuid = ?", uuid).Updates(updates).Error
	})
}

// HealthCheck Result Operations

func InsertHealthCheckResult(result *db.HealthCheckResult) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckResult{}).Create(result).Error
	})
}

func GetHealthCheckResultByUUID(uuid string) (*db.HealthCheckResult, error) {
	var result db.HealthCheckResult
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckResult{}).Where("uuid = ?", uuid).First(&result).Error
	})
	return &result, err
}

// GetHealthCheckResultByScope returns a report only when all source ownership
// identifiers match. Service-to-service readers must not resolve a report by
// UUID alone because report IDs can otherwise bypass the org/cluster boundary.
func GetHealthCheckResultByScope(dbaasOrgID, clusterID, uuid string) (*db.HealthCheckResult, error) {
	var result db.HealthCheckResult
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckResult{}).
			Where("dbaas_org_id = ? AND cluster_id = ? AND uuid = ?", dbaasOrgID, clusterID, uuid).
			First(&result).Error
	})
	return &result, err
}

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

// ListHealthCheckResultsByScope is the filtered, one-based paging query used
// by the canonical read API. The existing Health Check UI query remains
// unchanged so this addition cannot alter its pagination or status behavior.
func ListHealthCheckResultsByScope(
	dbaasOrgID, clusterID string,
	options HealthCheckResultListOptions,
) ([]db.HealthCheckResult, int64, error) {
	var (
		results []db.HealthCheckResult
		total   int64
	)

	err := db.Transaction(func(tx *gorm.DB) error {
		query := tx.Model(&db.HealthCheckResult{}).
			Where("dbaas_org_id = ? AND cluster_id = ?", dbaasOrgID, clusterID)
		if len(options.Statuses) > 0 {
			query = query.Where("status IN ?", options.Statuses)
		}
		if options.CreatedFrom != nil {
			query = query.Where("created_at >= ?", *options.CreatedFrom)
		}
		if options.CreatedTo != nil {
			query = query.Where("created_at <= ?", *options.CreatedTo)
		}
		if err := query.Count(&total).Error; err != nil {
			return errors.Annotate(err, "count healthcheck reports error")
		}

		pageSize := options.PageSize
		if pageSize <= 0 {
			pageSize = 20
		}
		return query.Order("created_at DESC").
			Limit(pageSize).
			Offset(pagination.GetStartIndex(options.Page, pageSize)).
			Find(&results).Error
	})
	if err != nil {
		return nil, 0, err
	}
	return results, total, nil
}

func ListHealthCheckResults(clusterID, dbaasOrgID string, page, perPage int) ([]db.HealthCheckResult, int64, error) {
	var (
		results []db.HealthCheckResult
		total   int64
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		trans := tx.Model(&db.HealthCheckResult{}).Where("cluster_id = ? AND dbaas_org_id = ?", clusterID, dbaasOrgID)

		if perPage > 0 {
			if err = trans.Count(&total).Error; err != nil {
				return errors.Annotate(err, "count healthcheck results error")
			}

			startIndex := pagination.GetStartIndex(page, perPage)
			trans = trans.Limit(perPage).Offset(startIndex)
		}

		if err := trans.Order("created_at DESC").Find(&results).Error; err != nil {
			return err
		}
		if perPage <= 0 {
			total = int64(len(results))
		}

		return nil
	})
	if err != nil {
		return nil, 0, err
	}

	return results, total, nil
}

func ListPendingHealthCheckResults() ([]*db.HealthCheckResult, error) {
	var results []*db.HealthCheckResult
	err := db.Transaction(func(tx *gorm.DB) error {
		return pendingOrRunningHealthCheckJobsQuery(tx, nil).Find(&results).Error
	})
	return results, err
}

func UpdateHealthCheckResult(result *db.HealthCheckResult) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.HealthCheckResult{}).Where("uuid = ?", result.UUID).Updates(result).Error
	})
}

func UpdateHealthCheckResultStatus(uuid, status, errorMsg string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updates := map[string]any{
			"status":    status,
			"error_msg": errorMsg,
		}
		return tx.Model(&db.HealthCheckResult{}).Where("uuid = ?", uuid).Updates(updates).Error
	})
}

func DeleteHealthCheckResult(uuid string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Where("uuid = ?", uuid).Delete(&db.HealthCheckResult{}).Error
	})
}

func FetchPendingOrRunningHealthCheckJobs(results *[]*db.HealthCheckResult) error {
	return db.Transaction(func(tx *gorm.DB) error {
		err := pendingOrRunningHealthCheckJobsQuery(tx, nil).Find(results).Error
		if err != nil {
			log.Error("fetch pending or running health check jobs failed", zap.Error(err))
			return err
		}
		return nil
	})
}

func FetchPendingOrRunningHealthCheckJobsBatch(results *[]*db.HealthCheckResult, limit int, excludeUUIDs []string) error {
	if limit <= 0 {
		*results = make([]*db.HealthCheckResult, 0)
		return nil
	}
	return db.Transaction(func(tx *gorm.DB) error {
		err := pendingOrRunningHealthCheckJobsQuery(tx, excludeUUIDs).
			Order("CASE status WHEN 'pending' THEN 0 WHEN 'running' THEN 1 WHEN 'partial' THEN 2 ELSE 3 END").
			Order("created_at ASC, id ASC").
			Limit(limit).
			Find(results).Error
		if err != nil {
			log.Error("fetch pending or running health check jobs batch failed",
				zap.Error(err),
				zap.Int("limit", limit),
				zap.Int("exclude_uuid_count", len(excludeUUIDs)),
			)
			return err
		}
		return nil
	})
}

func CountPendingOrRunningHealthCheckJobs(excludeUUIDs []string) (int64, error) {
	var count int64
	err := db.Transaction(func(tx *gorm.DB) error {
		err := pendingOrRunningHealthCheckJobsQuery(tx, excludeUUIDs).Count(&count).Error
		if err != nil {
			log.Error("count pending or running health check jobs failed",
				zap.Error(err),
				zap.Int("exclude_uuid_count", len(excludeUUIDs)),
			)
			return err
		}
		return nil
	})
	return count, err
}

func pendingOrRunningHealthCheckJobsQuery(tx *gorm.DB, excludeUUIDs []string) *gorm.DB {
	query := tx.Model(&db.HealthCheckResult{}).
		Where("status in ?", pendingOrRunningHealthCheckJobStatuses())
	if len(excludeUUIDs) > 0 {
		query = query.Where("uuid NOT IN ?", excludeUUIDs)
	}
	return query
}

func pendingOrRunningHealthCheckJobStatuses() []string {
	return []string{string(db.StatusPending), string(db.StatusRunning), string(db.StatusPartial)}
}
