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"
)

func GetHealthReportByTimeRange(clusterID string, startTime, endTime uint) ([]db.HealthReport, error) {
	var reports []db.HealthReport
	err := db.Transaction(func(tx *gorm.DB) error {
		var files []db.UploadFile
		err := tx.Model(&db.UploadFile{}).Find(&files).Where("cluster_id = ? AND begin_time >= ? AND end_time <= ?", clusterID, startTime, endTime).Error
		if err != nil {
			log.Error("fetch upload files failed", zap.Error(err))
			return err
		}
		err = tx.Model(&db.HealthReport{}).Find(&reports).Where("task_type = ? AND file_uuid IN ?", "health_report", []string{}).Error
		if err != nil {
			log.Error("fetch report failed", zap.Error(err))
			return err
		}
		return nil
	})
	return reports, err
}

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

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

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

func InsertOrUpdateReport(report *db.HealthReport) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		// https://gorm.io/docs/update.html#Update-Changed-Fields
		// Updates supports update with struct or map[string]interface{},
		// when updating with struct it will only update non-zero fields by default
		trans := tx.Model(&report).Where("file_uuid = ?", report.FileUUID).Updates(report)
		if trans.Error != nil {
			return trans.Error
		}
		if trans.RowsAffected != 0 {
			return nil
		}
		return tx.Model(&db.HealthReport{}).Create(report).Error
	})
	return err
}

// LandingTask set UploadFile's rebuild status and upsert health report record.
func LandingTask(file *db.UploadFile, rebuildType map[string]struct{}, needHealthReport bool, orgType string) ([]string, error) {
	var task []string
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		// set rebuild status for UploadFile
		task, err = SetRebuildStatus(file.FileUUID, rebuildType)
		if err != nil {
			log.Error("set rebuild status failed", zap.Error(err))
			return err
		}
		if !needHealthReport {
			log.Info("needn't report", zap.Any("id", file.FileUUID))
			return nil
		}

		// upsert health report record into db
		record, err := GetHealthReportByID(file.FileUUID)
		if err != nil && err != gorm.ErrRecordNotFound {
			log.Error("get healthreport failed", zap.Error(err))
			return err
		}
		if record != nil && (record.Status == string(db.StatusRunning) || record.Status == string(db.StatusPending)) {
			log.Info("have report record", zap.Any("id", file.FileUUID), zap.Any("report", record))
			return nil
		}
		if err := InsertOrUpdateReport(&db.HealthReport{
			FileUUID:  file.FileUUID,
			Status:    string(db.StatusPending),
			OrgType:   orgType,
			ClusterID: file.ClusterID,
			OrgID:     file.OrgID,
		}); err != nil {
			log.Error("insert healthreport failed", zap.Error(err))
			return err
		}
		return nil
	})
	return task, err
}

func LandingHealthReportTask(uuid string, startUnix, endUnix uint64, orgType string, orgID uint, clusterID string) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		// upsert health report record into db
		record, err := GetHealthReportByID(uuid)
		if err != nil && err != gorm.ErrRecordNotFound {
			log.Error("get healthreport failed", zap.Error(err))
			return err
		}
		if record != nil && (record.Status == string(db.StatusRunning) || record.Status == string(db.StatusPending)) {
			log.Info("have report record", zap.String("id", uuid), zap.Any("report", record))
			return nil
		}

		if err := InsertOrUpdateReport(&db.HealthReport{
			FileUUID:  uuid,
			Status:    string(db.StatusPending),
			StartUnix: startUnix,
			EndUnix:   endUnix,
			OrgType:   orgType,
			OrgID:     orgID,
			ClusterID: clusterID,
		}); err != nil {
			log.Error("insert healthreport failed", zap.Error(err))
			return err
		}
		return nil
	})
	return err
}

func FetchPendingOrRunningHealthReportJob(healthReports *[]*db.HealthReport) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		err := pendingOrRunningHealthReportJobsQuery(tx, nil).Find(healthReports).Error
		return err
	})
	return err
}

