package model

import (
	"fmt"
	"reflect"
	"strings"
	"time"

	"github.com/pingcap-inc/clinic/pkg/cluster"

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

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

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

func InsertUploadFile(uf *db.UploadFile) error {
	// todo if is dupOrgCluster, the record should be delete
	//uf := &db.UploadFile{
	//	Model: gorm.Model{
	//		CreatedAt: time.Now(),
	//		UpdatedAt: time.Now(),
	//	},
	//	//FileUUID:         fileUUID,
	//	//Alias:            alias,
	//	//UploadID:         uploadID,
	//	//ClusterID:        uint64(clusterId),
	//	//Creator:          userId,
	//	//OrgID:            orgID,
	//	//FileLen:          length,
	//	//StorageType:      string(storageType),
	//	//FileOriginalName: originalName,
	//}

	return db.Transaction(func(txn *gorm.DB) error {
		var ouf db.UploadFile
		err := txn.Unscoped().Where("file_uuid=?", uf.FileUUID).First(&ouf).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}

		if ouf.ID > 0 {
			if !ouf.DeletedAt.Valid {
				log.Warn("duplicate file record", zap.String("file", uf.FileUUID))
				return nil
			}
			if err := txn.Unscoped().Where("file_uuid=?", uf.FileUUID).Delete(&db.UploadFile{}).Error; err != nil {
				return err
			}
		}

		return txn.Create(uf).Error
	})
}

//func QueryClusterIDs(start int64, end int64) ([]uint64, error) {
//	files := []db.UploadFile{}
//	err := db.Transaction(func(tx *gorm.DB) error {
//		err := tx.Select("cluster_id").Where("cluster_id > 0").Find(&files).Error
//		if err != nil && err != gorm.ErrRecordNotFound {
//			return err
//		}
//		return nil
//	})
//
//	if err != nil {
//		return nil, err
//	}
//
//	if len(files) == 0 {
//		return nil, nil
//	}
//	ids := make([]uint64, 0)
//	for _, f := range files {
//		ids = append(ids, f.ClusterID)
//	}
//
//	return ids, nil
//}

func ListFilesByAlias(alias string, limit int) ([]string, error) {
	files := []db.UploadFile{}
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Where("alias=?", alias).Order("created_at desc").Limit(limit).Find(&files).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	if len(files) == 0 {
		return nil, nil
	}

	ids := make([]string, 0)
	for _, f := range files {
		ids = append(ids, f.FileUUID)
	}

	return ids, nil
}

func QueryAllUploadFiles() ([]db.UploadFile, error) {
	files := []db.UploadFile{}
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Find(&files).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	})
	return files, err
}

func QueryUploadFile(fileUUID string) (*db.UploadFile, error) {
	var uf db.UploadFile
	err := db.Transaction(func(txn *gorm.DB) error {
		err := txn.Where("file_uuid=?", fileUUID).First(&uf).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}

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

	if uf.ID == 0 {
		return nil, nil
	}

	return &uf, nil
}

func UploadFileCompleted(fileUUID string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		var uf db.UploadFile
		err := tx.Where("file_uuid=?", fileUUID).First(&uf).Error
		if err != nil {
			return err
		}

		if uf.ID == 0 {
			return errors.Errorf("record not found, file=%s", fileUUID)
		}

		return tx.Model(&uf).Updates(map[string]any{"upload_status": db.StatusSuccess, "updated_at": time.Now()}).Error
	})
}

func ListFilesWithNoDownloadURL(endT time.Time) ([]db.UploadFile, error) {
	files := []db.UploadFile{}
	startT := endT.Add(-1 * 30 * time.Minute)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Where(
			` UNIX_TIMESTAMP(updated_at) * 1000 < ? And
			UNIX_TIMESTAMP(updated_at) * 1000 > ? And (
			download_url='' Or 
			download_url is null)`,
			1,
			// Milli unix
			endT.UnixNano()/1000/1000,
			// Milli unix
			startT.UnixNano()/1000/1000).Find(&files).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	})

	return files, err

	//err := db.Transaction(func(tx *gorm.DB) error {
	//	err := tx.Where("updated_at").Where("download_url=''").Or("download_url is null").Find(&files).Error
	//	if err != nil && err != gorm.ErrRecordNotFound {
	//		return err
	//	}
	//	return nil
	//})
	//return files, err
}

