package idp

import (
	"net"
	"net/url"
	"path"
	"strings"
)

func redirectURL(raw string) (*url.URL, string) {
	u, err := url.Parse(raw)
	if err != nil || u.Hostname() == "" || u.Opaque != "" {
		return nil, "invalid_url"
	}
	if u.User != nil {
		return nil, "userinfo"
	}
	if u.RawQuery != "" || u.ForceQuery {
		return nil, "query"
	}
	if u.Fragment != "" || strings.Contains(raw, "#") {
		return nil, "fragment"
	}
	if u.Scheme != "https" {
		ip := net.ParseIP(u.Hostname())
		if u.Scheme != "http" || ip == nil || !ip.IsLoopback() {
			return nil, "scheme"
		}
	}
	// Reject alternate encodings and traversal rather than normalizing an allowlist match.
	cleanPath := path.Clean("/" + u.Path)
	if u.RawPath != "" {
		return nil, "encoded_path"
	}
	if strings.Contains(u.Path, "\\") {
		return nil, "backslash"
	}
	if u.Path != "" && u.Path != "/" && cleanPath != strings.TrimRight(u.Path, "/") {
		return nil, "noncanonical_path"
	}
	return u, ""
}

func validRedirectURIs(uris []string, config string) bool {
	return redirectURIRejection(uris, config) == ""
}

func redirectURIRejection(uris []string, config string) string {
	if len(uris) == 0 || len(uris) > 8 {
		return "redirect_count"
	}
	var prefixes []*url.URL
	if config != "" {
		for _, raw := range strings.Split(config, ",") {
			u, issue := redirectURL(strings.TrimSpace(raw))
			if issue != "" {
				return "invalid_policy"
			}
			prefixes = append(prefixes, u)
		}
	}
	for _, raw := range uris {
		if len(raw) > 2048 {
			return "redirect_too_long"
		}
		if config == "" {
			continue
		}
		u, issue := redirectURL(raw)
		if issue != "" {
			return issue
		}
		matched := false
		for _, prefix := range prefixes {
			base := strings.TrimRight(prefix.Path, "/")
			if u.Scheme == prefix.Scheme && u.Host == prefix.Host && (u.Path == base || strings.HasPrefix(u.Path, base+"/")) {
				matched = true
				break
			}
		}
		if !matched {
			return "unmatched_prefix"
		}
	}
	return ""
}
