package oidc

import (
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
)

type errorType string

const (
	InvalidRequest       errorType = "invalid_request"
	InvalidScope         errorType = "invalid_scope"
	InvalidClient        errorType = "invalid_client"
	InvalidGrant         errorType = "invalid_grant"
	UnauthorizedClient   errorType = "unauthorized_client"
	UnsupportedGrantType errorType = "unsupported_grant_type"
	ServerError          errorType = "server_error"
	InteractionRequired  errorType = "interaction_required"
	LoginRequired        errorType = "login_required"
	RequestNotSupported  errorType = "request_not_supported"

	// Additional error codes as defined in
	// https://www.rfc-editor.org/rfc/rfc8628#section-3.5
	// Device Access Token Response
	AuthorizationPending errorType = "authorization_pending"
	SlowDown             errorType = "slow_down"
	AccessDenied         errorType = "access_denied"
	ExpiredToken         errorType = "expired_token"

	// InvalidTarget error is returned by Token Exchange if
	// the requested target or audience is invalid.
	// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
	InvalidTarget errorType = "invalid_target"
)

var (
	ErrInvalidRequest = func() *Error {
		return &Error{
			ErrorType: InvalidRequest,
		}
	}
	ErrInvalidRequestRedirectURI = func() *Error {
		return &Error{
			ErrorType:        InvalidRequest,
			redirectDisabled: true,
		}
	}
	ErrInvalidScope = func() *Error {
		return &Error{
			ErrorType: InvalidScope,
		}
	}
	ErrInvalidClient = func() *Error {
		return &Error{
			ErrorType: InvalidClient,
		}
	}
	ErrInvalidGrant = func() *Error {
		return &Error{
			ErrorType: InvalidGrant,
		}
	}
	ErrUnauthorizedClient = func() *Error {
		return &Error{
			ErrorType: UnauthorizedClient,
		}
	}
	ErrUnsupportedGrantType = func() *Error {
		return &Error{
			ErrorType: UnsupportedGrantType,
		}
	}
	ErrServerError = func() *Error {
		return &Error{
			ErrorType: ServerError,
		}
	}
	ErrInteractionRequired = func() *Error {
		return &Error{
			ErrorType: InteractionRequired,
		}
	}
	ErrLoginRequired = func() *Error {
		return &Error{
			ErrorType: LoginRequired,
		}
	}
	ErrRequestNotSupported = func() *Error {
		return &Error{
			ErrorType: RequestNotSupported,
		}
	}

	// Device Access Token errors:
	ErrAuthorizationPending = func() *Error {
		return &Error{
			ErrorType:   AuthorizationPending,
			Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.",
		}
	}
	ErrSlowDown = func() *Error {
		return &Error{
			ErrorType:   SlowDown,
			Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.",
		}
	}
	ErrAccessDenied = func() *Error {
		return &Error{
			ErrorType:   AccessDenied,
			Description: "The authorization request was denied.",
		}
	}
	ErrExpiredDeviceCode = func() *Error {
		return &Error{
			ErrorType:   ExpiredToken,
			Description: "The \"device_code\" has expired.",
		}
	}

	// Token exchange error
	ErrInvalidTarget = func() *Error {
		return &Error{
			ErrorType:   InvalidTarget,
			Description: "The requested audience or target is invalid.",
		}
	}
)

type Error struct {
	Parent           error     `json:"-" schema:"-"`
	ErrorType        errorType `json:"error" schema:"error"`
	Description      string    `json:"error_description,omitempty" schema:"error_description,omitempty"`
	State            string    `json:"state,omitempty" schema:"state,omitempty"`
	SessionState     string    `json:"session_state,omitempty" schema:"session_state,omitempty"`
	redirectDisabled bool      `schema:"-"`
	returnParent     bool      `schema:"-"`
}

func (e *Error) MarshalJSON() ([]byte, error) {
	m := struct {
		Error            errorType `json:"error"`
		ErrorDescription string    `json:"error_description,omitempty"`
		State            string    `json:"state,omitempty"`
		SessionState     string    `json:"session_state,omitempty"`
		Parent           string    `json:"parent,omitempty"`
	}{
		Error:            e.ErrorType,
		ErrorDescription: e.Description,
		State:            e.State,
		SessionState:     e.SessionState,
	}
	if e.returnParent {
		m.Parent = e.Parent.Error()
	}
	return json.Marshal(m)
}

