package validator

import (
	"errors"
	"fmt"
	"net"
	"path"
	"strings"

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

// ValidateSession validates and update session configuration.
func ValidateSession(config *schema.Configuration, validator *schema.StructValidator) {
	if config.Session.Name == "" {
		config.Session.Name = schema.DefaultSessionConfiguration.Name
	}

	if config.Session.Redis != nil {
		if config.Session.Redis.HighAvailability != nil {
			validateRedisSentinel(&config.Session, validator)
		} else {
			validateRedis(&config.Session, validator)
		}
	}

	validateSession(config, validator)
}

func validateSession(config *schema.Configuration, validator *schema.StructValidator) {
	if config.Session.Expiration <= 0 {
		config.Session.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour.
	}

	if config.Session.Inactivity <= 0 {
		config.Session.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min.
	}

	switch {
	case config.Session.RememberMe == schema.RememberMeDisabled:
		config.Session.DisableRememberMe = true
	case config.Session.RememberMe <= 0:
		config.Session.RememberMe = schema.DefaultSessionConfiguration.RememberMe // 1 month.
	}

	if config.Session.SameSite == "" {
		config.Session.SameSite = schema.DefaultSessionConfiguration.SameSite
	} else if !utils.IsStringInSlice(config.Session.SameSite, validSessionSameSiteValues) {
		validator.Push(fmt.Errorf(errFmtSessionSameSite, utils.StringJoinOr(validSessionSameSiteValues), config.Session.SameSite))
	}

	cookies := len(config.Session.Cookies)
	n := len(config.Session.Domain) //nolint:staticcheck

	if cookies != 0 && config.DefaultRedirectionURL != nil { //nolint:staticcheck
		validator.Push(errors.New(errFmtSessionLegacyRedirectionURL))
	}

	switch {
	case cookies == 0 && n != 0:
		validator.PushWarning(errors.New(errFmtSessionDomainLegacy))
		// Add legacy configuration to the domains list.
		config.Session.Cookies = append(config.Session.Cookies, schema.SessionCookie{
			SessionCookieCommon: schema.SessionCookieCommon{
				Name:              config.Session.Name,
				SameSite:          config.Session.SameSite,
				Expiration:        config.Session.Expiration,
				Inactivity:        config.Session.Inactivity,
				RememberMe:        config.Session.RememberMe,
				DisableRememberMe: config.Session.DisableRememberMe,
			},
			Domain:                config.Session.Domain,        //nolint:staticcheck
			DefaultRedirectionURL: config.DefaultRedirectionURL, //nolint:staticcheck
			Legacy:                true,
		})
	case cookies != 0 && n != 0:
		validator.Push(errors.New(errFmtSessionLegacyAndWarning))
	}

	validateSessionCookieDomains(&config.Session, validator)
}

func validateSessionCookieDomains(config *schema.Session, validator *schema.StructValidator) {
	if len(config.Cookies) == 0 {
		validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "cookies"))
	}

	domains := make([]string, 0)

	for i, d := range config.Cookies {
		validateSessionDomainName(i, config, validator)

		validateSessionUniqueCookieDomain(i, config, domains, validator)

		validateSessionCookieName(i, config)

		validateSessionCookiesURLs(i, config, validator)

		validateSessionExpiration(i, config)

		validateSessionRememberMe(i, config)

		validateSessionSameSite(i, config, validator)

		domains = append(domains, d.Domain)
	}
}

// validateSessionDomainName returns error if the domain name is invalid.
func validateSessionDomainName(i int, config *schema.Session, validator *schema.StructValidator) {
	var d = config.Cookies[i]

	switch {
	case d.Domain == "":
		validator.Push(fmt.Errorf(errFmtSessionDomainOptionRequired, sessionDomainDescriptor(i, d), attrSessionDomain))
		return
	case strings.HasPrefix(d.Domain, "*."):
		validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, sessionDomainDescriptor(i, d), d.Domain))
		return
	case strings.HasPrefix(d.Domain, "."):
		validator.PushWarning(fmt.Errorf(errFmtSessionDomainHasPeriodPrefix, sessionDomainDescriptor(i, d)))
	case net.ParseIP(d.Domain) != nil:
		return
	case !strings.Contains(d.Domain, "."):
		validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainNoDots, sessionDomainDescriptor(i, d)))
		return
	case !reDomainCharacters.MatchString(d.Domain):
		validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomain, sessionDomainDescriptor(i, d)))
		return
	}

	if isCookieDomainAPublicSuffix(d.Domain) {
		validator.Push(fmt.Errorf(errFmtSessionDomainInvalidDomainPublic, sessionDomainDescriptor(i, d)))
	}
}

