package integration

import (
	"context"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/dbutil"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/model/integration"
	"github.com/tidbcloud/metrics-server/central/service"
	"github.com/tidbcloud/metrics-server/idl/pbgen/proto/central"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// 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) (resp *central.IntegrationStatusResp, err error) {
	defer func() {

		details := auditlog.Details{auditlog.KeyError: err}
		eventType := auditlog.EventCreateDatadogIntegration
		switch req.Type {
		case central.IntegrationType_INTEGRATION_TYPE_DATADOG:
			details[auditlog.KeyApiKey] = integration.MaskDatadogAPIKey(req.DatadogApiKey)
			details[auditlog.KeySite] = string(integration.DatadogSiteRestToDB(req.DatadogSite))
		case central.IntegrationType_INTEGRATION_TYPE_NEWRELIC:
			eventType = auditlog.EventCreateNewrelicIntegration
			details[auditlog.KeyApiKey] = integration.MaskDatadogAPIKey(req.NewrelicApiKey)
			details[auditlog.KeySite] = string(integration.NewrelicSiteRestToDB(req.NewrelicSite))
		}
		eventSubmitErr := auditlog.NewEvent(ctx, eventType).SetResult(err == nil).SetDetails(details.Build()).SetOrgID(req.OrgId).SetProjectID(req.ProjectId).Submit()
		if eventSubmitErr != nil {
			user := i.CurrentUser(ctx)
			log.Error(ctx, "CreateIntegration submit event fail",
				zap.Uint64("tenantID", user.TenantID.Uint64()),
				zap.Uint64("userID", user.ID.Uint64()),
				zap.Error(eventSubmitErr))
		}
	}()

	// 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

		nrUrl          string
		nrDashboardUrl string
		nrMaskedKey    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, entity.ID(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)
	case central.IntegrationType_INTEGRATION_TYPE_NEWRELIC:
		// Validate newrelic api key first.
		valid, err := i.model.ValidateNewrelicAPIKey(ctx, req.NewrelicApiKey, req.NewrelicSite)
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "validate newrelic api key failed"))
			return nil, err
		}
		if !valid {
			err = errcode.NewInvalidInputErr(errors.New("invalid newrelic api key"))
			return nil, err
		}

		s, nrMaskedKey, err = i.model.CreateOrEnableNewrelicIntegration(ctx, entity.ID(req.ProjectId), req.Payloads, req.NewrelicSite, req.NewrelicApiKey)
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "creating newrelic integration failed"))
			return nil, err
		}
		nrUrl = integration.GetNewrelicSiteEndpoint(req.NewrelicSite)
		nrDashboardUrl = integration.GetNewrelicSiteDashboard(req.NewrelicSite)
	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,
		NewrelicSite:         req.NewrelicSite,
		NewrelicSiteUrl:      nrUrl,
		NewrelicDashboardUrl: nrDashboardUrl,
		NewrelicMaskedApiKey: nrMaskedKey,
	}

	return resp, nil
}

func (i integrationService) DeleteIntegration(ctx context.Context, req *central.IntegrationDeleteReq) (resp *central.IntegrationStatusResp, err error) {
	defer func() {
		_ = db.Transaction(func(tx *gorm.DB) error {
			integration := entity.Integration{}
			if req.Type == central.IntegrationType_INTEGRATION_TYPE_DATADOG {
				queryErr := entity.NewIntegrationQuerySet(tx).ProjectIDEq(entity.ID(req.ProjectId)).ProviderTypeEq(entity.IntegrationTypeDatadog).One(&integration)
				if queryErr != nil {
					user := i.CurrentUser(ctx)
					log.Error(ctx, "DeleteIntegration query datadog integration info fail",
						zap.Uint64("tenantID", user.TenantID.Uint64()),
						zap.Uint64("userID", user.ID.Uint64()),
						zap.Error(queryErr))
				}
			}

			if req.Type == central.IntegrationType_INTEGRATION_TYPE_NEWRELIC {
				queryErr := entity.NewIntegrationQuerySet(tx).ProjectIDEq(entity.ID(req.ProjectId)).ProviderTypeEq(entity.IntegrationTypeNewrelic).One(&integration)
				if queryErr != nil {
					user := i.CurrentUser(ctx)
					log.Error(ctx, "DeleteIntegration query newrelic integration info fail",
						zap.Uint64("tenantID", user.TenantID.Uint64()),
						zap.Uint64("userID", user.ID.Uint64()),
						zap.Error(queryErr))
				}
			}

			details := auditlog.Details{auditlog.KeyError: err}
			eventType := auditlog.EventDeleteDatadogIntegration
			switch req.Type {
			case central.IntegrationType_INTEGRATION_TYPE_DATADOG:
				details[auditlog.KeyApiKey] = integration.MaskedDatadogApiKey
				details[auditlog.KeySite] = string(integration.DatadogSite)
			case central.IntegrationType_INTEGRATION_TYPE_NEWRELIC:
				eventType = auditlog.EventDeleteNewrelicIntegration
				details[auditlog.KeyApiKey] = integration.MaskedNewrelicApiKey
				details[auditlog.KeySite] = string(integration.NewrelicSite)
			}

			eventSubmitErr := auditlog.NewEvent(ctx, eventType).SetResult(err == nil).SetDetails(details.Build()).SetOrgID(req.OrgId).SetProjectID(req.ProjectId).Submit()
			if eventSubmitErr != nil {
				user := i.CurrentUser(ctx)
				log.Error(ctx, "DeleteIntegration submit event fail",
					zap.Uint64("tenantID", user.TenantID.Uint64()),
					zap.Uint64("userID", user.ID.Uint64()),
					zap.Error(eventSubmitErr))
				return eventSubmitErr
			}

			return nil
		})
	}()

	// 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
	switch req.Type {
	case central.IntegrationType_INTEGRATION_TYPE_DATADOG:
		s, err = i.model.DisableDatadogIntegration(ctx, entity.ID(req.ProjectId))
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "deleting datadog integration failed"))
			return nil, err
		}
	case central.IntegrationType_INTEGRATION_TYPE_NEWRELIC:
		s, err = i.model.DisableNewrelicIntegration(ctx, entity.ID(req.ProjectId))
		if err != nil {
			err = errcode.NewInternalErr(errors.Annotate(err, "deleting newrelic 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.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)
}