func (e *Error) Error() string {
	message := "ErrorType=" + string(e.ErrorType)
	if e.Description != "" {
		message += " Description=" + e.Description
	}
	if e.Parent != nil {
		message += " Parent=" + e.Parent.Error()
	}
	return message
}

func (e *Error) Unwrap() error {
	return e.Parent
}

func (e *Error) Is(target error) bool {
	t, ok := target.(*Error)
	if !ok {
		return false
	}
	return e.ErrorType == t.ErrorType &&
		(e.Description == t.Description || t.Description == "") &&
		(e.State == t.State || t.State == "") &&
		(e.SessionState == t.SessionState || t.SessionState == "")
}

func (e *Error) WithParent(err error) *Error {
	e.Parent = err
	return e
}

// WithReturnParentToClient allows returning the set parent error to the HTTP client.
// Currently, it only supports setting the parent inside JSON responses, not redirect URLs.
// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
//
// Warning: parent errors may contain sensitive data or unwanted details about the server status.
// Also, the `parent` field is not a standard error field and might confuse certain clients
// that require fully compliant responses.
func (e *Error) WithReturnParentToClient(b bool) *Error {
	e.returnParent = b
	return e
}

func (e *Error) WithDescription(desc string, args ...any) *Error {
	e.Description = fmt.Sprintf(desc, args...)
	return e
}

func (e *Error) IsRedirectDisabled() bool {
	return e.redirectDisabled
}

// DefaultToServerError checks if the error is an Error
// if not the provided error will be wrapped into a ServerError
func DefaultToServerError(err error, description string) *Error {
	oidcErr := new(Error)
	if ok := errors.As(err, &oidcErr); ok {
		clone := *oidcErr
		return &clone
	}
	switch {
	case errors.Is(err, ErrParse):
		oidcErr = ErrInvalidRequest().WithParent(err).WithDescription("%s", description)
	case errors.Is(err, ErrIssuerInvalid),
		errors.Is(err, ErrSubjectMissing),
		errors.Is(err, ErrAudience),
		errors.Is(err, ErrAzpMissing),
		errors.Is(err, ErrAzpInvalid),
		errors.Is(err, ErrSignatureMissing),
		errors.Is(err, ErrSignatureMultiple),
		errors.Is(err, ErrSignatureUnsupportedAlg),
		errors.Is(err, ErrSignatureInvalidPayload),
		errors.Is(err, ErrSignatureInvalid),
		errors.Is(err, ErrExpired),
		errors.Is(err, ErrIatMissing),
		errors.Is(err, ErrIatInFuture),
		errors.Is(err, ErrIatToOld),
		errors.Is(err, ErrNonceInvalid),
		errors.Is(err, ErrAcrInvalid),
		errors.Is(err, ErrAuthTimeNotPresent),
		errors.Is(err, ErrAuthTimeToOld),
		errors.Is(err, ErrAtHash):
		oidcErr = ErrInvalidGrant().WithParent(err).WithDescription("%s", description)
	case errors.Is(err, ErrDiscoveryFailed):
		fallthrough
	default:
		oidcErr = ErrServerError().WithParent(err).WithDescription("%s", description)
	}
	return oidcErr
}

func (e *Error) LogLevel() slog.Level {
	level := slog.LevelWarn
	if e.ErrorType == ServerError {
		level = slog.LevelError
	}
	if e.ErrorType == AuthorizationPending {
		level = slog.LevelInfo
	}
	return level
}

func (e *Error) LogValue() slog.Value {
	attrs := make([]slog.Attr, 0, 5)
	if e.Parent != nil {
		attrs = append(attrs, slog.Any("parent", e.Parent))
	}
	if e.Description != "" {
		attrs = append(attrs, slog.String("description", e.Description))
	}
	if e.ErrorType != "" {
		attrs = append(attrs, slog.String("type", string(e.ErrorType)))
	}
	if e.State != "" {
		attrs = append(attrs, slog.String("state", e.State))
	}
	if e.SessionState != "" {
		attrs = append(attrs, slog.String("session_state", e.SessionState))
	}
	if e.redirectDisabled {
		attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
	}
	return slog.GroupValue(attrs...)
}
