package jump

import (
	"context"
	"encoding/base64"
	"fmt"
	"slices"
	"strings"
	"time"

	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/entity/event"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	k8sutil "github.com/tidbcloud/devops/pkg/infra/utils/k8s"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	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/repo"
	ngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

var (
	phaseToSync = []jmpent.AuthorizationPhase{
		jmpent.AuthorizationPhaseInitialized,
		jmpent.AuthorizationPhaseApproving,
		jmpent.AuthorizationPhaseCreating,
		jmpent.AuthorizationPhaseCreated,
	}
)

func (s *svc) ListAuthorizations(ctx context.Context, param jmpsvcapi.ListAuthorizationParam) ([]*jmpent.Authorization, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, errs.APIErrorNew(api.ErrCodeInvalidArgument, err)
	}

	opts := []repoopt.Option{}

	if !*param.ShowUnaccessible {
		opts = append(opts, repo.AuthorizationOpts.PhaseNotIn(jmpent.AuthorizationPhaseFinishedValues))
	}
	if param.Applicant != nil {
		opts = append(opts, repo.AuthorizationOpts.ApplicantIs(*param.Applicant))
	}

	// jump filter
	jumpOpts := []repoopt.Option{}
	if param.DestinationTypeIs != nil {
		jumpOpts = append(jumpOpts, repo.JumpOpts.DestinationTypeIs(*param.DestinationTypeIs))
	}
	if param.DestinationIDLike != nil {
		jumpOpts = append(jumpOpts, repo.JumpOpts.DestinationIDLike(*param.DestinationIDLike))
	}
	if len(jumpOpts) > 0 {
		opts = append(opts, repo.AuthorizationOpts.FilterJump(repoopt.Aggregate(jumpOpts...)))
	}

	total, err := s.jmpRepo.CountAuthorization(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	if param.Page > 0 {
		opts = append(opts, repo.AuthorizationOpts.WithPager(param.Page, param.PerPage))
	}
	if param.Phase != nil {
		opts = append(opts, repo.AuthorizationOpts.PhaseIs(*param.Phase))
	}

	auths, err := s.jmpRepo.ListAuthorizations(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	return auths, total, nil
}

func (s *svc) GetAuthorization(ctx context.Context, id uint64) (*jmpent.Authorization, error) {
	auth, err := s.jmpRepo.GetAuthorization(ctx, id)
	if err != nil {
		return nil, err
	}

	return auth, nil
}

func (s *svc) CreateAuthorization(ctx context.Context, param jmpsvcapi.CreateAuthorizationParam) (*uint64, error) {
	var id *uint64

	err := s.validateParam(ctx, param)
	if err != nil {
		return id, err
	}

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := authactor.NewAuthorizationActor(ctx, s.insComps(ctx), nil)
		err := actor.Create(param)
		if err != nil {
			return err
		}
		newIns := actor.GetContext().JumpAuthorization

		ev := buildEvent(ctx, actor.GetContext(), event.JumpEventTypeCreate)
		err = s.eventbus.JumpEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %w", err)
		}

		id = &newIns.ID
		return nil
	})

	if id != nil {
		s.syncInsCh <- *id
	}

	return id, err
}

func (s *svc) CancelAuthorizaiton(ctx context.Context, id uint64, param jmpsvcapi.CancelAuthorizaitonParam) error {
	ins, err := s.jmpRepo.GetAuthorization(ctx, id)
	if err != nil {
		return err
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := authactor.NewAuthorizationActor(ctx, s.insComps(ctx), ins)
		err := actor.Cancel(param)
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.JumpEventTypeCancel)
		err = s.eventbus.JumpEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %w", err)
		}

		return nil
	})

}

