package integration

import (
	"context"

	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/model/integration"
	"github.com/pingcap/dbaas/central/service"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/idl/pbgen/proto/central"
)

// integrationService has two responsibilities:
// 1. Check and validate request parameters
// 2. Compose response body and status code
// All other business logic should be put in the corresponding model.
type integrationService struct {
	central.UnimplementedIntegrationServiceServer
	service.OperationsService
	model integration.Model
}

// Ensure that integrationService implements IntegrationServiceServer interface
var _ central.IntegrationServiceServer = (*integrationService)(nil)

func NewService(opsService service.OperationsService, model integration.Model) central.IntegrationServiceServer {
	return &integrationService{
		OperationsService: opsService,
		model:             model,
	}
}

func (i integrationService) CreateIntegration(ctx context.Context, req *central.IntegrationCreateReq) (*central.IntegrationStatusResp, error) {
	// check user permission first
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	if err := i.checkModifyPermission(ctx, orgID, projectID); err != nil {
		return nil, err
	}

	// check request parameters
	if len(req.Payloads) == 0 {
		err := errcode.NewInvalidInputErr(errors.New("empty payloads"))
		return nil, err
	}

	var (
		s              central.IntegrationStatus
		ddUrl          string
		ddDashboardUrl string
		ddMaskedKey    string
	)

	switch req.Type {
	case central.IntegrationType_INTEGRATION_TYPE_DATADOG:
		// Validate Datadog api key first.
		valid, err := i.model.ValidateDatadogAPIKey(ctx, req.DatadogApiKey, req.DatadogSite)
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "validate datadog api key failed"))
			return nil, err
		}
		if !valid {
			err = errcode.NewInvalidInputErr(errors.New("invalid datadog api key"))
			return nil, err
		}

		s, ddMaskedKey, err = i.model.CreateOrEnableDatadogIntegration(ctx, req.OrgId, req.ProjectId, req.Payloads, req.DatadogSite, req.DatadogApiKey)
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "creating datadog integration failed"))
			return nil, err
		}
		ddUrl = integration.GetDatadogSiteUrl(req.DatadogSite)
		ddDashboardUrl = integration.GetDatadogDashboardUrl(req.DatadogSite)
	default:
		err := errcode.NewInvalidInputErr(errors.New("invalid integration type"))
		return nil, err
	}

	resp := &central.IntegrationStatusResp{
		OrgId:               req.OrgId,
		ProjectId:           req.ProjectId,
		Type:                req.Type,
		Status:              s,
		Payloads:            req.Payloads,
		DatadogSite:         req.DatadogSite,
		DatadogSiteUrl:      ddUrl,
		DatadogDashboardUrl: ddDashboardUrl,
		DatadogMaskedApiKey: ddMaskedKey,
	}

	return resp, nil
}

func (i integrationService) DeleteIntegration(ctx context.Context, req *central.IntegrationDeleteReq) (*central.IntegrationStatusResp, error) {
	// check user permission first
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	if err := i.checkModifyPermission(ctx, orgID, projectID); err != nil {
		return nil, err
	}

	var (
		s   central.IntegrationStatus
		err error
	)
	switch req.Type {
	case central.IntegrationType_INTEGRATION_TYPE_DATADOG:
		s, err = i.model.DisableDatadogIntegration(ctx, req.OrgId, req.ProjectId)
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "deleting datadog integration failed"))
			return nil, err
		}
	default:
		err := errcode.NewInvalidInputErr(errors.New("invalid integration type"))
		return nil, err
	}
	resp := &central.IntegrationStatusResp{
		OrgId:     req.OrgId,
		ProjectId: req.ProjectId,
		Type:      req.Type,
		Status:    s,
	}
	return resp, nil
}

func (i integrationService) ListIntegrations(ctx context.Context, req *central.IntegrationListReq) (*central.IntegrationListResp, error) {
	// check user permission first
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	if err := i.checkReadPermission(ctx, orgID, projectID); err != nil {
		return nil, err
	}

	statuses, err := i.model.ListIntegrations(ctx, req.OrgId, req.ProjectId, req.Type)
	if err != nil {
		err = errcode.NewInternalErr(errors.Annotate(err, "list integrations failed"))
		return nil, err
	}

	resp := &central.IntegrationListResp{
		Items: statuses,
		Total: int32(len(statuses)),
	}

	return resp, nil
}

// checkModifyPermission checks whether the user is owner or member role.
// It's a whitelist style check.
// params:
//  ctx: the context which contains user info which is extracted from bearer token by middleware.
//  orgID, projectID: the orgID and projectID from the request.
func (i integrationService) checkModifyPermission(ctx context.Context, orgID, projectID entity.ID) error {
	u := i.CurrentUser(ctx)
	// check consistency of request org id.
	if orgID != u.TenantID {
		return errcode.NewForbiddenErr(errors.New("org_id in request is inconsistent with the user's org ID"))
	}
	// check whether the user is project owner.
	if u.Role == entity.UserRoleOwner {
		return nil
	}
	// check whether the user is project member.
	return dbutil.CheckProjectID(u.Projects, projectID)
}

func (i integrationService) checkReadPermission(ctx context.Context, orgID, projectID entity.ID) error {
	u := i.CurrentUser(ctx)
	// bypass if the user is impersonated.
	if u.IsImpersonated {
		return nil
	}
	return i.checkModifyPermission(ctx, orgID, projectID)
}
