package request

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

	validation "github.com/Thearas/ozzo-validation/v4"
	"github.com/go-playground/validator/v10"
	"github.com/pkg/errors"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/resource"
)

// CreateTestbedRequest ...
type CreateTestbedRequest struct {
	TestbedMeta TestBedMeta `json:"meta,omitempty"`
	model.TestBed
}

type TestBedMeta struct {
	AuthorizeTo []User `json:"to"`
	Accounting  User   `json:"accounting"`
}

type User struct {
	Name      string `json:"name"`
	Kind      string `json:"kind"`
	Namespace string `json:"namespace"`
}

func (c CreateTestbedRequest) Validate() error {
	for _, item := range c.Items {
		var spec interface{}
		switch item.Type {
		case resource.ResourceTypeTiDBCluster:
			spec = new(resource.TiDBClusterSpec)
		case resource.ResourceTypeWorkloadNode:
			spec = new(resource.WorkloadNodeSpec)
		case resource.ResourceTypeTiDBLightning:
			spec = new(resource.TiDBLightningSpec)
		case resource.ResourceTypeDMCluster:
			spec = new(resource.DMClusterSpec)
		case resource.ResourceTypeKafkaCluster:
			spec = new(resource.KafkaClusterSpec)
		case resource.ResourceTypeMySQLNode:
			spec = new(resource.MySQLNodeSpec)
		case resource.ResourceTypeBRNode:
			spec = new(resource.BRSpec)
		case resource.ResourceTypeDrainer:
			spec = new(resource.TiDBDrainer)
		case resource.ResourceTypeLoadBalancer:
			spec = new(resource.LoadBalancer)
		case resource.ResourceTypeTiDBCloudCluster:
			spec = new(resource.TiDBCloudCluster)
		default:
			return fmt.Errorf("unknown resource type: %s", item.Type)
		}
		if err := json.Unmarshal(item.Spec, spec); err != nil {
			return err
		}
	}
	return validation.ValidateStruct(&c, validation.Field(&c.TestBed))
}

func (c *CreateTestbedRequest) ValidateReclaimPolicy() error {
	if _, err := time.ParseDuration(c.ReclaimPolicy.Lease); err != nil {
		return errors.Wrapf(err, "parse reclaim lease `%s` error", c.ReclaimPolicy.Lease)
	}
	return nil
}

// GetTestbedRequest gets all status of resources which specified on the Items field
type GetTestbedRequest struct {
	Name string `json:"name" validate:"required"`
	// resources want to fetch
	// TODO(@mahjonp): deprecate Items when records sub-resources on the testbed
	Items []resource.ResourceHandle `json:"items" validate:"required"`
}

func (g *GetTestbedRequest) Validate() error {
	validate := validator.New()
	if err := validate.Struct(g); err != nil {
		return err
	}
	for _, item := range g.Items {
		if g.Name != item.Testbed {
			return fmt.Errorf("expect testbed name %s, got %s on resource %s", g.Name, item.Testbed, item.Name)
		}
	}
	return nil
}

type AuthTestbedRequest struct {
	Name        string `json:"name"`
	AuthorizeTo []User `json:"authorizeTo"`
}

func (d *AuthTestbedRequest) Validate() error {
	return validation.ValidateStruct(d,
		validation.Field(&d.Name, validation.Required))
}

type RestoreMonitorRequest struct {
	Testbed              string `json:"testbed,omitempty"`
	PrometheusTarballURL string `json:"prometheusTarballURL"`
	ClusterType          string `json:"clusterType,omitempty" default:"k8s"`
}

func (d *RestoreMonitorRequest) Validate() error {
	if len(d.Testbed) == 0 &&
		len(d.PrometheusTarballURL) == 0 {
		return fmt.Errorf("one of [testbed, prometheusTarballURL] should be provided")
	}

	return validation.ValidateStruct(d,
		validation.Field(&d.ClusterType, validation.In("k8s", "tiup")))
}

// DeleteTestbedRequest deletes the whole testbed and sub resources in it
type DeleteTestbedRequest struct {
	Name string `json:"name" validate:"required"`
}

func (d *DeleteTestbedRequest) Validate() error {
	validate := validator.New()
	if err := validate.Struct(d); err != nil {
		return err
	}
	return nil
}

type OnTestbedCompletionType string

func (o OnTestbedCompletionType) Validate() error {
	if o != OnSuccess && o != OnFailure {
		return fmt.Errorf("must be Success or Failure")
	}
	return nil
}

const (
	OnSuccess OnTestbedCompletionType = "Success"
	OnFailure OnTestbedCompletionType = "Failure"
)

type OnTestbedCompletionRequest struct {
	Name       string                  `json:"name"`
	Type       OnTestbedCompletionType `json:"type"`
	ClinicURLs map[string]string       `json:"clinicURLs,omitempty"` // map[testbed/clustername]url
}

// Validate ...
func (r OnTestbedCompletionRequest) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.Name, validation.Required),
		validation.Field(&r.Type),
	)
}

type CredentialRequest struct {
	PEM         []byte `json:"pem,omitempty"`
	TestbedName string `json:"testbedName,omitempty"`
}

func (r CredentialRequest) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.PEM, validation.Required),
		validation.Field(&r.TestbedName, validation.Required),
	)
}

type RenewalLeaseRequest struct {
	Name  string      `json:"name"`
	Lease DurationStr `json:"lease,omitempty"`
}

func (r *RenewalLeaseRequest) Validate() error {
	return validation.ValidateStruct(r,
		validation.Field(&r.Name, validation.Required),
		validation.Field(&r.Lease),
	)
}

type DurationStr string

func (d DurationStr) Validate() error {
	if d == "" {
		return nil
	}
	if _, err := time.ParseDuration(string(d)); err != nil {
		return err
	}
	return nil
}

type GetResourcePoolStatusRequest struct {
	ResourcePool string `json:"resourcePool"`
}

func (d *GetResourcePoolStatusRequest) Validate() error {
	return validation.ValidateStruct(d,
		validation.Field(&d.ResourcePool, validation.Required))
}

type GetTestbedsResourceRequest struct {
	ResourcePool string `json:"resourcePool"`
}

func (d *GetTestbedsResourceRequest) Validate() error {
	return validation.ValidateStruct(d,
		validation.Field(&d.ResourcePool, validation.Required))
}

type GetTestbedResourceOvertimeRequest struct {
	Testbed string    `json:"testbed"`
	Start   time.Time `json:"start"`
	End     time.Time `json:"end"`
}

func (d *GetTestbedResourceOvertimeRequest) Validate() error {
	return validation.ValidateStruct(d,
		validation.Field(&d.Testbed, validation.Required),
		validation.Field(&d.Testbed, validation.Required),
		validation.Field(&d.Testbed, validation.Required))
}

type GetShootKubeConfigRequest struct {
	ProjectID     string `json:"projectID"`
	CloudProvider string `json:"cloudProvider"`
	Region        string `json:"region"`
}

func (r GetShootKubeConfigRequest) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.ProjectID, validation.Required),
		validation.Field(&r.CloudProvider, validation.Required),
		validation.Field(&r.Region, validation.Required),
	)
}