func (s *svc) GetConnection(ctx context.Context, id uint64, applicant string, method jmpent.ConnectionMethod) (*jmpent.Connection, error) {
	ins, err := s.jmpRepo.GetAuthorization(ctx, id)
	if err != nil {
		return nil, err
	}
	if applicant != ins.Applicant {
		return nil, fmt.Errorf("applicant not match")
	}
	if ins.Phase != jmpent.AuthorizationPhaseCreated {
		return nil, fmt.Errorf("authorization phase %s is not ready for connection", ins.Phase)
	}
	if ins.ExpireAt == nil {
		return nil, fmt.Errorf("authorization expire_at is required for connection")
	}
	expireAt := time.Unix(*ins.ExpireAt, 0)
	if !expireAt.After(time.Now()) {
		return nil, fmt.Errorf("authorization has expired")
	}

	pod, err := s.jumpPodManager.GetJumpPod(ctx, authactor.EmailToJumpPodName(ins.Applicant))
	if err != nil {
		return nil, fmt.Errorf("jumppod not ready: %w", err)
	}

	conn := &jmpent.Connection{}
	switch method {
	case jmpent.ConnectionMethodWebTerminal:
		connToken, err := s.jumpserverCli.CreateConnectionToken(ctx, ins.Applicant)
		if err != nil {
			return nil, fmt.Errorf("generate connection token failed: %w", err)
		}

		conn.WebTerminal = &jmpent.ConnectionWebTerminal{
			URL: fmt.Sprintf("%s/koko/connect/?disableautohash=&token=%s&pod=%s&namespace=%s&container=jump-pod",
				s.conf.Console.JumpServerAccessUrl, connToken.ID, pod.Pod().Name, pod.Pod().Namespace),
		}
	case jmpent.ConnectionMethodSSH:
		conn.SSH = &jmpent.ConnectionSSH{
			Address:          s.conf.Console.JumpServerSSHAddr,
			BaseAssetName:    s.conf.Console.JumpServerBaseAssetName,
			JumpPodNamespace: pod.Pod().Namespace,
			JumpPodName:      pod.Pod().Name,
		}
	case jmpent.ConnectionMethodGateway:
		if !ins.EnableGatewayAccess {
			return nil, fmt.Errorf("gateway access is not enabled")
		}

		baseURL := strings.TrimRight(s.conf.Gateway.AccessURL, "/")

		serverURLs := map[string]string{}
		destinations := []*jmpent.DestinationUnion{}
		gatewayRoutes := []jmpent.GatewayRoute{}
		for _, jump := range ins.Jumps {
			if jump == nil || jump.Method != jmpent.JumpMethodK8s {
				continue
			}
			dest := jump.Destination.Extract()

			credID := jmpent.FormatGatewayCredentialID(dest)
			serverURL := fmt.Sprintf("%s/k8s/clusters/%s", baseURL, base64.RawURLEncoding.EncodeToString([]byte(credID)))
			destinations = append(destinations, jump.Destination)
			gatewayRoutes = append(gatewayRoutes, jmpent.GatewayRoute{
				CredID:    credID,
				ServerURL: serverURL,
			})
			serverURLs[string(credID)] = serverURL
		}
		if len(gatewayRoutes) == 0 {
			return nil, fmt.Errorf("no kubernetes destinations available for gateway connection")
		}

		claims := &jmpent.GatewayTokenClaims{
			UserEmail:        applicant,
			AuthID:           ins.ID,
			Privilege:        jmpent.JumpPrivilegeReadOnly,
			Destinations:     destinations,
			JumpPodName:      pod.Pod().Name,
			JumpPodNamespace: pod.Pod().Namespace,
		}
		claimsMap, err := pointer.StructToMap(claims)
		if err != nil {
			return nil, fmt.Errorf("convert claims to map failed: %w", err)
		}

		token, err := s.itkSvc.IssueJWTToken(ctx, itkapi.IssueJWTTokenParam{
			Subject:      applicant,
			Audience:     s.conf.Gateway.Audience,
			ExpiresAt:    &expireAt,
			CustomClaims: claimsMap,
		})
		if err != nil {
			return nil, fmt.Errorf("generate gateway connection token failed: %w", err)
		}

		kubeconfig, err := buildGatewayKubeconfig(token.Token, applicant, gatewayRoutes)
		if err != nil {
			return nil, fmt.Errorf("build gateway kubeconfig failed: %w", err)
		}

		conn.Gateway = &jmpent.ConnectionGateway{
			ServerURLs:      serverURLs,
			ConnectionToken: token.Token,
			Kubeconfig:      kubeconfig,
		}
	default:
		return nil, fmt.Errorf("unsupported connection method: %s", method)
	}

	return conn, nil
}

