// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package main

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/acme"
	"golang.org/x/crypto/acme/autocert"
	"tailscale.com/types/logger"
)

const (
	managedNaviFunnelHTTPReadTimeout     = 30 * time.Second
	managedNaviFunnelHTTPShutdownTimeout = 5 * time.Second
	managedNaviFunnelSyncInterval        = 15 * time.Second
)

const (
	managedNaviFunnelListenProtoHTTP  = "http"
	managedNaviFunnelListenProtoHTTPS = "https"
	managedNaviFunnelListenProtoWS    = "ws"
	managedNaviFunnelListenProtoWSS   = "wss"
)

type managedNaviFunnelListenerKey struct {
	Port int
	TLS  bool
}

type managedNaviFunnelPortFamily string

const (
	managedNaviFunnelPortFamilyHTTPPlain managedNaviFunnelPortFamily = "http"
	managedNaviFunnelPortFamilyHTTPTLS   managedNaviFunnelPortFamily = "https"
)

type managedNaviFunnelRoute struct {
	ServiceID      int64
	OrgID          int64
	Domain         string
	ListenPort     int
	ListenProto    string
	MountPath      string
	BackendScheme  string
	BackendAddress string
	Proxy          *httputil.ReverseProxy
}

type managedNaviFunnelPortRoutes struct {
	RoutesByHost map[string][]*managedNaviFunnelRoute
}

type managedNaviFunnelSnapshot struct {
	PlainPorts map[int]*managedNaviFunnelPortRoutes
	TLSPorts   map[int]*managedNaviFunnelPortRoutes
	TLSHosts   map[string]struct{}
}

type managedNaviFunnelListenerState struct {
	Key       managedNaviFunnelListenerKey
	Server    *http.Server
	Listeners []net.Listener
}

type managedNaviFunnelTLSConfig struct {
	Mode  string
	Dir   string
	KID   string
	Key   string
	Email string
}

type managedNaviFunnelRuntime struct {
	ctx    context.Context
	cancel context.CancelFunc
	logf   logger.Logf

	mu        sync.RWMutex
	closed    bool
	snapshot  *managedNaviFunnelSnapshot
	listeners map[managedNaviFunnelListenerKey]*managedNaviFunnelListenerState

	bindAddrs         []string
	externalListeners map[managedNaviFunnelListenerKey]struct{}
	dialContext       func(ctx context.Context, network, address string) (net.Conn, error)
	fallbackGetCert   func(*tls.ClientHelloInfo) (*tls.Certificate, error)
	certManager       *autocert.Manager

	wg sync.WaitGroup
}

func newManagedNaviFunnelRuntime(parent context.Context, logf logger.Logf, bindAddrs []string, external []managedNaviFunnelListenerKey, tlsCfg managedNaviFunnelTLSConfig) (*managedNaviFunnelRuntime, error) {
	if parent == nil {
		parent = context.Background()
	}
	if logf == nil {
		logf = log.Printf
	}
	ctx, cancel := context.WithCancel(parent)
	rt := &managedNaviFunnelRuntime{
		ctx:               ctx,
		cancel:            cancel,
		logf:              logf,
		listeners:         make(map[managedNaviFunnelListenerKey]*managedNaviFunnelListenerState),
		externalListeners: make(map[managedNaviFunnelListenerKey]struct{}, len(external)),
	}
	rt.bindAddrs = normalizeManagedNaviFunnelStringList(bindAddrs)
	if len(rt.bindAddrs) == 0 {
		rt.bindAddrs = []string{"0.0.0.0", "::"}
	}
	for _, key := range external {
		if key.Port <= 0 || key.Port > 65535 {
			continue
		}
		rt.externalListeners[key] = struct{}{}
	}
	rt.dialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
		var dialer net.Dialer
		return dialer.DialContext(ctx, network, address)
	}

	mode := strings.ToLower(strings.TrimSpace(tlsCfg.Mode))
	switch mode {
	case "", "manual":
		return rt, nil
	case "letsencrypt", "gcp":
		cacheDir := tlsCfg.Dir
		if cacheDir == "" {
			return nil, fmt.Errorf("managed funnel TLS cache directory is not configured")
		}
		cacheDir = filepath.Join(cacheDir, "mirage-funnel")
		if err := os.MkdirAll(cacheDir, 0o755); err != nil {
			return nil, fmt.Errorf("create managed funnel TLS cache dir: %w", err)
		}
		certManager := &autocert.Manager{
			Prompt:     autocert.AcceptTOS,
			Cache:      autocert.DirCache(cacheDir),
			HostPolicy: rt.autocertHostPolicy,
		}
		if mode == "gcp" {
			if tlsCfg.KID == "" || tlsCfg.Key == "" {
				return nil, errors.New("managed funnel --certmode=gcp requires ACME EAB credentials")
			}
			if tlsCfg.Email == "" {
				return nil, errors.New("managed funnel --certmode=gcp requires ACME email")
			}
			keyBytes, err := decodeEABKey(tlsCfg.Key)
			if err != nil {
				return nil, err
			}
			certManager.Client = &acme.Client{
				DirectoryURL: "https://dv.acme-v02.api.pki.goog/directory",
			}
			certManager.ExternalAccountBinding = &acme.ExternalAccountBinding{
				KID: tlsCfg.KID,
				Key: keyBytes,
			}
		}
		if tlsCfg.Email != "" {
			certManager.Email = tlsCfg.Email
		}
		rt.certManager = certManager
		return rt, nil
	default:
		return nil, fmt.Errorf("unsupported managed funnel cert mode %q", tlsCfg.Mode)
	}
}

