package dynamicproxy

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"

	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)

/*
	Zoraxy Dynamic Proxy
*/

func NewDynamicProxy(option RouterOption) (*Router, error) {
	proxyMap := sync.Map{}
	thisRouter := Router{
		Option:           &option,
		ProxyEndpoints:   &proxyMap,
		Running:          false,
		server:           nil,
		routingRules:     []*RoutingRule{},
		loadBalancer:     option.LoadBalancer,
		rateLimitCounter: RequestCountPerIpTable{},
	}

	thisRouter.mux = &ProxyHandler{
		Parent: &thisRouter,
	}

	return &thisRouter, nil
}

// Update TLS setting in runtime. Will restart the proxy server
// if it is already running in the background
func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
	router.Option.UseTls = tlsEnabled
	router.Restart()
}

// Update TLS Version in runtime. Will restart proxy server if running.
// Set this to true to force TLS 1.2 or above
func (router *Router) UpdateTLSVersion(requireLatest bool) {
	router.Option.ForceTLSLatest = requireLatest
	router.Restart()
}

// Update port 80 listener state
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
	router.Option.ListenOnPort80 = useRedirect
	router.Restart()
}

// Update https redirect, which will require updates
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
	router.Option.ForceHttpsRedirect = useRedirect
	router.Restart()
}

// Start the dynamic routing
func (router *Router) StartProxyService() error {
	//Create a new server object
	if router.server != nil {
		return errors.New("reverse proxy server already running")
	}

	//Check if root route is set
	if router.Root == nil {
		return errors.New("reverse proxy router root not set")
	}

	minVersion := tls.VersionTLS10
	if router.Option.ForceTLSLatest {
		minVersion = tls.VersionTLS12
	}
	config := &tls.Config{
		GetCertificate: router.Option.TlsManager.GetCert,
		MinVersion:     uint16(minVersion),
	}

	//Start rate limitor
	err := router.startRateLimterCounterResetTicker()
	if err != nil {
		return err
	}

	if router.Option.UseTls {
		router.server = &http.Server{
			Addr:      ":" + strconv.Itoa(router.Option.Port),
			Handler:   router.mux,
			TLSConfig: config,
		}
		router.Running = true

		if router.Option.Port != 80 && router.Option.ListenOnPort80 {
			//Add a 80 to 443 redirector
			httpServer := &http.Server{
				Addr: ":80",
				Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					//Check if the domain requesting allow non TLS mode
					domainOnly := r.Host
					if strings.Contains(r.Host, ":") {
						hostPath := strings.Split(r.Host, ":")
						domainOnly = hostPath[0]
					}
					sep := router.GetProxyEndpointFromHostname(domainOnly)
					if sep != nil && sep.BypassGlobalTLS {
						//Allow routing via non-TLS handler
						originalHostHeader := r.Host
						if r.URL != nil {
							r.Host = r.URL.Host
						} else {
							//Fallback when the upstream proxy screw something up in the header
							r.URL, _ = url.Parse(originalHostHeader)
						}

						//Access Check (blacklist / whitelist)
						ruleID := sep.AccessFilterUUID
						if sep.AccessFilterUUID == "" {
							//Use default rule
							ruleID = "default"
						}
						accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
						if err == nil {
							isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
							if isBlocked {
								return
							}
						}

						// Rate Limit
						if sep.RequireRateLimit {
							if err := router.handleRateLimit(w, r, sep); err != nil {
								return
							}
						}

						//Validate basic auth
						if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
							err := handleBasicAuth(w, r, sep)
							if err != nil {
								return
							}
						}

						selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession)
						if err != nil {
							http.ServeFile(w, r, "./web/hosterror.html")
							router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
							router.logRequest(r, false, 404, "vdir-http", r.Host, "")
						}

						endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
						if sep.HeaderRewriteRules != nil {
							endpointProxyRewriteRules = sep.HeaderRewriteRules
						}

						selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
							ProxyDomain:         selectedUpstream.OriginIpOrDomain,
							OriginalHost:        originalHostHeader,
							UseTLS:              selectedUpstream.RequireTLS,
							HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
							NoRemoveHopByHop:    endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
							PathPrefix:          "",
							Version:             sep.parent.Option.HostVersion,
						})
						return
					}

					if router.Option.ForceHttpsRedirect {
						//Redirect to https is enabled
						protocol := "https://"
						if router.Option.Port == 443 {
							http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
						} else {
							http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
						}
					} else {
						//Do not do redirection
						if sep != nil {
							//Sub-domain exists but not allow non-TLS access
							w.WriteHeader(http.StatusBadRequest)
							w.Write([]byte("400 - Bad Request"))
						} else {
							//No defined sub-domain
							if router.Root.DefaultSiteOption == DefaultSite_NoResponse {
								//No response. Just close the connection
								hijacker, ok := w.(http.Hijacker)
								if !ok {
									w.Header().Set("Connection", "close")
									return
								}
								conn, _, err := hijacker.Hijack()
								if err != nil {
									w.Header().Set("Connection", "close")
									return
								}
								conn.Close()
							} else {
								//Default behavior
								http.NotFound(w, r)
							}

						}

					}

				}),
				ReadTimeout:  3 * time.Second,
				WriteTimeout: 3 * time.Second,
				IdleTimeout:  120 * time.Second,
			}

			router.Option.Logger.PrintAndLog("dprouter", "Starting HTTP-to-HTTPS redirector (port 80)", nil)

			//Create a redirection stop channel
			stopChan := make(chan bool)

			//Start a blocking wait for shutting down the http to https redirection server
			go func() {
				<-stopChan
				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
				defer cancel()
				httpServer.Shutdown(ctx)
				router.Option.Logger.PrintAndLog("dprouter", "HTTP to HTTPS redirection listener stopped", nil)
			}()

			//Start the http server that listens to port 80 and redirect to 443
			go func() {
				if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
					//Unable to startup port 80 listener. Handle shutdown process gracefully
					stopChan <- true
					log.Fatalf("Could not start redirection server: %v\n", err)
				}
			}()
			router.tlsRedirectStop = stopChan
		}

		//Start the TLS server
		router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (TLS mode)", nil)
		go func() {
			if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
				router.Option.Logger.PrintAndLog("dprouter", "Could not start proxy server", err)
			}
		}()
	} else {
		//Serve with non TLS mode
		router.tlsListener = nil
		router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
		router.Running = true
		router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (Plain HTTP mode)", nil)
		go func() {
			router.server.ListenAndServe()
		}()
	}

	return nil
}

