package model

import (
	"fmt"
	"time"

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

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

func UpdateOrganizationRebuild(id uint, disableRebuild bool) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return db.NewOrganizationQuerySet(tx).IDEq(id).GetUpdater().SetDisableRebuild(disableRebuild).Update()
	})
}

func InsertOrUpdateCloudOrg(org *db.Organization, force bool) error {
	return db.Transaction(func(txn *gorm.DB) error {
		exist := db.Organization{}
		err := txn.Unscoped().Where("dbaas_org_id  = ?", org.DbaasOrgID).First(&exist).Error

		switch err {
		case nil:
		case gorm.ErrRecordNotFound:
			log.Info("create org", zap.Uint("tenant_id", org.DbaasOrgID))
			return txn.Create(org).Error
		default:
			return err
		}

		if exist.ID > 0 &&
			(!exist.UpdatedAt.Equal(org.UpdatedAt) || force) {
			log.Info("update org", zap.Uint("id", exist.ID), zap.Uint("tenant_id", org.DbaasOrgID))
			return txn.Model(&exist).UpdateColumns(*org).Error
		}

		return nil
	})
}

type organizationSyncState struct {
	ID         uint
	DbaasOrgID uint
	UpdatedAt  time.Time
}

type organizationSyncStateLoader func([]uint) ([]organizationSyncState, error)

const organizationSyncStateBatchSize = 5000

func ListCloudOrganizationUpdatedAt(dbaasOrgIDs []uint) (map[uint]time.Time, error) {
	return listCloudOrganizationUpdatedAt(dbaasOrgIDs, func(ids []uint) ([]organizationSyncState, error) {
		var states []organizationSyncState
		err := db.GetDB().
			Unscoped().
			Model(&db.Organization{}).
			Select("id", "dbaas_org_id", "updated_at").
			Where("dbaas_org_id IN ?", ids).
			Order("id ASC").
			Find(&states).Error
		return states, err
	})
}

func listCloudOrganizationUpdatedAt(dbaasOrgIDs []uint, load organizationSyncStateLoader) (map[uint]time.Time, error) {
	updatedAtByOrgID := make(map[uint]time.Time, len(dbaasOrgIDs))
	if len(dbaasOrgIDs) == 0 {
		return updatedAtByOrgID, nil
	}

	uniqueIDs := make([]uint, 0, len(dbaasOrgIDs))
	seenIDs := make(map[uint]struct{}, len(dbaasOrgIDs))
	for _, id := range dbaasOrgIDs {
		if _, exists := seenIDs[id]; exists {
			continue
		}
		seenIDs[id] = struct{}{}
		uniqueIDs = append(uniqueIDs, id)
	}

	for start := 0; start < len(uniqueIDs); start += organizationSyncStateBatchSize {
		end := start + organizationSyncStateBatchSize
		if end > len(uniqueIDs) {
			end = len(uniqueIDs)
		}

		states, err := load(uniqueIDs[start:end])
		if err != nil {
			return nil, err
		}

		// InsertOrUpdateCloudOrg uses First, so preserve the lowest-ID row when
		// legacy data contains more than one row for the same DBaaS organization.
		for _, state := range states {
			if _, exists := updatedAtByOrgID[state.DbaasOrgID]; !exists {
				updatedAtByOrgID[state.DbaasOrgID] = state.UpdatedAt
			}
		}
	}

	return updatedAtByOrgID, nil
}

func GetOrganizationCount(userID uint) (int64, error) {
	var (
		total int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		qs := db.NewOrganizationQuerySet(tx)
		if userID > 0 {
			qs.CreatedByEq(userID)
		}

		total, err = qs.Count()

		return err
	})

	return total, err
}

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

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

	return total, err
}

type ListOrganizationsCondition struct {
	Name        string
	IDs         []uint
	DbaasOrgIDs []uint // only used for cloud datasource
	StarBy      uint   // set to filter stared org
	Owner       uint   // set to filter owned org

	ViewBy uint // ViewBy always point to current user, must not be zero

	Page    int
	PerPage int
}

