package gateway

import (
	"encoding/base64"
	"errors"
	"fmt"
	"net/http/httputil"
	"net/url"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

	"github.com/tidbcloud/devops/api"
	jmpapi "github.com/tidbcloud/devops/api/jump"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/jumppodagentcli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	jmpgwmetrics "github.com/tidbcloud/devops/pkg/infra/metrics/jump-gateway"
	httputilx "github.com/tidbcloud/devops/pkg/infra/utils/http"
)

const (
	CtxKeyGatewayClaims = "jump_gateway_claims"
)

type JumpGatewayController struct {
	logger        logr.Logger
	clientFactory func(podNamespace, podName string) jumppodagentcli.Client
}

func NewJumpGatewayController(logger logr.Logger) *JumpGatewayController {
	return &JumpGatewayController{
		logger:        logger.WithName("JumpGatewayController"),
		clientFactory: jumppodagentcli.NewClient,
	}
}

func (c *JumpGatewayController) RegisterAPI(r gin.IRouter) {
	group := r.Group("/k8s/clusters/:encoded_cred_id")
	group.Any("", c.Proxy)
	group.Any("/*proxy_path", c.Proxy)
}

func (c *JumpGatewayController) Proxy(ctx *gin.Context) {
	// parse credential ID
	encodedCredID := ctx.Param("encoded_cred_id")
	credIDBytes, err := base64.RawURLEncoding.DecodeString(encodedCredID)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid encoded credential ID: %w", err), api.ErrCodeInvalidArgument)
		return
	}
	credID := jmpent.CredentialID(string(credIDBytes))

	// load claims
	v, ok := ctx.Get(CtxKeyGatewayClaims)
	if !ok || v == nil {
		ctrl.FailWithCode(ctx, errors.New("missing gateway claims in context"), api.ErrCodeUnauthenticated)
		return
	}
	claims := v.(*jmpent.GatewayTokenClaims)

	// validate required fields in claims
	if claims.JumpPodNamespace == "" || claims.JumpPodName == "" {
		ctrl.FailWithCode(ctx, errors.New("invalid gateway claims: missing jump pod info"), api.ErrCodeInvalidArgument)
		return
	}
	if !jmpent.IsGatewayCredentialID(credID) {
		ctrl.FailWithCode(ctx, errors.New("invalid credential ID"), api.ErrCodeInvalidArgument)
		return
	}
	targetType, targetID := jmpent.ParseCredentialID(credID)
	isAllowed := false
	for _, dest := range claims.Destinations {
		idest := dest.Extract()
		if idest.Type() == targetType && idest.ID() == targetID {
			isAllowed = true
			break
		}
	}
	if !isAllowed {
		ctrl.FailWithCode(ctx, errors.New("target is not allowed"), api.ErrCodePermissionDenied)
		return
	}

	client := c.clientFactory(claims.JumpPodNamespace, claims.JumpPodName)
	kubeconfigData, err := client.GetKubernetesCredential(ctx, jmpapi.GetKubernetesCredentialRequest{
		CredIDs: []jmpent.CredentialID{credID},
	})
	if err != nil {
		ctrl.Fail(ctx, fmt.Errorf("fetch credential failed: %w", err))
		return
	}
	kubeconfig, err := clientcmd.Load([]byte(kubeconfigData))
	if err != nil {
		ctrl.Fail(ctx, fmt.Errorf("load kubeconfig failed: %w", err))
		return
	}

	// build reverse proxy options and proxy the request
	ropts, popts, err := c.BuildProxyOptions(credID, kubeconfig, ctx.Param("proxy_path"))
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("build proxy failed: %w", err), api.ErrCodeUnavailable)
		return
	}

	proxyStart := time.Now()

	httputilx.Proxy(ctx, ropts, popts)

	proxyLatency := time.Since(proxyStart).Seconds()
	targetTypeLabel := string(targetType)
	if targetTypeLabel == "" {
		targetTypeLabel = "unknown"
	}
	statusCode := strconv.Itoa(ctx.Writer.Status())

	jmpgwmetrics.Proxy.RequestsTotal.WithLabelValues(
		ctx.Request.Method,
		statusCode,
		targetTypeLabel,
	).Inc()
	jmpgwmetrics.Proxy.RequestDuration.WithLabelValues(
		ctx.Request.Method,
		statusCode,
		targetTypeLabel,
	).Observe(proxyLatency)

	log.WithTraceID(ctx, log.AccessLogger).Info(ctx.Param("proxy_path"),
		"latency", proxyLatency,
		"method", ctx.Request.Method,
		"status_code", ctx.Writer.Status(),
		"query", ctx.Request.URL.RawQuery,
		"proxy_path", ctx.Param("proxy_path"),
		"credential_id", credID,
		"user_email", claims.UserEmail,
		"auth_id", claims.AuthID,
		"jump_pod_namespace", claims.JumpPodNamespace,
		"jump_pod_name", claims.JumpPodName,
	)
}

func (c *JumpGatewayController) BuildProxyOptions(credID jmpent.CredentialID, kubeconfig *clientcmdapi.Config, proxyPath string) ([]httputilx.RequestOpts, []httputilx.ProxyOpts, error) {
	restConfig, err := clientcmd.NewDefaultClientConfig(*kubeconfig, &clientcmd.ConfigOverrides{CurrentContext: string(credID)}).ClientConfig()
	if err != nil {
		return nil, nil, fmt.Errorf("build rest config failed: %w", err)
	}

	target, err := url.Parse(restConfig.Host)
	if err != nil {
		return nil, nil, fmt.Errorf("invalid host in rest config: %w", err)
	}

	transport, err := rest.TransportFor(restConfig)
	if err != nil {
		return nil, nil, fmt.Errorf("build transport failed: %w", err)
	}

	ropts := []httputilx.RequestOpts{
		func(req *httputil.ProxyRequest) {
			req.Out.Header.Del("Authorization")
			req.Out.URL.Path = proxyPath
			req.Out.URL.RawPath = ""

			req.SetURL(target)
			req.Out.Host = target.Host
		},
	}

	popts := []httputilx.ProxyOpts{
		func(proxy *httputil.ReverseProxy) {
			proxy.Transport = transport
		},
		httputilx.WithErrorLogger(c.logger),
	}

	return ropts, popts, nil
}
