// Package route provides structures and functions to represent a route.
package route

import (
	"fmt"
	"net/http"
	"net/http/httputil"
	"sync"
	"time"

	versioned2 "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
	"github.com/pingcap/log"
	"github.com/pingcap/tidb-operator/pkg/client/clientset/versioned"
	"go.uber.org/zap"
	"k8s.io/client-go/kubernetes"
	v1 "k8s.io/client-go/listers/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/clusterproxy/resolve"
	"github.com/tidbcloud/clusterproxy/util/cluster"
)

// Context represents the context required by a Route.
type Context struct {
	Cli            kubernetes.Interface
	K8sCli         client.Client
	OpCli          *versioned.Clientset
	CertCli        *versioned2.Clientset
	SvcLister      v1.ServiceLister
	EndpointLister v1.EndpointsLister
}

// Resolver resolves the requested cluster and the service route from the request
type Resolver interface {
	Resolve(*http.Request) (resolve.Cluster, string, error)
	Prefix() string
}

// Route represents a route.
type Route struct {
	Name       string                                       `json:"name"`    // Name of the route
	Version    Version                                      `json:"version"` // Version of the route
	Schema     *Schema                                      `json:"schema"`  // Schema of the route
	Path       string                                       `json:"path"`    // Path of the route
	GetHandler func(*Context, resolve.Cluster) http.Handler `json:"-"`       // Handler get function with the given context
}

// WithValidation returns a handler that validates the requests and trim the route path off the request path.
func (r *Route) WithValidation(c *Context, cl resolve.Cluster) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		if err := r.Schema.Validate(req); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		r.GetHandler(c, cl).ServeHTTP(w, req)
	})
}

var k8sServiceProxyMap = sync.Map{}

// GetKubernetesServiceProxy returns a reverse proxy for the Kubernetes service.
// It will cache the proxy for the same service.
func GetKubernetesServiceProxy(scheme, namespace, name, port string, transport *http.Transport) *httputil.ReverseProxy {
	url := cluster.ServiceURL(scheme, namespace, name, port)
	key := url.String()
	var proxy *httputil.ReverseProxy
	value, ok := k8sServiceProxyMap.Load(key)
	if ok {
		// FIXME(huaxi): the ReverseProxy is immutable, so there may be issues when TLS config is changed.
		log.Info("Get cached reverse proxy", zap.String("URL", url.String()))
		proxy = value.(*httputil.ReverseProxy)
	} else {
		proxy = httputil.NewSingleHostReverseProxy(url)
		proxy.ModifyResponse = setCORSHeader
		proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
			log.Error(fmt.Sprintf("Error while proxying the request %+v", err), zap.String("URL", r.URL.Path), zap.Error(err))
			// The first log statement has a stack trace.
			// This log statement is easier to look at if you don't need the stack.
			log.Error("Error while proxying the request", zap.String("URL", r.URL.Path), zap.Error(err))
			http.Error(w, err.Error(), http.StatusBadGateway)
		}

		if transport == nil {
			// If `transport` is nil, will use the default transport,
			// which will not limit idle connections.
			// So set `MaxIdleConns` and `IdleConnTimeout` to avoid too many idle connections.
			proxy.Transport = &http.Transport{
				MaxIdleConns:    1,
				IdleConnTimeout: 30 * time.Second,
			}
		} else {
			proxy.Transport = transport
		}

		k8sServiceProxyMap.Store(key, proxy)
	}

	return proxy
}

// setCORSHeader sets the CORS headers on the response.
func setCORSHeader(resp *http.Response) error {
	resp.Header.Set("Access-Control-Allow-Headers", "accept, content-type, authorization")
	resp.Header.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	resp.Header.Set("Access-Control-Allow-Origin", "*")
	return nil
}