// moved to datasource package
//func ListOrganizations(cond *ListOrganizationsCondition) ([]db.Organization, int64, error) {
//	var (
//		orgs  []db.Organization
//		total int64
//		err   error
//	)
//	err = db.Transaction(func(tx *gorm.DB) error {
//		// have to use model to query with soft deleted records considered
//		q := tx.Model(&db.Organization{})
//		// set base query conditions
//		if cond.Name != "" {
//			q = q.Where("organizations.name LIKE ?", "%"+cond.Name+"%")
//		}
//		if len(cond.IDs) > 0 {
//			q = q.Where("organizations.id IN (?)", cond.IDs)
//		}
//		if cond.Owner > 0 {
//			q = q.Where("organizations.created_by = ?", cond.Owner)
//		}
//		if cond.StarBy > 0 {
//			q = q.Joins("JOIN star ON star.org_id = organizations.id").
//				Where("star.user_id = ?", cond.StarBy)
//		}
//		// query count
//		if cond.PerPage > 0 {
//			if err = q.Count(&total).Error; err != nil {
//				return errors.Annotate(err, "count tenant error")
//			}
//
//			startIndex := pagination.GetStartIndex(cond.Page, cond.PerPage)
//			q = q.Limit(cond.PerPage).Offset(startIndex)
//		}
//		// order and find
//		if err := q.Order("updated_at DESC").
//			Find(&orgs).Error; err != nil {
//			return errors.Annotate(err, "fetch tenants failed")
//		}
//		if cond.PerPage <= 0 {
//			//no pagination
//			total = int64(len(orgs))
//		}
//		// extra step to set `isStarByViewer` field if necessary
//		if cond.ViewBy > 0 {
//			// query star records by the viewer
//			orgIDs := make([]uint, 0)
//			tx.Table("star").
//				Where("user_id = ?", cond.ViewBy).
//				Pluck("org_id", &orgIDs)
//			lookup := make(map[uint]struct{})
//			for _, v := range orgIDs {
//				lookup[v] = struct{}{}
//			}
//			// set org star status by the query result
//			for i := range orgs {
//				if _, ok := lookup[orgs[i].ID]; ok {
//					orgs[i].SetStarredByViewer(true)
//				}
//			}
//		}
//		return nil
//		// TODO: remove later
//		//qs := db.NewOrganizationQuerySet(tx)
//		//if cond.Name != "" {
//		//	qs = qs.NameLike("%" + cond.Name + "%")
//		//}
//		//
//		//if len(cond.IDs) > 0 {
//		//	qs = qs.IDIn(cond.IDs...)
//		//}
//		//if cond.Owner > 0 {
//		//	qs = qs.CreatedByEq(cond.Owner)
//		//}
//		//
//		//if cond.PerPage > 0 {
//		//	if total, err = qs.Count(); err != nil {
//		//		return errors.Annotate(err, "count tenant error")
//		//	}
//		//
//		//	startIndex := pagination.GetStartIndex(cond.Page, cond.PerPage)
//		//	qs = qs.Limit(cond.PerPage).Offset(startIndex)
//		//}
//		//
//		//if err := qs.OrderDescByUpdatedAt().All(&orgs); err != nil {
//		//	return errors.Annotate(err, "fetch tenants failed")
//		//}
//		//if cond.PerPage <= 0 {
//		//	//no pagination
//		//	total = int64(len(orgs))
//		//}
//		//return nil
//	})
//	if err != nil {
//		return nil, 0, err
//	}
//
//	return orgs, total, nil
//}

func CreateOrganization(name, description string, userID uint, consumeFunc func(orgID uint) error) (*db.Organization, error) {
	var org db.Organization

	err := db.Transaction(func(tx *gorm.DB) error {
		//err := tx.Unscoped().Where("name=?", name).First(&org).Error
		//if err != nil && err != gorm.ErrRecordNotFound {
		//	return err
		//}
		//
		//if org.ID > 0 {
		//	if !org.DeletedAt.Valid {
		//		log.Warn("duplicate org", zap.String("org", org.Name))
		//		return nil
		//	}
		//	if err := tx.Unscoped().Where("org_name=?", org.Name).Delete(&db.Organization{}).Error; err != nil {
		//		return err
		//	}
		//}
		org = db.Organization{
			Model: gorm.Model{
				CreatedAt: time.Now(),
				UpdatedAt: time.Now(),
			},
			Name:        name,
			Description: description,
			CreatedBy:   userID,
		}

		err := (&org).Create(tx)
		if err != nil {
			return err
		}

		return consumeFunc(org.ID)
	})

	return &org, err
}

func UpdateOrganization(id uint, name, description string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		o := db.NewOrganizationQuerySet(tx).IDEq(id).GetUpdater()

		if name != "" {
			o.SetName(name)
		}

		if description != "" {
			o.SetDescription(description)
		}

		return o.Update()
	})
}

func DeleteOrg(id uint) ([]db.UploadFile, error) {
	files := []db.UploadFile{}
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Model(&db.UploadFile{}).Where("org_id = ?", id).Find(&files).Error
		if err != nil {
			return err
		}
		err = tx.Where("org_id = ?", id).Unscoped().Delete(&db.UploadFile{}).Error
		if err != nil {
			return err
		}
		err = tx.Where("org_id = ?", id).Unscoped().Delete(&db.Cluster{}).Error
		if err != nil {
			return err
		}
		return tx.Where("id = ?", id).Unscoped().Delete(&db.Organization{}).Error
	})
	return files, err
}

func GetOrgByExternalID(externalID uint) (*db.Organization, error) {
	var (
		org db.Organization
		err error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		qs := db.NewOrganizationQuerySet(tx)
		return qs.DbaasOrgIDEq(externalID).One(&org)
	})

	return &org, err
}

// GetOrgIDByDbaasOrgID converts dbaas org id (string) to org id (uint)
func GetOrgIDByDbaasOrgID(dbaasOrgID string) (uint, error) {
	// Parse dbaas org id string to uint
	var externalID uint64
	if _, err := fmt.Sscanf(dbaasOrgID, "%d", &externalID); err != nil {
		return 0, err
	}

	org, err := GetOrgByExternalID(uint(externalID))
	if err != nil {
		return 0, err
	}
	return org.ID, nil
}
