package validator

import (
	"fmt"
	"regexp"
	"strings"

	"github.com/authelia/authelia/v4/internal/authorization"
	"github.com/authelia/authelia/v4/internal/configuration/schema"
	"github.com/authelia/authelia/v4/internal/utils"
)

// ValidateAccessControl validates access control configuration.
func ValidateAccessControl(config *schema.Configuration, validator *schema.StructValidator) {
	if config.AccessControl.DefaultPolicy == "" {
		config.AccessControl.DefaultPolicy = policyDeny
	}

	if !IsPolicyValid(config.AccessControl.DefaultPolicy) {
		validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyValue, utils.StringJoinOr(validACLRulePolicies), config.AccessControl.DefaultPolicy))
	}
}

// ValidateRules validates an ACL Rule configuration.
func ValidateRules(config *schema.Configuration, validator *schema.StructValidator) {
	if len(config.AccessControl.Rules) == 0 {
		if config.AccessControl.DefaultPolicy != policyOneFactor && config.AccessControl.DefaultPolicy != policyTwoFactor {
			validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyWithoutRules, config.AccessControl.DefaultPolicy))

			return
		}

		validator.PushWarning(fmt.Errorf(errFmtAccessControlWarnNoRulesDefaultPolicy, config.AccessControl.DefaultPolicy))

		return
	}

	for i, rule := range config.AccessControl.Rules {
		rulePosition := i + 1

		validateDomains(rulePosition, rule, validator)

		switch rule.Policy {
		case "":
			validator.Push(fmt.Errorf(errFmtAccessControlRuleNoPolicy, ruleDescriptor(rulePosition, rule)))
		default:
			if !IsPolicyValid(rule.Policy) {
				validator.Push(fmt.Errorf(errFmtAccessControlRuleInvalidPolicy, ruleDescriptor(rulePosition, rule), utils.StringJoinOr(validACLRulePolicies), rule.Policy))
			}
		}

		validateSubjects(rulePosition, rule, config, validator)

		validateMethods(rulePosition, rule, validator)

		validateQuery(i, rule, config, validator)

		if rule.Policy == policyBypass {
			validateBypass(rulePosition, rule, validator)
		}
	}
}

func validateBypass(rulePosition int, rule schema.AccessControlRule, validator *schema.StructValidator) {
	if len(rule.Subjects) != 0 {
		validator.Push(fmt.Errorf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(rulePosition, rule)))
	}

	for _, pattern := range rule.DomainsRegex {
		if utils.IsStringSliceContainsAny(authorization.IdentitySubexpNames, pattern.SubexpNames()) {
			validator.Push(fmt.Errorf(errAccessControlRuleBypassPolicyInvalidWithSubjectsWithGroupDomainRegex, ruleDescriptor(rulePosition, rule)))
			return
		}
	}
}

func validateDomains(rulePosition int, rule schema.AccessControlRule, validator *schema.StructValidator) {
	if len(rule.Domains)+len(rule.DomainsRegex) == 0 {
		validator.Push(fmt.Errorf(errFmtAccessControlRuleNoDomains, ruleDescriptor(rulePosition, rule)))
	}

	for i, domain := range rule.Domains {
		if len(domain) > 1 && domain[0] == '*' && domain[1] != '.' {
			validator.PushWarning(fmt.Errorf("access_control: rule #%d: domain #%d: domain '%s' is ineffective and should probably be '%s' instead", rulePosition, i+1, domain, fmt.Sprintf("*.%s", domain[1:])))
		}
	}
}

func validateSubjects(rulePosition int, rule schema.AccessControlRule, config *schema.Configuration, validator *schema.StructValidator) {
	var (
		id      string
		isValid bool
	)

	for _, subjectRule := range rule.Subjects {
		for _, subject := range subjectRule {
			if id, isValid = IsSubjectValid(subject); !isValid {
				validator.Push(fmt.Errorf(errFmtAccessControlRuleSubjectInvalid, ruleDescriptor(rulePosition, rule), subject))

				continue
			}

			if len(id) != 0 && !IsSubjectValidOAuth20(config, id) {
				validator.Push(fmt.Errorf(errFmtAccessControlRuleOAuth2ClientSubjectInvalid, ruleDescriptor(rulePosition, rule), subject, id))
			}
		}
	}
}

