package service

import (
	"github.com/tidbcloud/config-center/internal/db"
	pb "github.com/tidbcloud/config-center/proto/config_center"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	defaultClusterComponentSearchPageSize = 100
	maxClusterComponentSearchPageSize     = 500
)

func CreateClusterComponent(d db.DB, req *pb.CreateClusterComponentRequest) (*pb.ClusterComponent, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	expected := &pb.ClusterComponent{
		ClusterType:    req.ClusterType,
		ClusterId:      req.ClusterId,
		ComponentName:  req.ComponentName,
		Version:        req.Version,
		CloudProvider:  req.CloudProvider,
		NodeInstance:   req.NodeInstance,
		UpdateStrategy: req.GetUpdateStrategy(),
	}

	// in this case, the DB layer handled the fallback
	expected.BaseImage, expected.EffectiveImageTag, err = tx.GetComponentImage(req.GetClusterType().String(), req.GetComponentName(), expected.Version, req.GetTenantId())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "cannot find default image info of component %s", req.GetComponentName())
	}

	if req.GetOverwriteImageTag() != "" {
		expected.OverwriteImageTag = req.GetOverwriteImageTag()
		expected.EffectiveImageTag = req.GetOverwriteImageTag()
	}

	if req.GetValidateOnly() {
		return expected, nil
	}
	err = tx.InsertClusterComponent(req.GetClusterType().String(),
		req.GetClusterId(),
		req.GetComponentName(),
		expected.Version,
		expected.CloudProvider,
		expected.NodeInstance,
		expected.OverwriteImageTag,
		expected.BaseImage,
		expected.EffectiveImageTag,
		int(expected.UpdateStrategy.Number()),
	)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return expected, tx.Commit()
}

func BatchUpdateClusterComponents(d db.DB, req *pb.BatchUpdateClusterComponentsRequest) (*pb.BatchUpdateClusterComponentsResponse, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}

	var resp pb.BatchUpdateClusterComponentsResponse
	for _, r := range req.GetRequests() {
		if req.GetValidateOnly() {
			r.ValidateOnly = true
		}
		component, err := UpdateClusterComponent(tx, r)
		if err != nil {
			tx.Rollback()
			return nil, err
		}
		resp.Components = append(resp.Components, component)
	}

	return &resp, tx.Commit()
}

func GetClusterComponent(d db.DB, req *pb.GetClusterComponentRequest) (*pb.ClusterComponent, error) {
	c, err := d.GetClusterComponent(req.GetClusterType().String(), req.GetClusterId(), req.GetComponentName())
	if err == db.ErrNotFound {
		return nil, status.Errorf(codes.NotFound, "component %s not found", req.GetComponentName())
	} else if err != nil {
		return nil, err
	}

	result := &pb.ClusterComponent{
		ClusterType:       req.GetClusterType(),
		ClusterId:         c.ClusterID,
		ComponentName:     req.GetComponentName(),
		Version:           c.Version,
		CloudProvider:     c.CloudProvider,
		NodeInstance:      c.NodeInstance,
		OverwriteImageTag: c.OverwriteImageTag,
		BaseImage:         c.BaseImage,
		EffectiveImageTag: c.ImageTag,
		UpdateStrategy:    pb.UpdateStrategy(c.UpdateStrategy),
		CreateTime:        timestamppb.New(c.CreateTime),
		UpdateTime:        timestamppb.New(c.UpdateTime),
	}
	if c.DeleteTime != nil {
		result.DeleteTime = timestamppb.New(*c.DeleteTime)
	}
	return result, nil
}

func ListClusterComponents(d db.DB, req *pb.ListClusterComponentsRequest) (*pb.ListClusterComponentsResponse, error) {
	components, err := d.ListClusterComponents(req.GetClusterType().String(), req.GetClusterId())
	if err != nil {
		return nil, err
	}

	var resp pb.ListClusterComponentsResponse
	for _, c := range components {
		component := &pb.ClusterComponent{
			ClusterType:       req.GetClusterType(),
			ClusterId:         c.ClusterID,
			ComponentName:     c.ComponentName,
			Version:           c.Version,
			CloudProvider:     c.CloudProvider,
			NodeInstance:      c.NodeInstance,
			OverwriteImageTag: c.OverwriteImageTag,
			BaseImage:         c.BaseImage,
			EffectiveImageTag: c.ImageTag,
			UpdateStrategy:    pb.UpdateStrategy(c.UpdateStrategy),
			CreateTime:        timestamppb.New(c.CreateTime),
			UpdateTime:        timestamppb.New(c.UpdateTime),
		}
		if c.DeleteTime != nil {
			component.DeleteTime = timestamppb.New(*c.DeleteTime)
		}
		resp.Components = append(resp.Components, component)
	}
	return &resp, nil
}