func (s *svc) ListAvailableDestinations(ctx context.Context, typ jmpent.DestinationType) ([]string, error) {
	return s.jumpPodManager.SupportedDestinations(typ), nil
}

func (s *svc) WatchAuthorizations(ctx context.Context) error {
	logger := log.FromContextOrDef(ctx, s.Logger(ctx))

	phaseToSync = []jmpent.AuthorizationPhase{
		jmpent.AuthorizationPhaseInitialized,
		jmpent.AuthorizationPhaseApproving,
		jmpent.AuthorizationPhaseCreating,
	}
	auths, err := s.jmpRepo.ListAuthorizations(ctx, repo.AuthorizationOpts.PhaseIn(phaseToSync))
	if err != nil {
		return err
	}

	if len(auths) != 0 {
		logger.Info("syncing authorizations ...",
			"auth_ids", container.TransformSlice(auths, func(i *jmpent.Authorization) uint64 { return i.ID }),
		)
	}

	for _, auth := range auths {
		s.syncInsCh <- auth.ID
	}

	return nil
}

func (s *svc) WatchAuthorizationsSlowly(ctx context.Context) error {
	logger := log.FromContext(ctx)

	phaseToSync = []jmpent.AuthorizationPhase{
		jmpent.AuthorizationPhaseCreated,
	}
	auths, err := s.jmpRepo.ListAuthorizations(ctx, repo.AuthorizationOpts.PhaseIn(phaseToSync))
	if err != nil {
		return err
	}

	if len(auths) != 0 {
		logger.Info("syncing authorizations slowly ...",
			"auth_ids", container.TransformSlice(auths, func(i *jmpent.Authorization) uint64 { return i.ID }),
		)
	}

	for _, auth := range auths {
		s.syncInsCh <- auth.ID
	}

	return nil
}

func (s *svc) SyncAuthorization(ctx context.Context, id uint64) error {
	auth, err := s.jmpRepo.GetAuthorization(ctx, id)
	if err != nil {
		return err
	}

	ctx = log.WrapContext(ctx, "auth_id", auth.ID, "auth_phase", auth.Phase)
	actor := authactor.NewAuthorizationActor(ctx, s.insComps(ctx), auth)

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		var err error
		var evType event.EventType

		switch auth.Phase {
		case jmpent.AuthorizationPhaseInitialized:
			err = actor.SendApproval()
			evType = event.JumpEventTypeCreateApproval

		case jmpent.AuthorizationPhaseApproving:
			err = actor.ContinueIfApproved()
			evType = event.JumpEventTypeFinishApproval

		case jmpent.AuthorizationPhaseCreating:
			err = actor.ContinueIfPodReady()
			evType = event.JumpEventTypePrepare

		case jmpent.AuthorizationPhaseCreated:
			err = actor.ExpireOrRefresh()
			evType = event.JumpEventTypeExpire
		}

		if err == nil && evType != "" {
			ev := buildEvent(ctx, actor.GetContext(), evType)
			err = s.eventbus.JumpEvent.Publish(ev)
			if err != nil {
				return fmt.Errorf("publish event failed: %w", err)
			}
		}

		return err
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *svc) insComps(ctx context.Context) authactor.Components {
	return authactor.Components{
		Logger:                log.FromContextOrDef(ctx, s.logger),
		Conf:                  s.conf,
		JumpAuthorizationRepo: s.jmpRepo,
		JumpPodManager:        s.jumpPodManager,
		JumpServerCli:         s.jumpserverCli,
		ApprovalService:       s.apprSvc,
		PrivilegeService:      s.privSvc,
		Tailscale:             s.tailscale,
	}
}