func validateMethods(rulePosition int, rule schema.AccessControlRule, validator *schema.StructValidator) {
	invalid, duplicates := validateList(rule.Methods, validACLHTTPMethodVerbs, true)

	if len(invalid) != 0 {
		validator.Push(fmt.Errorf(errFmtAccessControlRuleInvalidEntries, ruleDescriptor(rulePosition, rule), "methods", utils.StringJoinOr(validACLHTTPMethodVerbs), utils.StringJoinAnd(invalid)))
	}

	if len(duplicates) != 0 {
		validator.Push(fmt.Errorf(errFmtAccessControlRuleInvalidDuplicates, ruleDescriptor(rulePosition, rule), "methods", utils.StringJoinAnd(duplicates)))
	}
}

//nolint:gocyclo
func validateQuery(i int, rule schema.AccessControlRule, config *schema.Configuration, validator *schema.StructValidator) {
	for j := 0; j < len(config.AccessControl.Rules[i].Query); j++ {
		for k := 0; k < len(config.AccessControl.Rules[i].Query[j]); k++ {
			if config.AccessControl.Rules[i].Query[j][k].Operator == "" {
				if config.AccessControl.Rules[i].Query[j][k].Key != "" {
					switch config.AccessControl.Rules[i].Query[j][k].Value {
					case "", nil:
						config.AccessControl.Rules[i].Query[j][k].Operator = operatorPresent
					default:
						config.AccessControl.Rules[i].Query[j][k].Operator = operatorEqual
					}
				}
			} else if !utils.IsStringInSliceFold(config.AccessControl.Rules[i].Query[j][k].Operator, validACLRuleOperators) {
				validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalid, ruleDescriptor(i+1, rule), utils.StringJoinOr(validACLRuleOperators), config.AccessControl.Rules[i].Query[j][k].Operator))
			}

			if config.AccessControl.Rules[i].Query[j][k].Key == "" {
				validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidNoValue, ruleDescriptor(i+1, rule), "key"))
			}

			op := config.AccessControl.Rules[i].Query[j][k].Operator

			if op == "" {
				continue
			}

			switch v := config.AccessControl.Rules[i].Query[j][k].Value.(type) {
			case nil:
				if op != operatorAbsent && op != operatorPresent {
					validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidNoValueOperator, ruleDescriptor(i+1, rule), "value", op))
				}
			case string:
				switch op {
				case operatorPresent, operatorAbsent:
					if v != "" {
						validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValue, ruleDescriptor(i+1, rule), "value", op))
					}
				case operatorPattern, operatorNotPattern:
					var (
						pattern *regexp.Regexp
						err     error
					)
					if pattern, err = regexp.Compile(v); err != nil {
						validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValueParse, ruleDescriptor(i+1, rule), "value", err))
					} else {
						config.AccessControl.Rules[i].Query[j][k].Value = pattern
					}
				}
			default:
				validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValueType, ruleDescriptor(i+1, rule), v))
			}
		}
	}
}

// IsPolicyValid check if policy is valid.
func IsPolicyValid(policy string) (isValid bool) {
	return utils.IsStringInSlice(policy, validACLRulePolicies)
}

// IsSubjectValid validates if a subject has a valid prefix and returns the client id if applicable.
func IsSubjectValid(subject string) (id string, isValid bool) {
	switch {
	case IsSubjectValidBasic(subject):
		return "", true
	case strings.HasPrefix(subject, "oauth2:client:"):
		return strings.TrimPrefix(subject, "oauth2:client:"), true
	default:
		return "", false
	}
}

func IsSubjectValidBasic(subject string) (isValid bool) {
	return strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
}

func IsSubjectValidOAuth20(config *schema.Configuration, id string) (isValid bool) {
	if config.IdentityProviders.OIDC == nil || len(config.IdentityProviders.OIDC.Clients) == 0 {
		return false
	}

	for _, client := range config.IdentityProviders.OIDC.Clients {
		if client.ID == id {
			return true
		}
	}

	return false
}

func ruleDescriptor(position int, rule schema.AccessControlRule) string {
	if len(rule.Domains) == 0 {
		return fmt.Sprintf("#%d", position)
	}

	return fmt.Sprintf("#%d (domain '%s')", position, strings.Join(rule.Domains, ","))
}
