package jumppod

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

	authenticationv1 "k8s.io/api/authentication/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sapitypes "k8s.io/apimachinery/pkg/types"

	jmpapi "github.com/tidbcloud/devops/api/jump"
	jpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/clients/jumppodagentcli"
	k8sutil "github.com/tidbcloud/devops/pkg/infra/utils/k8s"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/jumper"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/types"
)

const (
	//nolint:gosec
	jumpCredRefreshTimeAnnotationKey = "jump-service.devops.cloud.pingcap.com/cred-refresh-time"
)

var (
	_ jumper.JumpPodForJumper = &jumpPodForJumper{}
	_ JumpPod                 = &jumpPod{}
)

// jumpPodForJumper implements jumper.JumpPodForJumper.
type jumpPodForJumper struct {
	*jumpPod
}

func (jp *jumpPodForJumper) JumpContext() *types.JumpContext {
	return jp.jctx
}

func (jp *jumpPodForJumper) StoreKubernetesCred(ctx context.Context, credID jpent.CredentialID, value []byte) error {
	// store the cred to its secret.
	patchSecretData := fmt.Sprintf(`{"data": {"%s": "%s"}}`, credID, base64.StdEncoding.EncodeToString(value))
	obj := jp.jctx.Namer.JumpPodK8sCredSecret()
	_, err := jp.cluster.Kube.CoreV1().Secrets(obj.Namespace).
		Patch(ctx, obj.Name, k8sapitypes.StrategicMergePatchType, []byte(patchSecretData), metav1.PatchOptions{})
	if err != nil {
		return fmt.Errorf("update secret failed: %w", err)
	}

	// annotate the jump pod to make kubelet refresh the cred secret into the pod.
	patchAnnoData := fmt.Sprintf(`{"metadata": {"annotations": {"%s": "%s"}}}`,
		jumpCredRefreshTimeAnnotationKey, time.Now().Format(time.RFC3339))
	jp.pod, err = jp.cluster.Kube.CoreV1().Pods(jp.pod.Namespace).
		Patch(ctx, jp.pod.Name, k8sapitypes.StrategicMergePatchType, []byte(patchAnnoData), metav1.PatchOptions{})
	if err != nil {
		return fmt.Errorf("patch pod annotation failed: %w", err)
	}

	return nil
}

func (jp *jumpPodForJumper) RemoveKubernetesCred(ctx context.Context, credID jpent.CredentialID) error {
	patchSecretData := fmt.Sprintf(`{"data": {"%s": null }}`, credID)
	obj := jp.jctx.Namer.JumpPodK8sCredSecret()
	_, err := jp.cluster.Kube.CoreV1().Secrets(obj.Namespace).
		Patch(ctx, obj.Name, k8sapitypes.StrategicMergePatchType, []byte(patchSecretData), metav1.PatchOptions{})
	if err != nil {
		return fmt.Errorf("update secret failed: %w", err)
	}

	return nil
}

func (jp *jumpPodForJumper) TailscaleUp(ctx context.Context, opts jmpapi.TailscaleUpRequest) error {
	return jp.agentClient().TailscaleUp(ctx, opts)
}

func (jp *jumpPodForJumper) TailscaleDown(ctx context.Context) error {
	return jp.agentClient().TailscaleDown(ctx)
}

func (jp *jumpPodForJumper) TailscaleStatus(ctx context.Context) (*jmpapi.TailscaleStatus, error) {
	return jp.agentClient().TailscaleStatus(ctx)
}

func (jp *jumpPodForJumper) agentClient() jumppodagentcli.Client {
	return jumppodagentcli.NewClient(jp.pod.Namespace, jp.pod.Name)
}

// JumpPod implements JumpPod.
type jumpPod struct {
	jctx *types.JumpContext
	pod  *corev1.Pod

	cluster *k8sutil.UnionClient
	jumper  *jumper.Router
}

func (j *jumpPod) Pod() *corev1.Pod {
	return j.pod
}

func (j *jumpPod) Name() string {
	return j.jctx.Name
}

func (j *jumpPod) ID() types.ID {
	return j.jctx.ID
}

func (j *jumpPod) IsReady(ctx context.Context) error {
	if j.pod.Status.Phase == corev1.PodRunning {
		return nil
	}

	return fmt.Errorf("jump pod is %s not running", j.pod.Status.Phase)
}

func (j *jumpPod) GenerateAccessToken(ctx context.Context, param *types.GenerateAccessTokenParam) (*types.Token, error) {
	err := param.Normalize()
	if err != nil {
		return nil, fmt.Errorf("normalize param: %w", err)
	}

	accessSAKey := j.jctx.Namer.AccessServiceAccount()
	expireDur := time.Until(param.ExpireAt)
	request := &authenticationv1.TokenRequest{
		Spec: authenticationv1.TokenRequestSpec{
			Audiences:         []string{},
			ExpirationSeconds: pointer.Pointer(int64(expireDur / time.Second)),
		},
	}
	resp, err := j.cluster.Kube.CoreV1().ServiceAccounts(accessSAKey.Namespace).
		CreateToken(ctx, accessSAKey.Name, request, metav1.CreateOptions{})
	if err != nil {
		return nil, fmt.Errorf("create token failed: %w", err)
	}

	token := &types.Token{
		Data:      resp.Status.Token,
		ExpiredAt: resp.Status.ExpirationTimestamp.Time, // may be shorter than requested
	}

	return token, nil
}

func (j *jumpPod) PrepareJump(ctx context.Context, dest jpent.IDestination, param *types.PrepareJumpParam) (*types.JumpParam, error) {
	return j.jumper.PrepareJump(ctx, j.jumpPodForJumper(), dest, param)
}

func (j *jumpPod) FinalizeJump(ctx context.Context, dest jpent.IDestination, attr jpent.JumpAttributes) error {
	return j.jumper.FinalizeJump(ctx, j.jumpPodForJumper(), dest, attr)
}

func (j *jumpPod) RefreshJump(ctx context.Context, dest jpent.IDestination, attr jpent.JumpAttributes, param *types.RefreshJumpParam) (*types.JumpParam, error) {
	return j.jumper.RefreshJump(ctx, j.jumpPodForJumper(), dest, attr, param)
}

func (j *jumpPod) jumpPodForJumper() *jumpPodForJumper {
	return &jumpPodForJumper{
		jumpPod: j,
	}
}
