package jump

import (
	"context"
	"fmt"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	"github.com/tidbcloud/devops/entity/event"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	privent "github.com/tidbcloud/devops/entity/privilege"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/dedicatedcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/oncallcli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/secretsource"
	k8sutil "github.com/tidbcloud/devops/pkg/infra/utils/k8s"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	itkapi "github.com/tidbcloud/devops/pkg/service/itoken/api"
	jmpsvcapi "github.com/tidbcloud/devops/pkg/service/jump/api"
	"github.com/tidbcloud/devops/pkg/service/jump/authactor"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/types"
	"github.com/tidbcloud/devops/pkg/service/jump/jumpservercli"
	"github.com/tidbcloud/devops/pkg/service/jump/repo"
	privapi "github.com/tidbcloud/devops/pkg/service/privilege/api"
	"github.com/tidbcloud/devops/pkg/service/tenant"
	"k8s.io/client-go/rest"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/pkgv2/db"
	"go.uber.org/dig"
	"tailscale.com/client/tailscale/v2"
)

const (
	eventbusGroupKey = "devops_jump_service"
	seeUserGroup     = "see_team"
)

type ServiceParam struct {
	dig.In

	Conf          *config.RootConfig
	Logger        logr.Logger
	DBProvider    db.TransProvider
	UserService   userv1.UserServiceClient
	TenantService tenant.TenantServiceAPI
	CoreSvcCli    *coresvccli.CoreSvcCli
	MgmtHTTP      mgmthttpcli.MgmtHTTPClient
	InfraClient   dedicatedcli.InfraAPIClient
	SecretSource  secretsource.SecretSource
	EventBus      *eventbus.EventBus
	Approval      apprapi.ApprovalServiceAPI
	Privilege     privapi.PrivilegeServiceAPI
	InternalToken itkapi.InternalTokenServiceAPI
	OnCallCli     oncallcli.Client `optional:"true"`
}

type ServiceResult struct {
	dig.Out

	API     jmpsvcapi.JumpServiceAPI
	Service service.Service `group:"service"`
}

type svc struct {
	*service.JobService

	conf       config.JumpServiceConfig
	logger     logr.Logger
	dbp        db.TransProvider
	eventbus   *eventbus.EventBus
	coresvcCli *coresvccli.CoreSvcCli
	userSvc    userv1.UserServiceClient
	apprSvc    apprapi.ApprovalServiceAPI
	privSvc    privapi.PrivilegeServiceAPI
	itkSvc     itkapi.InternalTokenServiceAPI
	tailscale  *tailscale.Client

	onCallCli      oncallcli.Client
	jmpRepo        repo.JumpRepo
	jumpPodManager jumppod.JumpPodManager
	jumpserverCli  jumpservercli.JumpServerCli
	syncInsCh      chan uint64 // a channel to sync a instance
}