func (router *Router) StopProxyService() error {
	if router.server == nil {
		return errors.New("reverse proxy server already stopped")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	err := router.server.Shutdown(ctx)
	if err != nil {
		return err
	}

	//Stop TLS listener
	if router.tlsListener != nil {
		router.tlsListener.Close()
	}

	//Stop rate limiter
	if router.rateLimterStop != nil {
		go func() {
			// As the rate timer loop has a 1 sec ticker
			// stop the rate limiter in go routine can prevent
			// front end from freezing for 1 sec
			router.rateLimterStop <- true
		}()

	}

	//Stop TLS redirection (from port 80)
	if router.tlsRedirectStop != nil {
		router.tlsRedirectStop <- true
	}

	//Discard the server object
	router.tlsListener = nil
	router.server = nil
	router.Running = false
	router.tlsRedirectStop = nil
	return nil
}

// Restart the current router if it is running.
func (router *Router) Restart() error {
	//Stop the router if it is already running
	if router.Running {
		err := router.StopProxyService()
		if err != nil {
			return err
		}

		time.Sleep(100 * time.Millisecond)
		// Start the server
		err = router.StartProxyService()
		if err != nil {
			return err
		}
	}

	return nil
}

/*
	Check if a given request is accessed via a proxied subdomain
*/

func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
	hostname := r.Header.Get("X-Forwarded-Host")
	if hostname == "" {
		hostname = r.Host
	}
	hostname = strings.Split(hostname, ":")[0]
	subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
	return subdEndpoint != nil
}

/*
Load routing from RP
*/
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
	var targetProxyEndpoint *ProxyEndpoint
	router.ProxyEndpoints.Range(func(key, value interface{}) bool {
		key, ok := key.(string)
		if !ok {
			return true
		}
		v, ok := value.(*ProxyEndpoint)
		if !ok {
			return true
		}

		if key == strings.ToLower(matchingDomain) {
			targetProxyEndpoint = v
		}
		return true
	})

	if targetProxyEndpoint == nil {
		return nil, errors.New("target routing rule not found")
	}

	return targetProxyEndpoint, nil
}

// Deep copy a proxy endpoint, excluding runtime paramters
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
	js, _ := json.Marshal(endpoint)
	newProxyEndpoint := ProxyEndpoint{}
	err := json.Unmarshal(js, &newProxyEndpoint)
	if err != nil {
		return nil
	}
	return &newProxyEndpoint
}

func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
	m := make(map[string]*ProxyEndpoint)
	r.ProxyEndpoints.Range(func(key, value interface{}) bool {
		k, ok := key.(string)
		if !ok {
			return true
		}
		v, ok := value.(*ProxyEndpoint)
		if !ok {
			return true
		}
		m[k] = v
		return true
	})
	return m
}
