package useralert

import (
	"context"
	"fmt"
	"net/mail"
	"time"

	"github.com/pingcap/errcode"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/grpc/grpcli"
	"github.com/tidbcloud/metrics-server/central/model/cluster"
	"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"
)

type UserAlertService struct {
	central.UnsafeUserAlertServiceServer
	service.OperationsService
	clustersModel cluster.ModelV2
}

var _ central.UserAlertServiceServer = (*UserAlertService)(nil)

func NewService(s service.OperationsService, model cluster.ModelV2) central.UserAlertServiceServer {
	return &UserAlertService{
		OperationsService: s,
		clustersModel:     model,
	}
}

func (s *UserAlertService) userCluster(ctx context.Context) cluster.UserContextV2 {
	user := s.CurrentUser(ctx)
	return s.clustersModel.User(ctx, user)
}

func (s *UserAlertService) Log(ctx context.Context) *zap.Logger {
	return s.userCluster(ctx).Log().With(log.FieldsFromContext(ctx)...)
}

func (s *UserAlertService) SubscribeUserAlert(ctx context.Context, req *central.SubscribeUserAlertReq) (*central.SubscribeUserAlertResp, error) {
	logger := s.Log(ctx)
	cctx := s.userCluster(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err := cctx.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	if _, err := mail.ParseAddress(req.Email); err != nil {
		return nil, errcode.NewInvalidInputErr(fmt.Errorf("No active user of current project has the email %s", req.Email))
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		var user *account.GetUserByEmailResponse
		// query existing users by emails
		ctx, f := context.WithTimeout(context.Background(), 15*time.Second)
		defer f()
		user, err = grpcli.GetUserByEmail(ctx, &account.GetUserByEmailRequest{Email: req.Email})
		if err != nil {
			return errcode.NewInvalidInputErr(fmt.Errorf("No user found by the email %s", req.Email))
		}

		// list orgs
		ctx, f = context.WithTimeout(context.Background(), 15*time.Second)
		defer f()
		orgs, err := grpcli.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{UserId: user.User.Id})
		if err != nil {
			return errcode.NewInvalidInputErr(fmt.Errorf("No organization found by the user id %d", user.User.Id))
		}

		for i, org := range orgs.Orgs {
			if org.Id == orgID.Uint64() {
				break
			}
			if i+1 == len(orgs.Orgs) {
				return errcode.NewInvalidInputErr(
					fmt.Errorf("No active user of current project has the email %s with org id %d", req.Email, orgID))
			}
		}

		if user.User.Status != account.UserStatus_USER_STATUS_ACTIVE {
			return errcode.NewInvalidInputErr(fmt.Errorf("No active user of current project has the email %s", req.Email))
		}

		var userAlert entity.UserAlert

		_ = entity.NewUserAlertQuerySet(tx).UserIDEq(entity.ID(user.User.Id)).ProjectIDEq(projectID).One(&userAlert)

		if userAlert.ID > 0 {
			return errcode.NewInvalidInputErr(fmt.Errorf("A user with email %s is already subscribed", req.Email))
		}

		return tx.Create(&entity.UserAlert{ProjectID: projectID, UserID: entity.ID(user.User.Id), Type: entity.UserAlertTypeEmail}).Error
	})

	details := auditlog.Details{
		auditlog.KeyUserEmail:     req.Email,
		auditlog.KeyTidbProjectId: projectID.Uint64(),
		auditlog.KeyError:         err,
	}
	auditlog.NewEvent(ctx, auditlog.EventSubscribeAlerts).
		SetOrgID(orgID.Uint64()).SetResult(err == nil).SetDetails(details.Build()).Submit() // nolint:errcheck

	if err != nil {
		logger.Error("failed to subscribe user alerts", zap.Error(err), zap.Uint64("projectID", projectID.Uint64()), zap.String("email", req.Email))
		return nil, err
	}

	return &central.SubscribeUserAlertResp{}, nil
}

func (s *UserAlertService) UnsubscribeUserAlert(ctx context.Context, req *central.UnsubscribeUserAlertReq) (*central.UnsubscribeUserAlertResp, error) {
	logger := s.Log(ctx)
	cctx := s.userCluster(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err := cctx.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	var userId entity.ID
	err = db.Transaction(func(tx *gorm.DB) error {
		// query existing users by emails

		var userAlert entity.UserAlert

		err := entity.NewUserAlertQuerySet(tx).IDEq(entity.ID(req.UserAlertId)).One(&userAlert)
		if err != nil {
			return err
		}

		if userAlert.ProjectID.Uint64() != projectID.Uint64() {
			return errcode.NewInvalidInputErr(fmt.Errorf("No active user of current project has the email"))
		}
		userId = userAlert.UserID
		return entity.NewUserAlertQuerySet(tx).ProjectIDEq(entity.ID(projectID)).IDIn(userAlert.ID).Delete()
	})

	details := auditlog.Details{
		auditlog.KeyUser:          userId.Uint64(),
		auditlog.KeyTidbProjectId: projectID.Uint64(),
		auditlog.KeyError:         err,
		auditlog.KeyDescription:   fmt.Sprintf("unsubscribe user alert with AlertId %d", req.UserAlertId),
	}
	auditlog.NewEvent(ctx, auditlog.EventUnsubscribeAlerts).
		SetOrgID(orgID.Uint64()).SetResult(err == nil).SetDetails(details.Build()).Submit() // nolint:errcheck

	if err != nil {
		logger.Error("failed to unsubscribe user alerts", zap.Error(err), zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("user_alert_id", req.UserAlertId))

		return nil, err
	}

	return &central.UnsubscribeUserAlertResp{}, nil
}

func (s *UserAlertService) GetUserAlertList(ctx context.Context, req *central.GetUserAlertListReq) (*central.GetUserAlertListResp, error) {
	logger := s.Log(ctx)
	cctx := s.userCluster(ctx)
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	err := cctx.CheckOrgIDAndProjectID(orgID, projectID)
	if err != nil {
		return nil, err
	}

	var items []*central.UserAlertItem
	err = db.Transaction(func(tx *gorm.DB) error {

		userAlerts := &[]entity.UserAlert{}
		err := entity.NewUserAlertQuerySet(tx).ProjectIDEq(projectID).TypeEq(entity.UserAlertTypeEmail).All(userAlerts)
		if err != nil {
			return err
		}

		for _, i := range *userAlerts {
			user, err := grpcli.GetUserByID(ctx, &account.GetUserByIDRequest{UserId: uint64(i.UserID)})
			if err != nil {
				logger.Error("no user info found, alert cannot be list", zap.Int64("userID", int64(i.UserID)))
				continue
			}
			items = append(items, &central.UserAlertItem{
				Email:       user.User.Email,
				UserAlertId: uint64(i.ID),
			})

		}
		return nil
	})
	if err != nil {
		logger.Error("failed to get latest subscribed users", zap.Error(err), zap.Uint64("projectID", projectID.Uint64()))
		return nil, errcode.NewInternalErr(err)
	}

	return &central.GetUserAlertListResp{Items: items}, nil
}