func (rt *managedNaviFunnelRuntime) close() {
	rt.mu.Lock()
	if rt.closed {
		rt.mu.Unlock()
		return
	}
	rt.closed = true
	rt.mu.Unlock()

	rt.cancel()
	rt.shutdownAll()
	rt.wg.Wait()
}

func (rt *managedNaviFunnelRuntime) setFallbackGetCertificate(fn func(*tls.ClientHelloInfo) (*tls.Certificate, error)) {
	rt.mu.Lock()
	rt.fallbackGetCert = fn
	rt.mu.Unlock()
}

func (rt *managedNaviFunnelRuntime) applyPayload(payload *managedNaviFunnelPayload) error {
	rt.mu.RLock()
	closed := rt.closed
	rt.mu.RUnlock()
	if closed {
		return fmt.Errorf("managed funnel runtime is closed")
	}

	snapshot, err := rt.buildSnapshot(payload)
	if err != nil {
		return err
	}

	listenerErrors := rt.syncListeners(snapshot)

	rt.mu.Lock()
	rt.snapshot = snapshot
	rt.mu.Unlock()

	if len(listenerErrors) == 0 {
		return nil
	}

	keys := make([]managedNaviFunnelListenerKey, 0, len(listenerErrors))
	for key := range listenerErrors {
		keys = append(keys, key)
	}
	sort.Slice(keys, func(i, j int) bool {
		if keys[i].Port != keys[j].Port {
			return keys[i].Port < keys[j].Port
		}
		if keys[i].TLS == keys[j].TLS {
			return false
		}
		return !keys[i].TLS && keys[j].TLS
	})

	messages := make([]string, 0, len(keys))
	for _, key := range keys {
		messages = append(messages, listenerErrors[key])
	}
	return fmt.Errorf("failed to apply managed funnel payload: %s", strings.Join(messages, "; "))
}

