package server

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/RafayLabs/rcloud-base/pkg/service"

	"github.com/RafayLabs/rcloud-base/pkg/gateway"
	"github.com/RafayLabs/rcloud-base/pkg/log"
	"github.com/RafayLabs/rcloud-base/pkg/query"
	sentryrpc "github.com/RafayLabs/rcloud-base/proto/rpc/sentry"
	commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
	infrav3 "github.com/RafayLabs/rcloud-base/proto/types/infrapb/v3"
	"github.com/RafayLabs/rcloud-base/proto/types/sentry"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/RafayLabs/rcloud-base/pkg/sentry/cryptoutil"
	"github.com/RafayLabs/rcloud-base/pkg/sentry/util"
)

var _log = log.GetLogger()

type RelayNetworkDownloadData struct {
	SentryAddr     string
	PeerHost       string
	PeerToken      string
	UserToken      string
	ConnectorToken string
	RelayImage     string
	UserHostPort   string
	RelayHostPort  string
	RelayNetworkID string
}

type RelayAgentDownloadData struct {
	ClusterID     string
	SentryAddr    string
	Token         string
	RelayImage    string
	HostName      string
	TemplateName  string
	TemplateToken string
}

type bootstrapServer struct {
	bs       service.BootstrapService
	passFunc cryptoutil.PasswordFunc
	cs       service.ClusterService
}

var _ sentryrpc.BootstrapServer = (*bootstrapServer)(nil)

func (s *bootstrapServer) GetBootstrapInfra(ctx context.Context, in *sentry.BootstrapInfra) (*sentry.BootstrapInfra, error) {
	return s.bs.GetBootstrapInfra(ctx, in.Metadata.Name)
}

func (s *bootstrapServer) PatchBootstrapInfra(ctx context.Context, in *sentry.BootstrapInfra) (*sentry.BootstrapInfra, error) {
	err := s.bs.PatchBootstrapInfra(ctx, in)
	return in, err
}

func (s *bootstrapServer) PatchBootstrapAgentTemplate(ctx context.Context, in *sentry.BootstrapAgentTemplate) (*sentry.BootstrapAgentTemplate, error) {
	err := s.bs.PatchBootstrapAgentTemplate(ctx, in)
	return in, err
}

func (s *bootstrapServer) GetBootstrapAgentTemplate(ctx context.Context, in *sentry.BootstrapAgentTemplate) (*sentry.BootstrapAgentTemplate, error) {
	return s.bs.GetBootstrapAgentTemplate(ctx, in.Metadata.Name)
}

func (s *bootstrapServer) CreateBootstrapAgent(ctx context.Context, in *sentry.BootstrapAgent) (*sentry.BootstrapAgent, error) {
	err := s.bs.CreateBootstrapAgent(ctx, in)

	return in, err
}

func (s *bootstrapServer) GetBootstrapAgent(ctx context.Context, in *sentry.BootstrapAgent) (ret *sentry.BootstrapAgent, err error) {
	templateRef, err := util.GetTemplateScope(in.Spec.TemplateRef)
	if err != nil {
		return
	}

	ret, err = s.bs.GetBootstrapAgent(ctx, templateRef, query.WithMeta(in.Metadata))
	if err == sql.ErrNoRows {
		err = status.Error(codes.NotFound, err.Error())
	}
	return
}

func (s *bootstrapServer) GetBootstrapAgentTemplates(ctx context.Context, qo *commonv3.QueryOptions) (*sentry.BootstrapAgentTemplateList, error) {
	return s.bs.SelectBootstrapAgentTemplates(ctx, query.WithOptions(qo))
}

func (s *bootstrapServer) GetBootstrapAgents(ctx context.Context, in *sentryrpc.GetBootstrapAgentsRequest) (ret *sentry.BootstrapAgentList, err error) {
	templateRef, err := util.GetTemplateScope(in.TemplateScope)
	if err != nil {
		return
	}

	ret, err = s.bs.SelectBootstrapAgents(ctx, templateRef, query.WithOptions(in.Opts))
	if err != nil {
		return nil, err
	}

	return
}

func (s *bootstrapServer) DeleteBootstrapAgent(ctx context.Context, in *sentry.BootstrapAgent) (ret *sentryrpc.DeleteBootstrapAgentResponse, err error) {
	templateRef, err := util.GetTemplateScope(in.Spec.TemplateRef)
	if err != nil {
		return
	}

	err = s.bs.DeleteBootstrapAgent(ctx, templateRef, query.WithMeta(in.Metadata))
	if err == sql.ErrNoRows {
		err = status.Error(codes.NotFound, err.Error())
	}

	return &sentryrpc.DeleteBootstrapAgentResponse{}, err
}

func (s *bootstrapServer) UpdateBootstrapAgent(ctx context.Context, in *sentry.BootstrapAgent) (ret *sentry.BootstrapAgent, err error) {

	templateRef, err := util.GetTemplateScope(in.Spec.TemplateRef)
	if err != nil {
		return
	}

	err = s.bs.PatchBootstrapAgent(ctx, in, templateRef, query.WithMeta(in.Metadata))
	if err == sql.ErrNoRows {
		err = status.Error(codes.NotFound, err.Error())
	}

	ret = in

	return
}

