package simulator

import (
	"fmt"
	"sort"
	"strings"

	"github.com/google/go-github/v52/github"
)

type RunnerGroupScope int

const (
	Organization RunnerGroupScope = iota
	Enterprise
)

func (s RunnerGroupScope) String() string {
	switch s {
	case Organization:
		return "Organization"
	case Enterprise:
		return "Enterprise"
	default:
		panic(fmt.Sprintf("unimplemented RunnerGroupScope: %v", int(s)))
	}
}

type RunnerGroupKind int

const (
	Default RunnerGroupKind = iota
	Custom
)

func (s RunnerGroupKind) String() string {
	switch s {
	case Default:
		return "Default"
	case Custom:
		return "Custom"
	default:
		panic(fmt.Sprintf("unimplemented RunnerGroupKind: %v", int(s)))
	}
}

func NewRunnerGroupFromGitHub(g *github.RunnerGroup) RunnerGroup {
	var name string
	if !g.GetDefault() {
		name = g.GetName()
	}

	var scope RunnerGroupScope

	if g.GetInherited() {
		scope = Enterprise
	} else {
		scope = Organization
	}

	return newRunnerGroup(scope, name)
}

func NewRunnerGroupFromProperties(enterprise, organization, group string) RunnerGroup {
	var scope RunnerGroupScope

	if enterprise != "" {
		scope = Enterprise
	} else {
		scope = Organization
	}

	return newRunnerGroup(scope, group)
}

// newRunnerGroup creates a new RunnerGroup instance from the provided arguments.
// There's a convention that an empty name implies a default runner group.
func newRunnerGroup(scope RunnerGroupScope, name string) RunnerGroup {
	if name == "" {
		return RunnerGroup{
			Scope: scope,
			Kind:  Default,
			Name:  "",
		}
	}

	return RunnerGroup{
		Scope: scope,
		Kind:  Custom,
		Name:  name,
	}
}

type RunnerGroup struct {
	Scope RunnerGroupScope
	Kind  RunnerGroupKind
	Name  string
}

func (r RunnerGroup) String() string {
	return fmt.Sprintf("RunnerGroup{Scope:%s, Kind:%s, Name:%s}", r.Scope, r.Kind, r.Name)
}

// VisibleRunnerGroups is a set of enterprise and organization runner groups
// that are visible to a GitHub repository.
// GitHub Actions chooses one of such visible group on which the workflow job is scheduled.
// ARC chooses the same group as Actions as the scale target.
type VisibleRunnerGroups struct {
	// sortedGroups is a pointer to a mutable list of RunnerGroups that contains all the runner sortedGroups
	// that are visible to the repository, including organization sortedGroups defined at the organization level,
	// and enterprise sortedGroups that are inherited down to the organization.
	sortedGroups []RunnerGroup
}

func NewVisibleRunnerGroups() *VisibleRunnerGroups {
	return &VisibleRunnerGroups{}
}

func (g *VisibleRunnerGroups) String() string {
	var gs []string
	for _, g := range g.sortedGroups {
		gs = append(gs, g.String())
	}

	return strings.Join(gs, ", ")
}

func (g *VisibleRunnerGroups) IsEmpty() bool {
	return len(g.sortedGroups) == 0
}

func (r *VisibleRunnerGroups) Includes(ref RunnerGroup) bool {
	for _, r := range r.sortedGroups {
		if r.Scope == ref.Scope && r.Kind == ref.Kind && r.Name == ref.Name {
			return true
		}
	}
	return false
}

// Add adds a runner group into VisibleRunnerGroups
// at a certain position in the list so that
// Traverse can return runner groups in order of higher precedence to lower precedence.
func (g *VisibleRunnerGroups) Add(rg RunnerGroup) error {
	n := len(g.sortedGroups)
	i := sort.Search(n, func(i int) bool {
		data := g.sortedGroups[i]

		if rg.Kind > data.Kind {
			return false
		} else if rg.Kind < data.Kind {
			return true
		}

		if rg.Scope > data.Scope {
			return false
		} else if rg.Scope < data.Scope {
			return true
		}

		return false
	})

	g.insert(rg, i)

	return nil
}

func (g *VisibleRunnerGroups) insert(rg RunnerGroup, i int) {
	var result []RunnerGroup

	result = append(result, g.sortedGroups[:i]...)
	result = append(result, rg)
	result = append(result, g.sortedGroups[i:]...)

	g.sortedGroups = result
}

// Traverse traverses all the runner groups visible to a repository
// in order of higher precedence to lower precedence.
func (g *VisibleRunnerGroups) Traverse(f func(RunnerGroup) (bool, error)) error {
	for _, rg := range g.sortedGroups {
		ok, err := f(rg)
		if err != nil {
			return err
		}

		if ok {
			return nil
		}
	}

	return nil
}
