package store

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"entgo.io/ent/dialect/sql"
	"golang.org/x/sync/errgroup"

	"github.com/pingcap/test-infra/common/model"
	"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/service/store/v2/ent"
	"github.com/pingcap/test-infra/services/tcms/service/store/v2/ent/resourceduration"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

type resourceDurationService struct {
	ent         *ent.Client
	planExecSvc service.PlanExecutionService
	testbedSvc  service.TestbedService
	triageSvc   service.TriageService
}

func (s *resourceDurationService) init(
	ent *ent.Client,
	planExecSvc service.PlanExecutionService,
	testbedSvc service.TestbedService,
	triageSvc service.TriageService,
) {
	s.ent = ent
	s.planExecSvc = planExecSvc
	s.testbedSvc = testbedSvc
	s.triageSvc = triageSvc
}

func (s *resourceDurationService) upsert(ctx utils.Context, summary model.ResourceDuration) error {
	return s.ent.ResourceDuration.
		Create().
		SetCpuDuration(summary.CpuDuration).
		SetMemoryDuration(summary.MemoryDuration).
		SetType(summary.Type).
		SetPlanExecutionID(summary.PlanExecutionID).
		OnConflict(
			sql.ConflictColumns(resourceduration.FieldPlanExecutionID, resourceduration.FieldType),
		).
		SetCpuDuration(summary.CpuDuration).
		SetMemoryDuration(summary.MemoryDuration).
		Exec(ctx.Request().Context())
}

func (s *resourceDurationService) getTestBeds(ctx utils.Context, planExecID model.ID) (errorTestBeds []*model.TestBedInfo, normalTestBed *model.TestBedInfo, err error) {
	var normalTestBeds []*model.TestBedInfo

	testbeds, _, err := s.testbedSvc.List(ctx, common.TestbedQueryOpts{
		PlanExecID: &planExecID,
		Types:      []model.TestBedInfoType{model.TestBedInfoTypeNormal},
	})
	if err != nil {
		return nil, nil, err
	}
	for _, testbed := range testbeds {
		if testbed.Status == model.TestBedInfoStatusError {
			errorTestBeds = append(errorTestBeds, testbed)
		} else {
			normalTestBeds = append(normalTestBeds, testbed)
		}
	}
	if len(normalTestBeds) > 1 {
		return nil, nil, serror.NewMsg(http.StatusInternalServerError, "there should only have one testbed, but get %d", len(normalTestBeds))
	} else if len(normalTestBeds) == 0 {
		return
	}
	normalTestBed = normalTestBeds[0]

	return
}

func (s *resourceDurationService) calcResourceDuration(
	resourceDuration *model.ResourceDuration,
	resourceReq model.PhysicalResource,
	from time.Time,
	to time.Time,
) error {
	if to.Sub(from).Minutes() < 0 {
		return fmt.Errorf("calcResourceDuration for planExecution %d get internal error: calc %s, but start=%s, to=%s",
			resourceDuration.PlanExecutionID,
			resourceDuration.Type,
			from.String(),
			to.String(),
		)
	}

	resourceDuration.CpuDuration = uint64(resourceReq.CPU.Core * to.Sub(from).Minutes() / 60)
	resourceDuration.MemoryDuration = uint64(float64(resourceReq.Memory.Byte) * to.Sub(from).Minutes() / 60 / (1024 * 1024 * 1024))

	return nil
}