func (s *bootstrapServer) RegisterBootstrapAgent(ctx context.Context, in *sentryrpc.RegisterAgentRequest) (resp *sentryrpc.RegisterAgentResponse, err error) {
	_log.Infow("received agent register", "request", *in)

	resp = &sentryrpc.RegisterAgentResponse{}

	token, err := util.GetTemplateScope(in.TemplateToken)
	if err != nil {
		_log.Error(err.Error())
		return
	}

	var template *sentry.BootstrapAgentTemplate
	// bypass for auto registering core relay
	if (token == "-" || token == "cd-relay") && !gateway.IsGatewayRequest(ctx) {
		template, err = s.bs.GetBootstrapAgentTemplate(ctx, in.TemplateName)
		if err != nil {
			_log.Error(err.Error())
			return
		}
	} else {
		template, err = s.bs.GetBootstrapAgentTemplateForToken(ctx, token)
		if err != nil {
			_log.Error(err.Error())
			return
		}
	}

	infra, err := s.bs.GetBootstrapInfra(ctx, template.Spec.InfraRef)
	if err != nil {
		_log.Error(err.Error())
		return
	}

	var signer cryptoutil.Signer
	var opts []cryptoutil.Option

	opts = append(opts, cryptoutil.WithCAKeyDecrypt(s.passFunc))

	// only add altname for server or mixed templates
	if template.Spec.TemplateType == sentry.BootstrapAgentTemplateType_Server || template.Spec.TemplateType == sentry.BootstrapAgentTemplateType_Mixed {
		for _, host := range template.Spec.Hosts {
			h, _ := util.ParseAddr(host.Host)
			opts = append(opts, cryptoutil.WithAltName(h))
		}
	}

	if template.Spec.TemplateType == sentry.BootstrapAgentTemplateType_Client {
		opts = append(opts, cryptoutil.WithClient(), cryptoutil.WithCSRSubjectValidate(cryptoutil.CNShouldBe(in.Token)))
	} else if template.Spec.TemplateType == sentry.BootstrapAgentTemplateType_Server {
		opts = append(opts, cryptoutil.WithServer())
	} else if template.Spec.TemplateType == sentry.BootstrapAgentTemplateType_Mixed {
		opts = append(opts, cryptoutil.WithServer(), cryptoutil.WithClient())
	}

	signer, err = cryptoutil.NewSigner([]byte(infra.Spec.CaCert), []byte(infra.Spec.CaKey), opts...)
	if err != nil {
		_log.Errorw("error getting cert signer", "error", err.Error())
		return
	}

	signed, err := signer.Sign(in.Csr)
	if err != nil {
		_log.Error(err.Error())
		return
	}

	var agent *sentry.BootstrapAgent
	agent, err = s.bs.GetBootstrapAgentForToken(ctx, in.Token)

	// if agent is not found and template has auto register
	if err == sql.ErrNoRows && template.Spec.AutoRegister {
		agent = &sentry.BootstrapAgent{
			Metadata: &commonv3.Metadata{
				Name: in.Name,
			}, Spec: &sentry.BootstrapAgentSpec{Token: in.Token,
				TemplateRef: template.Metadata.Name,
			},
		}

		err = s.bs.CreateBootstrapAgent(ctx, agent)
		if err != nil {
			_log.Error(err.Error())
			return
		}
	} else {
		if err != nil {
			//agent is nil
			_log.Error(err.Error())
			return
		}
	}

	if agent.Spec.TemplateRef != template.Metadata.Name {
		err = fmt.Errorf("token %s cannot be registered for template %s", in.Token, in.TemplateToken)
		_log.Error(err.Error())
		return
	}

	err = s.bs.RegisterBootstrapAgent(ctx, in.Token)
	if err != nil {
		_log.Error(err.Error())
		return
	}

	resp.Certificate = signed
	resp.CaCertificate = []byte(infra.Spec.CaCert)

	if template.Metadata.Name == "rafay-core-relay-agent" {
		_log.Info("updating cluster status for :: ", agent.Metadata.Name)
		err = s.updateClusterStatus(ctx, agent.Metadata.Name)
	}

	return
}

func (s *bootstrapServer) updateClusterStatus(ctx context.Context, clusterID string) error {
	cluster := &infrav3.Cluster{
		Metadata: &commonv3.Metadata{
			Id: clusterID,
		},
		Spec: &infrav3.ClusterSpec{
			ClusterData: &infrav3.ClusterData{
				ClusterStatus: &infrav3.ClusterStatus{
					Conditions: []*infrav3.ClusterCondition{
						{
							Type:        infrav3.ClusterConditionType_ClusterCheckIn,
							Status:      commonv3.RafayConditionStatus_Success,
							LastUpdated: timestamppb.Now(),
							Reason:      "Relay agent established connection.",
						},
						{
							Type:        infrav3.ClusterConditionType_ClusterRegister,
							Status:      commonv3.RafayConditionStatus_Success,
							LastUpdated: timestamppb.Now(),
							Reason:      "Relay agent established connection.",
						},
						{
							Type:        infrav3.ClusterConditionType_ClusterReady,
							Status:      commonv3.RafayConditionStatus_Success,
							LastUpdated: timestamppb.Now(),
							Reason:      "Relay agent established connection.",
						},
					},
				},
			},
		},
	}
	return s.cs.UpdateClusterConditionStatus(ctx, cluster)
}

func (s *bootstrapServer) GetBootstrapAgentConfig(ctx context.Context, in *sentry.BootstrapAgent) (*commonv3.HttpBody, error) {
	return nil, nil
}

// NewBootstrapServer return new bootstrap server
func NewBootstrapServer(bs service.BootstrapService, f cryptoutil.PasswordFunc, cs service.ClusterService) sentryrpc.BootstrapServer {
	return &bootstrapServer{bs, f, cs}
}