func FetchPendingOrRunningHealthReportJobsBatch(healthReports *[]*db.HealthReport, limit int, excludeFileUUIDs []string) error {
	if limit <= 0 {
		*healthReports = make([]*db.HealthReport, 0)
		return nil
	}
	return db.Transaction(func(tx *gorm.DB) error {
		return pendingOrRunningHealthReportJobsQuery(tx, excludeFileUUIDs).
			Order("created_at ASC, id ASC").
			Limit(limit).
			Find(healthReports).Error
	})
}

func CountPendingOrRunningHealthReportJobs(excludeFileUUIDs []string) (int64, error) {
	var count int64
	err := db.Transaction(func(tx *gorm.DB) error {
		return pendingOrRunningHealthReportJobsQuery(tx, excludeFileUUIDs).Count(&count).Error
	})
	return count, err
}

func pendingOrRunningHealthReportJobsQuery(tx *gorm.DB, excludeFileUUIDs []string) *gorm.DB {
	query := tx.Model(&db.HealthReport{}).
		Where("status in ?", []string{string(db.StatusPending), string(db.StatusRunning)})
	if len(excludeFileUUIDs) > 0 {
		query = query.Where("file_uuid NOT IN ?", excludeFileUUIDs)
	}
	return query
}

func GetReportCntPerDayByClusterExcludeHealthReport(clusterID, fileUUID string) (uint64, error) {
	var healthReportCnt int64
	var cmpReportCnt int64
	err := db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&db.HealthReport{}).
			Joins("JOIN upload_files on upload_files.file_uuid = health_reports.file_uuid").
			Where("upload_files.cluster_id = ? AND health_reports.file_uuid != ? AND health_reports.updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND health_reports.status != 'deleted'", clusterID, fileUUID).
			Count(&healthReportCnt).Error; err != nil {
			return err
		}
		if err := tx.Model(&db.ComparisonReport{}).
			Where("cluster_id = ? AND updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status != 'deleted'", clusterID).
			Count(&cmpReportCnt).Error; err != nil {
			return err
		}
		return nil
	})
	return uint64(healthReportCnt + cmpReportCnt), err
}

func GetReportCntPerDayByClusterExcludeCmpReport(clusterID, hash string) (uint64, error) {
	var healthReportCnt int64
	var cmpReportCnt int64
	err := db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&db.HealthReport{}).
			Joins("JOIN upload_files on upload_files.file_uuid = health_reports.file_uuid").
			Where("upload_files.cluster_id = ? AND health_reports.updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND health_reports.status != 'deleted'", clusterID).
			Count(&healthReportCnt).Error; err != nil {
			return err
		}

		if err := tx.Model(&db.ComparisonReport{}).
			Where("cluster_id = ? AND hash != ? AND updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status != 'deleted'", clusterID, hash).
			Count(&cmpReportCnt).Error; err != nil {
			return err
		}
		return nil
	})
	return uint64(healthReportCnt + cmpReportCnt), err
}

func GetReportCntPerDayByClusterCreator(clusterID string, creatorID uint) (int64, error) {
	var count int64
	err := db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&db.Report{}).
			Where("cluster_id = ? AND creator_id != ? AND updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND status != 'deleted'", clusterID, creatorID).
			Count(&count).Error; err != nil {
			return err
		}
		return nil
	})
	return count, err
}

func InsertOrUpdateComparisonReport(cr *db.ComparisonReport) (uint, error) {
	clusterID := cr.ClusterID
	hash := cr.Hash
	err := db.Transaction(func(tx *gorm.DB) error {
		if cr.ID != 0 {
			return tx.Model(cr).Where("id = ?", cr.ID).Updates(cr).Error
		}
		trans := tx.Model(cr).Where("cluster_id = ? and hash = ?", clusterID, hash).Updates(cr)
		if trans.Error != nil {
			return trans.Error
		}
		if trans.RowsAffected != 0 {
			if err := tx.Model(cr).Where("cluster_id = ? and hash = ?", clusterID, hash).First(cr).Error; err != nil {
				return err
			}
			return nil
		}
		return tx.Model(cr).Create(cr).Error
	})
	return cr.ID, err
}

