package repo

import (
	"context"
	"fmt"

	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/models/base"
	ccmod "github.com/tidbcloud/devops/models/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"

	"gorm.io/gorm"
)

type ConfigurationChangeDriver interface {
	Save(ctx context.Context, chg *ccent.ConfigurationChange) (*ccent.ConfigurationChange, error)
	Load(ctx context.Context, id uint64) (*ccent.ConfigurationChange, error)
	Query(ctx context.Context, opts ...repoopt.Option) ([]*ccent.ConfigurationChange, error)
	Count(ctx context.Context, opts ...repoopt.Option) (int64, error)
}

type ccDriver driver

func (d *ccDriver) Save(ctx context.Context, chg *ccent.ConfigurationChange) (*ccent.ConfigurationChange, error) {
	wdb := d.dbProvider.WriteDB(ctx)
	chgMod := d.cvter.EntityToModel().ConfigurationChange(chg)

	if chg.ID > 0 {
		// update association of files
		{
			existIDs := []base.ID{}
			for _, a := range chgMod.Files {
				if a.ID != 0 {
					existIDs = append(existIDs, a.ID)
				}
			}
			db := wdb.Where("change_id = ?", chg.ID)
			if len(existIDs) != 0 {
				db = db.Where("NOT id IN ?", existIDs)
			}
			err := db.Delete(&ccmod.ConfigurationFileChange{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete files of change %d failed: %w", chg.ID, err)
			}
		}

		// update association of components
		{
			existIDs := []base.ID{}
			for _, a := range chgMod.Components {
				if a.ID != 0 {
					existIDs = append(existIDs, a.ID)
				}
			}
			db := wdb.Where("change_id = ?", chg.ID)
			if len(existIDs) != 0 {
				db = db.Where("NOT id IN ?", existIDs)
			}
			err := db.Delete(&ccmod.ConfigurationComponentChange{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete components of change %d failed: %w", chg.ID, err)
			}
		}

		// update association (delete first and create again later)
		if chgMod.Version != nil {
			err := wdb.Where("change_id = ?", chg.ID).Delete(&ccmod.ConfigurationVersionChange{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete version of change %d failed: %w", chg.ID, err)
			}
		}
		if chgMod.ToTenantInit != nil {
			err := wdb.Where("change_id = ?", chg.ID).Delete(&ccmod.ConfigurationChangeToTenantInit{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete target tenant init of change %d failed: %w", chg.ID, err)
			}
		}
		if chgMod.ToCluster != nil {
			err := wdb.Where("change_id = ?", chg.ID).Delete(&ccmod.ConfigurationChangeToCluster{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete target cluster of change %d failed: %w", chg.ID, err)
			}
		}
		if chgMod.ToTemplate != nil {
			err := wdb.Where("change_id = ?", chg.ID).Delete(&ccmod.ConfigurationChangeToTemplate{}).Error
			if err != nil {
				return nil, fmt.Errorf("delete target template of change %d failed: %w", chg.ID, err)
			}
		}

	}

	err := wdb.Session(&gorm.Session{FullSaveAssociations: true}).Save(chgMod).Error
	if err != nil {
		return nil, fmt.Errorf("save change failed: %w", err)
	}

	// fill back id
	return d.cvter.ModelToEntity().ConfigurationChange(chgMod), nil
}

func (d *ccDriver) Load(ctx context.Context, id uint64) (*ccent.ConfigurationChange, error) {
	rdb := d.dbProvider.ReadDB(ctx)
	chgMod := &ccmod.ConfigurationChange{}

	err := d.buildModel(rdb).Where("id = ?", id).First(chgMod).Error
	if err != nil {
		return nil, fmt.Errorf("load change %d failed: %w", id, err)
	}

	return d.cvter.ModelToEntity().ConfigurationChange(chgMod), nil
}

func (d *ccDriver) Query(ctx context.Context, opts ...repoopt.Option) ([]*ccent.ConfigurationChange, error) {
	rdb := d.dbProvider.ReadDB(ctx)
	chgMods := make([]*ccmod.ConfigurationChange, 0)

	rdb = d.buildModel(rdb)
	rdb = repoopt.Aggregate(opts...).Apply(rdb)

	err := rdb.Find(&chgMods).Error
	if err != nil {
		return nil, fmt.Errorf("query changes failed: %w", err)
	}

	return container.TransformSlice(chgMods, d.cvter.ModelToEntity().ConfigurationChange), nil
}

func (d *ccDriver) Count(ctx context.Context, opts ...repoopt.Option) (int64, error) {
	rdb := d.dbProvider.ReadDB(ctx).Model(&ccmod.ConfigurationChange{})
	rdb = repoopt.Aggregate(opts...).Apply(rdb)

	var count int64
	err := rdb.Count(&count).Error
	if err != nil {
		return 0, fmt.Errorf("count changes failed: %w", err)
	}

	return count, nil
}

func (d *ccDriver) buildModel(db *gorm.DB) *gorm.DB {
	db = db.Model(&ccmod.ConfigurationChange{}).
		Preload("Files").Preload("Components").Preload("Version").Preload("ToTenantInit").Preload("ToCluster").Preload("ToTemplate")

	return db
}
