package main

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/gookit/goutil/dump"
	"zeevdiukman.com/zprox/internal/config"
	"zeevdiukman.com/zprox/internal/logic"
	"zeevdiukman.com/zprox/internal/reverse_proxy"
	"zeevdiukman.com/zprox/internal/router"
	"zeevdiukman.com/zprox/pkg/helper"
)

const DEVELOPMENT bool = true

type EntryPoints map[string]EntryPoint
type EntryPoint struct {
	Name  string
	Group string
	*http.Server
}
type ReverseProxies map[string]ReverseProxy
type ReverseProxy *httputil.ReverseProxy

var (
	appData *logic.App = logic.NewApp()

	// configData *config.Config
)

func main() {
	helper.AppRunner(func() {
		configData := config.Get()
		//////////TESTING AREA//////////////////

		////////////////////////////////////////
		groups := logic.NewGroups()
		mainRouter := router.New()
		groups.ForEach(func(groupName string, g *logic.Group) {
			mainRouter.AddGroupRouter(groupName)
			mainRouter.GetGroupRouter(groupName).Mux.Use(Domain_Middleware)
			for k := range g.ReverseProxies {
				rpConfig := configData.ReverseProxies[k]
				domain := rpConfig.Domain
				proxy := reverse_proxy.New(rpConfig.Host)
				proxy.Name = domain
				mainRouter.AddHostSubRouter(groupName, domain)
				if rpConfig.Auth != "" {
					if _, ok := configData.AuthMap[rpConfig.Auth]; !ok {
						err := errors.New("Error: Auth " + rpConfig.Auth + " not exist!")
						panic(err.Error())
					}
					pths := configData.AuthMap[rpConfig.Auth].Paths
					hostRouterMux := mainRouter.GetHostSubRouter(groupName, domain).Mux
					hostRouterMux.Use(Middleware_SetHeaders)
					hostRouterMux.Use(authMiddleware)

					authRoute := hostRouterMux.NewRoute()
					authSubRouter := authRoute.PathPrefix(pths.Prefix).Subrouter()
					authSubRouter.Path(pths.Login).Handler(http.HandlerFunc(LoginHandler))
					authSubRouter.Path(pths.Logout).Handler(http.HandlerFunc(LogoutHandler))
					authSubRouter.Path(pths.Callback).Handler(http.HandlerFunc(CallbackHandler))
				}
				mainRouter.GetHostSubRouter(groupName, domain).Mux.PathPrefix("/").Handler(proxy.Httputil)
				// Filter out static file requests first

			}

			if len(g.ReverseProxies) > 0 {
				tlsConfig := &tls.Config{

					GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
						// crt, key := "", ""

						crt, key := configData.GetCertsPairByDomain(info.ServerName)

						if crt == "" && key == "" {
							// crt = configData.TLS.Certs["default"].Cert
							// key = configData.TLS.Certs["default"].Key
							// panic("Error: TLS cert and key not found!")

						}
						cert, err := loadCertificate(crt, key)
						if err != nil {
							return nil, err
						}
						return &cert, nil
					},
				}
				server := &http.Server{
					Addr:      ":" + g.Port,
					Handler:   appData.SessionManager.LoadAndSave(mainRouter.GetGroupRouter(groupName).Mux),
					TLSConfig: tlsConfig,
				}
				var err error
				go func() {
					ipAddr := helper.GetIP()
					log.Println("Test server is running at http://" + ipAddr + ":" + g.Port)
					if g.TLS {
						err = server.ListenAndServeTLS("", "")
					} else {
						err = server.ListenAndServe()
					}
					if err != nil {
						log.Println(err.Error())
					}
				}()
			}

		})
		helper.StartTestHTTPServer(3000, "app")
		helper.StartTestHTTPServer(3001, "oded")
	})
}

// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		configData := config.Get()
		currentPath := r.URL.Path
		authName := configData.GetAuthNameByDomain(r.Host)
		loginPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Login
		logoutPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Logout
		callbackPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Callback
		// TODO: mark auth reverse proxy in yaml
		proxyName := configData.GetProxyNameByDomain(r.Host)
		if configData.ReverseProxies[proxyName].IsAuthServer {
			next.ServeHTTP(w, r)
			return
		}

		switch currentPath {
		case loginPath:
			{
				// fmt.Fprintln(w, "LOGIN")
				next.ServeHTTP(w, r)

			}
		case logoutPath:
			{
				next.ServeHTTP(w, r)
				// return
			}
		case callbackPath:
			{

				next.ServeHTTP(w, r)
				// return
			}
		default:
			{

				accessToken := appData.SessionManager.GetString(r.Context(), "access_token")
				if accessToken == "" {

					authName := configData.DataMaps.DomainToAuth[r.Host]
					http.Redirect(w, r, configData.AuthMap[authName].Paths.Prefix+configData.AuthMap[authName].Paths.Login, http.StatusFound)
					return
				}
				// auth.SetAuthHeader(w, accessToken)
				a := configData.AuthMap[authName]
				pths := a.Paths
				prefix := pths.Prefix
				login := pths.Login
				logout := pths.Logout
				loginPath := prefix + login
				logoutPath := prefix + logout
				if loginPath == r.URL.Path || logoutPath == r.URL.Path {
					next.ServeHTTP(w, r)
					// return
				}

				tokenOk := IsAuthorizedJWT(accessToken, authName)

				if !tokenOk {
					http.Redirect(w, r, loginPath, http.StatusFound)
					return
				}
				next.ServeHTTP(w, r)
			}
		}
	})

}

func Domain_Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// c := config.Get()

		// requestedPath := r.URL.Path
		// authName := configData.GetAuthNameByDomain(r.Host)
		// auth := configData.AuthMap[authName]
		// excludedPaths := []string{
		// 	auth.Paths.Prefix + auth.Paths.Login,
		// 	auth.Paths.Prefix + auth.Paths.Callback,
		// 	auth.Paths.Prefix + auth.Paths.Logout,
		// }
		// contains := helper.IsSliceContains(excludedPaths, requestedPath)
		// contains := slices.Contains(excludedPaths, requestedPath)
		// if !contains {
		// appData.SessionManager.Put(r.Context(), "original_path", requestedPath)
		// }
		// dump.P(requestedPath)

		next.ServeHTTP(w, r)
	})
}

