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

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

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/core-portal/internal/central/config"
	"github.com/tidbcloud/core-portal/internal/central/model/cluster"
	"github.com/tidbcloud/core-portal/internal/central/pkg/auth/authuser"
	debugAuth "github.com/tidbcloud/core-portal/internal/central/pkg/auth/debug"
	"github.com/tidbcloud/pkgv2/auth/auth0"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	entity "github.com/tidbcloud/core-svc/model"
	"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, route string, handler http.Handler) {
	eng.Any(path.Join(route, "*any"), gin.WrapH(http.StripPrefix(route, handler)))
}

func isMetricsPath(urlPath string) bool {
	paths := strings.Split(path.Clean(urlPath), "/")
	if len(paths) < 3 {
		return false
	}

	if paths[2] == "prometheus" {
		return true
	}

	return false
}

func isDashboardPath(urlPath string) bool {
	paths := strings.Split(path.Clean(urlPath), "/")
	if len(paths) < 4 {
		return false
	}

	if paths[2] == "pd" && paths[3] == "dashboard" {
		return true
	}

	return false
}

func isK8sDashboardPath(urlPath string) bool {
	paths := strings.Split(path.Clean(urlPath), "/")
	if len(paths) < 3 {
		return false
	}

	if paths[2] == "kubernetes_dashboard" {
		return true
	}

	return false
}

func parseClusterIDFromProxyURL(urlPath string) (uint64, error) {
	// TODO: better way to find clusterID
	paths := strings.Split(path.Clean(urlPath), "/")
	if len(paths) < 2 {
		return 0, errors.New("invalid proxy url")
	}
	clusterID := paths[1]
	clusterIDParsed, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		return 0, errors.Annotate(err, "invalid proxy url")
	}
	return clusterIDParsed, nil
}

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) {
		clusterID, err := parseClusterIDFromProxyURL(req.URL.Path)
		if err != nil {
			return nil, nil, errcode.NewInvalidInputErr(err)
		}

		ctx := req.Context()
		var orgID *entity.ID

		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 := authuser.GetUserFromContext(req.Context())
			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.Uint64()))

			// At least when developing this can end up being sent as central instead of management
			if isK8sDashboardPath(req.URL.Path) {
				validatorID = auth0.AuthValidatorIDManagement
			}

			switch validatorID {
			case auth0.AuthValidatorIDCentral:
				orgID = &user.User.TenantID
			case auth0.AuthValidatorIDManagement:
				log.Debug("management id", zap.String("path", req.URL.Path))
				if !authuser.CheckAdminPermission(ctx) {
					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 isMetricsPath(req.URL.Path) {
			baseURL, err = cluster.FindClusterMetricsBaseURL(ctx, clusterModel.InfraAPI(), entity.ID(clusterID), orgID)
		} else if isDashboardPath(req.URL.Path) {
			baseURL, err = cluster.FindClusterDashboardBaseURL(ctx, clusterModel.Encryptor(), clusterModel.InfraAPI(), entity.ID(clusterID), orgID)
		} else {
			baseURL, err = cluster.FindClusterProxyBaseURL(ctx, clusterModel.InfraAPI(), entity.ID(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
	}

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

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

	if len(paths) >= 3 && paths[2] == "prometheus" {
		req.URL.Path = strings.Join(append([]string{u.Path}, paths[3:]...), "/")
	} else {
		req.URL.Path = strings.Join(append([]string{u.Path}, paths[2:]...), "/")
	}
	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
}
