package domain

//nolint:gosec // support old clients

import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"fmt"
	"hash"
	"strconv"
	"strings"

	"source.toby3d.me/toby3d/auth/internal/common"
)

// CodeChallengeMethod represent a PKCE challenge method for validate verifier.
//
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
type CodeChallengeMethod struct {
	codeChallengeMethod string
}

//nolint:gochecknoglobals // structs cannot be constants
var (
	CodeChallengeMethodUnd   = CodeChallengeMethod{codeChallengeMethod: ""}      // "und"
	CodeChallengeMethodPLAIN = CodeChallengeMethod{codeChallengeMethod: "plain"} // "PLAIN"
	CodeChallengeMethodMD5   = CodeChallengeMethod{codeChallengeMethod: "md5"}   // "MD5"
	CodeChallengeMethodS1    = CodeChallengeMethod{codeChallengeMethod: "s1"}    // "S1"
	CodeChallengeMethodS256  = CodeChallengeMethod{codeChallengeMethod: "s256"}  // "S256"
	CodeChallengeMethodS512  = CodeChallengeMethod{codeChallengeMethod: "s512"}  // "S512"
)

var ErrCodeChallengeMethodUnknown error = NewError(
	ErrorCodeInvalidRequest,
	"unknown code_challene_method",
	"https://indieauth.net/source/#authorization-request",
)

//nolint:gochecknoglobals // maps cannot be constants
var uidsMethods = map[string]CodeChallengeMethod{
	CodeChallengeMethodMD5.codeChallengeMethod:   CodeChallengeMethodMD5,
	CodeChallengeMethodPLAIN.codeChallengeMethod: CodeChallengeMethodPLAIN,
	CodeChallengeMethodS1.codeChallengeMethod:    CodeChallengeMethodS1,
	CodeChallengeMethodS256.codeChallengeMethod:  CodeChallengeMethodS256,
	CodeChallengeMethodS512.codeChallengeMethod:  CodeChallengeMethodS512,
}

// ParseCodeChallengeMethod parse string identifier of code challenge method
// into struct enum.
func ParseCodeChallengeMethod(uid string) (CodeChallengeMethod, error) {
	if method, ok := uidsMethods[strings.ToLower(uid)]; ok {
		return method, nil
	}

	return CodeChallengeMethodUnd, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, uid)
}

// UnmarshalForm implements custom unmarshler for form values.
func (ccm *CodeChallengeMethod) UnmarshalForm(v []byte) error {
	method, err := ParseCodeChallengeMethod(string(v))
	if err != nil {
		return fmt.Errorf("CodeChallengeMethod: UnmarshalForm: %w", err)
	}

	*ccm = method

	return nil
}

// UnmarshalJSON implements custom unmarshler for JSON.
func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error {
	src, err := strconv.Unquote(string(v))
	if err != nil {
		return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: %w", err)
	}

	if *ccm, err = ParseCodeChallengeMethod(src); err != nil {
		return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: %w", err)
	}

	return nil
}

func (ccm CodeChallengeMethod) MarshalJSON() ([]byte, error) {
	return []byte(strconv.Quote(ccm.codeChallengeMethod)), nil
}

// String returns string representation of code challenge method.
func (ccm CodeChallengeMethod) String() string {
	if ccm.codeChallengeMethod != "" {
		return strings.ToUpper(ccm.codeChallengeMethod)
	}

	return common.Und
}

func (ccm CodeChallengeMethod) GoString() string {
	return "domain.CodeChallengeMethod(" + ccm.String() + ")"
}

// Validate checks for a match to the verifier with the hashed version of the
// challenge via the chosen method.
func (ccm CodeChallengeMethod) Validate(codeChallenge, verifier string) bool {
	var h hash.Hash

	switch ccm {
	default:
		return false
	case CodeChallengeMethodPLAIN:
		return codeChallenge == verifier
	case CodeChallengeMethodMD5:
		h = md5.New()
	case CodeChallengeMethodS1:
		h = sha1.New()
	case CodeChallengeMethodS256:
		h = sha256.New()
	case CodeChallengeMethodS512:
		h = sha512.New()
	}

	if _, err := h.Write([]byte(verifier)); err != nil {
		return false
	}

	return codeChallenge == base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}