func (rt *managedNaviFunnelRuntime) buildSnapshot(payload *managedNaviFunnelPayload) (*managedNaviFunnelSnapshot, error) {
	snapshot := &managedNaviFunnelSnapshot{
		PlainPorts: make(map[int]*managedNaviFunnelPortRoutes),
		TLSPorts:   make(map[int]*managedNaviFunnelPortRoutes),
		TLSHosts:   make(map[string]struct{}),
	}
	if payload == nil || len(payload.Services) == 0 {
		return snapshot, nil
	}

	seenRouteKeys := make(map[string]int64)
	portFamilies := make(map[int]managedNaviFunnelPortFamily)

	for i := range payload.Services {
		item := payload.Services[i]
		listenProto := strings.ToLower(strings.TrimSpace(item.Public.ListenProto))
		if !managedNaviFunnelHTTPFamilyProto(listenProto) {
			return nil, fmt.Errorf("managed funnel runtime does not support protocol %q", item.Public.ListenProto)
		}
		if item.Backend.Network != "" && !strings.EqualFold(strings.TrimSpace(item.Backend.Network), "tcp") {
			return nil, fmt.Errorf("managed funnel runtime does not support backend network %q", item.Backend.Network)
		}

		portFamily, ok := managedNaviFunnelPortFamilyForProto(listenProto)
		if !ok {
			return nil, fmt.Errorf("unsupported managed funnel listen proto %q", item.Public.ListenProto)
		}
		if existingFamily, ok := portFamilies[item.Public.ListenPort]; ok && existingFamily != portFamily {
			return nil, fmt.Errorf("listen port %d is already assigned to %s family", item.Public.ListenPort, existingFamily)
		}
		portFamilies[item.Public.ListenPort] = portFamily

		if externalKey, ok := rt.externalListenerForPort(item.Public.ListenPort); ok && externalKey.TLS != managedNaviFunnelPortFamilyUsesTLS(portFamily) {
			return nil, fmt.Errorf("listen port %d is already occupied by managed derper %s traffic", item.Public.ListenPort, managedNaviFunnelListenerLabel(externalKey))
		}

		host := normalizeManagedNaviFunnelBaseDomain(item.Public.Host)
		if host == "" {
			return nil, fmt.Errorf("managed funnel host is required")
		}
		if item.Public.ListenPort <= 0 || item.Public.ListenPort > 65535 {
			return nil, fmt.Errorf("invalid managed funnel listen port %d", item.Public.ListenPort)
		}
		if strings.TrimSpace(item.Backend.Address) == "" || item.Backend.Port <= 0 || item.Backend.Port > 65535 {
			return nil, fmt.Errorf("managed funnel backend address is incomplete")
		}

		backendScheme := strings.ToLower(strings.TrimSpace(item.Backend.Scheme))
		if backendScheme == "" {
			backendScheme = "http"
		}

		route := &managedNaviFunnelRoute{
			ServiceID:      managedNaviFunnelSyncServiceID(item, i),
			OrgID:          item.OrgID,
			Domain:         host,
			ListenPort:     item.Public.ListenPort,
			ListenProto:    listenProto,
			MountPath:      normalizeManagedNaviFunnelMountPath(item.Public.MountPath),
			BackendScheme:  backendScheme,
			BackendAddress: net.JoinHostPort(strings.TrimSpace(item.Backend.Address), strconv.Itoa(item.Backend.Port)),
		}

		routeKey := managedNaviFunnelRouteKey(portFamily, route)
		if existingID, ok := seenRouteKeys[routeKey]; ok {
			return nil, fmt.Errorf("duplicate managed funnel route on port %d host %s path %s conflicts with service %d", route.ListenPort, route.Domain, route.MountPath, existingID)
		}
		seenRouteKeys[routeKey] = route.ServiceID
		route.Proxy = rt.newReverseProxy(route)

		if managedNaviFunnelPortFamilyUsesTLS(portFamily) {
			snapshot.TLSHosts[host] = struct{}{}
			portRoutes := snapshot.TLSPorts[route.ListenPort]
			if portRoutes == nil {
				portRoutes = &managedNaviFunnelPortRoutes{RoutesByHost: make(map[string][]*managedNaviFunnelRoute)}
				snapshot.TLSPorts[route.ListenPort] = portRoutes
			}
			hostRoutes := append(portRoutes.RoutesByHost[route.Domain], route)
			sort.SliceStable(hostRoutes, func(i, j int) bool {
				return len(hostRoutes[i].MountPath) > len(hostRoutes[j].MountPath)
			})
			portRoutes.RoutesByHost[route.Domain] = hostRoutes
			continue
		}

		portRoutes := snapshot.PlainPorts[route.ListenPort]
		if portRoutes == nil {
			portRoutes = &managedNaviFunnelPortRoutes{RoutesByHost: make(map[string][]*managedNaviFunnelRoute)}
			snapshot.PlainPorts[route.ListenPort] = portRoutes
		}
		hostRoutes := append(portRoutes.RoutesByHost[route.Domain], route)
		sort.SliceStable(hostRoutes, func(i, j int) bool {
			return len(hostRoutes[i].MountPath) > len(hostRoutes[j].MountPath)
		})
		portRoutes.RoutesByHost[route.Domain] = hostRoutes
	}

	return snapshot, nil
}

