package authorization

import (
	"github.com/authelia/authelia/v4/internal/configuration/schema"
	"github.com/authelia/authelia/v4/internal/logging"
)

// Authorizer the component in charge of checking whether a user can access a given resource.
type Authorizer struct {
	defaultPolicy Level
	rules         []*AccessControlRule
	configuration *schema.Configuration
}

// NewAuthorizer create an instance of authorizer with a given access control configuration.
func NewAuthorizer(configuration *schema.Configuration) *Authorizer {
	return &Authorizer{
		defaultPolicy: PolicyToLevel(configuration.AccessControl.DefaultPolicy),
		rules:         NewAccessControlRules(configuration.AccessControl),
		configuration: configuration,
	}
}

// IsSecondFactorEnabled return true if at least one policy is set to second factor.
func (p Authorizer) IsSecondFactorEnabled() bool {
	if p.defaultPolicy == TwoFactor {
		return true
	}

	for _, rule := range p.rules {
		if rule.Policy == TwoFactor {
			return true
		}
	}

	if p.configuration.IdentityProviders.OIDC != nil {
		for _, client := range p.configuration.IdentityProviders.OIDC.Clients {
			if client.Policy == twoFactor {
				return true
			}
		}
	}

	return false
}

// GetRequiredLevel retrieve the required level of authorization to access the object.
func (p Authorizer) GetRequiredLevel(subject Subject, object Object) Level {
	logger := logging.Logger()

	logger.Debugf("Check authorization of subject %s and object %s (method %s).",
		subject.String(), object.String(), object.Method)

	for _, rule := range p.rules {
		if rule.IsMatch(subject, object) {
			logger.Tracef(traceFmtACLHitMiss, "HIT", rule.Position, subject.String(), object.String(), object.Method)

			return rule.Policy
		}

		logger.Tracef(traceFmtACLHitMiss, "MISS", rule.Position, subject.String(), object.String(), object.Method)
	}

	logger.Debugf("No matching rule for subject %s and url %s... Applying default policy.",
		subject.String(), object.String())

	return p.defaultPolicy
}

// GetRuleMatchResults iterates through the rules and produces a list of RuleMatchResult provided a subject and object.
func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results []RuleMatchResult) {
	skipped := false

	results = make([]RuleMatchResult, len(p.rules))

	for i, rule := range p.rules {
		results[i] = RuleMatchResult{
			Rule:    rule,
			Skipped: skipped,

			MatchDomain:        isMatchForDomains(subject, object, rule),
			MatchResources:     isMatchForResources(object, rule),
			MatchMethods:       isMatchForMethods(object, rule),
			MatchNetworks:      isMatchForNetworks(subject, rule),
			MatchSubjects:      isMatchForSubjects(subject, rule),
			MatchSubjectsExact: isExactMatchForSubjects(subject, rule),
		}

		skipped = skipped || results[i].IsMatch()
	}

	return results
}