func SetRebuildMetricStatus(fileUUID string, status db.StatusType) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("file_uuid=?", fileUUID).Update("rebuild_metric_status", status).Error
	})
}

func SetRebuildMetricStatusByClusterID(clusterID string, status db.StatusType) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("cluster_id=?", clusterID).Update("rebuild_metric_status", status).Error
	})
}

func SetRebuildLogStatus(fileUUID string, status db.StatusType) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("file_uuid=?", fileUUID).Update("rebuild_log_status", status).Error
	})
}

func SetRebuildConfigurationStatus(fileUUID string, status db.StatusType) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("file_uuid=?", fileUUID).Update("rebuild_configuration_status", status).Error
	})
}

func SetReportStatus(fileUUID string, status db.StatusType) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("file_uuid=?", fileUUID).Update("report_status", status).Error
	})
}

func UpdateFileInfoAfterSubmitRebuild(fileUUID string, clusterInfo *cluster.ClusterInfo) error {
	return db.Transaction(func(tx *gorm.DB) error {
		var ouf db.UploadFile
		err := tx.Where("file_uuid=?", fileUUID).First(&ouf).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}

		if ouf.ID <= 0 {
			log.Error("the file not found", zap.String("fileUUID", fileUUID))
			return nil
		}

		orinalName := ouf.FileOriginalName
		if !strings.HasSuffix(orinalName, "zst") {
			if strings.HasSuffix(orinalName, "tar") {
				orinalName += ".zst"
			} else {
				orinalName += ".tar.zst"
			}
		}

		err = tx.Model(&db.UploadFile{}).Where("file_uuid=?", fileUUID).Updates(db.UploadFile{
			DownloadURL:      ouf.ClusterID,
			ClusterID:        ouf.ClusterID,
			FileOriginalName: orinalName,
			SubmitedRebuild:  true,
			Collectors:       clusterInfo.Collectors,
		}).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return UpdateClusterByPackage(ouf.ClusterID, ouf.BeginTime, ouf.EndTime, clusterInfo)
	})
}

func UpdateClusterByPackage(clusterID string, begin, end *time.Time, clusterInfo *cluster.ClusterInfo) error {
	topology := cluster.GetClusterToplogy(clusterInfo)
	version := cluster.GetClusterVersion(clusterInfo)
	deployVersion := cluster.GetDeployVersion(clusterInfo)

	return db.Transaction(func(tx *gorm.DB) error {
		c, err := GetCluster(clusterID)
		if err != nil {
			//_, err := AddClusterIfPresent(&db.Cluster{ClusterID: clusterID, OrgID: orgId, StartTime: uint64(begin.Unix()), EndTime: uint64(end.Unix()), Topology: &topology, Version: version})
			return err
		}
		// update cluster table
		changes := make(map[string]any)
		if c.StartTime == 0 || c.StartTime > uint64(begin.Unix()) {
			changes["start_time"] = uint64(begin.Unix())
		}
		if c.EndTime == 0 || c.EndTime < uint64(end.Unix()) {
			changes["end_time"] = uint64(end.Unix())
		}

		if version != "" {
			changes["version"] = version
		}

		if deployVersion != "" {
			changes["deploy_type"] = deployVersion
			changes["deploy_type_v2"] = deployVersion
		}

		if c.Topology == nil || c.EndTime < uint64(end.Unix()) {
			changes["topology"] = topology
		}
		err = tx.Model(&db.Cluster{}).Where("cluster_id = ?", clusterID).Updates(changes).Error
		if err != nil {
			return err
		}
		return nil
	})
}

func DeleteFileRecord(fileUUID string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		err := tx.Where("file_uuid=?", fileUUID).Unscoped().Delete(&db.UploadFile{}).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	})
}

func FetchPackageByCusterID(clusterID string) ([]uint64, int64, error) {
	files := []uint64{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(db.UploadFile{}).Select("id").Where("cluster_id = ?", clusterID).Find(&files).Error
	})
	if err != nil {
		return nil, 0, err
	}
	return files, int64(len(files)), nil
}