func (rt *managedNaviFunnelRuntime) syncListeners(snapshot *managedNaviFunnelSnapshot) map[managedNaviFunnelListenerKey]string {
	desired := make(map[managedNaviFunnelListenerKey]struct{})
	for port := range snapshot.PlainPorts {
		key := managedNaviFunnelListenerKey{Port: port}
		if rt.isExternallyServed(key) {
			continue
		}
		desired[key] = struct{}{}
	}
	for port := range snapshot.TLSPorts {
		key := managedNaviFunnelListenerKey{Port: port, TLS: true}
		if rt.isExternallyServed(key) {
			continue
		}
		desired[key] = struct{}{}
	}

	errorsByKey := make(map[managedNaviFunnelListenerKey]string)
	toClose := make([]*managedNaviFunnelListenerState, 0)

	rt.mu.Lock()
	for key, state := range rt.listeners {
		if _, ok := desired[key]; ok {
			continue
		}
		toClose = append(toClose, state)
		delete(rt.listeners, key)
	}
	rt.mu.Unlock()

	for _, state := range toClose {
		rt.closeListenerState(state)
	}

	for key := range desired {
		rt.mu.RLock()
		_, ok := rt.listeners[key]
		rt.mu.RUnlock()
		if ok {
			continue
		}
		state, err := rt.startListenerState(key)
		if err != nil {
			errorsByKey[key] = err.Error()
			continue
		}
		rt.mu.Lock()
		if rt.closed {
			rt.mu.Unlock()
			rt.closeListenerState(state)
			errorsByKey[key] = fmt.Sprintf("managed funnel listen port %d stopped because runtime is closed", key.Port)
			continue
		}
		if _, ok := rt.listeners[key]; ok {
			rt.mu.Unlock()
			rt.closeListenerState(state)
			continue
		}
		rt.listeners[key] = state
		rt.mu.Unlock()
	}

	return errorsByKey
}

func (rt *managedNaviFunnelRuntime) startListenerState(key managedNaviFunnelListenerKey) (*managedNaviFunnelListenerState, error) {
	handler := rt.wrapHandler(key, http.NotFoundHandler())
	server := &http.Server{
		Addr:         ":" + strconv.Itoa(key.Port),
		Handler:      handler,
		ReadTimeout:  managedNaviFunnelHTTPReadTimeout,
		WriteTimeout: 0,
	}
	if key.TLS {
		server.TLSConfig = &tls.Config{
			MinVersion: tls.VersionTLS12,
			GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
				return rt.getCertificateForHost(hello)
			},
		}
	}

	state := &managedNaviFunnelListenerState{
		Key:    key,
		Server: server,
	}

	var bindErrs []string
	for _, bindAddr := range rt.bindAddrs {
		addr := net.JoinHostPort(bindAddr, strconv.Itoa(key.Port))
		ln, err := net.Listen("tcp", addr)
		if err != nil {
			bindErrs = append(bindErrs, fmt.Sprintf("%s: %v", addr, err))
			continue
		}
		state.Listeners = append(state.Listeners, ln)
		rt.wg.Add(1)
		go func(listener net.Listener) {
			defer rt.wg.Done()
			var serveErr error
			if key.TLS {
				serveErr = server.ServeTLS(listener, "", "")
			} else {
				serveErr = server.Serve(listener)
			}
			if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) && !errors.Is(serveErr, net.ErrClosed) {
				rt.logf("Mirage funnel listener %s failed: %v", managedNaviFunnelListenerLabel(key), serveErr)
			}
		}(ln)
	}

	if len(state.Listeners) == 0 {
		return nil, fmt.Errorf("managed funnel listen port %d bind failed: %s", key.Port, strings.Join(bindErrs, "; "))
	}

	return state, nil
}

func (rt *managedNaviFunnelRuntime) shutdownAll() {
	rt.mu.Lock()
	toClose := make([]*managedNaviFunnelListenerState, 0, len(rt.listeners))
	for key, state := range rt.listeners {
		toClose = append(toClose, state)
		delete(rt.listeners, key)
	}
	rt.mu.Unlock()

	for _, state := range toClose {
		rt.closeListenerState(state)
	}
}

func (rt *managedNaviFunnelRuntime) closeListenerState(state *managedNaviFunnelListenerState) {
	if state == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), managedNaviFunnelHTTPShutdownTimeout)
	defer cancel()
	_ = state.Server.Shutdown(ctx)
	for _, listener := range state.Listeners {
		_ = listener.Close()
	}
}

