package store

import (
	"net/http"

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

	"github.com/pingcap/test-infra/common/model"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	"github.com/pingcap/test-infra/services/tcms/service"
	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"
)

const (
	// TABLE_RESOURCE_COHORTS_COLUMNS is all the columns of `resource_cohorts` table.
	TABLE_RESOURCE_COHORTS_COLUMNS = "resource_cohorts.id,resource_cohorts.name,resource_cohorts.summary,resource_cohorts.created_at,resource_cohorts.updated_at"
)

type resourceCohortService struct {
	db              *sql.Database
	resourcepoolSvc service.ResourcePoolService
}

func (s *resourceCohortService) init(db *sql.Database, resourcepoolSvc service.ResourcePoolService) {
	s.db = db
	s.resourcepoolSvc = resourcepoolSvc
}

func (s *resourceCohortService) List(ctx utils.Context, resourcepool string) (cohorts []*model.ResourceCohort, err error) {
	c := ctx.Request().Context()

	stmt := sql.Builder.From("resource_cohorts").Prepared(true)
	stmt = stmt.Select(goqu.L(TABLE_RESOURCE_COHORTS_COLUMNS)).
		Order(goqu.I("resource_cohorts.name").Asc()).
		Limit(999).
		Offset(0)
	if resourcepool != "" {
		stmt = stmt.
			InnerJoin(goqu.T("resource_cohort_resourcepools"), goqu.On(goqu.I("resource_cohort_resourcepools.resource_cohort_id").Eq(goqu.I("resource_cohorts.id")))).
			InnerJoin(goqu.T("resource_pools"), goqu.On(goqu.I("resource_pools.id").Eq(goqu.I("resource_cohort_resourcepools.resourcepool_id")))).
			Where(goqu.I("resource_pools.name").Eq(resourcepool))
	}

	query, args, _ := stmt.ToSQL()
	cohorts = []*model.ResourceCohort{}
	if err := s.db.SelectContext(c, &cohorts, query, args...); err != nil {
		ctx.Log().Errorf(err, "list cohorts failed")
		return nil, err
	}

	// One resourcepool can only belong to one cohort,
	// we also returns borrowedPolicy for convenience
	// when filter by resourcepool name.
	if resourcepool != "" {
		for _, cohort := range cohorts {
			cohort.Resourcepools, err = s.getRespool(ctx, cohort.ID)
		}
	}
	return cohorts, nil
}

func (s *resourceCohortService) Get(ctx utils.Context, name string) (cohort *model.ResourceCohort, err error) {
	query, args, _ := sql.Builder.Select(goqu.L(TABLE_RESOURCE_COHORTS_COLUMNS)).Prepared(true).
		From("resource_cohorts").
		Where(goqu.I("resource_cohorts.name").Eq(name)).
		ToSQL()

	cohort = &model.ResourceCohort{}
	if err := s.db.GetContext(ctx.Request().Context(), cohort, query, args...); err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		ctx.Log().Errorf(err, "get cohort failed")
		return nil, err
	}
	cohort.Resourcepools, err = s.getRespool(ctx, cohort.ID)
	if err != nil {
		return nil, err
	}
	return cohort, nil
}

