package oidc

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/zitadel/oidc/v3/pkg/client"
	"github.com/zitadel/oidc/v3/pkg/client/rp"
	utils "github.com/zitadel/oidc/v3/pkg/http"
	"github.com/zitadel/oidc/v3/pkg/oidc"

	"github.com/photoprism/photoprism/internal/event"
	"github.com/photoprism/photoprism/pkg/authn"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/log/status"
	"github.com/photoprism/photoprism/pkg/rnd"
)

// Client represents an OpenID Connect (OIDC) Relying Party Client.
type Client struct {
	rp.RelyingParty
	insecure bool
}

// NewClient creates a new OpenID Connect (OIDC) Relying Party (RP) client using the provided discovery URL,
// client credentials, scopes, and site URL.
func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl string, insecure bool) (result *Client, err error) {
	if issuerUri == nil {
		err = errors.New("issuer uri required")
		event.AuditErr([]string{"oidc", "provider", status.Error(err)})
		return nil, errors.New("issuer uri required")
	} else if !insecure && issuerUri.Scheme != "https" {
		err = errors.New("issuer uri must use https")
		event.AuditErr([]string{"oidc", "provider", status.Error(err)})
		return nil, err
	}

	// Get redirect URL based on site URL.
	redirectUrl, urlErr := RedirectURL(siteUrl)

	if urlErr != nil {
		event.AuditErr([]string{"oidc", "redirect url", status.Error(urlErr)})
		return nil, urlErr
	}

	// Generate cryptographic keys.
	var hashKey, encryptKey []byte

	if hashKey, err = rnd.RandomBytes(16); err != nil {
		event.AuditErr([]string{"oidc", "hash key", status.Error(err)})
		return nil, err
	}

	if encryptKey, err = rnd.RandomBytes(16); err != nil {
		event.AuditErr([]string{"oidc", "encrypt key", status.Error(err)})
		return nil, err
	}

	// Create cookie handler.
	cookieHandler := utils.NewCookieHandler(hashKey, encryptKey, utils.WithUnsecure())

	// Create HTTP client.
	httpClient := HttpClient(insecure)

	// Set OIDC Relying Party client options.
	clientOpt := []rp.Option{
		rp.WithHTTPClient(httpClient),
		rp.WithCookieHandler(cookieHandler),
		rp.WithVerifierOpts(
			rp.WithIssuedAtOffset(5 * time.Second),
		),
		rp.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
			event.AuditErr([]string{"oidc", "%s", "%s (state %s)"}, errorType, errorDesc, state)
			w.WriteHeader(http.StatusInternalServerError)
			w.Header().Add("oidc_error", fmt.Sprintf("oidc: %s", errorDesc))
		}),
	}

	// Perform service discovery through the standardized /.well-known/openid-configuration endpoint.
	discover, err := client.Discover(context.TODO(), issuerUri.String(), httpClient)

	if err != nil {
		event.AuditErr([]string{"oidc", "provider", "service discovery", status.Error(err)})
		return nil, err
	}

	// If possible, use Proof of Key Code Exchange (PKCE).
	for _, v := range discover.CodeChallengeMethodsSupported {
		if v == oidc.CodeChallengeMethodS256 {
			clientOpt = append(clientOpt, rp.WithPKCE(cookieHandler))
		}
	}

	// Set default scopes if no scopes were specified.
	if oidcScopes == "" {
		oidcScopes = authn.OidcRequiredScopes
	}

	event.AuditDebug([]string{"oidc", "provider", "scopes", oidcScopes})

	// Parse scopes into string slice.
	scopes := clean.Scopes(oidcScopes)

	// Create RelyingParty provider.
	provider, err := rp.NewRelyingPartyOIDC(context.TODO(), issuerUri.String(), oidcClient, oidcSecret, redirectUrl, scopes, clientOpt...)

	if err != nil {
		event.AuditErr([]string{"oidc", "provider", status.Error(err)})
		return nil, err
	}

	if provider.IsPKCE() {
		event.AuditDebug([]string{"oidc", "provider", "pkce", "enabled"})
	} else {
		event.AuditDebug([]string{"oidc", "provider", "pkce", "disabled"})
	}

	// Return OIDC Client with RelyingParty provider.
	return &Client{
		provider,
		insecure,
	}, nil
}

// AuthURLHandler redirects a browser to the login page of the configured OIDC identity provider.
func (c *Client) AuthURLHandler(ctx *gin.Context) {
	handle := rp.AuthURLHandler(rnd.State, c)
	handle(ctx.Writer, ctx.Request)
}

// CodeExchangeUserInfo verifies a redirect auth request and returns the user information and tokens if successful.
func (c *Client) CodeExchangeUserInfo(ctx *gin.Context) (userInfo *oidc.UserInfo, tokens *oidc.Tokens[*oidc.IDTokenClaims], err error) {
	getInfo := func(w http.ResponseWriter, r *http.Request, t *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, i *oidc.UserInfo) {
		userInfo = i
		tokens = t
	}

	// It would also be possible to directly get the user info from the oidc.IDTokenClaims
	// without performing a request to the userinfo endpoint of the OIDC identity provider.
	handle := rp.CodeExchangeHandler(rp.UserinfoCallback(getInfo), c)

	handle(ctx.Writer, ctx.Request)

	if sc := ctx.Writer.Status(); sc != 0 && sc != http.StatusOK {
		if oidcErr := ctx.Writer.Header().Get("oidc_error"); oidcErr == "" {
			err = errors.New("failed to exchange token for user info")
		} else {
			err = errors.New(oidcErr)
		}

		event.SystemError([]string{"oidc", "code exchange", "status %d", "%s"}, sc, err.Error())

		return userInfo, tokens, err
	}

	return userInfo, tokens, nil
}