func validateSessionCookieName(i int, config *schema.Session) {
	if config.Cookies[i].Name == "" {
		config.Cookies[i].Name = config.Name
	}
}

func validateSessionExpiration(i int, config *schema.Session) {
	if config.Cookies[i].Expiration <= 0 {
		config.Cookies[i].Expiration = config.Expiration
	}

	if config.Cookies[i].Inactivity <= 0 {
		config.Cookies[i].Inactivity = config.Inactivity
	}
}

// validateSessionUniqueCookieDomain Check the current domains do not share a root domain with previous domains.
func validateSessionUniqueCookieDomain(i int, config *schema.Session, domains []string, validator *schema.StructValidator) {
	var d = config.Cookies[i]
	if utils.IsStringInSliceF(d.Domain, domains, utils.HasDomainSuffix) {
		if utils.IsStringInSlice(d.Domain, domains) {
			validator.Push(fmt.Errorf(errFmtSessionDomainDuplicate, sessionDomainDescriptor(i, d)))
		} else {
			validator.Push(fmt.Errorf(errFmtSessionDomainDuplicateCookieScope, sessionDomainDescriptor(i, d)))
		}
	}
}

// validateSessionCookiesURLs validates the AutheliaURL and DefaultRedirectionURL.
//
//nolint:gocyclo
func validateSessionCookiesURLs(i int, config *schema.Session, validator *schema.StructValidator) {
	var d = config.Cookies[i]

	if d.AutheliaURL == nil {
		if !d.Legacy && d.Domain != "" {
			validator.Push(fmt.Errorf(errFmtSessionDomainOptionRequired, sessionDomainDescriptor(i, d), attrSessionAutheliaURL))
		}
	} else {
		switch d.AutheliaURL.Path {
		case "", "/":
			break
		default:
			if strings.HasSuffix(d.AutheliaURL.Path, "/") || !d.AutheliaURL.IsAbs() {
				break
			}

			d.AutheliaURL.Path += "/"
		}

		if !d.AutheliaURL.IsAbs() {
			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotAbsolute, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.AutheliaURL))
		} else if !utils.IsURISecure(d.AutheliaURL) {
			validator.Push(fmt.Errorf(errFmtSessionDomainURLInsecure, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.AutheliaURL))
		}

		if d.Domain != "" && !utils.HasURIDomainSuffix(d.AutheliaURL, d.Domain) {
			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotInCookieScope, sessionDomainDescriptor(i, d), attrSessionAutheliaURL, d.Domain, d.AutheliaURL))
		}
	}

	if d.DefaultRedirectionURL != nil {
		if !d.DefaultRedirectionURL.IsAbs() {
			validator.Push(fmt.Errorf(errFmtSessionDomainURLNotAbsolute, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.DefaultRedirectionURL))
		} else if !utils.IsURISecure(d.DefaultRedirectionURL) {
			validator.Push(fmt.Errorf(errFmtSessionDomainURLInsecure, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.DefaultRedirectionURL))
		}

		if d.Domain != "" && !utils.HasURIDomainSuffix(d.DefaultRedirectionURL, d.Domain) {
			if d.Legacy {
				validator.PushWarning(fmt.Errorf(errFmtSessionDomainURLNotInCookieScope, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.Domain, d.DefaultRedirectionURL))
				d.DefaultRedirectionURL = nil
			} else {
				validator.Push(fmt.Errorf(errFmtSessionDomainURLNotInCookieScope, sessionDomainDescriptor(i, d), attrDefaultRedirectionURL, d.Domain, d.DefaultRedirectionURL))
			}
		}

		if d.AutheliaURL != nil && utils.EqualURLs(d.AutheliaURL, d.DefaultRedirectionURL) {
			validator.Push(fmt.Errorf(errFmtSessionDomainAutheliaURLAndRedirectionURLEqual, sessionDomainDescriptor(i, d), d.DefaultRedirectionURL, d.AutheliaURL))
		}
	}

	config.Cookies[i] = d
}

