package proxy

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

	"github.com/pingcap/errcode"
	"github.com/pingcap/log"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
)

// ReverseProxy is a reverse proxy with CORS and a dynamic target URL based on the request.
type ReverseProxy struct {
	proxy         *httputil.ReverseProxy
	authenticator ReverseProxyAuthenticator
	makeTargetURL func(req *http.Request) (*tls.Config, *url.URL, error)
}

// ReverseProxyAuthenticator add auth information to request
type ReverseProxyAuthenticator func(*http.Request) error

// NoAuth returns no auth
func NoAuth(r *http.Request) error {
	return nil
}

// NewReverseProxy returns a auth0 m2m authenticator
func NewAuth0M2MAuth(domain, clientID, auth0ClientSecret, auth0Audience string) ReverseProxyAuthenticator {
	return func(r *http.Request) error {
		token, err := auth0.GetClientToken(domain, clientID, auth0ClientSecret, auth0Audience)
		if err != nil {
			return err
		}
		r.Header.Add("Authorization", fmt.Sprintf("bearer %s", token))
		return nil
	}
}

// NewReverseProxy creates a reverse proxy with CORS and a dynamic target URL based on the request.
func NewReverseProxy(authenticator ReverseProxyAuthenticator, makeTargetURL func(req *http.Request) (*tls.Config, *url.URL, error)) ReverseProxy {
	director := func(req *http.Request) {
		_, target, _ := makeTargetURL(req)
		if target != nil {
			defaultDirector(target, req)
		}
	}

	rp := ReverseProxy{
		authenticator: authenticator,
		makeTargetURL: makeTargetURL,
		proxy: &httputil.ReverseProxy{
			Director:       director,
			ModifyResponse: popCORSHeader,
		},
	}
	return rp
}

func (rp ReverseProxy) SetTransport(t http.RoundTripper) {
	rp.proxy.Transport = t
}

// Middleware applies the ReverseProxy as middleware
// This is necessary to handle errors from the url generation
func (rp ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path
	tlsConfig, target, errURL := rp.makeTargetURL(r)
	// undo the side effect of makeTargetURL since it will be called again from the director
	r.URL.Path = path

	if errURL == nil {
		if target == nil {
			log.Error("reverse proxy empty url")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		} else {
			if tlsConfig != nil {
				rp.proxy.Transport = &http.Transport{TLSClientConfig: tlsConfig}
			}
			if rp.authenticator != nil {
				err := rp.authenticator(r)
				if err != nil {
					log.Error("reverse proxy auth failed", zap.Error(err))
					http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
					return
				}
			}
			rp.proxy.ServeHTTP(w, r)
		}
	} else if coded := errcode.CodeChain(errURL); coded != nil {
		log.Info("reverse proxy error code", zap.Error(errURL))
		statusCode := coded.Code().HTTPCode()
		http.Error(w, http.StatusText(statusCode), statusCode)
	} else {
		log.Info("reverse proxy internal error", zap.Error(errURL))
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
}

// setCORSHeader adds CORS headers to the http reverse proxy
func popCORSHeader(resp *http.Response) error {
	resp.Header.Del("Access-Control-Allow-Headers")
	resp.Header.Del("Access-Control-Allow-Methods")
	resp.Header.Del("Access-Control-Allow-Origin")
	return nil
}

// SingleJoiningSlash joins two strings with a slash
// is taken from go ReveresProxy source
func SingleJoiningSlash(a, b string) string {
	aslash := strings.HasSuffix(a, "/")
	bslash := strings.HasPrefix(b, "/")
	switch {
	case aslash && bslash:
		return a + b[1:]
	case !aslash && !bslash:
		return a + "/" + b
	}
	return a + b
}

// taken from go RevereProxy source
func defaultDirector(target *url.URL, req *http.Request) {
	targetQuery := target.RawQuery
	req.URL.Scheme = target.Scheme
	req.URL.Host = target.Host
	req.URL.Path = SingleJoiningSlash(target.Path, req.URL.Path)
	log.Info("req.URL.Path", zap.String("path", req.URL.Path))
	if targetQuery == "" || req.URL.RawQuery == "" {
		req.URL.RawQuery = targetQuery + req.URL.RawQuery
	} else {
		req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
	}
	if _, ok := req.Header["User-Agent"]; !ok {
		// explicitly disable User-Agent so it's not set to default value
		req.Header.Set("User-Agent", "")
	}
}
