package main

import (
	"encoding/json"
	"log"
	"net/http"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"imuslab.com/zoraxy/mod/auth"
	"imuslab.com/zoraxy/mod/dynamicproxy"
	"imuslab.com/zoraxy/mod/uptime"
	"imuslab.com/zoraxy/mod/utils"
)

var (
	dynamicProxyRouter *dynamicproxy.Router
)

// Add user customizable reverse proxy
func ReverseProxtInit() {
	inboundPort := 80
	if sysdb.KeyExists("settings", "inbound") {
		sysdb.Read("settings", "inbound", &inboundPort)
		log.Println("Serving inbound port ", inboundPort)
	} else {
		log.Println("Inbound port not set. Using default (80)")
	}

	useTls := false
	sysdb.Read("settings", "usetls", &useTls)
	if useTls {
		log.Println("TLS mode enabled. Serving proxxy request with TLS")
	} else {
		log.Println("TLS mode disabled. Serving proxy request with plain http")
	}

	forceLatestTLSVersion := false
	sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
	if forceLatestTLSVersion {
		log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
	} else {
		log.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
	}

	forceHttpsRedirect := false
	sysdb.Read("settings", "redirect", &forceHttpsRedirect)
	if forceHttpsRedirect {
		log.Println("Force HTTPS mode enabled")
	} else {
		log.Println("Force HTTPS mode disabled")
	}

	dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
		HostUUID:           nodeUUID,
		Port:               inboundPort,
		UseTls:             useTls,
		ForceTLSLatest:     forceLatestTLSVersion,
		ForceHttpsRedirect: forceHttpsRedirect,
		TlsManager:         tlsCertManager,
		RedirectRuleTable:  redirectTable,
		GeodbStore:         geodbStore,
		StatisticCollector: statisticCollector,
	})
	if err != nil {
		log.Println(err.Error())
		return
	}

	dynamicProxyRouter = dprouter

	//Load all conf from files
	confs, _ := filepath.Glob("./conf/proxy/*.config")
	for _, conf := range confs {
		record, err := LoadReverseProxyConfig(conf)
		if err != nil {
			log.Println("Failed to load "+filepath.Base(conf), err.Error())
			return
		}

		if record.ProxyType == "root" {
			dynamicProxyRouter.SetRootProxy(&dynamicproxy.RootOptions{
				ProxyLocation: record.ProxyTarget,
				RequireTLS:    record.UseTLS,
			})
		} else if record.ProxyType == "subd" {
			dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
				MatchingDomain:       record.Rootname,
				Domain:               record.ProxyTarget,
				RequireTLS:           record.UseTLS,
				SkipCertValidations:  record.SkipTlsValidation,
				RequireBasicAuth:     record.RequireBasicAuth,
				BasicAuthCredentials: record.BasicAuthCredentials,
			})
		} else if record.ProxyType == "vdir" {
			dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
				RootName:             record.Rootname,
				Domain:               record.ProxyTarget,
				RequireTLS:           record.UseTLS,
				SkipCertValidations:  record.SkipTlsValidation,
				RequireBasicAuth:     record.RequireBasicAuth,
				BasicAuthCredentials: record.BasicAuthCredentials,
			})
		} else {
			log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
		}
	}

	//Start Service
	//Not sure why but delay must be added if you have another
	//reverse proxy server in front of this service
	time.Sleep(300 * time.Millisecond)
	dynamicProxyRouter.StartProxyService()
	log.Println("Dynamic Reverse Proxy service started")

	//Add all proxy services to uptime monitor
	//Create a uptime monitor service
	go func() {
		//This must be done in go routine to prevent blocking on system startup
		uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
			Targets:         GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
			Interval:        300, //5 minutes
			MaxRecordsStore: 288, //1 day
		})
		log.Println("Uptime Monitor background service started")
	}()

}

func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
	enable, _ := utils.PostPara(r, "enable") //Support root, vdir and subd
	if enable == "true" {
		err := dynamicProxyRouter.StartProxyService()
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}
	} else {
		//Check if it is loopback
		if dynamicProxyRouter.IsProxiedSubdomain(r) {
			//Loopback routing. Turning it off will make the user lost control
			//of the whole system. Do not allow shutdown
			utils.SendErrorResponse(w, "Unable to shutdown in loopback rp mode. Remove proxy rules for management interface and retry.")
			return
		}

		err := dynamicProxyRouter.StopProxyService()
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}
	}

	utils.SendOK(w)
}

