package packs

import (
	"fmt"
	"sort"
	"strings"
)

func connectedAppOwnershipKeyFromProjection(projection sourceBackedConnectedAppProjection) ConnectedAppOwnershipKey {
	return ConnectedAppOwnershipKey{
		ID:                      projection.ID,
		Name:                    projection.Name,
		ConsumerKey:             projection.ConsumerKey,
		CallbackURL:             projection.CallbackURL,
		RedirectURIs:            append([]string(nil), projection.RedirectURIs...),
		Scopes:                  append([]string(nil), projection.Scopes...),
		IsActive:                projection.IsActive,
		ClientType:              projection.ClientType,
		RequirePKCE:             projection.RequirePKCE,
		TokenEndpointAuthMethod: projection.TokenEndpointAuthMethod,
		AllowedGrantTypes:       append([]string(nil), projection.AllowedGrantTypes...),
		ConsentPolicy:           projection.ConsentPolicy,
		ConsentVersion:          projection.ConsentVersion,
		IssueRefreshTokens:      projection.IssueRefreshTokens,
		PermittedUsers:          projection.PermittedUsers,
		ConsumerSecretPresent:   !projection.ConsumerSecretPlaintextEmpty || !projection.ConsumerSecretMatches,
	}
}

func (k ConnectedAppOwnershipKey) OwnerlessLegacyAdoptionMismatch(candidate ConnectedAppOwnershipKey) string {
	expected := k.normalized()
	live := candidate.normalized()
	if reason := expected.ownerlessLegacyAdoptionEligibilityMismatch("source-backed connected_app"); reason != "" {
		return reason
	}
	if reason := live.ownerlessLegacyAdoptionEligibilityMismatch("ownerless legacy connected_app"); reason != "" {
		return reason
	}
	checks := []struct {
		label string
		got   string
		want  string
	}{
		{"ConsumerKey", live.ConsumerKey, expected.ConsumerKey},
		{"CallbackURL", live.CallbackURL, expected.CallbackURL},
		{"TokenEndpointAuthMethod", live.TokenEndpointAuthMethod, expected.TokenEndpointAuthMethod},
		{"ConsentPolicy", live.ConsentPolicy, expected.ConsentPolicy},
		{"PermittedUsers", live.PermittedUsers, expected.PermittedUsers},
	}
	for _, check := range checks {
		if check.got != check.want {
			return fmt.Sprintf("%s = %q, want %q", check.label, check.got, check.want)
		}
	}
	if expected.ConsentVersion != live.ConsentVersion {
		return fmt.Sprintf("ConsentVersion = %d, want %d", live.ConsentVersion, expected.ConsentVersion)
	}
	if expected.IssueRefreshTokens != live.IssueRefreshTokens {
		return fmt.Sprintf("IssueRefreshTokens = %t, want %t", live.IssueRefreshTokens, expected.IssueRefreshTokens)
	}
	if !sameConnectedAppOwnershipStringSet(live.RedirectURIs, expected.RedirectURIs) {
		return fmt.Sprintf("RedirectURIs = %q, want %q", strings.Join(live.RedirectURIs, " "), strings.Join(expected.RedirectURIs, " "))
	}
	if !sameConnectedAppOwnershipStringSet(live.Scopes, expected.Scopes) {
		return fmt.Sprintf("Scopes = %q, want %q", strings.Join(live.Scopes, " "), strings.Join(expected.Scopes, " "))
	}
	if !sameConnectedAppOwnershipStringSet(live.AllowedGrantTypes, expected.AllowedGrantTypes) {
		return fmt.Sprintf("AllowedGrantTypes = %q, want %q", strings.Join(live.AllowedGrantTypes, " "), strings.Join(expected.AllowedGrantTypes, " "))
	}
	return ""
}

func (k ConnectedAppOwnershipKey) OwnerlessLegacyAdoptionEligible() bool {
	return k.normalized().ownerlessLegacyAdoptionEligibilityMismatch("source-backed connected_app") == ""
}

func (k ConnectedAppOwnershipKey) normalized() ConnectedAppOwnershipKey {
	return ConnectedAppOwnershipKey{
		ID:                      strings.TrimSpace(k.ID),
		Name:                    strings.TrimSpace(k.Name),
		ConsumerKey:             strings.TrimSpace(k.ConsumerKey),
		CallbackURL:             strings.TrimSpace(k.CallbackURL),
		RedirectURIs:            connectedAppOwnershipStringSet(k.RedirectURIs),
		Scopes:                  connectedAppOwnershipStringSet(k.Scopes),
		IsActive:                k.IsActive,
		ClientType:              strings.ToLower(strings.TrimSpace(k.ClientType)),
		RequirePKCE:             k.RequirePKCE,
		TokenEndpointAuthMethod: strings.ToLower(strings.TrimSpace(k.TokenEndpointAuthMethod)),
		AllowedGrantTypes:       connectedAppOwnershipStringSet(k.AllowedGrantTypes),
		ConsentPolicy:           strings.ToLower(strings.TrimSpace(k.ConsentPolicy)),
		ConsentVersion:          k.ConsentVersion,
		IssueRefreshTokens:      k.IssueRefreshTokens,
		PermittedUsers:          strings.TrimSpace(k.PermittedUsers),
		ConsumerSecretPresent:   k.ConsumerSecretPresent,
	}
}

func (k ConnectedAppOwnershipKey) ownerlessLegacyAdoptionEligibilityMismatch(subject string) string {
	switch {
	case strings.TrimSpace(k.ID) == "":
		return subject + " id is empty"
	case strings.TrimSpace(k.Name) == "":
		return subject + " name is empty"
	case strings.TrimSpace(k.ConsumerKey) == "":
		return subject + " ConsumerKey is empty"
	case strings.TrimSpace(k.CallbackURL) == "":
		return subject + " CallbackURL is empty"
	case !k.IsActive:
		return subject + " is inactive"
	case strings.ToLower(strings.TrimSpace(k.ClientType)) != sourceConnectedAppClientTypePublic:
		return subject + " is not public"
	case !k.RequirePKCE:
		return subject + " does not require PKCE"
	case strings.ToLower(strings.TrimSpace(k.TokenEndpointAuthMethod)) != "none":
		return subject + " tokenEndpointAuthMethod is not none"
	case k.ConsumerSecretPresent:
		return subject + " has consumer secret material"
	case !sameConnectedAppOwnershipStringSet(k.AllowedGrantTypes, []string{"authorization_code"}):
		return subject + " allowedGrantTypes is not authorization_code"
	case !strings.EqualFold(strings.TrimSpace(k.PermittedUsers), "AllUsers"):
		return subject + " PermittedUsers is not AllUsers"
	}
	return ""
}

func connectedAppOwnershipStringSet(values []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		key := strings.ToLower(trimmed)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, trimmed)
	}
	sort.Slice(out, func(i, j int) bool {
		return strings.ToLower(out[i]) < strings.ToLower(out[j])
	})
	return out
}

func sameConnectedAppOwnershipStringSet(a, b []string) bool {
	left := connectedAppOwnershipStringSet(a)
	right := connectedAppOwnershipStringSet(b)
	if len(left) != len(right) {
		return false
	}
	for i := range left {
		if !strings.EqualFold(left[i], right[i]) {
			return false
		}
	}
	return true
}
