package gitx

import (
	"context"
	"fmt"
	"time"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/storer"

	"go.bigb.es/sourcehut-compare/core"
)

// CommitInfo is the metadata of a single commit.
type CommitInfo struct {
	SHA         string
	ShortSHA    string
	AuthorName  string
	AuthorEmail string
	Date        time.Time
	Subject     string
	Body        string
	// ParentSHAs lists the commit's parents in order. Empty for a root commit;
	// length > 1 marks a merge commit.
	ParentSHAs []string
}

// resolveCommit validates rev, resolves it to a commit, and returns the go-git
// object. A syntactically valid but unresolvable revision yields core.ErrBadRef.
func (r *Repo) resolveCommit(rev string) (*object.Commit, error) {
	if !core.ValidRef(rev) {
		return nil, fmt.Errorf("%w: %q", core.ErrBadRef, rev)
	}
	hash, err := r.repo.ResolveRevision(plumbing.Revision(rev))
	if err != nil {
		return nil, badRef(rev, err)
	}
	c, err := r.repo.CommitObject(*hash)
	if err != nil {
		return nil, badRef(rev, err)
	}
	return c, nil
}

// ResolveCommit resolves an arbitrary revision (branch, tag, short or full SHA)
// to a commit and returns its metadata.
func (r *Repo) ResolveCommit(ctx context.Context, rev string) (*CommitInfo, error) {
	_, cancel := r.withTimeout(ctx)
	defer cancel()

	c, err := r.resolveCommit(rev)
	if err != nil {
		return nil, err
	}
	return commitInfo(c), nil
}

// Log returns up to limit commits in the range base..head (reachable from head
// but not from base), newest first. A non-positive limit defaults to 50.
func (r *Repo) Log(ctx context.Context, base, head string, limit int) ([]CommitInfo, error) {
	if limit <= 0 {
		limit = 50
	}
	_, cancel := r.withTimeout(ctx)
	defer cancel()

	baseCommit, err := r.resolveCommit(base)
	if err != nil {
		return nil, err
	}
	headCommit, err := r.resolveCommit(head)
	if err != nil {
		return nil, err
	}

	// Exclude everything reachable from base so a shared ancestor reached via a
	// non-base path is still dropped — matching git's base..head semantics.
	excluded, err := reachableSet(baseCommit)
	if err != nil {
		return nil, err
	}

	var out []CommitInfo
	iter := object.NewCommitPreorderIter(headCommit, excluded, nil)
	err = iter.ForEach(func(c *object.Commit) error {
		if len(out) >= limit {
			return storer.ErrStop
		}
		out = append(out, *commitInfo(c))
		return nil
	})
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Parents returns the parent SHAs of a commit in order. A root commit yields an
// empty slice; length > 1 marks a merge commit.
func (r *Repo) Parents(ctx context.Context, rev string) ([]string, error) {
	_, cancel := r.withTimeout(ctx)
	defer cancel()

	c, err := r.resolveCommit(rev)
	if err != nil {
		return nil, err
	}
	if len(c.ParentHashes) == 0 {
		return nil, nil
	}
	parents := make([]string, 0, len(c.ParentHashes))
	for _, p := range c.ParentHashes {
		parents = append(parents, p.String())
	}
	return parents, nil
}

// reachableSet returns the set of every commit hash reachable from c
// (inclusive).
func reachableSet(c *object.Commit) (map[plumbing.Hash]bool, error) {
	set := make(map[plumbing.Hash]bool)
	iter := object.NewCommitPreorderIter(c, nil, nil)
	err := iter.ForEach(func(x *object.Commit) error {
		set[x.Hash] = true
		return nil
	})
	if err != nil {
		return nil, err
	}
	return set, nil
}