func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
	eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
	if err != nil {
		utils.SendErrorResponse(w, "type not defined")
		return
	}

	endpoint, err := utils.PostPara(r, "ep")
	if err != nil {
		utils.SendErrorResponse(w, "endpoint not defined")
		return
	}

	tls, _ := utils.PostPara(r, "tls")
	if tls == "" {
		tls = "false"
	}

	useTLS := (tls == "true")

	stv, _ := utils.PostPara(r, "tlsval")
	if stv == "" {
		stv = "false"
	}

	skipTlsValidation := (stv == "true")

	rba, _ := utils.PostPara(r, "bauth")
	if rba == "" {
		rba = "false"
	}

	requireBasicAuth := (rba == "true")

	//Prase the basic auth to correct structure
	cred, _ := utils.PostPara(r, "cred")
	basicAuthCredentials := []*dynamicproxy.BasicAuthCredentials{}
	if requireBasicAuth {
		preProcessCredentials := []*dynamicproxy.BasicAuthUnhashedCredentials{}
		err = json.Unmarshal([]byte(cred), &preProcessCredentials)
		if err != nil {
			utils.SendErrorResponse(w, "invalid user credentials")
			return
		}

		//Check if there are empty password credentials
		for _, credObj := range preProcessCredentials {
			if strings.TrimSpace(credObj.Password) == "" {
				utils.SendErrorResponse(w, credObj.Username+" has empty password")
				return
			}
		}

		//Convert and hash the passwords
		for _, credObj := range preProcessCredentials {
			basicAuthCredentials = append(basicAuthCredentials, &dynamicproxy.BasicAuthCredentials{
				Username:     credObj.Username,
				PasswordHash: auth.Hash(credObj.Password),
			})
		}
	}

	rootname := ""
	if eptype == "vdir" {
		vdir, err := utils.PostPara(r, "rootname")
		if err != nil {
			utils.SendErrorResponse(w, "vdir not defined")
			return
		}

		//Vdir must start with /
		if !strings.HasPrefix(vdir, "/") {
			vdir = "/" + vdir
		}
		rootname = vdir

		thisOption := dynamicproxy.VdirOptions{
			RootName:             vdir,
			Domain:               endpoint,
			RequireTLS:           useTLS,
			SkipCertValidations:  skipTlsValidation,
			RequireBasicAuth:     requireBasicAuth,
			BasicAuthCredentials: basicAuthCredentials,
		}
		dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)

	} else if eptype == "subd" {
		subdomain, err := utils.PostPara(r, "rootname")
		if err != nil {
			utils.SendErrorResponse(w, "subdomain not defined")
			return
		}
		rootname = subdomain
		thisOption := dynamicproxy.SubdOptions{
			MatchingDomain:       subdomain,
			Domain:               endpoint,
			RequireTLS:           useTLS,
			SkipCertValidations:  skipTlsValidation,
			RequireBasicAuth:     requireBasicAuth,
			BasicAuthCredentials: basicAuthCredentials,
		}
		dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
	} else if eptype == "root" {
		rootname = "root"
		thisOption := dynamicproxy.RootOptions{
			ProxyLocation: endpoint,
			RequireTLS:    useTLS,
		}
		dynamicProxyRouter.SetRootProxy(&thisOption)
	} else {
		//Invalid eptype
		utils.SendErrorResponse(w, "Invalid endpoint type")
		return
	}

	//Save it
	thisProxyConfigRecord := Record{
		ProxyType:            eptype,
		Rootname:             rootname,
		ProxyTarget:          endpoint,
		UseTLS:               useTLS,
		SkipTlsValidation:    skipTlsValidation,
		RequireBasicAuth:     requireBasicAuth,
		BasicAuthCredentials: basicAuthCredentials,
	}
	SaveReverseProxyConfig(&thisProxyConfigRecord)

	//Update utm if exists
	if uptimeMonitor != nil {
		uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
		uptimeMonitor.CleanRecords()
	}

	utils.SendOK(w)
}

