package vault

import (
	"context"
	"errors"
	"fmt"
	"time"

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

// ErrNothingToPush is returned by Push when the local bound branch is
// already in sync with origin. Callers should swallow this and treat it as a
// successful no-op round.
var ErrNothingToPush = errors.New("vault: nothing to push")

// DefaultPushTimeout bounds how long a single push can run. Memory repos are
// small (markdown), but upstream latency varies; this prevents a wedged
// network call from blocking session teardown indefinitely.
const DefaultPushTimeout = 60 * time.Second

// PushOpts configures Push.
type PushOpts struct {
	// Auth is the caller-provided go-git transport credential for HTTPS
	// origins. nil works for file:// origins in tests; HTTPS origins without
	// auth will fail at the transport layer.
	Auth gittransport.AuthMethod
	// Force allows non-fast-forward pushes by prefixing the refspec with "+".
	// Memory branches should normally fast-forward; force is an escape hatch
	// for recovery from a desynced local state.
	Force bool
}

// Push pushes the clone's bound branch to origin. Returns ErrNothingToPush
// when the local branch matches origin (go-git reports this as
// NoErrAlreadyUpToDate). Any other error wraps the underlying transport or
// reference error.
//
// Push is best-effort from the dispatcher's perspective: failures are logged
// and the watcher continues to commit locally. The next Push attempt carries
// the full backlog of pending commits.
func (c *Clone) Push(ctx context.Context, opts PushOpts) (string, error) {
	if c == nil || c.repo == nil {
		return "", fmt.Errorf("vault: nil clone or repository")
	}
	unlock, err := lockMemoryPath(ctx, c.path)
	if err != nil {
		return "", fmt.Errorf("vault: acquire memory path lock: %w", err)
	}
	defer unlock()
	return c.pushLocked(ctx, opts)
}

func (c *Clone) pushLocked(ctx context.Context, opts PushOpts) (string, error) {
	refSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", c.branch, c.branch)
	if opts.Force {
		refSpec = "+" + refSpec
	}

	pushCtx, cancel := context.WithTimeout(ctx, DefaultPushTimeout)
	defer cancel()

	err := c.repo.PushContext(pushCtx, &git.PushOptions{
		RemoteName: "origin",
		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
		Auth:       opts.Auth,
	})
	if err == nil {
		return c.branch, nil
	}
	if errors.Is(err, git.NoErrAlreadyUpToDate) {
		return "", ErrNothingToPush
	}
	return "", fmt.Errorf("vault: push %s: %w", c.branch, err)
}
