package gitx

import (
	"context"
	"sort"
	"time"

	"github.com/go-git/go-git/v5/plumbing"
)

// Ref is a named git reference paired with the object id it points at (the tag
// object for annotated tags, matching git for-each-ref's %(objectname)).
type Ref struct {
	Name string
	SHA  string
}

// Refs lists the repository's branches and tags. Branches are returned with the
// default (HEAD) branch first and the remainder alphabetical; tags are ordered
// newest-first by creator date (tagger date for annotated tags, committer date
// for lightweight tags).
func (r *Repo) Refs(ctx context.Context) (branches, tags []Ref, err error) {
	_, cancel := r.withTimeout(ctx)
	defer cancel()

	bIter, err := r.repo.Branches()
	if err != nil {
		return nil, nil, err
	}
	err = bIter.ForEach(func(ref *plumbing.Reference) error {
		branches = append(branches, Ref{Name: ref.Name().Short(), SHA: ref.Hash().String()})
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	def, _ := r.DefaultBranch(ctx)
	sortBranches(branches, def)

	type tagRef struct {
		ref  Ref
		when time.Time
	}
	var trefs []tagRef
	tIter, err := r.repo.Tags()
	if err != nil {
		return nil, nil, err
	}
	err = tIter.ForEach(func(ref *plumbing.Reference) error {
		trefs = append(trefs, tagRef{
			ref:  Ref{Name: ref.Name().Short(), SHA: ref.Hash().String()},
			when: r.tagWhen(ref.Hash()),
		})
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	sort.SliceStable(trefs, func(i, j int) bool {
		if !trefs[i].when.Equal(trefs[j].when) {
			return trefs[i].when.After(trefs[j].when)
		}
		return trefs[i].ref.Name > trefs[j].ref.Name
	})
	for _, t := range trefs {
		tags = append(tags, t.ref)
	}

	return branches, tags, nil
}

// DefaultBranch returns the short name of the branch HEAD points at (e.g.
// "main"). It fails if HEAD is detached (not a symbolic reference).
func (r *Repo) DefaultBranch(ctx context.Context) (string, error) {
	_, cancel := r.withTimeout(ctx)
	defer cancel()

	ref, err := r.repo.Reference(plumbing.HEAD, false)
	if err != nil {
		return "", err
	}
	if ref.Type() != plumbing.SymbolicReference {
		return "", plumbing.ErrReferenceNotFound
	}
	return ref.Target().Short(), nil
}

// tagWhen returns the creation time of a tag reference: the tagger time for an
// annotated tag, else the committer time of the pointed-at commit. A zero time
// is returned when neither can be resolved.
func (r *Repo) tagWhen(h plumbing.Hash) time.Time {
	if t, err := r.repo.TagObject(h); err == nil {
		return t.Tagger.When
	}
	if c, err := r.repo.CommitObject(h); err == nil {
		return c.Committer.When
	}
	return time.Time{}
}

// sortBranches orders refs alphabetically but floats the default branch to the
// front.
func sortBranches(refs []Ref, defaultBranch string) {
	sort.SliceStable(refs, func(i, j int) bool {
		if refs[i].Name == defaultBranch {
			return refs[j].Name != defaultBranch
		}
		if refs[j].Name == defaultBranch {
			return false
		}
		return refs[i].Name < refs[j].Name
	})
}
