// Package mesh helps us mesh the different services together
package mesh

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/internal-portal/core-portal/auth"
	"github.com/tidbcloud/internal-portal/core-portal/central/config"
	"github.com/tidbcloud/internal-portal/core-portal/central/model/cluster"
	debugAuth "github.com/tidbcloud/internal-portal/core-portal/central/pkg/auth/debug"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
)

const (
	userIDHeader = "X-USER-ID"
)

// AuthType of Dashboard
type AuthType int

// HandleRouteStripGin handles the route and strips it as a prefix. This is useful for proxying.
func HandleRouteStripGin(eng *gin.Engine, prefix string, handler http.Handler) {
	eng.Any(path.Join(prefix, "*any"), gin.WrapH(http.StripPrefix(prefix, handler)))
}

func pingcapEmail(email string) bool {
	parts := strings.Split(email, "@")
	return len(parts) > 1 && parts[1] == "pingcap.com"
}

// ProxyShootEndpoint uses the callback to get the shoot proxy service address
func ProxyShootEndpoint(clusterModel cluster.Model) func(req *http.Request) (*tls.Config, *url.URL, error) {
	return func(req *http.Request) (*tls.Config, *url.URL, error) {
		orgID, clusterID, err := auth.ParseOrgIDAndClusterID(req.URL.Path)
		if err != nil {
			return nil, nil, errcode.NewInvalidInputErr(err)
		}

		ctx := req.Context()
		session, debugErr := debugAuth.GetDebugSessionUser(ctx)
		if session != nil {
			log.Debug("debug session", zap.String("path", req.URL.Path))
			if debugErr != nil || !pingcapEmail(session.Email) {
				return nil, nil, errcode.NewNotAuthenticatedErr(err)
			}
		} else if auth0.IsAuth0SessionAuth(ctx) {
			validatorID, err := auth0.GetAuthValidatorIDFromContext(ctx)
			if err != nil {
				return nil, nil, errcode.NewInternalErr(err)
			}
			user, err := auth.GetUserFromContext(ctx)
			if err != nil {
				return nil, nil, errcode.NewInternalErr(err)
			}
			// HACK: Currently userID is necessary for some kind of proxy requests, e.g. web shell.
			// So that we record this specifically for Auth0 authentications. This field will not be
			// correct in other cases, e.g. in Debug Session, or in Management Portal.
			req.Header.Add(userIDHeader, fmt.Sprintf("%d", user.ID))

			if auth.IsK8sDashboardPath(req.URL.Path) {
				validatorID = auth0.AuthValidatorIDManagement
			}

			switch validatorID {
			case auth0.AuthValidatorIDCentral:
				// Do nothing
			case auth0.AuthValidatorIDManagement:
				log.Debug("management id", zap.String("path", req.URL.Path))
				if user.Tenant.ID != entity.PingCAPTenantID {
					return nil, nil, errcode.NewForbiddenErr(errors.New("forbidden"))
				}
				// send portal token to proxy
				rawToken := ctx.Value(auth0.RawTokenKey).(string)
				req.Header.Set(config.PortalAuthorizationHeader, fmt.Sprintf("Bearer %s", rawToken))
			default:
				return nil, nil, errcode.NewInternalErr(errors.New("unexpected validator id"))
			}
		} else {
			// This must be an internal error, since unauthorized requests have been rejected
			return nil, nil, errcode.NewInternalErr(fmt.Errorf("unknown authenticator"))
		}

		var baseURL string
		if auth.IsMetricsPath(req.URL.Path) {
			baseURL, err = cluster.FindClusterMetricsBaseURL(ctx, clusterModel.InfraAPI(), clusterID, orgID)
		} else if auth.IsDashboardPath(req.URL.Path) {
			baseURL, err = cluster.FindClusterDashboardBaseURL(ctx, clusterModel.Encryptor(), clusterModel.InfraAPI(), clusterID, orgID)
		} else {
			baseURL, err = cluster.FindClusterProxyBaseURL(ctx, clusterModel.InfraAPI(), clusterID, orgID)
		}

		if err != nil {
			return nil, nil, errcode.NewInternalErr(errors.Annotatef(err, "could not get proxy target endpoint"))
		}
		return getByBaseURL(req, baseURL)
	}
}

func getByBaseURL(req *http.Request, baseURL string) (*tls.Config, *url.URL, error) {
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		baseURL = "https://" + baseURL
	}

	u, err := url.Parse(baseURL)
	if err != nil {
		return nil, nil, errors.Annotatef(err, "failed to parse base url: %s", baseURL)
	}

	paths := strings.Split(req.URL.Path, "/")
	if len(paths) < 4 {
		return nil, nil, errors.Errorf("invalid proxy url path %q", req.URL.Path)
	}

	if auth.IsMetricsPath(req.URL.Path) {
		req.URL.Path = strings.Join(append([]string{u.Path}, paths[5:]...), "/")
	} else {
		req.URL.Path = strings.Join(append([]string{u.Path}, paths[4:]...), "/")
	}
	log.Debug("connecting to endpoint", zap.String("endpoint", u.Host), zap.String("path", req.URL.Path))

	target := u.Scheme + "://" + u.Host + "?" + u.Query().Encode()
	parsedURL, err := url.Parse(target)
	return nil, parsedURL, err
}
