package store

import (
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/labstack/echo/v4"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/rms/response"
	"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/testbedinfo"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

type testbedService struct {
	ent                 *ent.Client
	rmsrpc              service.RMSRPCService
	planExecSvc         service.PlanExecutionService
	resourcePoolSvc     service.ResourcePoolService
	resourceDurationSvc *resourceDurationService
}

func (s *testbedService) init(entCli *ent.Client, rmsrpc service.RMSRPCService, planExecSvc service.PlanExecutionService, resourcePoolSvc service.ResourcePoolService, resourceDurationSvc *resourceDurationService) {
	s.ent = entCli
	s.rmsrpc = rmsrpc
	s.planExecSvc = planExecSvc
	s.resourcePoolSvc = resourcePoolSvc
	s.resourceDurationSvc = resourceDurationSvc
}

func (s *testbedService) List(ctx utils.Context, opts common.TestbedQueryOpts) ([]*model.TestBedInfo, int, error) {
	log := ctx.Log()

	query := s.ent.TestbedInfo.Query()

	if len(opts.Names) != 0 {
		query = query.Where(testbedinfo.NameIn(opts.Names...))
	}

	if opts.PlanExecID != nil {
		query = query.Where(testbedinfo.PlanExecutionID(*opts.PlanExecID)).Order(ent.Desc(testbedinfo.FieldCreatedAt))
	}

	if len(opts.Status) != 0 {
		query = query.Where(testbedinfo.StatusIn(opts.Status...))
	}

	if len(opts.Types) != 0 {
		query = query.Where(testbedinfo.TypeIn(opts.Types...))
	}

	total, err := query.Count(ctx.Request().Context())
	if err != nil {
		log.Errorf(err, "ListTestbeds failed")
		return nil, 0, err
	}

	if opts.Count != 0 {
		query = query.Offset(int(opts.Offset)).Limit(int(opts.Count))
	}

	testbeds, err := query.All(ctx.Request().Context())
	if err != nil {
		log.Errorf(err, "ListTestbeds failed")
		return nil, total, err
	}

	res, err := s.toModel(ctx, testbeds)
	return res, total, err
}

func (s *testbedService) Create(ctx utils.Context, testbed *model.TestBedInfo) (*model.TestBedInfo, error) {
	log := ctx.Log()
	var rpID model.ID
	if testbed.ResourcePool.ID != 0 {
		rpID = testbed.ResourcePool.ID
	} else if testbed.ResourcePool.Name != "" {
		rp, total, err := s.resourcePoolSvc.List(ctx, common.ResourcePoolQueryOpts{
			Names:   []string{testbed.ResourcePool.Name},
			RmsName: testbed.ResourcePool.RmsName,
		})
		if err != nil {
			return nil, err
		} else if total != 1 {
			return nil, serror.NewMsg(http.StatusBadRequest, "no specified legal resource pool")
		}
		rpID = rp[0].ID
	} else {
		return nil, serror.NewMsg(http.StatusBadRequest, "no specified legal resource pool")
	}

	res, err := s.ent.TestbedInfo.Create().
		SetName(testbed.Name).
		SetPlanExecutionID(testbed.PlanExecutionID).
		SetURL(testbed.URL).
		SetPhysicalResourceRequest(testbed.PhysicalResourceRequest).
		SetResourcePoolID(rpID).
		Save(ctx.Request().Context())
	if err != nil {
		if ent.IsConstraintError(err) {
			return nil, serror.New(http.StatusBadRequest, err)
		}
		log.Errorf(err, "CreateTestBedInfo: TestBedInfo.Create() failed")
		return nil, err
	}

	testbeds, err := s.toModel(ctx, []*ent.TestbedInfo{res})
	if err != nil {
		return nil, err
	}
	return testbeds[0], nil
}

func (s *testbedService) Update(ctx utils.Context, opts common.TestbedUpdateOpts) error {
	log := ctx.Log()

	if opts.PlanExecID == nil {
		return serror.NewMsg(http.StatusBadRequest,
			"UpdateTestBedInfoStatus with plan_exec_id can only set status from INITIATING to READY or ERROR")
	}

	// only update Status from INITIATING to READY/ERROR
	if opts.UpdatedStatus != model.TestBedInfoStatusReady && opts.UpdatedStatus != model.TestBedInfoStatusError {
		return serror.NewMsg(http.StatusBadRequest,
			"UpdateTestBedInfoStatus with plan_exec_id can only set status from INITIATING to READY or ERROR")
	}

	// There should only have one INITIATING testbed
	res, err := s.ent.TestbedInfo.Query().
		Where(testbedinfo.PlanExecutionIDEQ(*opts.PlanExecID),
			testbedinfo.NameEQ(opts.Name),
			testbedinfo.StatusEQ(model.TestBedInfoStatusInitiating)).
		Only(ctx.Request().Context())
	if err != nil {
		log.With("testbed.PlanExecutionID", *opts.PlanExecID, "testbed", opts.Name).Errorf(err, "query testbed error")
		return serror.New(http.StatusBadRequest,
			fmt.Errorf("no INITIATING testbed ref to planExecution %d, get an error: %+v",
				*opts.PlanExecID, err))
	}

	// update status
	update := res.Update().
		SetStatus(opts.UpdatedStatus)
	if opts.SetUpLogURL != "" {
		update = update.SetSetUpLogURL(opts.SetUpLogURL)
	}

	if opts.UpdatedStatus == model.TestBedInfoStatusReady {
		if opts.InitiatedAt == nil {
			tt := time.Now()
			opts.InitiatedAt = &tt
		}
		update = update.SetInitiatedAt(*opts.InitiatedAt)

		if res, err = update.Save(ctx.Request().Context()); err != nil {
			return err
		}

		// If READY, sync from rms (in Get())
		_, err = s.Get(ctx, res.PlanExecutionID, res.Name, true)
		if err != nil {
			return err
		}
	} else if opts.UpdatedStatus == model.TestBedInfoStatusError {
		if res, err = update.SetReleasedAt(time.Now()).
			Save(ctx.Request().Context()); err != nil {
			return err
		}

		// If ERROR, release from rms (in Release())
		// Sometimes ERROR in testbed setup might cause resource leak
		// We need release it here
		statusErr := s.rmsrpc.ReleaseTestBed(res.URL, ctx.Request().Header.Get(echo.HeaderAuthorization))
		if statusErr != nil {
			if statusErr.HTTPStatus == http.StatusNotFound {
				log.With("testbed.PlanExecutionID", *opts.PlanExecID).Warnf("release a non-exist testbed %s, please check the status", res.Name)
			} else {
				log.With("testbed.PlanExecutionID", *opts.PlanExecID).Errorf(statusErr, "release testbed %s error", res.Name)
				return statusErr
			}
		}

		if err = s.resourceDurationSvc.ProcessResourceDuration(ctx, *opts.PlanExecID); err != nil {
			log.With("testbed.PlanExecutionID", *opts.PlanExecID).Errorf(err, "ProcessResourceDuration error")
		}
	}

	return nil
}

func (s *testbedService) Get(ctx utils.Context, planExecID model.ID, testbedName string, withResourceDetails bool) (*model.TestBedInfo, error) {
	log := ctx.Log()

	query := s.ent.TestbedInfo.Query().
		Where(testbedinfo.Name(testbedName), testbedinfo.PlanExecutionID(planExecID))
	testbed, err := query.Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, serror.New(http.StatusNotFound, err)
		}
		log.Errorf(err, "GetTestbed failed")
		return nil, err
	} else if testbed.Status == model.TestBedInfoStatusReady {
		var res *response.GetTestbedResponse
		res, statusErr := s.rmsrpc.GetTestBed(testbed.URL, ctx.Request().Header.Get(echo.HeaderAuthorization), withResourceDetails)
		if statusErr != nil {
			if statusErr.HTTPStatus == http.StatusNotFound {

				leaseTime := time.Now()
				if testbed.Type == model.TestBedInfoTypeRestoreMonitor {
					if _, err := s.ent.TestbedInfo.Delete().Where(testbedinfo.Name(testbed.Name)).Exec(ctx.Request().Context()); err != nil {
						return nil, err
					}
				} else if testbed, err = testbed.
					Update().
					SetStatus(model.TestBedInfoStatusReleased).
					SetReleasedAt(leaseTime).
					Save(ctx.Request().Context()); err != nil {
					return nil, err
				}

				if err = s.resourceDurationSvc.ProcessResourceDuration(ctx, testbed.PlanExecutionID); err != nil {
					log.With("testbed.PlanExecutionID", testbed.PlanExecutionID).Errorf(err, "ProcessResourceDuration error")
				}

			} else {
				return nil, statusErr
			}
		} else {
			leaseDDL, err := time.Parse("2006/01/02 15:04:05.000 -07:00", res.Lease.ExpireDate)
			if err != nil {
				return nil, err
			}

			testbedUpdate := testbed.Update().SetLeaseDDL(leaseDDL)

			if withResourceDetails {
				items := make([]model.TestBedInfoResourceItem, 0)
				for _, item := range res.Items {
					items = append(items, model.TestBedInfoResourceItem(item))
				}
				testbedUpdate.SetResourceItems(items)
			}

			if testbed, err = testbedUpdate.
				Save(ctx.Request().Context()); err != nil {
				return nil, err
			}
		}
	}

	testbeds, err := s.toModel(ctx, []*ent.TestbedInfo{testbed})
	if err != nil {
		return nil, err
	}
	return testbeds[0], nil
}

