package sessionv1

import (
	"context"
	"strings"
	"unicode"
	"unicode/utf8"
)

func ValidateCreateRequest(ctx context.Context, request CreateRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if err := validateAppInstallationID(request.AppInstallationID); err != nil {
		return err
	}
	if !validOptional(request.SourceAddress, MaxSourceAddressBytes) ||
		!validOptional(request.UserAgent, MaxUserAgentBytes) ||
		!validOptional(request.RequestedScope, MaxRequestedScopeBytes) {
		return invalid(InvalidComponentFieldValue)
	}
	switch request.Method {
	case AuthenticationMethodPassword:
		if !validRequired(request.ApplicationClientID, MaxApplicationClientIDBytes) ||
			request.FederatedProviderID != "" {
			return invalid(InvalidComponentOperation)
		}
	case AuthenticationMethodFederatedIDToken:
		if request.ApplicationClientID != "" || request.RequestedScope != "" ||
			!validRequired(request.FederatedProviderID, MaxProviderIDBytes) {
			return invalid(InvalidComponentOperation)
		}
	default:
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateReadRequest(ctx context.Context, request ReadRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if err := validateAppInstallationID(request.AppInstallationID); err != nil {
		return err
	}
	if request.UseControlProof && !request.AllowRenewal {
		return invalid(InvalidComponentOperation)
	}
	if request.DocumentPolicy != nil && len(request.DocumentPolicy.LinkRoutes) > MaxDocumentLinkRoutes {
		return &Failure{Category: FailureResourceExhausted, LimitKind: LimitOperationCount, Maximum: MaxDocumentLinkRoutes}
	}
	if !validDocumentPolicyRequest(request.DocumentPolicy) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateRevokeRequest(ctx context.Context, request RevokeRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	return validateAppInstallationID(request.AppInstallationID)
}

func ValidateCreateResponse(request CreateRequest, response CreateResponse) error {
	if !response.AccessTokenIssued || response.ControlProofIssued != request.IssueControlProof ||
		!response.Session.valid() {
		return &Failure{Category: FailureInternal}
	}
	return nil
}

func ValidateReadResponse(request ReadRequest, response ReadResponse) error {
	if !response.Session.valid() || response.ControlProofExpiresAtUnixSecond < 0 ||
		(response.ReplacementAccessTokenIssued && !request.AllowRenewal) ||
		(response.ReplacementAccessTokenIssued && request.UseControlProof &&
			response.ControlProofExpiresAtUnixSecond == 0) ||
		(response.ControlProofExpiresAtUnixSecond > 0 &&
			(!request.UseControlProof || !response.ReplacementAccessTokenIssued)) {
		return &Failure{Category: FailureInternal}
	}
	if (request.DocumentPolicy != nil) != (response.DocumentPolicy != nil) || !validDocumentPolicy(response.DocumentPolicy) {
		return &Failure{Category: FailureInternal}
	}
	return nil
}

func ValidateRevokeResponse(RevokeResponse) error { return nil }

func (session Session) valid() bool {
	return session.ExpiresAtUnixSecond > 0 && session.Principal.valid()
}

func (principal Principal) valid() bool {
	return validRequired(principal.PrincipalID, MaxIdentifierBytes) &&
		validRequired(principal.DisplayName, MaxDisplayNameBytes) &&
		validOptional(principal.Email, MaxEmailBytes) &&
		validRequired(principal.ProfileID, MaxIdentifierBytes) &&
		validOptional(principal.RoleID, MaxIdentifierBytes)
}

func validateContext(ctx context.Context) error {
	if ctx == nil {
		return invalid(InvalidComponentDeadline)
	}
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: FailureDeadlineExceeded}
		}
		return &Failure{Category: FailureCanceled}
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(InvalidComponentDeadline)
	}
	return nil
}

func validateAppInstallationID(value string) error {
	if validRequired(value, MaxIdentifierBytes) {
		return nil
	}
	if len(value) > MaxIdentifierBytes {
		return &Failure{Category: FailureResourceExhausted, LimitKind: LimitAppInstallationID, Maximum: MaxIdentifierBytes}
	}
	return invalid(InvalidComponentAppInstallationID)
}

func validRequired(value string, maximum int) bool {
	return value != "" && validOptional(value, maximum)
}

func validOptional(value string, maximum int) bool {
	if len(value) > maximum || !utf8.ValidString(value) || strings.TrimSpace(value) != value {
		return false
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return false
		}
	}
	return true
}

func invalid(component InvalidComponent) *Failure {
	return &Failure{Category: FailureInvalidRequest, InvalidComponent: component}
}

func validDocumentPolicyRequest(policy *DocumentPolicyRequest) bool {
	if policy == nil {
		return true
	}
	if !validDocumentPath(policy.RouteFamilyPrefix) || !strings.HasSuffix(policy.RouteFamilyPrefix, "/") {
		return false
	}
	for _, route := range policy.LinkRoutes {
		if !validRequired(route.Link, 128) || !validDocumentPath(route.Path) || !validRequired(route.Method, 16) || strings.ContainsAny(route.Method, " \t\r\n") {
			return false
		}
	}
	return true
}
func validDocumentPath(value string) bool {
	return validRequired(value, 2048) && strings.HasPrefix(value, "/") && !strings.HasPrefix(value, "//") && !strings.ContainsAny(value, "?#\\\r\n") && !strings.Contains(value, "..")
}
func validDocumentPolicy(policy *DocumentPolicy) bool {
	if policy == nil {
		return true
	}
	if policy.HourlyLimit < 0 || policy.DailyLimit < 0 || (policy.HourlyLimit == 0) != (policy.DailyLimit == 0) || len(policy.AllowedLinks) > MaxDocumentAllowedLinks || (!policy.Restricted && len(policy.AllowedLinks) != 0) {
		return false
	}
	seen := map[string]bool{}
	for _, link := range policy.AllowedLinks {
		if !validRequired(link, 128) || seen[link] {
			return false
		}
		seen[link] = true
	}
	return true
}
