package store

import (
	"fmt"
	"net/http"

	"github.com/doug-martin/goqu/v9"
	"github.com/hashicorp/go-multierror"
	"github.com/jmoiron/sqlx"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/services/tcms/db"
	"github.com/pingcap/test-infra/services/tcms/service"
	"github.com/pingcap/test-infra/services/tcms/service/common"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
)

type resourcePoolService struct {
	db     *sql.Database
	rmsSvc service.RMSService
}

func (s *resourcePoolService) Init(db *sql.Database, rmsSvc service.RMSService) {
	s.db = db
	s.rmsSvc = rmsSvc
}

func (s *resourcePoolService) Create(ctx utils.Context, rp *model.ResourcePool) (*model.ResourcePool, error) {
	_, err := s.db.Exec("INSERT INTO resource_pools (rms_name, name, aliases, borrowed_policy) VALUES (?, ?, ?, ?)",
		rp.RmsName, rp.Name, rp.Aliases, rp.BorrowedPolicy)
	if err != nil {
		if sql.IsErrDuplicate(err) {
			return nil, serror.NewMsg(http.StatusBadRequest, "resource_pool is already exist, name= %s", rp.Name)
		}
		ctx.Log().Errorf(err, "create resource pool failed")
		return nil, err
	}
	return rp, nil
}

func (s *resourcePoolService) UpdateStatus(ctx utils.Context, rps []*model.ResourcePool) ([]*model.ResourcePool, error) {
	var errs error

	for _, rp := range rps {
		_, err := s.db.Exec(`UPDATE resource_pools SET total_cpu=?, total_mem=?, nontestbed_occupied_cpu=?, nontestbed_occupied_mem=?, 
		idle_cpu=?, idle_mem=? WHERE rms_name=? and name=?`,
			rp.TotalResource.CPU.Core,
			rp.TotalResource.Memory.Byte,
			rp.NonTestbedOccupiedResource.CPU.Core,
			rp.NonTestbedOccupiedResource.Memory.Byte,
			rp.IdleResource.CPU.Core,
			rp.IdleResource.Memory.Byte,
			rp.RmsName,
			rp.Name,
		)
		if err != nil {
			ctx.Log().Errorf(err, "update resource pool failed")
			errs = multierror.Append(errs, err)
		}
	}
	return rps, errs
}

func (s *resourcePoolService) Update(ctx utils.Context, rp *model.ResourcePool) error {
	query, args, _ := sql.Builder.
		Update("resource_pools").Prepared(true).
		Where(goqu.Ex{"rms_name": rp.RmsName, "name": rp.Name}).
		Set(map[string]any{
			"aliases":         rp.Aliases,
			"borrowed_policy": rp.BorrowedPolicy,
		}).
		ToSQL()
	_, err := s.db.ExecContext(ctx.Request().Context(), query, args...)
	if err != nil {
		ctx.Log().Errorf(err, "update resource pool aliases failed")
	}
	return nil
}

func (s *resourcePoolService) Delete(ctx utils.Context, RmsName string, Names []string) error {
	query, args, err := sqlx.In("DELETE FROM resource_pools WHERE rms_name=? AND name IN(?)", RmsName, Names)
	if err != nil {
		return err
	}

	_, err = s.db.Exec(query, args...)
	if err != nil {
		ctx.Log().Errorf(err, "delete resource pool failed")
		return err
	}
	return nil
}

func (s *resourcePoolService) List(ctx utils.Context, opts common.ResourcePoolQueryOpts) ([]*model.ResourcePool, int, error) {
	c := ctx.Request().Context()

	stmt := sql.Builder.From("resource_pools").Prepared(true)
	if opts.RmsName != "" {
		stmt = stmt.Where(goqu.C("rms_name").Eq(opts.RmsName))
	}
	if len(opts.Names) != 0 {
		stmt = stmt.Where(goqu.C("name").In(opts.Names))
	}
	if len(opts.IDs) != 0 {
		stmt = stmt.Where(goqu.C("id").In(opts.IDs))
	}

	var total int
	query, args, _ := stmt.Select(goqu.COUNT("*")).ToSQL()
	if err := s.db.GetContext(c, &total, query, args...); err != nil {
		return nil, 0, err
	}

	var rps []*db.ResourcePool
	query, args, _ = stmt.Select(goqu.L("id,rms_name,name,aliases,created_at,updated_at,total_cpu,total_mem,nontestbed_occupied_cpu,nontestbed_occupied_mem,idle_cpu,idle_mem,borrowed_policy")).
		Order(goqu.I("id").Asc()).
		ToSQL()
	err := s.db.SelectContext(c, &rps, query, args...)
	if err != nil {
		ctx.Log().Errorf(err, "ListResourcePools")
		return nil, 0, err
	}

	_rps, err := s.toModel(ctx, rps)
	if err != nil {
		ctx.Log().Errorf(err, "ListResourcePools conversion error")
		return nil, 0, err
	}

	return _rps, total, nil
}

// toModel converts `db.ResourcePool` into `model.ResourcePool`.
func (s *resourcePoolService) toModel(ctx utils.Context, rps []*db.ResourcePool) ([]*model.ResourcePool, error) {
	_rps := make([]*model.ResourcePool, len(rps))
	for i, rp := range rps {
		rms, err := s.rmsSvc.Get(ctx, rp.RmsName)
		if err != nil {
			return nil, err
		} else if rms == nil {
			return nil, fmt.Errorf("found orphan resource pool belongs to a nonexist rms: %s", rp.RmsName)
		}
		_rp := &model.ResourcePool{
			ID:        rp.ID,
			RmsName:   rp.RmsName,
			RmsURL:    rms.Endpoint,
			Name:      rp.Name,
			Aliases:   rp.Aliases,
			CreatedAt: &rp.CreatedAt,
			UpdatedAt: &rp.UpdatedAt,
			TotalResource: model.PhysicalResource{
				CPU: model.CPUResource{
					Core: rp.TotalCPU,
				},
				Memory: model.MemoryResource{
					Byte: int(rp.TotalMEM),
				},
			},
			NonTestbedOccupiedResource: model.PhysicalResource{
				CPU: model.CPUResource{
					Core: rp.NonTestbedOccupiedCPU,
				},
				Memory: model.MemoryResource{
					Byte: int(rp.NonTestbedOccupiedMEM),
				},
			},
			IdleResource: model.PhysicalResource{
				CPU: model.CPUResource{
					Core: rp.IdleCPU,
				},
				Memory: model.MemoryResource{
					Byte: int(rp.IdleMEM),
				},
			},
			BorrowedPolicy: rp.BorrowedPolicy,
		}
		_rps[i] = _rp
	}
	return _rps, nil
}