func MarkPackageUnavailable(fileUUID string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(db.UploadFile{}).Where("file_uuid = ?", fileUUID).Update("deleted_at", time.Now()).Error
	})
}

//func ListPackages(orgID uint, clusterID string, beginTimeInSec int64, endTimeInSec int64) ([]db.UploadFile, error) {
//	files := []db.UploadFile{}
//	err := db.Transaction(func(tx *gorm.DB) error {
//		err := tx.Where(
//			`org_id = ? And cluster_id =? And
//			file_uuid not in (select file_uuid from upload_files where org_id = ? And cluster_id =? and (unix_timestamp(begin_time) > ? or unix_timestamp(end_time) < ?))
// 			`,
//			orgID,
//			clusterID,
//			orgID,
//			clusterID,
//			endTimeInSec,
//			beginTimeInSec).Find(&files).Error
//		if err != nil && err != gorm.ErrRecordNotFound {
//			return err
//		}
//		return nil
//	})
//
//	return files, err
//}

// TODO(shenjun): the sql may has time zone issue
func GetExpiredFiles(expiredDay int, rebuildType string) ([]db.UploadFile, error) {
	files := &[]db.UploadFile{}
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Where(fmt.Sprintf("updated_at < ? AND  %s = ?", rebuildType), time.Now().Add(-24*time.Duration(expiredDay)*time.Hour), db.StatusSuccess).Find(files).Error
	})
	return *files, err
}

func FetchTotalClusterDataSize(clusterID string) (uint64, error) {
	var datasize uint64
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Select("COALESCE(SUM(file_len), 0)").Group("cluster_id").Having("cluster_id = ?", clusterID).Find(&datasize).Error
	})
	return datasize, err
}

func GetTotalUploadCntByUser(userID uint64) (uint64, error) {
	var uploadtime int64
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("creator = ? AND created_at > ?", userID, time.Now().Add(-24*time.Hour)).Count(&uploadtime).Error
	})
	return uint64(uploadtime), err
}

// TODO(shenjun): make sure the per day define is per 24 hours
func GetUploadCntPerDayByCluster(clusterID string) (uint64, error) {
	var uploadCnt int64
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Where("cluster_id = ? AND updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)", clusterID).Count(&uploadCnt).Error
	})
	return uint64(uploadCnt), err
}

type uploadFileSortedField int

const (
	SortedByCreatedAt uploadFileSortedField = iota
	SortedByUpdatedAt
)

func ListUploadFilesInOrg(userID, orgID uint, limit int, sortedBy uploadFileSortedField, excludeUnScheduled, excludeUnCompleted, excludedDeleted bool) ([]db.UploadFile, error) {
	var (
		files []db.UploadFile
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		qs := db.NewUploadFileQuerySet(tx).OrgIDEq(orgID)
		if userID > 0 {
			qs.CreatorEq(userID)
		}

		if sortedBy == SortedByUpdatedAt {
			qs.OrderDescByUpdatedAt()
		} else {
			qs.OrderDescByCreatedAt()
		}

		if excludeUnScheduled {
			qs.SubmitedRebuildEq(true)
		}

		if excludedDeleted {
			// TODO: need to or clause
			qs.RebuildLogStatusNe(string(db.StatusDeleted))
		}

		if excludeUnCompleted {
			qs.UploadStatusEq(string(db.StatusSuccess))
		}

		return qs.Limit(limit).All(&files)
	})

	return files, err
}

func UploadFileCount() (int64, error) {
	var (
		total int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		total, err = db.NewUploadFileQuerySet(tx).Count()
		return err
	})

	return total, err
}

func FetchPendingJob(uf *[]*db.UploadFile) error {
	// we only fetch pending upload file records which are updated in recent 1 hour
	err := db.Transaction(func(txn *gorm.DB) error {
		err := txn.Where("upload_status = ? AND (rebuild_metric_status = ? OR rebuild_log_status = ? OR rebuild_configuration_status = ?) AND updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
			db.StatusSuccess,
			db.StatusPending,
			db.StatusPending,
			db.StatusPending).
			Find(uf).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	})
	return err
}

