package server

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"strings"

	"github.com/MicahParks/keyfunc/v3"
	"github.com/golang-jwt/jwt/v5"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/ioz"
)

const (
	temporalNamespaceHeaderName = "X-Namespace"
	decodePath                  = "/decode"
)

type oidcProvider struct {
	namespace            string
	authorizedUserEmails []string
	jwks                 keyfunc.Keyfunc
}

type openIDConfig struct {
	JWKSURI string `json:"jwks_uri"`
}

type temporalClaims struct {
	jwt.RegisteredClaims
	Email string `json:"https://saas-api.tmprl.cloud/user/email"`
}

func newOIDCProvider(ctx context.Context, rt http.RoundTripper, url, namespace string, authorizedUserEmails []string) (provider *oidcProvider, retErr error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
	if err != nil {
		return nil, err
	}
	resp, err := rt.RoundTrip(req) //nolint:bodyclose
	if err != nil {
		return nil, err
	}
	defer errz.SafeClose(resp.Body, &retErr)

	config := &openIDConfig{}
	err = ioz.ReadAllFunc(resp.Body, func(body []byte) error {
		return json.Unmarshal(body, config)
	})
	if err != nil {
		return nil, err
	}

	jwks, err := keyfunc.NewDefault([]string{config.JWKSURI})
	if err != nil {
		return nil, err
	}

	return &oidcProvider{
		namespace:            namespace,
		authorizedUserEmails: authorizedUserEmails,
		jwks:                 jwks,
	}, nil
}

func (p *oidcProvider) authorize(r *http.Request) error {
	if r.URL.Path != decodePath {
		return errors.New("invalid decode URI path")
	}

	if !httpz.HasHeaderValue(r.Header, temporalNamespaceHeaderName, p.namespace) {
		return errors.New("invalid namespace")
	}

	authHeader := r.Header.Get(httpz.AuthorizationHeader)
	if authHeader == "" {
		return errors.New("missing authorization header")
	}

	encodedToken, found := strings.CutPrefix(authHeader, "Bearer")
	if !found {
		return errors.New("authorization header is not a Bearer token")
	}

	encodedToken = strings.TrimSpace(encodedToken)
	var claims temporalClaims
	_, err := jwt.ParseWithClaims(encodedToken, &claims, p.jwks.Keyfunc)
	if err != nil {
		return err
	}

	if !slices.Contains(p.authorizedUserEmails, claims.Email) {
		return fmt.Errorf("user %q is not authorized", claims.Email)
	}

	return nil
}
