package auth

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"net/http"
	"sort"
	"strings"
)

// DynamicClientRegistrationRequest is the RFC 7591 client-metadata subset
// accepted by the administrator-gated registration surface. Unknown RFC 7591
// members are intentionally ignored by the transport.
type DynamicClientRegistrationRequest struct {
	ClientName              string
	RedirectURIs            []string
	GrantTypes              []string
	ResponseTypes           []string
	Scope                   string
	SourcePackName          string
	TokenEndpointAuthMethod string
}

// DynamicClientRegistration is the normalized public-client record that the
// privileged persistence path turns into a Connected App.
type DynamicClientRegistration struct {
	ClientID       string
	ClientName     string
	RedirectURIs   []string
	GrantTypes     []string
	ResponseTypes  []string
	Scope          string
	SourcePackName string
}

const (
	dynamicClientIDPrefix       = "nxa_"
	maxDynamicRedirectURIs      = 10
	maxDynamicRedirectURILength = 512
	connectedAppNameColumnLen   = 255
)

// BuildAdminDynamicClientRegistration validates and normalizes an
// administrator-approved public client. This is deliberately not anonymous
// DCR: the caller already crossed an interactive browser + ManageConnectedApps
// gate, and the selected scopes must be present in the current L1 public-scope
// capability catalog. The resulting client is admin_preapproved, never
// silently trusted merely because it was dynamically shaped.
func BuildAdminDynamicClientRegistration(req DynamicClientRegistrationRequest, provider PublicScopePolicyProvider) (*DynamicClientRegistration, error) {
	if strings.TrimSpace(req.TokenEndpointAuthMethod) != "none" {
		return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata",
			"token_endpoint_auth_method must be \"none\"; only public clients are supported", "dcr_confidential_client")
	}

	redirectURIs, err := normalizeAdminDynamicRedirectURIs(req.RedirectURIs)
	if err != nil {
		return nil, err
	}
	grantTypes, err := normalizeAdminDynamicGrantTypes(req.GrantTypes)
	if err != nil {
		return nil, err
	}
	responseTypes, err := normalizeAdminDynamicResponseTypes(req.ResponseTypes)
	if err != nil {
		return nil, err
	}
	sourcePackName := strings.TrimSpace(req.SourcePackName)
	scope, err := normalizeAdminDynamicScope(req.Scope, sourcePackName, provider)
	if err != nil {
		return nil, err
	}
	clientID, err := generateAdminDynamicClientID()
	if err != nil {
		return nil, newOAuthProtocolError(http.StatusInternalServerError, "server_error", "could not generate client identifier", "dcr_client_id_generation")
	}
	clientName := strings.TrimSpace(req.ClientName)
	maxNameLen := connectedAppNameColumnLen - len(clientID) - len(" ()")
	if len(clientName) > maxNameLen {
		return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata",
			fmt.Sprintf("client_name must be at most %d characters", maxNameLen), "dcr_client_name_too_long")
	}

	return &DynamicClientRegistration{
		ClientID: clientID, ClientName: clientName, RedirectURIs: redirectURIs,
		GrantTypes: grantTypes, ResponseTypes: responseTypes, Scope: scope, SourcePackName: sourcePackName,
	}, nil
}

func normalizeAdminDynamicRedirectURIs(raw []string) ([]string, error) {
	seen := make(map[string]struct{}, len(raw))
	result := make([]string, 0, len(raw))
	for _, redirectURI := range raw {
		redirectURI = strings.TrimSpace(redirectURI)
		if redirectURI == "" {
			continue
		}
		if len(redirectURI) > maxDynamicRedirectURILength {
			return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_redirect_uri", "redirect_uri is too long", "dcr_redirect_uri_too_long")
		}
		if err := validateRedirectURIShape(redirectURI); err != nil {
			return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_redirect_uri", "redirect_uri must be https, or http on a loopback host", "dcr_redirect_uri_invalid_shape")
		}
		if _, duplicate := seen[redirectURI]; duplicate {
			continue
		}
		seen[redirectURI] = struct{}{}
		result = append(result, redirectURI)
	}
	if len(result) == 0 {
		return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_redirect_uri", "redirect_uris is required and must contain at least one valid URI", "dcr_redirect_uris_empty")
	}
	if len(result) > maxDynamicRedirectURIs {
		return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_redirect_uri", fmt.Sprintf("at most %d redirect_uris are allowed", maxDynamicRedirectURIs), "dcr_redirect_uris_too_many")
	}
	return result, nil
}

func normalizeAdminDynamicGrantTypes(raw []string) ([]string, error) {
	if len(raw) == 0 {
		// A refresh credential is durable access, so it must be explicit in the
		// administrator request rather than an implicit DCR default.
		return []string{OAuthGrantAuthorizationCode}, nil
	}
	seen := map[string]bool{}
	for _, grant := range raw {
		switch grant = strings.TrimSpace(grant); grant {
		case "", OAuthGrantAuthorizationCode, OAuthGrantRefreshToken:
			if grant != "" {
				seen[grant] = true
			}
		default:
			return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "grant_types may only contain authorization_code and refresh_token", "dcr_unsupported_grant_type")
		}
	}
	if !seen[OAuthGrantAuthorizationCode] {
		return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "grant_types must include authorization_code", "dcr_missing_authorization_code")
	}
	result := []string{OAuthGrantAuthorizationCode}
	if seen[OAuthGrantRefreshToken] {
		result = append(result, OAuthGrantRefreshToken)
	}
	return result, nil
}

func normalizeAdminDynamicResponseTypes(raw []string) ([]string, error) {
	for _, responseType := range raw {
		if responseType = strings.TrimSpace(responseType); responseType != "" && responseType != "code" {
			return nil, newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "response_types may only contain code", "dcr_unsupported_response_type")
		}
	}
	return []string{"code"}, nil
}

func normalizeAdminDynamicScope(requested, sourcePackName string, provider PublicScopePolicyProvider) (string, error) {
	// Connected App scope persistence and token validation use canonical lower
	// case. Normalize at this DCR boundary so an administrator-approved mixed
	// case request cannot be persisted successfully and then fail at exchange.
	scopes := normalizeScopeSet(strings.ToLower(requested))
	if len(scopes) == 0 {
		return "", newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "scope is required", "dcr_scope_required")
	}
	if provider == nil {
		return "", newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "dynamic client registration is not available", "dcr_no_scope_policy")
	}
	for _, scope := range scopes {
		if !publicScopePolicyAllows(provider, scope, sourcePackName) {
			return "", newOAuthProtocolError(http.StatusBadRequest, "invalid_client_metadata", "scope is not available to public clients: "+scope, "dcr_unsupported_scope")
		}
	}
	sort.Strings(scopes)
	return strings.Join(scopes, " "), nil
}

func generateAdminDynamicClientID() (string, error) {
	raw := make([]byte, 24)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return dynamicClientIDPrefix + base64.RawURLEncoding.EncodeToString(raw), nil
}
