package vault

import (
	"context"
	"errors"
	"fmt"

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

// ErrNoUpstreamBranch is returned when origin does not have the agent's
// branch after fetch. Callers treat this as "local ahead" — the branch
// hasn't been pushed yet.
var ErrNoUpstreamBranch = errors.New("vault: no origin/<branch> reference")

// SyncOpts configures Sync.
type SyncOpts struct {
	// Auth is the caller-provided go-git transport credential for HTTPS
	// origins. nil works for file:// origins in tests.
	Auth gittransport.AuthMethod
}

// SyncState classifies the relationship between local HEAD and the remote
// tracking branch after a fetch.
type SyncState int

const (
	// SyncUpToDate: HEAD == origin/<branch>. No remote changes.
	SyncUpToDate SyncState = iota
	// SyncLocalAhead: HEAD has commits origin/<branch> does not.
	SyncLocalAhead
	// SyncRemoteAhead: origin/<branch> has commits HEAD does not. The remote
	// advanced since the last push — caller should not amend.
	SyncRemoteAhead
	// SyncDiverged: HEAD and origin/<branch> have diverged. Caller should
	// not amend.
	SyncDiverged
)

// SyncResult reports the outcome of a Sync call.
type SyncResult struct {
	State SyncState
}

// Sync fetches origin and compares local HEAD with origin/<branch> (the
// agent's own remote tracking branch). It does NOT modify the worktree or
// perform any merge — it is a read-only check so the caller can decide
// whether to amend or create a new commit.
//
// When origin does not have the branch yet (first push), returns
// ErrNoUpstreamBranch.
func (c *Clone) Sync(ctx context.Context, opts SyncOpts) (*SyncResult, error) {
	if c == nil || c.repo == nil {
		return nil, fmt.Errorf("vault: nil clone or repository")
	}
	unlock, err := lockMemoryPath(ctx, c.path)
	if err != nil {
		return nil, fmt.Errorf("vault: acquire memory path lock: %w", err)
	}
	defer unlock()

	if err := fetch(ctx, c.repo, opts.Auth); err != nil {
		return nil, fmt.Errorf("vault: sync fetch: %w", err)
	}

	head, err := c.repo.Head()
	if err != nil {
		return nil, fmt.Errorf("vault: sync read head: %w", err)
	}

	upstreamRef, err := c.upstreamBranchRef()
	if err != nil {
		return nil, err
	}

	if head.Hash() == upstreamRef.Hash() {
		return &SyncResult{State: SyncUpToDate}, nil
	}

	headIsAncestor, err := isAncestor(c.repo, head.Hash(), upstreamRef.Hash())
	if err != nil {
		return nil, fmt.Errorf("vault: sync ancestry check: %w", err)
	}
	if headIsAncestor {
		return &SyncResult{State: SyncRemoteAhead}, nil
	}

	upstreamIsAncestor, err := isAncestor(c.repo, upstreamRef.Hash(), head.Hash())
	if err != nil {
		return nil, fmt.Errorf("vault: sync ancestry check: %w", err)
	}
	if upstreamIsAncestor {
		return &SyncResult{State: SyncLocalAhead}, nil
	}

	return &SyncResult{State: SyncDiverged}, nil
}

// upstreamBranchRef returns the origin/<branch> reference for the clone's
// agent branch. Returns ErrNoUpstreamBranch when the branch does not exist
// on origin (first push).
func (c *Clone) upstreamBranchRef() (*plumbing.Reference, error) {
	refName := plumbing.NewRemoteReferenceName("origin", c.branch)
	ref, err := c.repo.Reference(refName, true)
	if err != nil {
		return nil, ErrNoUpstreamBranch
	}
	return ref, nil
}

// isAncestor reports whether ancestor is reachable from descendant.
func isAncestor(repo *git.Repository, ancestor, descendant plumbing.Hash) (bool, error) {
	anc, err := repo.CommitObject(ancestor)
	if err != nil {
		return false, err
	}
	desc, err := repo.CommitObject(descendant)
	if err != nil {
		return false, err
	}
	return anc.IsAncestor(desc)
}