/*
ReverseProxyHandleEditEndpoint handles proxy endpoint edit
This endpoint do not handle
basic auth credential update. The credential
will be loaded from old config and reused
*/
func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
	eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
	if err != nil {
		utils.SendErrorResponse(w, "type not defined")
		return
	}

	rootNameOrMatchingDomain, err := utils.PostPara(r, "rootname")
	if err != nil {
		utils.SendErrorResponse(w, "Target proxy rule not defined")
		return
	}

	endpoint, err := utils.PostPara(r, "ep")
	if err != nil {
		utils.SendErrorResponse(w, "endpoint not defined")
		return
	}

	tls, _ := utils.PostPara(r, "tls")
	if tls == "" {
		tls = "false"
	}

	useTLS := (tls == "true")

	stv, _ := utils.PostPara(r, "tlsval")
	if stv == "" {
		stv = "false"
	}

	skipTlsValidation := (stv == "true")

	rba, _ := utils.PostPara(r, "bauth")
	if rba == "" {
		rba = "false"
	}

	requireBasicAuth := (rba == "true")

	//Load the previous basic auth credentials from current proxy rules
	targetProxyEntry, err := dynamicProxyRouter.LoadProxy(eptype, rootNameOrMatchingDomain)
	if err != nil {
		utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
		return
	}

	if eptype == "vdir" {
		thisOption := dynamicproxy.VdirOptions{
			RootName:             targetProxyEntry.RootOrMatchingDomain,
			Domain:               endpoint,
			RequireTLS:           useTLS,
			SkipCertValidations:  skipTlsValidation,
			RequireBasicAuth:     requireBasicAuth,
			BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
		}
		dynamicProxyRouter.RemoveProxy("vdir", thisOption.RootName)
		dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)

	} else if eptype == "subd" {
		thisOption := dynamicproxy.SubdOptions{
			MatchingDomain:       targetProxyEntry.RootOrMatchingDomain,
			Domain:               endpoint,
			RequireTLS:           useTLS,
			SkipCertValidations:  skipTlsValidation,
			RequireBasicAuth:     requireBasicAuth,
			BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
		}
		dynamicProxyRouter.RemoveProxy("subd", thisOption.MatchingDomain)
		dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
	}

	//Save it to file
	thisProxyConfigRecord := Record{
		ProxyType:            eptype,
		Rootname:             targetProxyEntry.RootOrMatchingDomain,
		ProxyTarget:          endpoint,
		UseTLS:               useTLS,
		SkipTlsValidation:    skipTlsValidation,
		RequireBasicAuth:     requireBasicAuth,
		BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
	}
	SaveReverseProxyConfig(&thisProxyConfigRecord)
	utils.SendOK(w)
}

func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
	ep, err := utils.GetPara(r, "ep")
	if err != nil {
		utils.SendErrorResponse(w, "Invalid ep given")
		return
	}

	ptype, err := utils.PostPara(r, "ptype")
	if err != nil {
		utils.SendErrorResponse(w, "Invalid ptype given")
		return
	}

	err = dynamicProxyRouter.RemoveProxy(ptype, ep)
	if err != nil {
		utils.SendErrorResponse(w, err.Error())
		return
	}

	RemoveReverseProxyConfig(ep)

	//Update utm if exists
	if uptimeMonitor != nil {
		uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
		uptimeMonitor.CleanRecords()
	}

	utils.SendOK(w)
}

/*
Handle update request for basic auth credential
Require paramter: ep (Endpoint) and pytype (proxy Type)
if request with GET, the handler will return current credentials
on this endpoint by its username

if request is POST, the handler will write the results to proxy config
*/
func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		ep, err := utils.GetPara(r, "ep")
		if err != nil {
			utils.SendErrorResponse(w, "Invalid ep given")
			return
		}

		ptype, err := utils.GetPara(r, "ptype")
		if err != nil {
			utils.SendErrorResponse(w, "Invalid ptype given")
			return
		}

		//Load the target proxy object from router
		targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}

		usernames := []string{}
		for _, cred := range targetProxy.BasicAuthCredentials {
			usernames = append(usernames, cred.Username)
		}

		js, _ := json.Marshal(usernames)
		utils.SendJSONResponse(w, string(js))

	} else if r.Method == http.MethodPost {
		//Write to target
		ep, err := utils.PostPara(r, "ep")
		if err != nil {
			utils.SendErrorResponse(w, "Invalid ep given")
			return
		}

		ptype, err := utils.PostPara(r, "ptype")
		if err != nil {
			utils.SendErrorResponse(w, "Invalid ptype given")
			return
		}

		if ptype != "vdir" && ptype != "subd" {
			utils.SendErrorResponse(w, "Invalid ptype given")
			return
		}

		creds, err := utils.PostPara(r, "creds")
		if err != nil {
			utils.SendErrorResponse(w, "Invalid ptype given")
			return
		}

		//Load the target proxy object from router
		targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}

		//Try to marshal the content of creds into the suitable structure
		newCredentials := []*dynamicproxy.BasicAuthUnhashedCredentials{}
		err = json.Unmarshal([]byte(creds), &newCredentials)
		if err != nil {
			utils.SendErrorResponse(w, "Malformed credential data")
			return
		}

		//Merge the credentials into the original config
		//If a new username exists in old config with no pw given, keep the old pw hash
		//If a new username is found with new password, hash it and push to credential slice
		mergedCredentials := []*dynamicproxy.BasicAuthCredentials{}
		for _, credential := range newCredentials {
			if credential.Password == "" {
				//Check if exists in the old credential files
				keepUnchange := false
				for _, oldCredEntry := range targetProxy.BasicAuthCredentials {
					if oldCredEntry.Username == credential.Username {
						//Exists! Reuse the old hash
						mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{
							Username:     oldCredEntry.Username,
							PasswordHash: oldCredEntry.PasswordHash,
						})
						keepUnchange = true
					}
				}

				if !keepUnchange {
					//This is a new username with no pw given
					utils.SendErrorResponse(w, "Access password for "+credential.Username+" is empty!")
					return
				}
			} else {
				//This username have given password
				mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{
					Username:     credential.Username,
					PasswordHash: auth.Hash(credential.Password),
				})
			}
		}

		targetProxy.BasicAuthCredentials = mergedCredentials

		//Save it to file
		thisProxyConfigRecord := Record{
			ProxyType:            ptype,
			Rootname:             targetProxy.RootOrMatchingDomain,
			ProxyTarget:          targetProxy.Domain,
			UseTLS:               targetProxy.RequireTLS,
			SkipTlsValidation:    targetProxy.SkipCertValidations,
			RequireBasicAuth:     targetProxy.RequireBasicAuth,
			BasicAuthCredentials: targetProxy.BasicAuthCredentials,
		}
		SaveReverseProxyConfig(&thisProxyConfigRecord)

		//Replace runtime configuration
		dynamicProxyRouter.SaveProxy(ptype, ep, targetProxy)
		utils.SendOK(w)
	} else {
		http.Error(w, "invalid usage", http.StatusMethodNotAllowed)
	}

}

