package repo

import (
	"context"
	"fmt"

	apprent "github.com/tidbcloud/devops/entity/approval"
	apprmod "github.com/tidbcloud/devops/models/approval"
	"github.com/tidbcloud/devops/models/base"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service/approval/repo/cvt"

	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

type Driver interface {
	Save(ctx context.Context, appr *apprent.Approval) (*apprent.Approval, error)
	Load(ctx context.Context, id uint64) (*apprent.Approval, error)
	Query(ctx context.Context, opts ...repoopt.Option) ([]*apprent.Approval, error)
	Count(ctx context.Context, opts ...repoopt.Option) (int64, error)
}

type driver struct {
	cvter      *cvt.Converter
	dbProvider db.Provider
}

func (d *driver) Save(ctx context.Context, appr *apprent.Approval) (*apprent.Approval, error) {
	wdb := d.dbProvider.WriteDB(ctx)
	mod := d.cvter.EntityToModel().Approval(appr)

	if appr.ID > 0 {
		// update association of nodes
		existNodeIDs := []base.ID{}
		for _, n := range mod.Nodes {
			if n.ID != 0 {
				existNodeIDs = append(existNodeIDs, n.ID)
			}
		}
		err := wdb.Where("approval_id = ?", appr.ID).
			Where("NOT id IN ?", existNodeIDs).
			Delete(&apprmod.ApprovalNode{}).Error
		if err != nil {
			return nil, fmt.Errorf("delete nodes of approval %d failed: %w", appr.ID, err)
		}

		// update association of tasks
		existTaskIDs := []base.ID{}
		for _, t := range mod.Tasks {
			if t.ID != 0 {
				existTaskIDs = append(existTaskIDs, t.ID)
			}
		}
		err = wdb.Where("approval_id = ?", appr.ID).
			Where("NOT id IN ?", existTaskIDs).
			Delete(&apprmod.ApprovalNode{}).Error
		if err != nil {
			return nil, fmt.Errorf("delete tasks of approval %d failed: %w", appr.ID, err)
		}

		// update association of cc tasks
		existCCTaskIDs := []base.ID{}
		for _, cct := range mod.CCTasks {
			if cct.ID != 0 {
				existCCTaskIDs = append(existCCTaskIDs, cct.ID)
			}
		}
		err = wdb.Where("approval_id = ?", appr.ID).
			Where("NOT id IN ?", existCCTaskIDs).
			Delete(&apprmod.ApprovalCCTask{}).Error
		if err != nil {
			return nil, fmt.Errorf("delete cc tasks of approval %d failed: %w", appr.ID, err)
		}

		// update association of admin tasks
		existAdminTaskIDs := []base.ID{}
		for _, at := range mod.AdminTasks {
			if at.ID != 0 {
				existAdminTaskIDs = append(existAdminTaskIDs, at.ID)
			}
		}
		err = wdb.Where("approval_id = ?", appr.ID).
			Where("NOT id IN ?", existAdminTaskIDs).
			Delete(&apprmod.ApprovalAdminTask{}).Error
		if err != nil {
			return nil, fmt.Errorf("delete admin tasks of approval %d failed: %w", appr.ID, err)
		}
	}

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

	// fill back id
	return d.cvter.ModelToEntity().Approval(mod), nil
}

func (d *driver) Load(ctx context.Context, id uint64) (*apprent.Approval, error) {
	rdb := d.dbProvider.ReadDB(ctx)
	mod := &apprmod.Approval{}

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

	return d.cvter.ModelToEntity().Approval(mod), nil
}

func (d *driver) Query(ctx context.Context, opts ...repoopt.Option) ([]*apprent.Approval, error) {
	rdb := d.dbProvider.ReadDB(ctx)
	mods := []*apprmod.Approval{}

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

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

	return container.TransformSlice(mods, d.cvter.ModelToEntity().Approval), nil
}

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

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

	return count, nil
}

func (d *driver) buildModel(db *gorm.DB) *gorm.DB {
	db = db.Model(&apprmod.Approval{}).
		Preload("Nodes").Preload("Tasks").Preload("CCTasks").Preload("AdminTasks")

	return db
}