func (s *testbedService) Renew(ctx utils.Context, planExecID model.ID, testbedName string, lease string) error {
	log := ctx.Log()

	query := s.ent.TestbedInfo.Query().
		Where(testbedinfo.Name(testbedName), testbedinfo.PlanExecutionID(planExecID))
	testbed, err := query.Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return serror.New(http.StatusNotFound, err)
		}
		log.Errorf(err, "GetTestbed failed")
		return err
	} else if testbed.Status != model.TestBedInfoStatusReady {
		return serror.NewMsg(
			http.StatusBadRequest,
			"testbed %s is not READY [%s], can not release it",
			testbedName,
			testbed.Status)
	}

	statusErr := s.rmsrpc.RenewTestBed(lease, testbed.URL, ctx.Request().Header.Get(echo.HeaderAuthorization))
	if statusErr != nil {
		if statusErr.HTTPStatus == http.StatusNotFound {
			log.Errorf(statusErr, "testbed not found, updating the testbedinfo record")
		} else {
			return statusErr
		}
	}
	return nil
}

func (s *testbedService) Release(ctx utils.Context, planExecID model.ID, testbedName string) error {
	log := ctx.Log()

	query := s.ent.TestbedInfo.Query().
		Where(testbedinfo.Name(testbedName), testbedinfo.PlanExecutionID(planExecID))
	testbed, err := query.Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return serror.New(http.StatusNotFound, err)
		}
		log.Errorf(err, "GetTestbed failed")
		return err
	} else if testbed.Status != model.TestBedInfoStatusReady {
		return serror.NewMsg(
			http.StatusBadRequest,
			"testbed %s is not READY [%s], can not release it",
			testbedName,
			testbed.Status)
	}

	// testbed referred a RUNNING plan-execution should not be released
	if exec, err := s.planExecSvc.Get(ctx, testbed.PlanExecutionID); err != nil {
		return err
	} else if exec.Status == model.ExecutionStatusRunning {
		return serror.NewMsg(
			http.StatusForbidden,
			"plan-execution %d is still %s, can not release the testbed",
			testbed.PlanExecutionID,
			exec.Status)
	}

	statusErr := s.rmsrpc.ReleaseTestBed(testbed.URL, ctx.Request().Header.Get(echo.HeaderAuthorization))
	if statusErr != nil {
		if statusErr.HTTPStatus == http.StatusNotFound {
			log.Errorf(statusErr, "testbed not found, updating the testbedInfo record")
		} else {
			return statusErr
		}
	}

	if testbed.Type == model.TestBedInfoTypeRestoreMonitor {
		return s.ent.TestbedInfo.DeleteOne(testbed).Exec(ctx.Request().Context())
	}
	_, err = testbed.Update().
		SetReleasedAt(time.Now()).
		SetReleasedBy(ctx.User().ID).
		SetStatus(model.TestBedInfoStatusReleased).
		Save(ctx.Request().Context())
	if err != nil {
		return err
	}

	if err = s.resourceDurationSvc.ProcessResourceDuration(ctx, testbed.PlanExecutionID); err != nil {
		log.With("testbed.PlanExecutionID", testbed.PlanExecutionID).Errorf(err, "ProcessResourceDuration error")
	}

	return nil
}

