package authentik

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/url"
	"strings"

	"imuslab.com/zoraxy/mod/database"
	"imuslab.com/zoraxy/mod/info/logger"
	"imuslab.com/zoraxy/mod/utils"
)

type AuthentikRouterOptions struct {
	UseHTTPS     bool   //If the Authentik server is using HTTPS
	AuthentikURL string //The URL of the Authentik server
	Logger       *logger.Logger
	Database     *database.Database
}

type AuthentikRouter struct {
	options *AuthentikRouterOptions
}

// NewAuthentikRouter creates a new AuthentikRouter object
func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter {
	options.Database.NewTable("authentik")

	//Read settings from database, if exists
	options.Database.Read("authentik", "authentikURL", &options.AuthentikURL)
	options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS)

	return &AuthentikRouter{
		options: options,
	}
}

// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS
func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		//Return the current settings
		js, _ := json.Marshal(map[string]interface{}{
			"useHTTPS":     ar.options.UseHTTPS,
			"authentikURL": ar.options.AuthentikURL,
		})

		utils.SendJSONResponse(w, string(js))
		return
	} else if r.Method == http.MethodPost {
		//Update the settings
		AuthentikURL, err := utils.PostPara(r, "authentikURL")
		if err != nil {
			utils.SendErrorResponse(w, "authentikURL not found")
			return
		}

		useHTTPS, err := utils.PostBool(r, "authentikUseHttps")
		if err != nil {
			useHTTPS = false
		}

		//Write changes to runtime
		ar.options.AuthentikURL = AuthentikURL
		ar.options.UseHTTPS = useHTTPS

		//Write changes to database
		ar.options.Database.Write("authentik", "authentikURL", AuthentikURL)
		ar.options.Database.Write("authentik", "useHTTPS", useHTTPS)

		utils.SendOK(w)
	} else {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

}

// HandleAuthentikAuth is the internal handler for Authentik authentication
// Set useHTTPS to true if your Authentik server is using HTTPS
// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com
func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
	const outpostPrefix = "outpost.goauthentik.io"
	client := &http.Client{}

	if ar.options.AuthentikURL == "" {
		ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil)
		w.WriteHeader(500)
		w.Write([]byte("500 - Internal Server Error"))
		return errors.New("authentik URL not set")
	}
	protocol := "http"
	if ar.options.UseHTTPS {
		protocol = "https"
	}

	authentikBaseURL := protocol + "://" + ar.options.AuthentikURL
	//Remove tailing slash if any
	authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/")

	scheme := "http"
	if r.TLS != nil {
		scheme = "https"
	}
	reqUrl := scheme + "://" + r.Host + r.RequestURI
	// Pass request to outpost if path matches outpost prefix
	if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) {
		req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body)
		if err != nil {
			ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
			w.WriteHeader(401)
			return errors.New("unauthorized")
		}
		req.Header.Set("X-Original-URL", reqUrl)
		req.Header.Set("Host", r.Host)
		for _, cookie := range r.Cookies() {
			req.AddCookie(cookie)
		}
		if resp, err := client.Do(req); err != nil {
			ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err)
			w.WriteHeader(http.StatusInternalServerError)
			return errors.New("internal server error")
		} else {
			defer resp.Body.Close()
			for k := range resp.Header {
				w.Header().Set(k, resp.Header.Get(k))
			}
			w.WriteHeader(resp.StatusCode)
			if _, err = io.Copy(w, resp.Body); err != nil {
				ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err)
				w.WriteHeader(http.StatusInternalServerError)
				return errors.New("internal server error")
			}
		}
		return nil
	}

	//Make a request to Authentik to verify the request
	req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil)
	if err != nil {
		ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
		w.WriteHeader(401)
		return errors.New("unauthorized")
	}

	req.Header.Set("X-Original-URL", reqUrl)

	// Copy cookies from the incoming request
	for _, cookie := range r.Cookies() {
		req.AddCookie(cookie)
	}

	// Making the verification request
	resp, err := client.Do(req)
	if err != nil {
		ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err)
		w.WriteHeader(401)
		return errors.New("unauthorized")
	}

	if resp.StatusCode != 200 {
		redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String())
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return errors.New("unauthorized")
	}

	return nil
}