func validateSessionRememberMe(i int, config *schema.Session) {
	if config.Cookies[i].RememberMe <= 0 && config.Cookies[i].RememberMe != schema.RememberMeDisabled {
		config.Cookies[i].RememberMe = config.RememberMe
	}

	if config.Cookies[i].RememberMe == schema.RememberMeDisabled {
		config.Cookies[i].DisableRememberMe = true
	}
}

func validateSessionSameSite(i int, config *schema.Session, validator *schema.StructValidator) {
	if config.Cookies[i].SameSite == "" {
		if utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
			config.Cookies[i].SameSite = config.SameSite
		} else {
			config.Cookies[i].SameSite = schema.DefaultSessionConfiguration.SameSite
		}
	} else if !utils.IsStringInSlice(config.Cookies[i].SameSite, validSessionSameSiteValues) {
		validator.Push(fmt.Errorf(errFmtSessionDomainSameSite, sessionDomainDescriptor(i, config.Cookies[i]), utils.StringJoinOr(validSessionSameSiteValues), config.Cookies[i].SameSite))
	}
}

func sessionDomainDescriptor(position int, domain schema.SessionCookie) string {
	return fmt.Sprintf("#%d (domain '%s')", position+1, domain.Domain)
}

func validateRedisCommon(config *schema.Session, validator *schema.StructValidator) {
	if config.Secret == "" {
		validator.Push(fmt.Errorf(errFmtSessionSecretRequired, "redis"))
	}

	if config.Redis.TLS != nil {
		configDefaultTLS := &schema.TLS{
			ServerName:     config.Redis.Host,
			MinimumVersion: schema.DefaultRedisConfiguration.TLS.MinimumVersion,
			MaximumVersion: schema.DefaultRedisConfiguration.TLS.MaximumVersion,
		}

		if err := ValidateTLSConfig(config.Redis.TLS, configDefaultTLS); err != nil {
			validator.Push(fmt.Errorf(errFmtSessionRedisTLSConfigInvalid, err))
		}
	}
}

func validateRedis(config *schema.Session, validator *schema.StructValidator) {
	if config.Redis.Host == "" {
		validator.Push(errors.New(errFmtSessionRedisHostRequired))
	}

	validateRedisCommon(config, validator)

	abs := path.IsAbs(config.Redis.Host)

	if !abs && config.Redis.Port == 0 {
		config.Redis.Port = schema.DefaultRedisConfiguration.Port
	} else if !abs && (config.Redis.Port < 1 || config.Redis.Port > 65535) {
		validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
	}

	if config.Redis.MaximumActiveConnections <= 0 {
		config.Redis.MaximumActiveConnections = schema.DefaultRedisConfiguration.MaximumActiveConnections
	}
}

func validateRedisSentinel(config *schema.Session, validator *schema.StructValidator) {
	if config.Redis.HighAvailability.SentinelName == "" {
		validator.Push(errors.New(errFmtSessionRedisSentinelMissingName))
	}

	if config.Redis.Port == 0 {
		config.Redis.Port = 26379
	} else if config.Redis.Port < 1 || config.Redis.Port > 65535 {
		validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
	}

	if config.Redis.Host == "" && len(config.Redis.HighAvailability.Nodes) == 0 {
		validator.Push(errors.New(errFmtSessionRedisHostOrNodesRequired))
	}

	validateRedisCommon(config, validator)

	hostMissing := false

	for i, node := range config.Redis.HighAvailability.Nodes {
		if node.Host == "" {
			hostMissing = true
		}

		if node.Port == 0 {
			config.Redis.HighAvailability.Nodes[i].Port = 26379
		}
	}

	if hostMissing {
		validator.Push(errors.New(errFmtSessionRedisSentinelNodeHostMissing))
	}
}