func (s *resourceDurationService) ProcessResourceDuration(ctx utils.Context, planExecID model.ID) error {
	// do resource duration summary into plan-execution
	ctx.Log().With("planExecID", planExecID).Infof("Processing ResourceDuration")

	g := errgroup.Group{}

	var (
		errorTestBeds []*model.TestBedInfo
		normalTestBed *model.TestBedInfo
		planExec      *model.TestPlanExecution
		triageInfo    *model.Triage
	)

	g.Go(func() (err error) {
		errorTestBeds, normalTestBed, err = s.getTestBeds(ctx, planExecID)
		if err != nil {
			return err
		}
		return nil
	})

	g.Go(func() (err error) {
		planExec, err = s.planExecSvc.Get(ctx, planExecID)
		if err != nil {
			return err
		} else if planExec == nil {
			return serror.NewMsg(http.StatusNotFound, "planExecution %d not found", planExecID)
		}
		return nil
	})

	if err := g.Wait(); err != nil {
		ctx.Log().Errorf(err, "")
		return err
	}

	resourceDuration := model.ResourceDuration{
		PlanExecutionID: planExecID,
	}

	// prepareErrorDur: Error testbed createdAt->releasedAt * resourceReq
	resourceDuration.Type = model.PrepareErrorDuration
	for _, testbed := range errorTestBeds {
		tempResourceDuration := model.ResourceDuration{
			PlanExecutionID: planExecID,
		}
		if err := s.calcResourceDuration(
			&tempResourceDuration,
			testbed.PhysicalResourceRequest,
			testbed.CreatedAt,
			*testbed.ReleasedAt,
		); err != nil {
			return err
		}
		resourceDuration.CpuDuration += tempResourceDuration.CpuDuration
		resourceDuration.MemoryDuration += tempResourceDuration.MemoryDuration
	}
	if err := s.upsert(ctx, resourceDuration); err != nil {
		return err
	}

	// All the testbeds are Error testbeds
	if normalTestBed == nil {
		return nil
	}

	// normal resource lifecycle
	resourceDuration.Type = model.PrepareDuration
	if normalTestBed.InitiatedAt == nil {
		return nil
	}
	if err := s.calcResourceDuration(
		&resourceDuration,
		normalTestBed.PhysicalResourceRequest,
		normalTestBed.CreatedAt,
		*normalTestBed.InitiatedAt,
	); err != nil {
		return err
	}
	if err := s.upsert(ctx, resourceDuration); err != nil {
		return err
	}

	resourceDuration.Type = model.RunningDuration
	if planExec.CompletedAt == nil {
		return nil
	}
	if err := s.calcResourceDuration(
		&resourceDuration,
		normalTestBed.PhysicalResourceRequest,
		*normalTestBed.InitiatedAt,
		planExec.CompletedAt.Time(),
	); err != nil {
		return err
	}
	if err := s.upsert(ctx, resourceDuration); err != nil {
		return err
	}

	triageInfo, err := s.triageSvc.GetPlanTriage(ctx, planExecID)
	if err != nil {
		return err
	}

	resourceDuration.Type = model.WaitTriageDuration
	if triageInfo == nil || triageInfo.StartedAt == nil {
		if normalTestBed.ReleasedAt != nil {
			if err := s.calcResourceDuration(
				&resourceDuration,
				normalTestBed.PhysicalResourceRequest,
				planExec.CompletedAt.Time(),
				*normalTestBed.ReleasedAt,
			); err != nil {
				return err
			}
			if err := s.upsert(ctx, resourceDuration); err != nil {
				return err
			}
		}
		return nil
	} else {
		if err := s.calcResourceDuration(
			&resourceDuration,
			normalTestBed.PhysicalResourceRequest,
			triageInfo.CreatedAt,
			*triageInfo.StartedAt,
		); err != nil {
			return err
		}
		if err := s.upsert(ctx, resourceDuration); err != nil {
			return err
		}
	}

	resourceDuration.Type = model.TriageDuration
	if normalTestBed.ReleasedAt != nil {
		if err := s.calcResourceDuration(
			&resourceDuration,
			normalTestBed.PhysicalResourceRequest,
			*triageInfo.StartedAt,
			*normalTestBed.ReleasedAt,
		); err != nil {
			return err
		}
		if err := s.upsert(ctx, resourceDuration); err != nil {
			return err
		}
	}

	return nil
}

func (s *resourceDurationService) Get(ctx utils.Context, planExecID model.ID) ([]model.ResourceDuration, error) {
	res, err := s.ent.ResourceDuration.Query().
		Where(resourceduration.PlanExecutionID(planExecID)).
		Order(ent.Asc(resourceduration.FieldCreatedAt)).
		All(ctx.Request().Context())
	if err != nil {
		return nil, err
	}
	bs := utils.ToJSON(res)

	var resourceDurations []model.ResourceDuration
	if err := json.Unmarshal(bs, &resourceDurations); err != nil {
		return nil, err
	}
	return resourceDurations, nil
}