// RestoreMonitor will create a restore-monitor testbed with a grafana by rms-api
// The restore-monitor testbed will also be inserted into testbedInfos
// and ref to the same plan Execution, however the InitiatedAt will not be set
func (s *testbedService) RestoreMonitor(ctx utils.Context, planExecID model.ID, testbedName, clusterType, promTarballURL string) (*model.TestBedInfo, error) {
	log := ctx.Log()

	query := s.ent.TestbedInfo.Query().
		Where(testbedinfo.Name(testbedName), testbedinfo.PlanExecutionID(planExecID))
	testbed, err := query.Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, serror.New(http.StatusNotFound, err)
		}
		log.Errorf(err, "GetTestbed failed")
		return nil, err
	} else if testbed.Status != model.TestBedInfoStatusReleased {
		return nil, serror.NewMsg(http.StatusBadRequest, "Only released testbed can restore monitor")
	} else if testbed.Type != model.TestBedInfoTypeNormal {
		return nil, serror.NewMsg(http.StatusBadRequest, "Only NORMAL testbed can restore monitor(not RESTORE_MONITOR)")
	}

	resp, statusErr := s.rmsrpc.RestoreMonitor(
		testbed.Name,
		clusterType,
		testbed.URL,
		promTarballURL,
		ctx.Request().Header.Get(echo.HeaderAuthorization))

	if statusErr != nil {
		if statusErr.HTTPStatus == http.StatusNotFound {
			log.Errorf(statusErr, "testbed not found, updating the testbedInfo record")
		} else {
			return nil, statusErr
		}
	}

	url, err := url.Parse(testbed.URL)
	if err != nil {
		return nil, err
	}
	url.Path = "/api/v1/testbeds/" + resp.TestbedName

	restoredMonitor, err := s.ent.TestbedInfo.Create().
		SetPlanExecutionID(testbed.PlanExecutionID).
		SetName(resp.TestbedName).
		SetURL(url.String()).
		SetStatus(model.TestBedInfoStatusReady).
		SetType(model.TestBedInfoTypeRestoreMonitor).
		SetResourcePoolID(testbed.ResourcePoolID).
		Save(ctx.Request().Context())
	if err != nil {
		log.Errorf(err, "create restored monitor testbed error")
		return nil, err
	}

	testbeds, err := s.toModel(ctx, []*ent.TestbedInfo{restoredMonitor})
	if err != nil {
		return nil, err
	}
	return testbeds[0], nil
}