func GetComparisonReport(clusterID, hashKey string) (*db.ComparisonReport, error) {
	cr := &db.ComparisonReport{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(cr).Where("cluster_id = ? and hash = ?", clusterID, hashKey).First(cr).Error
	})
	return cr, err
}

func GetComparisonReportByID(id uint) (*db.ComparisonReport, error) {
	cr := &db.ComparisonReport{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(cr).Where("id = ?", id).First(cr).Error
	})
	return cr, err
}

func HideComparisonReportByID(id uint) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Where("id = ?", id).Delete(&db.ComparisonReport{}).Error
	})
}

func FetchPendingOrRunningComparisonJob(crs *[]*db.ComparisonReport) error {
	return db.Transaction(func(tx *gorm.DB) error {
		err := pendingOrRunningComparisonJobsQuery(tx, nil).Find(crs).Error
		return err
	})
}

func FetchPendingOrRunningComparisonJobsBatch(crs *[]*db.ComparisonReport, limit int, excludeIDs []uint) error {
	if limit <= 0 {
		*crs = make([]*db.ComparisonReport, 0)
		return nil
	}
	return db.Transaction(func(tx *gorm.DB) error {
		return pendingOrRunningComparisonJobsQuery(tx, excludeIDs).
			Order("created_at ASC, id ASC").
			Limit(limit).
			Find(crs).Error
	})
}

func CountPendingOrRunningComparisonJobs(excludeIDs []uint) (int64, error) {
	var count int64
	err := db.Transaction(func(tx *gorm.DB) error {
		return pendingOrRunningComparisonJobsQuery(tx, excludeIDs).Count(&count).Error
	})
	return count, err
}

func pendingOrRunningComparisonJobsQuery(tx *gorm.DB, excludeIDs []uint) *gorm.DB {
	query := tx.Model(&db.ComparisonReport{}).
		Where("status in ?", []string{string(db.StatusPending), string(db.StatusRunning)})
	if len(excludeIDs) > 0 {
		query = query.Where("id NOT IN ?", excludeIDs)
	}
	return query
}

func LandingComparisonTask(files []*db.UploadFile, report *db.ComparisonReport, rebuildType map[string]struct{}) (uint, error) {
	var id uint
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		for _, f := range files {
			_, err = SetRebuildStatus(f.FileUUID, rebuildType)
			if err != nil {
				log.Error("set rebuild status failed", zap.Error(err))
				return err
			}
		}

		record, err := GetComparisonReport(report.ClusterID, report.Hash)
		if err != nil && err != gorm.ErrRecordNotFound {
			log.Error("get healthreport failed", zap.Error(err))
			return err
		}

		if record != nil && (record.Status == string(db.StatusRunning) || record.Status == string(db.StatusPending)) {
			id = record.ID
			log.Info("have running report record", zap.Any("id", record.ID))
			return nil
		}
		report.Status = string(db.StatusPending)
		if id, err = InsertOrUpdateComparisonReport(report); err != nil {
			log.Error("insert healthreport failed", zap.Error(err))
			return err
		}
		return nil
	})
	return id, err
}

func QueryComparisonReport(orgID uint, clusterID string, page, perPage int) ([]db.ComparisonReport, int64, error) {
	var (
		reports []db.ComparisonReport
		total   int64
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		trans := tx.Model(&db.ComparisonReport{}).Where("org_id = ? and cluster_id = ?", orgID, clusterID)

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

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

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

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

	return reports, total, nil
}

// below is function about new report table

func InsertReport(report *db.Report) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(report).Create(report).Error
	})
	return err
}

func ReportIDtoUUID(id uint) (string, error) {
	cr := &db.Report{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(cr).Select("uuid").Where("id = ?", id).First(cr).Error
	})
	if err != nil {
		return "", err
	}
	return cr.UUID, nil
}

