package config

import (
	"fmt"
	"net/url"
	"regexp"
	"strings"
)

type URLConfig struct {
	git Environment
}

func NewURLConfig(git Environment) *URLConfig {
	if git == nil {
		git = EnvironmentOf(make(mapFetcher))
	}

	return &URLConfig{
		git: git,
	}
}

// Get retrieves a `http.{url}.{key}` for the given key and urls, following the
// rules in https://git-scm.com/docs/git-config#git-config-httplturlgt.
// The value for `http.{key}` is returned as a fallback if no config keys are
// set for the given urls.
func (c *URLConfig) Get(prefix, rawurl, key string) (string, bool) {
	if c == nil {
		return "", false
	}

	key = strings.ToLower(key)
	prefix = strings.ToLower(prefix)
	if v := c.getAll(prefix, rawurl, key); len(v) > 0 {
		return v[len(v)-1], true
	}
	return c.git.Get(strings.Join([]string{prefix, key}, "."))
}

func (c *URLConfig) GetAll(prefix, rawurl, key string) []string {
	if c == nil {
		return nil
	}

	key = strings.ToLower(key)
	prefix = strings.ToLower(prefix)
	if v := c.getAll(prefix, rawurl, key); len(v) > 0 {
		return v
	}
	return c.git.GetAll(strings.Join([]string{prefix, key}, "."))
}

func (c *URLConfig) Bool(prefix, rawurl, key string, def bool) bool {
	s, _ := c.Get(prefix, rawurl, key)
	return Bool(s, def)
}

func (c *URLConfig) getAll(prefix, rawurl, key string) []string {
	type urlMatch struct {
		key       string // The full configuration key
		hostScore int    // A score indicating the strength of the host match
		pathScore int    // A score indicating the strength of the path match
		userMatch int    // Whether we matched on a username. 1 for yes, else 0
	}

	searchURL, err := url.Parse(rawurl)
	if err != nil {
		return nil
	}

	config := c.git.All()

	re := regexp.MustCompile(fmt.Sprintf(`\A%s\.(\S+)\.%s\z`, prefix, key))

	bestMatch := urlMatch{
		key:       "",
		hostScore: 0,
		pathScore: 0,
		userMatch: 0,
	}

	for k := range config {
		// Ensure we're examining the correct type of key and parse out the URL
		matches := re.FindStringSubmatch(k)
		if matches == nil {
			continue
		}
		configURL, err := url.Parse(matches[1])
		if err != nil {
			continue
		}

		match := urlMatch{
			key: k,
		}

		// Rule #1: Scheme must match exactly
		if searchURL.Scheme != configURL.Scheme {
			continue
		}

		// Rule #2: Hosts must match exactly, or through wildcards. More exact
		// matches should take priority over wildcard matches
		match.hostScore = compareHosts(searchURL.Hostname(), configURL.Hostname())

		if match.hostScore == 0 {
			continue
		}

		if match.hostScore < bestMatch.hostScore {
			continue
		}

		// Rule #3: Port Number must match exactly
		if portForURL(searchURL) != portForURL(configURL) {
			continue
		}

		// Rule #4: Configured path must match exactly, or as a prefix of
		// slash-delimited path elements
		match.pathScore = comparePaths(searchURL.Path, configURL.Path)

		if match.pathScore == 0 {
			continue
		}

		// Rule #5: Username must match exactly if present in the config.
		// If not present, config matches on any username but with lower
		// priority than an exact username match.
		if configURL.User != nil {
			if searchURL.User == nil {
				continue
			}

			if searchURL.User.Username() != configURL.User.Username() {
				continue
			}

			match.userMatch = 1
		}

		// Now combine our various scores to determine if we have found a best
		// match. Host score > path score > user score
		if match.hostScore > bestMatch.hostScore {
			bestMatch = match
			continue
		}

		if match.pathScore > bestMatch.pathScore {
			bestMatch = match
			continue
		}

		if match.pathScore == bestMatch.pathScore && match.userMatch > bestMatch.userMatch {
			bestMatch = match
			continue
		}
	}

	if bestMatch.key == "" {
		return nil
	}

	return c.git.GetAll(bestMatch.key)
}

func portForURL(u *url.URL) string {
	port := u.Port()
	if port != "" {
		return port
	}
	switch u.Scheme {
	case "http":
		return "80"
	case "https":
		return "443"
	case "ssh":
		return "22"
	default:
		return ""
	}
}

// compareHosts compares a hostname with a configuration hostname to determine
// a match. It returns an integer indicating the strength of the match, or 0 if
// the two hostnames did not match.
func compareHosts(searchHostname, configHostname string) int {
	searchHost := strings.Split(searchHostname, ".")
	configHost := strings.Split(configHostname, ".")

	if len(searchHost) != len(configHost) {
		return 0
	}

	score := len(searchHost) + 1

	for i, subdomain := range searchHost {
		if configHost[i] == "*" {
			score--
			continue
		}

		if subdomain != configHost[i] {
			return 0
		}
	}

	return score
}

// comparePaths compares a path with a configuration path to determine a match.
// It returns an integer indicating the strength of the match, or 0 if the two
// paths did not match.
func comparePaths(rawSearchPath, rawConfigPath string) int {
	f := func(c rune) bool {
		return c == '/'
	}
	searchPath := strings.FieldsFunc(rawSearchPath, f)
	configPath := strings.FieldsFunc(rawConfigPath, f)

	if len(searchPath) < len(configPath) {
		return 0
	}

	// Start with a base score of 1, so we return something above 0 for a
	// zero-length path
	score := 1

	for i, element := range configPath {
		searchElement := searchPath[i]

		if element == searchElement {
			score += 2
			continue
		}

		if isDefaultLFSUrl(searchElement, searchPath, i+1) {
			if searchElement[0:len(searchElement)-4] == element {
				// Since we matched without the `.git` prefix, only add one
				// point to the score instead of 2
				score++
				continue
			}
		}

		return 0
	}

	return score
}

func (c *URLConfig) hostsAndPaths(rawurl string) (hosts, paths []string) {
	u, err := url.Parse(rawurl)
	if err != nil {
		return nil, nil
	}

	return c.hosts(u), c.paths(u.Path)
}

func (c *URLConfig) hosts(u *url.URL) []string {
	hosts := make([]string, 0, 1)

	if u.User != nil {
		hosts = append(hosts, fmt.Sprintf("%s://%s@%s", u.Scheme, u.User.Username(), u.Host))
	}
	hosts = append(hosts, fmt.Sprintf("%s://%s", u.Scheme, u.Host))

	return hosts
}

func (c *URLConfig) paths(path string) []string {
	pLen := len(path)
	if pLen <= 2 {
		return nil
	}

	end := pLen
	if strings.HasSuffix(path, slash) {
		end--
	}
	return strings.Split(path[1:end], slash)
}

const (
	gitExt   = ".git"
	infoPart = "info"
	lfsPart  = "lfs"
	slash    = "/"
)

func isDefaultLFSUrl(path string, parts []string, index int) bool {
	if len(path) < 5 {
		return false // shorter than ".git"
	}

	if !strings.HasSuffix(path, gitExt) {
		return false
	}

	if index > len(parts)-2 {
		return false
	}

	return parts[index] == infoPart && parts[index+1] == lfsPart
}