func NewService(p ServiceParam) (ServiceResult, error) {
	var err error
	s := &svc{
		JobService: service.NewJobService("JumpService"),

		conf:       p.Conf.Services.Jump,
		logger:     p.Logger.WithName("JumpService"),
		dbp:        p.DBProvider,
		eventbus:   p.EventBus,
		coresvcCli: p.CoreSvcCli,
		userSvc:    p.UserService,
		apprSvc:    p.Approval,
		privSvc:    p.Privilege,
		itkSvc:     p.InternalToken,
		onCallCli:  p.OnCallCli,
		syncInsCh:  make(chan uint64, 100),
	}

	jumpCluster, err := k8sutil.NewUnionClientInCluster(func(c *rest.Config) {
		c.QPS = 50
		c.Burst = 100
	})
	if err != nil {
		return ServiceResult{}, fmt.Errorf("init jump cluster client failed: %w", err)
	}

	s.jmpRepo = repo.NewJumpRepo(s.dbp)

	s.tailscale = &tailscale.Client{
		Tailnet: s.conf.Tailscale.TailscaleTailnetName,
		HTTP: tailscale.OAuthConfig{
			ClientID:     s.conf.Tailscale.TailscaleClientID,
			ClientSecret: s.conf.Tailscale.TailscaleClientSecret,
			Scopes:       []string{"all:write"},
		}.HTTPClient(),
	}

	jumpPodManagerParam := jumppod.ManagerParam{
		Conf:         s.conf,
		Logger:       s.logger,
		CoreSvc:      p.CoreSvcCli,
		MgmtHTTP:     p.MgmtHTTP,
		InfraClient:  p.InfraClient,
		SecretSource: p.SecretSource,
		JumpCluster:  jumpCluster,
		Tailscale:    s.tailscale,
	}
	s.jumpPodManager, err = jumppod.NewManager(jumpPodManagerParam)
	if err != nil {
		return ServiceResult{}, err
	}

	s.jumpserverCli, err = jumpservercli.NewJumpServerCli(jumpservercli.Config{
		Scheme:        "https",
		Addr:          s.conf.Console.JumpServerAddr,
		AccessKey:     s.conf.Console.JumpServerAccessKeyID,
		SecretKey:     s.conf.Console.JumpServerAccessKeySecret,
		BaseAssetName: s.conf.Console.JumpServerBaseAssetName,
	})
	if err != nil {
		return ServiceResult{}, fmt.Errorf("init jumpserver client failed: %w", err)
	}

	s.AddJob(
		service.NewConstantCronJob("WatchAuthorization", *s.conf.WatchJumpAuthorizationInterval, s.WatchAuthorizations),
		service.NewConstantCronJob("WatchAuthorizationSlowly", *s.conf.WatchJumpAuthorizationSlowlyInterval, s.WatchAuthorizationsSlowly),
		service.NewEventJob("SyncAuthorization", s.syncInsCh, s.SyncAuthorization).
			With(service.WithConcurrency(10)).
			Use(service.DedupeMiddleware(func(k uint64) uint64 { return k })),
		service.NewConstantCronJob("SyncJumpPods", *s.conf.SyncJumpPodInterval, s.SyncJumpPods),
	)
	if s.conf.GrantSEEPrivilegeCron != "" {
		job, err := service.NewCronJob("GrantPrivilegeForSEE", s.conf.GrantSEEPrivilegeCron, s.GrantPrivilegeForSEE)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("invalid cron spec: %w", err)
		}
		s.AddJob(job)
	}
	if s.onCallCli != nil {
		s.AddJob(
			service.NewConstantCronJob("SyncOncallerPrivileges", *s.conf.SyncOncallerInterval, s.SyncOncallerPrivileges),
		)
	}

	s.SetHook(service.Hook{
		BeforeRun: func(ctx context.Context) error {
			err := s.jumpPodManager.Initialize(ctx)
			if err != nil {
				return fmt.Errorf("init jump pod manager failed: %w", err)
			}
			return nil
		},
	})

	// subscribe
	s.eventbus.ApprovalEvent.Subscribe(eventbusGroupKey, s.HandleApprovalEvent)

	return ServiceResult{API: s, Service: s}, nil
}

func (s *svc) InitUser(ctx context.Context, email string) error {
	// check if user is existing in our system
	_, err := s.userSvc.GetUser(ctx, &userv1.GetUserRequest{Email: email})
	if err != nil {
		return fmt.Errorf("get user %s failed: %w", email, err)
	}

	user, err := s.jumpserverCli.GetUser(ctx, email)
	if err != nil {
		return err
	}
	if user != nil {
		return fmt.Errorf("user %s is existing", email)
	}

	param := jumpservercli.InitUserParam{
		Username: email,
		Email:    email,
	}
	_, _, err = s.jumpserverCli.InitUser(ctx, param)
	return err
}

func (s *svc) GetUser(ctx context.Context, email string) (*string, error) {
	user, err := s.jumpserverCli.GetUser(ctx, email)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, nil
	}

	return &user.ID, nil
}

func (s *svc) SyncJumpPods(ctx context.Context) error {
	pods, err := s.jumpPodManager.ListJumpPods(ctx)
	if err != nil {
		return fmt.Errorf("list jumppod failed: %w", err)
	}

	for _, pod := range pods {
		owner := authactor.EmailFromJumpPodName(pod.Name())

		err := s.syncJumpPod(ctx, pod, owner)
		if err != nil {
			log.FromContext(ctx).Error(err, "sync a jump pod failed", "pod_id", pod.ID(), "pod_name", pod.Name())
		}
	}

	return nil
}