func (s *svc) validateParam(ctx context.Context, param jmpsvcapi.CreateAuthorizationParam) error {
	if !slices.Contains(jmpent.JumpExpirationValues, param.Expiration) {
		return fmt.Errorf("unsupported expiration type")
	}

	if len(param.Destinations) == 0 {
		return fmt.Errorf("destination cannot be empty")
	}

	// check each destination is real
	for _, dest := range param.Destinations {

		if !slices.Contains(jmpent.DestinationTypeValues, dest.Extract().Type()) {
			return fmt.Errorf("unsupported cluster deploy type")
		}

		switch dest.Extract().Type() {
		case jmpent.DestinationTypeControlPlane:
			if dest.ControlPlane == nil {
				return fmt.Errorf("empty control plane")
			}

			supportedDests, err := s.ListAvailableDestinations(ctx, jmpent.DestinationTypeControlPlane)
			if err != nil {
				return fmt.Errorf("failed to get control plane options: %w", err)
			}

			if !slices.Contains(supportedDests, dest.ControlPlane.Name) {
				return fmt.Errorf("unsupported control plane %s", dest.ControlPlane.Name)
			}

		case jmpent.DestinationTypeDedicated:
			if dest.DedicatedCluster == nil {
				return fmt.Errorf("empty dedicated cluster")
			}

			cluster, err := s.coresvcCli.GetCluster(ctx, dest.DedicatedCluster.ClusterID, false)
			if err != nil {
				return fmt.Errorf("fail to get dedicated cluster '%d' info: %w", dest.DedicatedCluster.ClusterID, err)
			}
			if cluster.IsDevTier {
				return fmt.Errorf("the cluster '%d' is not a dedicated cluster", dest.DedicatedCluster.ClusterID)
			}

		case jmpent.DestinationTypeServerless:
			if dest.ServerlessCluster == nil {
				return fmt.Errorf("empty serverless clusters")
			}

			supportedDests, err := s.ListAvailableDestinations(ctx, jmpent.DestinationTypeServerless)
			if err != nil {
				return fmt.Errorf("failed to get control plane options: %w", err)
			}

			if !slices.Contains(supportedDests, dest.ServerlessCluster.Name) {
				return fmt.Errorf("unsupported serverless cluster %s", dest.ServerlessCluster.Name)
			}

		case jmpent.DestinationTypePremium:
			if dest.PremiumCluster == nil {
				return fmt.Errorf("empty premium cluster")
			}

			cluster, err := s.coresvcCli.Mgmt.GetCluster(ctx, dest.PremiumCluster.ClusterID)
			if err != nil {
				return fmt.Errorf("fail to get premium cluster '%d' info: %w", dest.PremiumCluster.ClusterID, err)
			}
			if _, ok := cluster.ClusterInfo.(*mgmtv1.Cluster_NextGenCluster); !ok {
				return fmt.Errorf("the cluster '%d' is not a premium cluster", dest.PremiumCluster.ClusterID)
			}

		case jmpent.DestinationTypePremiumResourcePool:
			if dest.PremiumResourcePool == nil {
				return fmt.Errorf("empty premium resource pool")
			}

			_, err := s.coresvcCli.Mgmt.ResourcePool.GetResourcePool(ctx, &ngv1.GetResourcePoolRequest{
				ResourcePoolId: dest.PremiumResourcePool.ResourcePoolID,
			})
			if err != nil {
				return fmt.Errorf("fail to get premium resource pool '%d' info: %w", dest.PremiumResourcePool.ResourcePoolID, err)
			}
		}
	}

	return nil
}

func buildGatewayKubeconfig(token string, applicant string, routes []jmpent.GatewayRoute) (string, error) {
	builder := k8sutil.NewConfigBuilder().AddTokenAuth(applicant, token)

	currentContext := ""
	for _, route := range routes {
		clusterName := string(route.CredID)
		builder.
			AddCluster(clusterName, &clientcmdapi.Cluster{Server: route.ServerURL}).
			AddContext(string(route.CredID), clientcmdapi.Context{
				Cluster:  clusterName,
				AuthInfo: applicant,
			})
		if currentContext == "" {
			currentContext = string(route.CredID)
		}
	}

	cfg := builder.CurrentContext(currentContext).Build()
	data, err := clientcmd.Write(*cfg)
	if err != nil {
		return "", err
	}
	return string(data), nil
}