func (s *resourceCohortService) Create(ctx utils.Context, cohort *reqM.ResourceCohortCreateRequest) error {
	log := ctx.Log().With("name", cohort.Name)

	cohortRespools := make([]string, len(cohort.Resourcepools))
	for i, pool := range cohort.Resourcepools {
		cohortRespools[i] = pool.Name
	}

	err := s.db.Transaction(ctx.Request().Context(), func(tx *sqlx.Tx) (err error) {
		// 1. Insert the cohort.
		r, err := tx.Exec(`INSERT INTO resource_cohorts (name, summary) VALUES (?, ?)`,
			cohort.Name,
			cohort.Summary,
		)
		if err != nil {
			if sql.IsErrDuplicate(err) {
				return serror.NewMsg(http.StatusBadRequest, "object is already exist")
			}
			log.Errorf(err, "Create cohort failed")
			return err
		}
		id, err := r.LastInsertId()
		if err != nil {
			return err
		}

		// 2. Insert the cohort-respools relation.
		if len(cohortRespools) == 0 {
			return nil
		}
		if err := s.addResourcePools(ctx, tx, model.ID(id), cohortRespools); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		ctx.Log().Errorf(err, "create cohort failed")
		return err
	}
	return nil
}

func (s *resourceCohortService) Delete(ctx utils.Context, name string) error {
	log := ctx.Log().With("name", name)
	err := s.db.Transaction(ctx.Request().Context(), func(tx *sqlx.Tx) error {
		cohortID, err := s.getIDForUpdate(tx, name)
		if err != nil {
			return err
		}

		_, err = tx.Exec("DELETE FROM resource_cohorts WHERE id = ?", cohortID)
		if err != nil {
			return err
		}

		_, err = tx.Exec("DELETE FROM resource_cohort_resourcepools WHERE resource_cohort_id = ?", cohortID)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		log.Errorf(err, "delete cohort failed")
		return err
	}
	return nil
}

// Add reourcepool to the resource cohort.
func (s *resourceCohortService) AddResourcePool(ctx utils.Context, name string, resourcepool string) error {
	log := ctx.Log().With("name", name, "respool", resourcepool)

	err := s.db.Transaction(ctx.Request().Context(), func(tx *sqlx.Tx) error {
		cohortID, err := s.getIDForUpdate(tx, name)
		if err != nil {
			return err
		}
		if err := s.addResourcePools(ctx, tx, cohortID, []string{resourcepool}); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		log.Errorf(err, "cohort add respool failed")
		return err
	}
	return nil
}

// Remove reourcepool from the resource cohort.
func (s *resourceCohortService) RemoveResourcePool(ctx utils.Context, name string, resourcepool string) error {
	log := ctx.Log().With("name", name, "respool", resourcepool)

	err := s.db.Transaction(ctx.Request().Context(), func(tx *sqlx.Tx) error {
		cohortID, err := s.getIDForUpdate(tx, name)
		if err != nil {
			return err
		}
		_, err = tx.Exec(`
		DELETE FROM
			resource_cohort_resourcepools
		WHERE
			resource_cohort_id = ? AND resourcepool_id = (
				SELECT id FROM resource_pools WHERE resource_pools.name = ?
			)`,
			cohortID,
			resourcepool,
		)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		log.Errorf(err, "cohort remove respool failed")
		return err
	}
	return nil
}

func (s *resourceCohortService) getRespool(ctx utils.Context, id model.ID) ([]*model.ResourceCohortPool, error) {
	query, args, _ := sql.Builder.Select(
		"resource_pools.id",
		"resource_pools.name",
		"resource_pools.borrowed_policy",
	).
		Prepared(true).
		From("resource_pools").
		Join(goqu.T("resource_cohort_resourcepools"), goqu.On(goqu.I("resource_cohort_resourcepools.resourcepool_id").Eq(goqu.I("resource_pools.id")))).
		Join(goqu.T("resource_cohorts"), goqu.On(goqu.I("resource_cohorts.id").Eq(goqu.I("resource_cohort_resourcepools.resource_cohort_id")))).
		Where(goqu.I("resource_cohorts.id").Eq(id)).
		ToSQL()

	pools := []*model.ResourceCohortPool{}
	if err := s.db.SelectContext(ctx.Request().Context(), &pools, query, args...); err != nil {
		ctx.Log().Errorf(err, "list cohort respools failed")
		return nil, err
	}
	return pools, nil
}

func (s *resourceCohortService) addResourcePools(ctx utils.Context, tx *sqlx.Tx, cohortID model.ID, respools []string) error {
	if len(respools) == 0 {
		return nil
	}
	respools = utils.UniqSlice(respools)

	respoolIDs := []model.ID{}
	query, args, _ := sqlx.In("SELECT id FROM resource_pools WHERE name IN (?) FOR UPDATE", respools)
	if err := tx.Select(&respoolIDs, query, args...); err != nil {
		return err
	}
	if len(respoolIDs) != len(respools) {
		return serror.NewMsg(http.StatusBadRequest, "one or more resource pool not found: %v", respools)
	}

	stmt := sql.Builder.Insert("resource_cohort_resourcepools").Cols("resource_cohort_id", "resourcepool_id").Prepared(true)
	for _, poolID := range respoolIDs {
		stmt = stmt.Vals(goqu.Vals{cohortID, poolID})
	}

	query, args, _ = stmt.ToSQL()
	if _, err := tx.Exec(query, args...); err != nil {
		if sql.IsErrDuplicate(err) {
			return serror.NewMsg(http.StatusBadRequest, "the relation of cohort and respool is already exist")
		}
		ctx.Log().Errorf(err, "Create cohort-respools failed")
		return err
	}
	return nil
}

func (s *resourceCohortService) getIDForUpdate(tx *sqlx.Tx, name string) (model.ID, error) {
	var cohortID model.ID
	err := tx.Get(&cohortID, "SELECT id FROM resource_cohorts WHERE name = ? FOR UPDATE", name)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return 0, serror.NewMsg(http.StatusBadRequest, "cohort %s not found", name)
		}
		return 0, err
	}
	return cohortID, nil
}
