package gitstore

import (
	"context"
	"errors"
	"fmt"
	"os/exec"
	"strings"

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

// HeadSHA returns the commit SHA of the named branch.
func (s *Store) HeadSHA(ctx context.Context, fullName, branch string) (string, error) {
	repo, err := s.open(ctx, fullName)
	if err != nil {
		return "", err
	}
	ref, err := repo.Reference(plumbing.NewBranchReferenceName(branch), true)
	if err != nil {
		return "", err
	}
	return ref.Hash().String(), nil
}

// ErrNonFastForward is returned by the private ref mutation owner when the proposed SHA
// is not a fast-forward of the existing ref's SHA and the caller has
// not opted into the force path. Callers (REST PATCH /git/refs/...)
// map this to HTTP 422 "Update is not a fast forward" to match
// github.com's documented contract.
var ErrNonFastForward = errors.New("non-fast-forward update")

// IsAncestor reports whether older is reachable from newer.
func (s *Store) IsAncestor(ctx context.Context, fullName, older, newer string) (bool, error) {
	if !plumbing.IsHash(older) {
		return false, fmt.Errorf("%w: %q", ErrInvalidSHA, older)
	}
	if !plumbing.IsHash(newer) {
		return false, fmt.Errorf("%w: %q", ErrInvalidSHA, newer)
	}
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return false, err
	}
	cmd := exec.CommandContext(ctx, "git", "-C", dir, "merge-base", "--is-ancestor", older, newer)
	if err := cmd.Run(); err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
			return false, nil
		}
		return false, fmt.Errorf("git merge-base --is-ancestor: %w", err)
	}
	return true, nil
}

// RefInfo pairs a full ref name (e.g. "refs/locks/issue-42") with its
// current target SHA. Returned by LookupRef and ListRefsWithPrefix.
type RefInfo struct {
	Ref string
	SHA string
}

// ErrRefAlreadyExists reports an atomic create-only ref conflict. Callers map
// this to HTTP 422 "Reference already exists" to match github.com's
// POST /repos/:o/:r/git/refs contract.
var ErrRefAlreadyExists = errors.New("ref already exists")

// ErrRefNotFound is returned by LookupRef when the ref does not exist.
var ErrRefNotFound = errors.New("ref not found")

// LookupRef resolves a fully-qualified ref (e.g. "refs/locks/issue-42") to
// its current SHA. Unlike HeadSHA (which is hard-coded to refs/heads/*),
// this works for any namespace — tags, custom, pull — which is what
// GET /repos/:o/:r/git/refs/:ref needs to support distributed-coordination
// use cases. Returns ErrRefNotFound if the ref isn't present.
func (s *Store) LookupRef(ctx context.Context, fullName, ref string) (string, error) {
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "", err
	}
	// `show-ref --verify --hash` emits just the SHA (or exits non-zero on miss).
	cmd := exec.CommandContext(ctx, "git", "-C", dir, "show-ref", "--verify", "--hash", ref)
	out, err := cmd.Output()
	if err != nil {
		return "", ErrRefNotFound
	}
	sha := strings.TrimSpace(string(out))
	if sha == "" {
		return "", ErrRefNotFound
	}
	return sha, nil
}

// ListRefsWithPrefix returns every ref whose full name begins with prefix,
// using GitHub's character-prefix semantics (not git for-each-ref's default
// "match up to a slash" rule). prefix is expected in git's "refs/..." form
// (e.g. "refs/locks", "refs/heads/octoswarm/fix-"); an empty prefix lists
// every ref in the repo. Used by the REST GET /git/matching-refs/:ref
// endpoint. Results are sorted by ref name for deterministic output.
func (s *Store) ListRefsWithPrefix(ctx context.Context, fullName, prefix string) ([]RefInfo, error) {
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return nil, err
	}
	args := []string{"-C", dir, "for-each-ref", "--format=%(objectname) %(refname)"}
	// git for-each-ref matches "literal up to a slash" or wildmatch with
	// FNM_PATHNAME — "*" does not cross "/". For a mid-component prefix
	// like "refs/heads/octoswarm/fix-" this misses GitHub's character-
	// prefix semantics, so we narrow the pattern to the path up to the
	// last "/" and filter by the full prefix below. When the prefix
	// already ends at a slash boundary, git's native matching is correct
	// and listing the wider namespace would over-fetch.
	gitPattern, needFilter := prefix, false
	if !strings.HasSuffix(prefix, "/") {
		if idx := strings.LastIndex(prefix, "/"); idx > 0 {
			gitPattern, needFilter = prefix[:idx], true
		} else if idx == 0 {
			gitPattern, needFilter = "", true
		} else if prefix != "" {
			needFilter = true
		}
	}
	if gitPattern != "" {
		args = append(args, gitPattern)
	}
	cmd := exec.CommandContext(ctx, "git", args...)
	out, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("git for-each-ref %s: %w", prefix, err)
	}
	var refs []RefInfo
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		if line == "" {
			continue
		}
		parts := strings.SplitN(line, " ", 2)
		if len(parts) != 2 {
			continue
		}
		if needFilter && !strings.HasPrefix(parts[1], prefix) {
			continue
		}
		refs = append(refs, RefInfo{SHA: parts[0], Ref: parts[1]})
	}
	return refs, nil
}

// BranchInfo holds a branch name and its commit SHA.
type BranchInfo struct {
	Name string
	SHA  string
}

// ListBranches returns all branches in a bare git repository.
func (s *Store) ListBranches(ctx context.Context, fullName string) ([]BranchInfo, error) {
	repo, err := s.open(ctx, fullName)
	if err != nil {
		return nil, err
	}
	iter, err := repo.Branches()
	if err != nil {
		return nil, err
	}
	var branches []BranchInfo
	if err := iter.ForEach(func(ref *plumbing.Reference) error {
		branches = append(branches, BranchInfo{
			Name: ref.Name().Short(),
			SHA:  ref.Hash().String(),
		})
		return nil
	}); err != nil {
		return nil, fmt.Errorf("iterating branches: %w", err)
	}
	return branches, nil
}

// HeadBranch returns the repository's valid symbolic HEAD branch. A dangling
// HEAD and an empty repository have no effective default branch.
func (s *Store) HeadBranch(ctx context.Context, fullName string) (string, error) {
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "", err
	}
	out, err := exec.CommandContext(ctx, "git", "-C", dir, "symbolic-ref", "--short", "HEAD").Output()
	if err != nil {
		return "", nil
	}
	branch := strings.TrimSpace(string(out))
	if branch == "" {
		return "", nil
	}
	if err := exec.CommandContext(ctx, "git", "-C", dir, "rev-parse", "--verify", plumbing.NewBranchReferenceName(branch).String()).Run(); err != nil {
		return "", nil
	}
	return branch, nil
}