func (s *svc) GrantPrivilegeForSEE(ctx context.Context) error {
	now := time.Now()
	expireAt := now.Add(7 * 24 * time.Hour)
	// use source to identify the auto granted privileges
	source := fmt.Sprintf("%s-%s-%s", privent.PrivilegeSourceSEEAuto,
		now.Format("20060102"), expireAt.Format("20060102"))

	param := privapi.RequestPrivilegeParam{
		Source:    source,
		Reason:    "Grant these SEE colleagues privileges for Jump Service automatically.",
		ExpiredAt: expireAt.Unix(),
		Applicant: consts.InternalVirtualEmail,
		Approval: &privapi.RequestApprovalPrivilegeParam{
			Scopes: []privent.ApprovalPrivilegeScope{
				privent.PrivilegeApprovalScopeJumpService,
			},
		},
	}

	usersResp, err := s.userSvc.ListUsers(ctx, &userv1.ListUsersRequest{
		GroupIs: pointer.Pointer(seeUserGroup),
		Page:    1,
		PerPage: 100, // 100 is enough
	})
	if err != nil {
		return fmt.Errorf("list users in group '%s' failed: %w", seeUserGroup, err)
	}
	for _, u := range usersResp.Users {
		param.BindingUsers = append(param.BindingUsers, u.Email)
	}

	_, err = s.privSvc.RequestPrivilege(ctx, param)
	if err != nil {
		return fmt.Errorf("request privilege for SEE users failed: %w", err)
	}

	return nil
}

func (s *svc) syncJumpPod(ctx context.Context, pod jumppod.JumpPod, owner string) error {
	now := time.Now()
	logger := log.FromContextOrDef(ctx, s.logger, "pod_id", pod.ID(), "pod_name", pod.Name(), "applicant", owner)

	// find inuse auths for the applicant
	numInUseAuths, err := s.jmpRepo.CountAuthorization(ctx,
		repo.AuthorizationOpts.ApplicantIs(owner),
		repo.AuthorizationOpts.PhaseNotIn(jmpent.AuthorizationPhaseFinishedValues),
	)
	if err != nil {
		return fmt.Errorf("find in-use auths for applicant %s failed: %w", owner, err)
	}
	if numInUseAuths > 0 {
		// refresh the access token
		genAKParam := &types.GenerateAccessTokenParam{
			GenerateTokenParam: types.GenerateTokenParam{
				ExpireAt: now.Add(*s.conf.SyncJumpPodInterval).Add(time.Hour), // TODO: use db to record the expire time
			},
		}
		accessToken, err := pod.GenerateAccessToken(ctx, genAKParam)
		if err != nil {
			return fmt.Errorf("pod failed to generate access token: %w", err)
		}

		err = s.jumpserverCli.UpdateAccountToken(ctx, owner, accessToken.Data)
		if err != nil {
			return fmt.Errorf("refrest user '%s' access token failed: %w", owner, err)
		}

		logger.Info("refreshed access token")
		return nil
	}

	// find the latest auth for the applicant
	latestAuths, err := s.jmpRepo.ListAuthorizations(ctx,
		repo.AuthorizationOpts.ApplicantIs(owner),
		repo.AuthorizationOpts.WithPager(1, 1),
	)
	if err != nil {
		return fmt.Errorf("find latest auth for applicant %s failed: %w", owner, err)
	}
	if len(latestAuths) == 0 {
		logger.Info("no in-use auths found, skip sync jump pod")
		return nil
	}
	latestAuth := latestAuths[0]

	// check if need to recycle the jump pod
	if latestAuth.ClosedAt != nil && *latestAuth.ClosedAt > now.Unix() {
		logger.Info("the latest auth is still in use, skip recycle jump pod")
		return nil
	}
	if *latestAuth.ClosedAt > now.Add(-*s.conf.JumpPodRecycleThresold).Unix() {
		logger.Info("the latest auth is closed recently, skip recycle jump pod")
		return nil
	}

	err = s.jumpPodManager.TearDownJumpPod(ctx, pod.Name())
	if err != nil {
		return fmt.Errorf("destroy jumppod failed for applicant %s: %w", owner, err)
	}

	logger.Info("recycled the jump pod")
	return nil
}

func buildEvent(ctx context.Context, objs *authactor.Context, typ event.EventType) *event.JumpEvent {
	ev := &event.JumpEvent{
		BaseEvent: event.BaseEvent{
			ID:            objs.JumpAuthorization.ID,
			OperatorEmail: consts.GetOperatorFromCtx(ctx, consts.InternalVirtualEmail),
			OperateAt:     pointer.Pointer(time.Now()),
			EventType:     typ,
		},
		Details: struct{}{},
	}
	if objs.EventDetail != nil {
		ev.Details = objs.EventDetail
	}

	return ev
}