// const status=pending
func SetRebuildStatus(fileUUID string, rebuildType map[string]struct{}) ([]string, error) {
	var (
		uf           db.UploadFile
		touchRebuild []string
	)

	filterNotRunning := func(u *db.UploadFile, rebuildType map[string]struct{}) {
		rt := reflect.TypeOf(u).Elem()
		rv := reflect.ValueOf(u).Elem()

		for i := 0; i < rt.NumField(); i++ {
			v, ok := rt.Field(i).Tag.Lookup("rebuild")
			if !ok {
				continue
			}
			_, ok = rebuildType[v]
			if !ok {
				continue
			}
			status, ok := rv.Field(i).Interface().(string)
			if !ok {
				continue
			}
			if status == string(db.StatusRunning) || status == string(db.StatusSuccess) || status == string(db.StatusPending) {
				continue
			}

			cv := reflect.ValueOf(string(db.StatusPending))
			rv.Field(i).Set(cv)
			touchRebuild = append(touchRebuild, v)
		}
	}
	err := db.Transaction(func(txn *gorm.DB) error {
		err := txn.Where("file_uuid=?", fileUUID).First(&uf).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		filterNotRunning(&uf, rebuildType)
		return txn.Updates(uf).Error
	})
	return touchRebuild, err
}

func FindOldestCompletedFileByClusterID(clusterID string) (*db.UploadFile, bool, error) {
	var file db.UploadFile
	if err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Where("cluster_id = ? AND upload_status = ?", clusterID, db.StatusSuccess).Order("created_at ASC").First(&file).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		return nil
	}); err != nil {
		return nil, false, err
	}
	if file.ID == 0 {
		return nil, false, nil
	}
	return &file, true, nil
}

func ListUploadFiles(orgID uint, clusterID string, page, perPage int, beginTimeInSec, endTimeInSec int64) ([]db.UploadFile, int64, error) {
	var (
		files []db.UploadFile
		total int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		qs := db.NewUploadFileQuerySet(tx)
		// tx.Or

		qs = qs.OrgIDEq(orgID).ClusterIDEq(clusterID).UploadStatusEq(string(db.StatusSuccess))

		if beginTimeInSec > 0 && endTimeInSec > 0 {
			qs.GetDB().Where("file_uuid not in (select file_uuid from upload_files where org_id = ? And cluster_id =? and (unix_timestamp(begin_time) > ? or unix_timestamp(end_time) < ?))",
				orgID,
				clusterID,
				endTimeInSec,
				beginTimeInSec)
		}

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

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

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

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

	return files, total, nil
}

func ListUploadFilesAndReportInCluster(clusterID string, beginTimeInSec, endTimeInSec time.Time, res any) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(&db.UploadFile{}).Select(
			"upload_files.file_uuid, health_reports.report_uuid, health_reports.status, upload_files.begin_time, upload_files.end_time, health_reports.created_at, health_reports.updated_at").Joins(
			"left join health_reports on health_reports.file_uuid = upload_files.file_uuid").Where(
			`upload_files.upload_status = ? AND upload_files.cluster_id = ? AND ((upload_files.begin_time between ? AND ?) OR 
			(upload_files.end_time between ? AND ?) OR 
			(? >= upload_files.begin_time AND ? <= upload_files.end_time) OR 
			(? <= upload_files.begin_time AND ? >= upload_files.end_time))`,
			db.StatusSuccess,
			clusterID,
			beginTimeInSec, endTimeInSec,
			beginTimeInSec, endTimeInSec,
			beginTimeInSec, endTimeInSec,
			beginTimeInSec, endTimeInSec,
		).Order("updated_at desc").Scan(res).Error
	})
}

func ListAllReportInCloudTypeCluster(clusterID string) ([]db.HealthReport, error) {
	reports := make([]db.HealthReport, 0)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Model(&db.HealthReport{}).
			Where("file_uuid like ? AND org_type = 'cloud'", fmt.Sprintf("%s%%", clusterID)).
			Find(&reports).Error
		return err
	})
	return reports, err
}