func ReportUUIDtoID(uuid string) (uint, error) {
	cr := &db.Report{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(cr).Select("id").Where("uuid = ?", uuid).First(cr).Error
	})
	if err != nil {
		return 0, err
	}
	return cr.ID, nil
}

func GetReportByUUID(uuid string) (*db.Report, error) {
	cr := &db.Report{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(cr).Where("uuid = ?", uuid).First(cr).Error
	})
	return cr, err
}

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

// UpdateReport update non-zero field of given report struct
func UpdateReport(report *db.Report) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		// https://gorm.io/docs/update.html#Update-Changed-Fields
		// Updates supports update with struct or map[string]interface{},
		// when updating with struct it will only update non-zero fields by default
		return tx.Model(&db.Report{}).Where("uuid = ?", report.UUID).Updates(report).Error
	})
	return err
}

// ListReportsOverview list reports filtered by param, but ignore zero value
func ListReportsOverview(param *db.Report, page, perPage int) (reports []db.Report, total int64, err error) {
	err = db.Transaction(func(tx *gorm.DB) error {
		trans := tx.Model(&db.Report{}).Select("uuid", "report_type", "status", "progress", "arg", "creator_id", "created_at").Where(param)

		if err = trans.Count(&total).Error; err != nil {
			return errors.Annotate(err, "count files error")
		}
		if perPage > 0 {
			startIndex := pagination.GetStartIndex(page, perPage)
			trans = trans.Limit(perPage).Offset(startIndex)
		}

		return trans.Order("created_at DESC").Find(&reports).Error
	})
	return reports, total, err
}

func SelectReportByStatus(status db.StatusType) ([]*db.Report, error) {
	var reports []*db.Report
	err := db.Transaction(func(tx *gorm.DB) error {
		return reportsByStatusQuery(tx, status, nil).Find(&reports).Error
	})
	return reports, err
}

func SelectReportByStatusBatch(status db.StatusType, limit int, excludeUUIDs []string) ([]*db.Report, error) {
	if limit <= 0 {
		return make([]*db.Report, 0), nil
	}
	var reports []*db.Report
	err := db.Transaction(func(tx *gorm.DB) error {
		return reportsByStatusQuery(tx, status, excludeUUIDs).
			Order("created_at ASC, id ASC").
			Limit(limit).
			Find(&reports).Error
	})
	return reports, err
}

func CountReportsByStatus(status db.StatusType, excludeUUIDs []string) (int64, error) {
	var count int64
	err := db.Transaction(func(tx *gorm.DB) error {
		return reportsByStatusQuery(tx, status, excludeUUIDs).Count(&count).Error
	})
	return count, err
}

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

// Donot use
func QueryReportsByAdmin(reportType string, page, perPage int) ([]db.Report, int64, error) {
	var (
		reports []db.Report
		total   int64
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		qs := db.NewReportQuerySet(tx).ReportTypeEq(reportType)

		if perPage > 0 {
			if total, err = qs.Count(); err != nil {
				return errors.Annotate(err, "count tenant error")
			}

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

		if err := qs.OrderDescByCreatedAt().All(&reports); err != nil {
			return errors.Annotate(err, "fetch reports failed")
		}
		if perPage <= 0 {
			total = int64(len(reports))
		}

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

	return reports, total, nil
}

// Do not use it
func QueryComparisonReportByAdmin(page, perPage int) ([]db.ComparisonReport, int64, error) {
	var (
		reports []db.ComparisonReport
		total   int64
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		trans := tx.Model(&db.ComparisonReport{})

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

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

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

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

	return reports, total, nil
}

func LatestReportCount(reportType, clusterID string, start time.Time) (int64, error) {
	var (
		total int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		total, err = db.NewReportQuerySet(tx).ClusterIDEq(clusterID).ReportTypeEq(reportType).CreatedAtGte(start).Count()
		return err
	})
	if err != nil {
		return 0, err
	}

	return total, nil
}