func Middleware_SetHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		r.Header.Set("X-Forwarded-Proto", getProto(r))
		r.Header.Set("X-Forwarded-For", r.RemoteAddr)
		r.Header.Set("X-Forwarded-Host", r.Host)
		r.Header.Set("X-Real-IP", r.RemoteAddr)
		next.ServeHTTP(w, r)
	})
}
func getProto(req *http.Request) string {
	if req.TLS != nil {
		return "https"
	} else {
		return "http"
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////

type TokenResponse struct {
	AccessToken      string `json:"access_token"`
	ExpiresIn        int    `json:"expires_in"`
	RefreshExpiresIn int    `json:"refresh_expires_in"`
	TokenType        string `json:"token_type"`
	NotBeforePolicy  int    `json:"not-before-policy"`
	SessionState     string `json:"session_state"`
	Scope            string `json:"scope"`
	RefreshToken     string `json:"refresh_token"`
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

type Res401Struct struct {
	Status   string `json:"status" example:"FAILED"`
	HTTPCode int    `json:"httpCode" example:"401"`
	Message  string `json:"message" example:"authorisation failed"`
}

type Claims struct {
	ResourceAccess client `json:"resource_access,omitempty"`
	JTI            string `json:"jti,omitempty"`
}

type client struct {
	DemoServiceClient clientRoles `json:"DemoServiceClient,omitempty"`
}

type clientRoles struct {
	Roles []string `json:"roles,omitempty"`
}
type HandlerFuncConfigWrapper func(*config.Config, http.ResponseWriter, *http.Request) *http.Handler

// HANDLERS
// ////////////

// AUTH FUNCTIONS
// //////////////////
func generateState(redirectURL string) string {
	b := make([]byte, 32)
	rand.Read(b)
	stateData := map[string]string{"redirect_uri": redirectURL, "random": base64.StdEncoding.EncodeToString(b)}
	jsonData, _ := json.Marshal(stateData)
	return base64.StdEncoding.EncodeToString(jsonData)
}
func decodeState(encodedState string) (string, error) {
	input := encodedState
	b64data := input[strings.IndexByte(input, ',')+1:]

	decoded, err := base64.StdEncoding.DecodeString(b64data)
	if err != nil {
		return "", err
	}
	var stateData map[string]string
	err = json.Unmarshal(decoded, &stateData)
	if err != nil {
		return "", err
	}
	return stateData["redirect_uri"], nil
}

func exchangeCode(code string, verifier string, authName string) (*TokenResponse, string, error) {
	configData := config.Get()
	clientID := configData.AuthMap[authName].OpenID.ClientID
	clientSecert := configData.AuthMap[authName].OpenID.ClientSecert
	redirectURI := configData.AuthMap[authName].OpenID.RedirectURI
	// if strings.Contains(redirectURI,"<{{dynamic}}>") {
	// 	configData.GenerateDynamicRedirectUri()
	// }

	// GenerateDynamicRedirectUri
	data := url.Values{}
	data.Set("grant_type", "authorization_code")
	data.Set("client_id", clientID)
	data.Set("client_secret", clientSecert)
	data.Set("redirect_uri", redirectURI)
	data.Set("code", code)
	data.Set("scope", "openid zapp")
	if verifier != "" {
		data.Set("code_verifier", verifier)
	}
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: DEVELOPMENT},
	}
	client := &http.Client{Transport: tr}
	u := configData.AuthMap[authName].OpenID.EndPoints.Token
	r, _ := http.NewRequest(http.MethodPost, u, strings.NewReader(data.Encode()))
	r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	resp, err := client.Do(r)

	if err != nil {
		dump.Println("ERROR exchange code: " + err.Error())
		return nil, "", err
	}
	respBytes, err := io.ReadAll(resp.Body)
	tokenResponse := &TokenResponse{}

	json.Unmarshal(respBytes, &tokenResponse)

	if err != nil {
		dump.Println("ERROR exchange code Unmarshal: " + err.Error())
		return nil, "", err
	}
	if tokenResponse.Error != "" {
		dump.Println(tokenResponse.Error + ": " + tokenResponse.ErrorDescription)
	}
	fullResponse := string(respBytes)

	return tokenResponse, fullResponse, nil
}

func generateCodeVerifier() (string, error) {
	verifier := make([]byte, 32)
	_, err := rand.Read(verifier)
	if err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(verifier), nil
}

func generateCodeChallenge(verifier string) string {
	hash := sha256.Sum256([]byte(verifier))
	return base64.RawURLEncoding.EncodeToString(hash[:])
}

func IsAuthorizedJWT(rawAccessToken string, authName string) bool {
	configData := config.Get()

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{
		Timeout:   time.Duration(6000) * time.Second,
		Transport: tr,
	}
	ctx := oidc.ClientContext(context.Background(), client)
	provider, err := oidc.NewProvider(ctx, configData.AuthMap[authName].OpenID.EndPoints.Issuer)
	if err != nil {
		dump.Println("authorisation failed while getting the provider: " + err.Error())
		return false
	}

	oidcConfig := &oidc.Config{
		ClientID: configData.AuthMap[authName].OpenID.ClientID,
	}
	verifier := provider.Verifier(oidcConfig)
	idToken, err := verifier.Verify(ctx, rawAccessToken)
	if err != nil {
		dump.Println("authorisation failed while verifying the token: " + err.Error())
		return false
	}

	var IDTokenClaims Claims // ID Token payload is just JSON.
	if err := idToken.Claims(&IDTokenClaims); err != nil {
		dump.Println("claims: " + err.Error())
		return false
	}
	return true
}

///////////////////////////////////
///////////////////////////////////

// func isStaticFileRequest(path string) bool {
// 	// Check for common static file prefixes
// 	if strings.HasPrefix(path, "/static/") || strings.HasPrefix(path, "/assets/") {
// 		return true
// 	}

// 	// Check for common static file extensions
// 	staticExtensions := []string{
// 		".css", ".js", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".ico",
// 	}

// 	for _, ext := range staticExtensions {
// 		if strings.HasSuffix(path, ext) {
// 			return true
// 		}
// 	}

// 	return false
// }