func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
	js, _ := json.Marshal(dynamicProxyRouter)
	utils.SendJSONResponse(w, string(js))
}

func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
	eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
	if err != nil {
		utils.SendErrorResponse(w, "type not defined")
		return
	}

	if eptype == "vdir" {
		results := []*dynamicproxy.ProxyEndpoint{}
		dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
			results = append(results, value.(*dynamicproxy.ProxyEndpoint))
			return true
		})

		sort.Slice(results, func(i, j int) bool {
			return results[i].Domain < results[j].Domain
		})

		js, _ := json.Marshal(results)
		utils.SendJSONResponse(w, string(js))
	} else if eptype == "subd" {
		results := []*dynamicproxy.ProxyEndpoint{}
		dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
			results = append(results, value.(*dynamicproxy.ProxyEndpoint))
			return true
		})

		sort.Slice(results, func(i, j int) bool {
			return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
		})

		js, _ := json.Marshal(results)
		utils.SendJSONResponse(w, string(js))
	} else if eptype == "root" {
		js, _ := json.Marshal(dynamicProxyRouter.Root)
		utils.SendJSONResponse(w, string(js))
	} else {
		utils.SendErrorResponse(w, "Invalid type given")
	}
}

// Handle https redirect
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
	useRedirect, err := utils.GetPara(r, "set")
	if err != nil {
		currentRedirectToHttps := false
		//Load the current status
		err = sysdb.Read("settings", "redirect", &currentRedirectToHttps)
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}
		js, _ := json.Marshal(currentRedirectToHttps)
		utils.SendJSONResponse(w, string(js))
	} else {
		if dynamicProxyRouter.Option.Port == 80 {
			utils.SendErrorResponse(w, "This option is not available when listening on port 80")
			return
		}
		if useRedirect == "true" {
			sysdb.Write("settings", "redirect", true)
			log.Println("Updating force HTTPS redirection to true")
			dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
		} else if useRedirect == "false" {
			sysdb.Write("settings", "redirect", false)
			log.Println("Updating force HTTPS redirection to false")
			dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
		}

		utils.SendOK(w)
	}
}

// Handle checking if the current user is accessing via the reverse proxied interface
// Of the management interface.
func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
	isProxied := dynamicProxyRouter.IsProxiedSubdomain(r)
	js, _ := json.Marshal(isProxied)
	utils.SendJSONResponse(w, string(js))
}

// Handle incoming port set. Change the current proxy incoming port
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
	newIncomingPort, err := utils.PostPara(r, "incoming")
	if err != nil {
		utils.SendErrorResponse(w, "invalid incoming port given")
		return
	}

	newIncomingPortInt, err := strconv.Atoi(newIncomingPort)
	if err != nil {
		utils.SendErrorResponse(w, "invalid incoming port given")
		return
	}

	//Check if it is identical as proxy root (recursion!)
	proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
	if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
		//Listening port is same as proxy root
		//Not allow recursive settings
		utils.SendErrorResponse(w, "Recursive listening port! Check your proxy root settings.")
		return
	}

	//Stop and change the setting of the reverse proxy service
	if dynamicProxyRouter.Running {
		dynamicProxyRouter.StopProxyService()
		dynamicProxyRouter.Option.Port = newIncomingPortInt
		dynamicProxyRouter.StartProxyService()
	} else {
		//Only change setting but not starting the proxy service
		dynamicProxyRouter.Option.Port = newIncomingPortInt
	}

	sysdb.Write("settings", "inbound", newIncomingPortInt)

	utils.SendOK(w)
}