func SearchClusterComponents(d db.DB, req *pb.SearchClusterComponentsRequest) (*pb.SearchClusterComponentsResponse, error) {
	if req.GetClusterType() == pb.ClusterType_CLUSTER_TYPE_UNSPECIFIED {
		return nil, status.Error(codes.InvalidArgument, "cluster_type is required")
	}
	if req.GetComponentName() == "" && len(req.GetClusterIds()) == 0 {
		return nil, status.Error(codes.InvalidArgument, "component_name or cluster_ids is required")
	}

	pageSize := req.GetPageSize()
	if pageSize <= 0 {
		pageSize = defaultClusterComponentSearchPageSize
	} else if pageSize > maxClusterComponentSearchPageSize {
		pageSize = maxClusterComponentSearchPageSize
	}
	page := req.GetPage()
	if page <= 0 {
		page = 1
	}

	total, err := d.CountClusterComponents(req.GetClusterType().String(), req.GetComponentName(), req.GetClusterIds(), req.GetComponentVersion(), req.GetImageTag(), req.GetShowDeleted())
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	rows, err := d.SearchClusterComponents(req.GetClusterType().String(), req.GetComponentName(), req.GetClusterIds(), req.GetComponentVersion(), req.GetImageTag(), req.GetShowDeleted(), pageSize, page)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	resp := &pb.SearchClusterComponentsResponse{
		Total: total,
	}
	for _, row := range rows {
		component := &pb.ClusterComponent{
			ClusterType:       req.GetClusterType(),
			ClusterId:         row.ClusterID,
			ComponentName:     row.ComponentName,
			Version:           row.Version,
			CloudProvider:     row.CloudProvider,
			NodeInstance:      row.NodeInstance,
			OverwriteImageTag: row.OverwriteImageTag,
			BaseImage:         row.BaseImage,
			EffectiveImageTag: row.ImageTag,
			UpdateStrategy:    pb.UpdateStrategy(row.UpdateStrategy),
			CreateTime:        timestamppb.New(row.CreateTime),
			UpdateTime:        timestamppb.New(row.UpdateTime),
		}
		if row.DeleteTime != nil {
			component.DeleteTime = timestamppb.New(*row.DeleteTime)
		}
		resp.Components = append(resp.Components, component)
	}
	return resp, nil
}

func UpdateClusterComponent(d db.DB, req *pb.UpdateClusterComponentRequest) (*pb.ClusterComponent, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	create := false
	c, err := tx.GetClusterComponent(req.GetClusterType().String(), req.GetClusterId(), req.GetComponentName())
	if err == db.ErrNotFound {
		if req.GetAllowMissing() {
			if req.Version == nil {
				return nil, status.Errorf(codes.InvalidArgument, "version is required for missing component")
			}
			create = true
		} else {
			return nil, status.Errorf(codes.NotFound, "component %s not found", req.GetComponentName())
		}
	} else if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	if req.CloudProvider != nil {
		c.CloudProvider = req.GetCloudProvider()
	}
	if req.NodeInstance != nil {
		c.NodeInstance = req.GetNodeInstance()
	}
	if req.UpdateStrategy != nil {
		c.UpdateStrategy = int(req.GetUpdateStrategy().Number())
	}

	if req.Version != nil {
		// not update image tag if version is not changed
		c.Version = req.GetVersion()
		c.BaseImage, c.ImageTag, err = tx.GetComponentImage(req.GetClusterType().String(), req.GetComponentName(), c.Version, req.GetTenantId())
		if err != nil {
			return nil, status.Errorf(codes.Internal, "cannot find default image info of component %s", req.GetComponentName())
		}
	}

	if req.OverwriteImageTag != nil {
		c.OverwriteImageTag = req.GetOverwriteImageTag()
	}
	// overwrite image tag has higher priority
	if c.OverwriteImageTag != "" {
		c.ImageTag = c.OverwriteImageTag
	}

	expectedComponent := &pb.ClusterComponent{
		ClusterType:       req.GetClusterType(),
		ClusterId:         req.GetClusterId(),
		ComponentName:     req.GetComponentName(),
		Version:           c.Version,
		CloudProvider:     c.CloudProvider,
		NodeInstance:      c.NodeInstance,
		BaseImage:         c.BaseImage,
		OverwriteImageTag: c.OverwriteImageTag,
		EffectiveImageTag: c.ImageTag,
		UpdateStrategy:    pb.UpdateStrategy(c.UpdateStrategy),
	}

	if req.GetValidateOnly() {
		return expectedComponent, nil
	}

	if create {
		err = tx.InsertClusterComponent(req.GetClusterType().String(),
			req.GetClusterId(),
			req.GetComponentName(),
			c.Version,
			c.CloudProvider,
			c.NodeInstance,
			c.OverwriteImageTag,
			c.BaseImage,
			c.ImageTag,
			c.UpdateStrategy,
		)
	} else {
		err = tx.UpdateClusterComponent(req.GetClusterType().String(),
			req.GetClusterId(),
			req.GetComponentName(),
			c.Version,
			c.CloudProvider,
			c.NodeInstance,
			c.OverwriteImageTag,
			c.BaseImage,
			c.ImageTag,
			c.UpdateStrategy,
		)
	}
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	if req.UpdateRelatedConfigurationFiles {
		files, err := tx.ListClusterConfigInfo2(req.GetClusterType().String(), req.GetClusterId(), "", req.GetComponentName())
		if err != nil {
			return nil, status.Error(codes.Internal, err.Error())
		}
		for _, file := range files {
			// 1. find suitable config template
			template, err := SearchDefaultConfigurationTemplate(tx, &pb.SearchDefaultConfigurationTemplateRequest{
				ClusterType:       pb.ClusterType(pb.ClusterType_value[file.ClusterType]),
				ConfigurationType: file.ConfigType,
				Version:           expectedComponent.Version,
				CloudProvider:     expectedComponent.CloudProvider,
				NodeInstance:      expectedComponent.NodeInstance,
			})
			if err != nil {
				return nil, err
			}

			// 2. update config file
			_, err = UpdateClusterConfigurationFile(tx, &pb.UpdateClusterConfigurationFileRequest{
				ClusterType:           pb.ClusterType(pb.ClusterType_value[file.ClusterType]),
				ClusterId:             req.GetClusterId(),
				ConfigurationFilename: file.Filename,
				TemplateId:            &template.TemplateId,
				CustomParams:          nil,   // must be nil to avoid overwrite
				AllowMissing:          false, // not automatically create missing file when updating component, double check since it is inside a loop of list result
			})

			if err != nil {
				return nil, err
			}
		}
	}

	return expectedComponent, tx.Commit()
}