func (s *testbedService) toModel(ctx utils.Context, objs []*ent.TestbedInfo) ([]*model.TestBedInfo, error) {
	res := make([]*model.TestBedInfo, 0, len(objs))
	for _, item := range objs {
		rps, total, err := s.resourcePoolSvc.List(ctx, common.ResourcePoolQueryOpts{
			IDs: []model.ID{item.ResourcePoolID},
		})
		if err != nil {
			return nil, err
		}
		if total != 1 {
			return nil, serror.NewMsg(http.StatusInternalServerError, "")
		}
		res = append(res, &model.TestBedInfo{
			ID:                      item.ID,
			Name:                    item.Name,
			URL:                     item.URL,
			PlanExecutionID:         item.PlanExecutionID,
			Type:                    item.Type,
			Status:                  item.Status,
			ResourceItems:           item.ResourceItems,
			PhysicalResourceRequest: item.PhysicalResourceRequest,
			CreatedAt:               item.CreatedAt,
			UpdatedAt:               item.UpdatedAt,
			LeaseDDL:                item.LeaseDDL,
			InitiatedAt:             item.InitiatedAt,
			ReleasedAt:              item.ReleasedAt,
			ReleasedBy:              item.ReleasedBy,
			SetUpLogURL:             item.SetUpLogURL,
			ResourcePool:            *rps[0],
		})
	}
	return res, nil
}
