package server

import (
	stdlog "log"
	"log/slog"
	"net/http"
	"strings"

	"github.com/jcmturner/goidentity/v6"
	"github.com/jcmturner/gokrb5/v8/keytab"
	"github.com/jcmturner/gokrb5/v8/service"
	"github.com/jcmturner/gokrb5/v8/spnego"
)

// internalUserHeader is the request header the built-in authentication
// middlewares (Kerberos, Basic) use to hand the validated username to the
// downstream access pipeline when no external TrustedUserHeader is configured. It
// is always stripped from inbound requests before validation, so a client cannot
// spoof it.
const internalUserHeader = "X-Uopi-User"

// kerberosAuth wraps next with SPNEGO/Kerberos ("Negotiate") authentication. On a
// successful handshake the authenticated principal's short username (realm
// stripped) is written into userHeader, so the existing access pipeline
// (accessMiddleware / wsHandler, which both read userHeader) resolves identity
// uniformly whether it originated from a trusted proxy header or a Kerberos
// ticket.
//
// challenge controls behaviour when a request carries no valid Negotiate
// credentials:
//   - challenge=true  (REST/page requests): delegate to gokrb5, which replies
//     401 + WWW-Authenticate: Negotiate so the browser performs SPNEGO.
//   - challenge=false (WebSocket upgrades): fall through unauthenticated.
//     Browsers cannot attach an Authorization header when opening a WebSocket;
//     they only send Negotiate proactively to trusted URIs. When the header is
//     present we still validate it, otherwise the session resolves to
//     default_user exactly as before.
//
// Any inbound value of userHeader is always discarded before validation: with
// native Kerberos there is no trusted proxy stripping client-supplied headers, so
// only the SPNEGO-validated identity may set it.
func kerberosAuth(kt *keytab.Keytab, spn, userHeader string, challenge bool, log *slog.Logger, next http.Handler) http.Handler {
	opts := []func(*service.Settings){
		service.Logger(stdlog.New(slogWriter{log: log}, "", 0)),
	}
	if spn != "" {
		opts = append(opts, service.KeytabPrincipal(spn))
	}

	// inner runs only after a successful SPNEGO handshake: it copies the resolved
	// identity into userHeader and continues down the chain.
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := goidentity.FromHTTPRequestContext(r)
		if id == nil {
			http.Error(w, "kerberos: missing identity", http.StatusUnauthorized)
			return
		}
		user := id.UserName()
		if i := strings.IndexByte(user, '@'); i >= 0 {
			user = user[:i]
		}
		r.Header.Set(userHeader, user)
		next.ServeHTTP(w, r)
	})
	validate := spnego.SPNEGOKRB5Authenticate(inner, kt, opts...)

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Never trust a client-supplied identity header under native Kerberos.
		r.Header.Del(userHeader)

		negotiate := strings.HasPrefix(r.Header.Get("Authorization"), spnego.HTTPHeaderAuthResponseValueKey)
		if !negotiate && !challenge {
			// Best-effort path (WebSocket without proactive credentials): continue
			// unauthenticated; downstream resolves the session to default_user.
			next.ServeHTTP(w, r)
			return
		}
		// Credentials are present (validate them) or a challenge is required.
		validate.ServeHTTP(w, r)
	})
}

// slogWriter adapts the std logger gokrb5 expects to slog at debug level so SPNEGO
// validation diagnostics surface without polluting normal output.
type slogWriter struct{ log *slog.Logger }

func (s slogWriter) Write(p []byte) (int, error) {
	if s.log != nil {
		s.log.Debug("kerberos", "msg", strings.TrimRight(string(p), "\n"))
	}
	return len(p), nil
}