func (rt *managedNaviFunnelRuntime) wrapHandler(key managedNaviFunnelListenerKey, next http.Handler) http.Handler {
	if next == nil {
		next = http.NotFoundHandler()
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if route, ok := rt.matchRoute(key, r); ok && route != nil {
			route.Proxy.ServeHTTP(w, r)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (rt *managedNaviFunnelRuntime) wrapHTTPChallengeHandler(next http.Handler) http.Handler {
	if next == nil {
		next = http.NotFoundHandler()
	}
	rt.mu.RLock()
	manager := rt.certManager
	rt.mu.RUnlock()
	if manager == nil {
		return next
	}
	return manager.HTTPHandler(next)
}

func (rt *managedNaviFunnelRuntime) matchRoute(key managedNaviFunnelListenerKey, r *http.Request) (*managedNaviFunnelRoute, bool) {
	rt.mu.RLock()
	snapshot := rt.snapshot
	rt.mu.RUnlock()
	if snapshot == nil {
		return nil, false
	}

	host := normalizeManagedNaviFunnelRequestHost(r.Host)
	if host == "" {
		return nil, false
	}

	portRoutes := snapshot.PlainPorts[key.Port]
	if key.TLS {
		portRoutes = snapshot.TLSPorts[key.Port]
	}
	if portRoutes == nil {
		return nil, false
	}
	routes := portRoutes.RoutesByHost[host]
	if len(routes) == 0 {
		return nil, false
	}
	for _, route := range routes {
		if managedNaviFunnelPathMatches(route.MountPath, r.URL.Path) {
			return route, true
		}
	}
	return nil, false
}

func (rt *managedNaviFunnelRuntime) newReverseProxy(route *managedNaviFunnelRoute) *httputil.ReverseProxy {
	target := &url.URL{
		Scheme: route.BackendScheme,
		Host:   route.BackendAddress,
	}
	transport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
			return rt.dialContext(ctx, network, address)
		},
		ForceAttemptHTTP2: false,
	}
	if route.BackendScheme == "https" {
		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
	}

	return &httputil.ReverseProxy{
		Transport: transport,
		Rewrite: func(pr *httputil.ProxyRequest) {
			pr.SetURL(target)
			pr.SetXForwarded()
			pr.Out.Host = pr.In.Host
			pr.Out.URL.Path = stripManagedNaviFunnelMountPath(route.MountPath, pr.In.URL.Path)
			pr.Out.URL.RawPath = ""
			pr.Out.URL.RawQuery = pr.In.URL.RawQuery
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			http.Error(w, "funnel backend unavailable", http.StatusBadGateway)
		},
	}
}

func (rt *managedNaviFunnelRuntime) autocertHostPolicy(_ context.Context, host string) error {
	host = normalizeManagedNaviFunnelBaseDomain(host)
	rt.mu.RLock()
	defer rt.mu.RUnlock()
	if rt.snapshot == nil {
		return fmt.Errorf("managed funnel snapshot not ready")
	}
	if _, ok := rt.snapshot.TLSHosts[host]; !ok {
		return fmt.Errorf("managed funnel host %s is not allowed for tls", host)
	}
	return nil
}

func (rt *managedNaviFunnelRuntime) getCertificateForHost(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	host := ""
	if hello != nil {
		host = normalizeManagedNaviFunnelBaseDomain(hello.ServerName)
	}
	if host == "" {
		return nil, nil
	}
	rt.mu.RLock()
	snapshot := rt.snapshot
	manager := rt.certManager
	fallback := rt.fallbackGetCert
	rt.mu.RUnlock()

	if snapshot == nil {
		return nil, fmt.Errorf("managed funnel snapshot not ready")
	}
	if _, ok := snapshot.TLSHosts[host]; !ok {
		return nil, nil
	}

	var primaryErr error
	if manager != nil {
		cert, err := manager.GetCertificate(hello)
		if err == nil && cert != nil {
			return cert, nil
		}
		primaryErr = err
	}
	if fallback != nil {
		cert, err := fallback(hello)
		if err == nil && cert != nil {
			return cert, nil
		}
		if primaryErr != nil {
			return nil, primaryErr
		}
		return nil, err
	}
	if primaryErr != nil {
		return nil, primaryErr
	}
	return nil, fmt.Errorf("managed funnel TLS is not configured for host %s", host)
}

func (rt *managedNaviFunnelRuntime) externalListenerForPort(port int) (managedNaviFunnelListenerKey, bool) {
	for key := range rt.externalListeners {
		if key.Port == port {
			return key, true
		}
	}
	return managedNaviFunnelListenerKey{}, false
}

func (rt *managedNaviFunnelRuntime) isExternallyServed(key managedNaviFunnelListenerKey) bool {
	_, ok := rt.externalListeners[key]
	return ok
}

func normalizeManagedNaviFunnelStringList(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	normalized := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		normalized = append(normalized, value)
	}
	return normalized
}

