package profile

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

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

// KnowledgeBaseGitRunner is the git surface needed by managed KB sync. Network
// clone/fetch uses go-git so MeshX sync leases stay in-process; worktree
// materialization uses the git CLI because linked worktrees are a native git
// feature that go-git does not model.
type KnowledgeBaseGitRunner interface {
	CloneBare(ctx context.Context, url, branch, dest string, auth gittransport.AuthMethod) (revision string, err error)
	FetchBare(ctx context.Context, bareDir, branch string, auth gittransport.AuthMethod) (revision string, err error)
	WorktreeCheckout(ctx context.Context, bareDir, worktreePath, revision string) error
}

func (r *DefaultGitRunner) CloneBare(ctx context.Context, url, branch, dest string, auth gittransport.AuthMethod) (string, error) {
	if err := validateCloneArgs(url, branch, dest); err != nil {
		return "", err
	}
	if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
		return "", fmt.Errorf("create parent of %s: %w", dest, err)
	}
	runCtx, cancel := context.WithTimeout(ctx, r.timeout)
	defer cancel()
	repo, err := git.PlainCloneContext(runCtx, dest, true, &git.CloneOptions{
		URL:           url,
		Auth:          auth,
		ReferenceName: plumbing.NewBranchReferenceName(branch),
		SingleBranch:  true,
		Depth:         1,
	})
	if err != nil {
		return "", fmt.Errorf("git bare clone %s: %w", redactURL(url), err)
	}
	return branchRevisionOf(repo, branch)
}

func (r *DefaultGitRunner) FetchBare(ctx context.Context, bareDir, branch string, auth gittransport.AuthMethod) (string, error) {
	if err := validateRepoDir(bareDir); err != nil {
		return "", err
	}
	if err := validateGitRef(branch, "fetch branch"); err != nil {
		return "", err
	}
	repo, err := git.PlainOpen(bareDir)
	if err != nil {
		return "", fmt.Errorf("git open bare %s: %w", bareDir, err)
	}
	runCtx, cancel := context.WithTimeout(ctx, r.timeout)
	defer cancel()
	refSpec := config.RefSpec("+refs/heads/" + branch + ":refs/heads/" + branch)
	if err := repo.FetchContext(runCtx, &git.FetchOptions{RemoteName: "origin", RefSpecs: []config.RefSpec{refSpec}, Auth: auth, Depth: 1}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
		return "", fmt.Errorf("git bare fetch %s: %w", branch, err)
	}
	return branchRevisionOf(repo, branch)
}

func (r *DefaultGitRunner) WorktreeCheckout(ctx context.Context, bareDir, worktreePath, revision string) error {
	if err := validateRepoDir(bareDir); err != nil {
		return err
	}
	if strings.TrimSpace(worktreePath) == "" {
		return errors.New("git worktree: empty worktree path")
	}
	revision = strings.TrimSpace(revision)
	if revision == "" {
		return errors.New("git worktree: empty revision")
	}
	if err := os.MkdirAll(filepath.Dir(worktreePath), 0o755); err != nil {
		return fmt.Errorf("create worktree parent: %w", err)
	}
	runCtx, cancel := context.WithTimeout(ctx, r.timeout)
	defer cancel()
	if _, err := os.Stat(filepath.Join(worktreePath, ".git")); err == nil {
		if err := runGitCommand(runCtx, worktreePath, "checkout", "--detach", revision); err != nil {
			return err
		}
		return runGitCommand(runCtx, worktreePath, "reset", "--hard", revision)
	}
	if err := os.RemoveAll(worktreePath); err != nil {
		return fmt.Errorf("remove non-git worktree path: %w", err)
	}
	if err := runGitCommand(runCtx, "", "--git-dir", bareDir, "worktree", "prune"); err != nil {
		return err
	}
	return runGitCommand(runCtx, "", "--git-dir", bareDir, "worktree", "add", "--detach", worktreePath, revision)
}

func branchRevisionOf(repo *git.Repository, branch string) (string, error) {
	ref, err := repo.Reference(plumbing.NewBranchReferenceName(branch), true)
	if err == nil {
		return ref.Hash().String(), nil
	}
	return headRevisionOf(repo)
}

func runGitCommand(ctx context.Context, dir string, args ...string) error {
	cmd := exec.CommandContext(ctx, "git", args...)
	if strings.TrimSpace(dir) != "" {
		cmd.Dir = dir
	}
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
	}
	if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) <= 0 {
		return context.DeadlineExceeded
	}
	return nil
}
