package scopes

import (
	"slices"
	"sort"
)

// Scope represents a GitHub OAuth scope.
// These constants define all OAuth scopes used by the GitHub MCP server tools.
// See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
type Scope string

const (
	// NoScope indicates no scope is required (public access).
	NoScope Scope = ""

	// Repo grants full control of private repositories
	Repo Scope = "repo"

	// PublicRepo grants access to public repositories
	PublicRepo Scope = "public_repo"

	// ReadOrg grants read-only access to organization membership, teams, and projects
	ReadOrg Scope = "read:org"

	// WriteOrg grants write access to organization membership and teams
	WriteOrg Scope = "write:org"

	// AdminOrg grants full control of organizations and teams
	AdminOrg Scope = "admin:org"

	// Gist grants write access to gists
	Gist Scope = "gist"

	// Notifications grants access to notifications
	Notifications Scope = "notifications"

	// ReadProject grants read-only access to projects
	ReadProject Scope = "read:project"

	// Project grants full control of projects
	Project Scope = "project"

	// SecurityEvents grants read and write access to security events
	SecurityEvents Scope = "security_events"

	// User grants read/write access to profile info
	User Scope = "user"

	// ReadUser grants read-only access to profile info
	ReadUser Scope = "read:user"

	// UserEmail grants read access to user email addresses
	UserEmail Scope = "user:email"

	// ReadPackages grants read access to packages
	ReadPackages Scope = "read:packages"

	// WritePackages grants write access to packages
	WritePackages Scope = "write:packages"
)

// ScopeHierarchy defines parent-child relationships between scopes.
// A parent scope implicitly grants access to all child scopes.
// For example, "repo" grants access to "public_repo" and "security_events".
var ScopeHierarchy = map[Scope][]Scope{
	Repo:          {PublicRepo, SecurityEvents},
	AdminOrg:      {WriteOrg, ReadOrg},
	WriteOrg:      {ReadOrg},
	Project:       {ReadProject},
	WritePackages: {ReadPackages},
	User:          {ReadUser, UserEmail},
}

// ScopeSet represents a set of OAuth scopes.
type ScopeSet map[Scope]bool

// NewScopeSet creates a new ScopeSet from the given scopes.
func NewScopeSet(scopes ...Scope) ScopeSet {
	set := make(ScopeSet)
	for _, scope := range scopes {
		set[scope] = true
	}
	return set
}

// ToSlice converts a ScopeSet to a slice of Scope values.
func (s ScopeSet) ToSlice() []Scope {
	scopes := make([]Scope, 0, len(s))
	for scope := range s {
		scopes = append(scopes, scope)
	}
	// Sort for deterministic output
	slices.Sort(scopes)
	return scopes
}

// ToStringSlice converts a ScopeSet to a slice of string values.
// The returned slice is sorted for deterministic output.
func (s ScopeSet) ToStringSlice() []string {
	scopes := make([]string, 0, len(s))
	for scope := range s {
		scopes = append(scopes, string(scope))
	}
	sort.Strings(scopes)
	return scopes
}

// ToStringSlice converts a slice of Scopes to a slice of strings.
func ToStringSlice(scopes ...Scope) []string {
	result := make([]string, len(scopes))
	for i, scope := range scopes {
		result[i] = string(scope)
	}
	return result
}

// ExpandScopes takes a list of required scopes and returns all accepted scopes
// including parent scopes from the hierarchy.
// For example, if "public_repo" is required, "repo" is also accepted since
// having the "repo" scope grants access to "public_repo".
// The returned slice is sorted for deterministic output.
func ExpandScopes(required ...Scope) []string {
	if len(required) == 0 {
		return nil
	}

	accepted := make(map[string]bool)

	// Add required scopes
	for _, scope := range required {
		accepted[string(scope)] = true
	}

	// Add parent scopes that grant access to required scopes
	for parent, children := range ScopeHierarchy {
		for _, child := range children {
			if accepted[string(child)] {
				accepted[string(parent)] = true
			}
		}
	}

	// Convert to slice and sort for deterministic output
	result := make([]string, 0, len(accepted))
	for scope := range accepted {
		result = append(result, scope)
	}
	sort.Strings(result)
	return result
}

// expandScopeSet returns a set of all scopes granted by the given scopes,
// including child scopes from the hierarchy.
// For example, if "repo" is provided, the result includes "repo", "public_repo",
// and "security_events" since "repo" grants access to those child scopes.
func expandScopeSet(scopes []string) map[string]bool {
	expanded := make(map[string]bool, len(scopes))
	for _, scope := range scopes {
		expanded[scope] = true
		// Add child scopes granted by this scope
		if children, ok := ScopeHierarchy[Scope(scope)]; ok {
			for _, child := range children {
				expanded[string(child)] = true
			}
		}
	}
	return expanded
}

// HasRequiredScopes checks if tokenScopes satisfy the acceptedScopes requirement.
// A tool's acceptedScopes includes both the required scopes AND parent scopes
// that implicitly grant the required permissions (via ExpandScopes).
//
// For PAT filtering: if ANY of the acceptedScopes are granted by the token
// (directly or via scope hierarchy), the tool should be visible.
//
// Returns true if the tool should be visible to the token holder.
func HasRequiredScopes(tokenScopes []string, acceptedScopes []string) bool {
	// No scopes required = always allowed
	if len(acceptedScopes) == 0 {
		return true
	}

	// Expand token scopes to include child scopes they grant
	grantedScopes := expandScopeSet(tokenScopes)

	// Check if any accepted scope is granted by the token
	for _, accepted := range acceptedScopes {
		if grantedScopes[accepted] {
			return true
		}
	}
	return false
}
