package core

import "strings"

// maxRepoNameLen bounds repository names. Owners are bounded by meta.sr.ht at
// registration time, so ValidOwner enforces only structure, not length.
const maxRepoNameLen = 100

// isNameByte reports whether c is allowed in a sourcehut owner or repo name:
// lowercase alphanumerics plus '_', '-', and '.'. Note '/' is deliberately
// excluded, so a name can never span path components.
func isNameByte(c byte) bool {
	switch {
	case c >= 'a' && c <= 'z':
		return true
	case c >= '0' && c <= '9':
		return true
	case c == '_' || c == '-' || c == '.':
		return true
	default:
		return false
	}
}

// validName holds the rules shared by owners and repos: non-empty, drawn from
// the allowed byte set, not starting with '-' (which would look like a git or
// shell option), and containing no ".." (path traversal). '/' is rejected
// implicitly because it is not an allowed byte.
func validName(s string) bool {
	if s == "" {
		return false
	}
	if s[0] == '-' {
		return false
	}
	if strings.Contains(s, "..") {
		return false
	}
	for i := 0; i < len(s); i++ {
		if !isNameByte(s[i]) {
			return false
		}
	}
	return true
}

// ValidOwner reports whether s is a well-formed sourcehut owner name (the part
// after '~' in a URL). Callers must strip the leading '~' first.
func ValidOwner(s string) bool {
	return validName(s)
}

// ValidRepoName reports whether s is a well-formed repository name (same
// character family as an owner, capped at maxRepoNameLen).
func ValidRepoName(s string) bool {
	return len(s) <= maxRepoNameLen && validName(s)
}

// refForbiddenByte reports whether c is a byte git-check-ref-format forbids
// anywhere in a ref: ASCII control characters, space, DEL, and the special
// set ~ ^ : ? * [ \. High bytes (>= 0x80) are allowed so UTF-8 refs pass.
func refForbiddenByte(c byte) bool {
	if c <= 0x20 || c == 0x7f {
		return true
	}
	switch c {
	case '~', '^', ':', '?', '*', '[', '\\':
		return true
	default:
		return false
	}
}

// isHexSHA reports whether s is a bare 40-char (SHA-1) or 64-char (SHA-256)
// hexadecimal object id. These are accepted as refs directly.
func isHexSHA(s string) bool {
	if len(s) != 40 && len(s) != 64 {
		return false
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}

// ValidRef reports whether s is acceptable as a git ref or revision to hand to
// git. The rules follow git-check-ref-format(1) closely enough to keep hostile
// input (option injection, path traversal, revision-syntax tricks) away from
// the git command line, while still accepting ordinary multi-level branch and
// tag names such as "feature/foo" and bare object ids.
//
// git invocations additionally use "--end-of-options"/"--" as defense in
// depth; this function is the first line.
func ValidRef(s string) bool {
	if s == "" {
		return false
	}
	// Bare object ids are always fine and skip the component rules.
	if isHexSHA(s) {
		return true
	}
	// "@" alone is a git shorthand for HEAD and is not a valid ref name.
	if s == "@" {
		return false
	}
	if s[0] == '-' || s[0] == '.' || s[0] == '/' {
		return false
	}
	if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
		return false
	}
	if strings.Contains(s, "..") || strings.Contains(s, "@{") || strings.Contains(s, "//") {
		return false
	}
	for i := 0; i < len(s); i++ {
		if refForbiddenByte(s[i]) {
			return false
		}
	}
	// Per-component rules: no component may start with '.' or end with
	// ".lock". Empty components are already excluded by the '//', leading
	// '/', and trailing '/' checks above.
	for _, comp := range strings.Split(s, "/") {
		if strings.HasPrefix(comp, ".") {
			return false
		}
		if strings.HasSuffix(comp, ".lock") {
			return false
		}
	}
	return true
}
