package security

import (
	"encoding/base64"
	"net/http"

	g8 "github.com/TwiN/g8/v2"
	"github.com/gorilla/mux"
	"golang.org/x/crypto/bcrypt"
)

const (
	cookieNameState   = "gatus_state"
	cookieNameNonce   = "gatus_nonce"
	cookieNameSession = "gatus_session"
)

// Config is the security configuration for Gatus
type Config struct {
	Basic *BasicConfig `yaml:"basic,omitempty"`
	OIDC  *OIDCConfig  `yaml:"oidc,omitempty"`

	gate *g8.Gate
}

// IsValid returns whether the security configuration is valid or not
func (c *Config) IsValid() bool {
	return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.isValid())
}

// RegisterHandlers registers all handlers required based on the security configuration
func (c *Config) RegisterHandlers(router *mux.Router) error {
	if c.OIDC != nil {
		if err := c.OIDC.initialize(); err != nil {
			return err
		}
		router.HandleFunc("/oidc/login", c.OIDC.loginHandler)
		router.HandleFunc("/authorization-code/callback", c.OIDC.callbackHandler)
	}
	return nil
}

// ApplySecurityMiddleware applies an authentication middleware to the router passed.
// The router passed should be a subrouter in charge of handlers that require authentication.
func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
	if c.OIDC != nil {
		// We're going to use g8 for session handling
		clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
			if _, exists := sessions.Get(token); exists {
				return g8.NewClient(token)
			}
			return nil
		})
		customTokenExtractorFunc := func(request *http.Request) string {
			sessionCookie, err := request.Cookie(cookieNameSession)
			if err != nil {
				return ""
			}
			return sessionCookie.Value
		}
		// TODO: g8: Add a way to update cookie after? would need the writer
		authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
		c.gate = g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
		api.Use(c.gate.Protect)
	} else if c.Basic != nil {
		var decodedBcryptHash []byte
		if len(c.Basic.PasswordBcryptHashBase64Encoded) > 0 {
			var err error
			decodedBcryptHash, err = base64.URLEncoding.DecodeString(c.Basic.PasswordBcryptHashBase64Encoded)
			if err != nil {
				return err
			}
		}
		api.Use(func(handler http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				usernameEntered, passwordEntered, ok := r.BasicAuth()
				if len(c.Basic.PasswordBcryptHashBase64Encoded) > 0 {
					if !ok || usernameEntered != c.Basic.Username || bcrypt.CompareHashAndPassword(decodedBcryptHash, []byte(passwordEntered)) != nil {
						w.Header().Set("WWW-Authenticate", "Basic")
						w.WriteHeader(http.StatusUnauthorized)
						_, _ = w.Write([]byte("Unauthorized"))
						return
					}
				}
				handler.ServeHTTP(w, r)
			})
		})
	}
	return nil
}

// IsAuthenticated checks whether the user is authenticated
// If the Config does not warrant authentication, it will always return true.
func (c *Config) IsAuthenticated(r *http.Request) bool {
	if c.gate != nil {
		token := c.gate.ExtractTokenFromRequest(r)
		_, hasSession := sessions.Get(token)
		return hasSession
	}
	return false
}
