package vault

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

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

// Errors surfaced by commit operations.
var (
	// ErrNothingToCommit is returned by CommitPending when the worktree has
	// no staged or unstaged changes. Callers (e.g. the watcher) should
	// swallow this and treat it as a no-op round.
	ErrNothingToCommit = errors.New("vault: nothing to commit")
)

// DefaultAuthorName is the fallback commit author name when neither the
// caller nor the local git config provides one.
const DefaultAuthorName = "AgentTeam Desktop"

// DefaultAuthorEmail is the fallback commit author email.
const DefaultAuthorEmail = "memory@agentteam.local"

// CommitPendingOpts configures CommitPending.
type CommitPendingOpts struct {
	// Author is the commit author signature. Zero values fall back to
	// DefaultAuthorName / DefaultAuthorEmail. Callers should populate
	// Name (and optionally Email) with the acting user or agent identity.
	Author *Signature
	// Committer is the commit committer signature. When nil, falls back to
	// Author. The watcher uses identical Author/Committer so commits are
	// attributable to a single actor.
	Committer *Signature
	// Amend replaces the previous commit on the branch instead of creating
	// a new one. Used by the watcher when the same conversation context
	// (channel+thread+task) produces back-to-back memory updates within a
	// short window.
	Amend bool
	// AllowEmpty creates a new immutable journal version even when its tree is
	// identical to HEAD. This is reserved for adopting a distinct server-owned
	// ContentVersion; ordinary local flushes must keep the default no-op
	// behavior on a clean worktree.
	AllowEmpty bool
}

// Signature is a name/email/time commit signature. Mirrors object.Signature
// but without pulling go-git types into the caller's surface; the vault
// package converts internally.
type Signature struct {
	Name  string
	Email string
	When  time.Time
}

// CommitPending stages every change (adds, modifications, deletions) in the
// clone's worktree and commits it on the bound branch. Returns the new commit
// hash, or ErrNothingToCommit when the worktree is clean.
//
// The clone must come from EnsureClone on the expected branch; this function
// does not check the branch name itself but assumes HEAD points at
// the clone's bound branch.
func (c *Clone) CommitPending(ctx context.Context, msg string, opts CommitPendingOpts) (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.commitPendingLocked(ctx, msg, opts)
}

func (c *Clone) commitPendingLocked(ctx context.Context, msg string, opts CommitPendingOpts) (string, error) {
	msg = strings.TrimSpace(msg)
	if msg == "" {
		return "", fmt.Errorf("vault: commit message is required")
	}

	wt, err := c.repo.Worktree()
	if err != nil {
		return "", fmt.Errorf("vault: open worktree: %w", err)
	}

	// Stage everything (new files, modifications, deletions) using go-git's
	// AddWithOptions(All: true). CommitOptions.All only re-stages Modified
	// and Deleted entries; it does NOT pick up untracked files, so the
	// explicit Add is required for new memory notes.
	if err := wt.AddWithOptions(&git.AddOptions{All: true}); err != nil {
		return "", fmt.Errorf("vault: stage changes: %w", err)
	}

	status, err := wt.Status()
	if err != nil {
		return "", fmt.Errorf("vault: read status: %w", err)
	}
	if status.IsClean() && !opts.AllowEmpty {
		return "", ErrNothingToCommit
	}

	author := resolveSignature(opts.Author)
	committer := resolveSignature(opts.Committer)
	if opts.Committer == nil {
		committer = author
	}

	hash, err := wt.Commit(msg, &git.CommitOptions{
		Amend:             opts.Amend,
		AllowEmptyCommits: opts.AllowEmpty,
		Author:            &author,
		Committer:         &committer,
	})
	if err != nil {
		return "", fmt.Errorf("vault: commit: %w", err)
	}
	return hash.String(), nil
}

// resolveSignature converts the local Signature type to object.Signature,
// applying default name/email when fields are empty. When is zero, time.Now
// is used.
func resolveSignature(sig *Signature) object.Signature {
	out := object.Signature{
		Name:  DefaultAuthorName,
		Email: DefaultAuthorEmail,
		When:  time.Now(),
	}
	if sig == nil {
		return out
	}
	if strings.TrimSpace(sig.Name) != "" {
		out.Name = strings.TrimSpace(sig.Name)
	}
	if strings.TrimSpace(sig.Email) != "" {
		out.Email = strings.TrimSpace(sig.Email)
	}
	if !sig.When.IsZero() {
		out.When = sig.When
	}
	return out
}

// LastCommitMessage returns the HEAD commit's message, or empty string when
// HEAD does not point at a commit yet (fresh clone with no commits). Used by
// the memorywatcher to derive the amend key for the next commit.
//
// Errors only propagate when the repository cannot be read at all — an empty
// HEAD (e.g., unborn branch on a brand-new clone) is reported as ("", nil).
func (c *Clone) LastCommitMessage(ctx context.Context) (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()
	head, err := c.repo.Head()
	if err != nil {
		// Unborn HEAD on a fresh clone is not an error from the watcher's
		// perspective — there is simply no commit to amend against.
		if err == plumbing.ErrReferenceNotFound {
			return "", nil
		}
		return "", fmt.Errorf("vault: read head: %w", err)
	}
	commit, err := c.repo.CommitObject(head.Hash())
	if err != nil {
		return "", fmt.Errorf("vault: read head commit: %w", err)
	}
	return commit.Message, nil
}

// LastCommitTime returns the HEAD commit's author timestamp. Returns zero
// time when HEAD is unborn (fresh clone with no commits).
func (c *Clone) LastCommitTime(ctx context.Context) (time.Time, error) {
	if c == nil || c.repo == nil {
		return time.Time{}, fmt.Errorf("vault: nil clone or repository")
	}
	unlock, err := lockMemoryPath(ctx, c.path)
	if err != nil {
		return time.Time{}, fmt.Errorf("vault: acquire memory path lock: %w", err)
	}
	defer unlock()
	head, err := c.repo.Head()
	if err != nil {
		if err == plumbing.ErrReferenceNotFound {
			return time.Time{}, nil
		}
		return time.Time{}, fmt.Errorf("vault: read head: %w", err)
	}
	commit, err := c.repo.CommitObject(head.Hash())
	if err != nil {
		return time.Time{}, fmt.Errorf("vault: read head commit: %w", err)
	}
	return commit.Author.When, nil
}

// HeadRevision returns the immutable local journal head or an empty string for
// an unborn journal. It performs no remote I/O.
func (c *Clone) HeadRevision(ctx context.Context) (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()
	head, err := c.repo.Head()
	if err == plumbing.ErrReferenceNotFound {
		return "", nil
	}
	if err != nil {
		return "", fmt.Errorf("vault: read head: %w", err)
	}
	return head.Hash().String(), nil
}