func normalizeManagedNaviFunnelBaseDomain(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if strings.Contains(raw, "://") {
		if parsed, err := url.Parse(raw); err == nil {
			raw = parsed.Host
			if raw == "" {
				raw = strings.Trim(parsed.Path, "/")
			}
		}
	}
	raw = strings.Trim(raw, "/")
	return strings.ToLower(raw)
}

func normalizeManagedNaviFunnelRequestHost(host string) string {
	host = strings.TrimSpace(host)
	if host == "" {
		return ""
	}
	if parsedHost, _, err := net.SplitHostPort(host); err == nil {
		host = parsedHost
	}
	return normalizeManagedNaviFunnelBaseDomain(host)
}

func normalizeManagedNaviFunnelMountPath(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" || raw == "/" {
		return "/"
	}
	if !strings.HasPrefix(raw, "/") {
		raw = "/" + raw
	}
	raw = strings.TrimRight(raw, "/")
	if raw == "" {
		return "/"
	}
	return raw
}

func managedNaviFunnelHTTPFamilyProto(proto string) bool {
	switch proto {
	case managedNaviFunnelListenProtoHTTP, managedNaviFunnelListenProtoHTTPS, managedNaviFunnelListenProtoWS, managedNaviFunnelListenProtoWSS:
		return true
	default:
		return false
	}
}

func managedNaviFunnelPortFamilyForProto(proto string) (managedNaviFunnelPortFamily, bool) {
	switch proto {
	case managedNaviFunnelListenProtoHTTP, managedNaviFunnelListenProtoWS:
		return managedNaviFunnelPortFamilyHTTPPlain, true
	case managedNaviFunnelListenProtoHTTPS, managedNaviFunnelListenProtoWSS:
		return managedNaviFunnelPortFamilyHTTPTLS, true
	default:
		return "", false
	}
}

func managedNaviFunnelPortFamilyUsesTLS(family managedNaviFunnelPortFamily) bool {
	return family == managedNaviFunnelPortFamilyHTTPTLS
}

func managedNaviFunnelRouteKey(family managedNaviFunnelPortFamily, route *managedNaviFunnelRoute) string {
	return fmt.Sprintf("%s:%d:%s:%s", family, route.ListenPort, route.Domain, route.MountPath)
}

func managedNaviFunnelPathMatches(mountPath, requestPath string) bool {
	if mountPath == "/" {
		return true
	}
	if !strings.HasPrefix(requestPath, mountPath) {
		return false
	}
	if len(requestPath) == len(mountPath) {
		return true
	}
	return requestPath[len(mountPath)] == '/'
}

func stripManagedNaviFunnelMountPath(mountPath, requestPath string) string {
	if mountPath == "/" {
		if requestPath == "" {
			return "/"
		}
		return requestPath
	}
	stripped := strings.TrimPrefix(requestPath, mountPath)
	if stripped == "" {
		return "/"
	}
	if !strings.HasPrefix(stripped, "/") {
		return "/" + stripped
	}
	return stripped
}

func managedNaviFunnelSyncServiceID(item managedNaviFunnelSyncRecord, index int) int64 {
	if item.Service != nil {
		if rawID, ok := item.Service["id"]; ok {
			switch value := rawID.(type) {
			case string:
				if parsed, err := strconv.ParseInt(value, 10, 64); err == nil && parsed > 0 {
					return parsed
				}
			case float64:
				if value > 0 {
					return int64(value)
				}
			case int64:
				if value > 0 {
					return value
				}
			case int:
				if value > 0 {
					return int64(value)
				}
			}
		}
	}
	return int64(index + 1)
}

func managedNaviFunnelListenerLabel(key managedNaviFunnelListenerKey) string {
	if key.TLS {
		return "https"
	}
	return "http"
}
