package host

import (
	"crypto/tls"
	"net/http"
	"net/http/httputil"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/kataras/iris/v12/core/netutil"
)

// ProxyHandler returns a new ReverseProxy that rewrites
// URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir.
//
// Relative to httputil.NewSingleHostReverseProxy with some additions.
//
// Look `ProxyHandlerRemote` too.
func ProxyHandler(target *url.URL, config *tls.Config) *httputil.ReverseProxy {
	if config == nil {
		config = &tls.Config{MinVersion: tls.VersionTLS13}
	}

	director := func(req *http.Request) {
		modifyProxiedRequest(req, target)
		req.Host = target.Host
		req.URL.Path = path.Join(target.Path, req.URL.Path)
	}

	// TODO: when go 1.20 released:
	/*
		rewrite := func(r *httputil.ProxyRequest) {
			r.SetURL(target)  // Forward request to outboundURL.
			r.SetXForwarded() // Set X-Forwarded-* headers.
			// r.Out.Header.Set("X-Additional-Header", "header set by the proxy")
			// To preserve the inbound request's Host header (the default behavior of NewSingleHostReverseProxy):
			// r.Out.Host = r.In.Host
		}
	*/

	p := &httputil.ReverseProxy{Director: director /*, Rewrite: rewrite */}

	if netutil.IsLoopbackHost(target.Host) {
		transport := &http.Transport{
			TLSClientConfig: config, // lint:ignore
		}
		p.Transport = transport
	}

	return p
}

// mergeQuery return a query string that combines targetQuery and reqQuery
// and remove the duplicated query parameters of them.
func mergeQuery(targetQuery, reqQuery string) string {
	var paramSlice []string
	if targetQuery != "" {
		paramSlice = strings.Split(targetQuery, "&")
	}

	if reqQuery != "" {
		paramSlice = append(paramSlice, strings.Split(reqQuery, "&")...)
	}

	var mergedSlice []string
	queryMap := make(map[string]bool)
	for _, param := range paramSlice {
		size := len(queryMap)
		queryMap[param] = true
		if size != len(queryMap) {
			mergedSlice = append(mergedSlice, param)
		}
	}
	return strings.Join(mergedSlice, "&")
}

func modifyProxiedRequest(req *http.Request, target *url.URL) {
	req.URL.Scheme = target.Scheme
	req.URL.Host = target.Host
	req.URL.RawQuery = mergeQuery(target.RawQuery, 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", "")
	}
}

// ProxyHandlerRemote returns a new ReverseProxy that rewrites
// URLs to the scheme, host, and path provided in target.
// Case 1: req.Host == target.Host
// behavior same as ProxyHandler
// Case 2: req.Host != target.Host
// the target request will be forwarded to the target's url
// insecureSkipVerify indicates enable ssl certificate verification or not.
//
// Look `ProxyHandler` too.
func ProxyHandlerRemote(target *url.URL, config *tls.Config) *httputil.ReverseProxy {
	if config == nil {
		config = &tls.Config{MinVersion: tls.VersionTLS13}
	}

	director := func(req *http.Request) {
		modifyProxiedRequest(req, target)

		if req.Host != target.Host {
			req.URL.Path = target.Path
		} else {
			req.URL.Path = path.Join(target.Path, req.URL.Path)
		}

		req.Host = target.Host
	}
	p := &httputil.ReverseProxy{Director: director}

	if netutil.IsLoopbackHost(target.Host) {
		config.InsecureSkipVerify = true
	}

	transport := &http.Transport{
		TLSClientConfig: config, // lint:ignore
	}
	p.Transport = transport
	return p
}

// NewProxy returns a new host (server supervisor) which
// proxies all requests to the target.
// It uses the httputil.NewSingleHostReverseProxy.
//
// Usage:
// target, _ := url.Parse("https://mydomain.com")
// proxy := NewProxy("mydomain.com:80", target)
// proxy.ListenAndServe() // use of `proxy.Shutdown` to close the proxy server.
func NewProxy(hostAddr string, target *url.URL, config *tls.Config) *Supervisor {
	proxyHandler := ProxyHandler(target, config)
	proxy := New(&http.Server{
		Addr:    hostAddr,
		Handler: proxyHandler,
	})

	return proxy
}

// NewProxyRemote returns a new host (server supervisor) which
// proxies all requests to the target.
// It uses the httputil.NewSingleHostReverseProxy.
//
// Usage:
// target, _ := url.Parse("https://anotherdomain.com/abc")
// proxy := NewProxyRemote("mydomain.com", target, false)
// proxy.ListenAndServe() // use of `proxy.Shutdown` to close the proxy server.
func NewProxyRemote(hostAddr string, target *url.URL, config *tls.Config) *Supervisor {
	proxyHandler := ProxyHandlerRemote(target, config)
	proxy := New(&http.Server{
		Addr:    hostAddr,
		Handler: proxyHandler,
	})

	return proxy
}

// NewRedirection returns a new host (server supervisor) which
// redirects all requests to the target.
// Usage:
// target, _ := url.Parse("https://mydomain.com")
// r := NewRedirection(":80", target, 307)
// r.ListenAndServe() // use of `r.Shutdown` to close this server.
func NewRedirection(hostAddr string, target *url.URL, redirectStatus int) *Supervisor {
	redirectSrv := &http.Server{
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 60 * time.Second,
		Addr:         hostAddr,
		Handler:      RedirectHandler(target, redirectStatus),
	}

	return New(redirectSrv)
}

// RedirectHandler returns a simple redirect handler.
// See `NewProxy` or `ProxyHandler` for more features.
func RedirectHandler(target *url.URL, redirectStatus int) http.Handler {
	targetURI := target.String()
	if redirectStatus <= 300 {
		// here we should use StatusPermanentRedirect but
		// that may result on unexpected behavior
		// for end-developers who might change their minds
		// after a while, so keep status temporary.
		// Note thatwe could also use StatusFound
		// as we do on the `Context#Redirect`.
		// It will also help us to prevent any post data issues.
		redirectStatus = http.StatusTemporaryRedirect
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		redirectTo := path.Join(targetURI, r.URL.Path)
		if len(r.URL.RawQuery) > 0 {
			redirectTo += "?" + r.URL.RawQuery
		}
		http.Redirect(w, r, redirectTo, redirectStatus)
	})
}