// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package github

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"strings"

	libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git/client"
	slogctx "github.com/veqryn/slog-context"
)

// ensureGitRepoDir repoDir verifies that the `dir` exists and is a directory.
// If the `dir` is unset, a temporary directory will be created. A boolean
// is returned which can be used to determine whether or not the path returned
// is a temporary directory.
func ensureGitRepoDir(ctx context.Context, dir string) (string, error, bool) {
	if dir == "" {
		slog.Default().DebugContext(ctx, "creating repository directory")
		dir, err := os.MkdirTemp("", "pipeline-cmd-git-rep-dir")
		return dir, err, true
	}

	ctx = slogctx.Append(ctx, slog.String("repo-dir", dir))
	slog.Default().DebugContext(ctx, "verifying repository directory")
	info, err := os.Stat(dir)
	if err != nil {
		return dir, fmt.Errorf("stating repository directory: %w", err), false
	}

	if !info.IsDir() {
		return dir, errors.New("repo dir must be a directory"), false
	}

	return dir, nil, false
}

// initializeExistingRepo initializes an existing repository. It assumes that
// at least one remote origin exists and that some branch is checked out. If
// the current branch is our baseRef we'll pull in the latest changes, otherwise
// we'll fetch baseRef.
func initializeExistingRepo(
	ctx context.Context,
	git *libgit.Client,
	repoDir string,
	baseOrigin string,
	baseRef string,
) error {
	ctx = slogctx.Append(ctx,
		slog.String("repo-dir", repoDir),
		slog.String("base-origin", baseOrigin),
		slog.String("base-ref", baseRef),
	)
	// We've been given an already initialized git directory. We'll have to
	// assume it's the correct repo that has been cloned.
	slog.Default().WarnContext(ctx, "using an already initialized git repository")

	slog.Default().DebugContext(ctx, "changing working directory to repository dir")
	err := os.Chdir(repoDir)
	if err != nil {
		return fmt.Errorf("changing directory to the repository dir: %w", err)
	}

	// Determine if we're on the correct branch. If we are, pull it, otherwise
	// fetch it.
	slog.Default().DebugContext(ctx, "getting existing repo current branch")
	res, err := git.Branch(ctx, &libgit.BranchOpts{
		NoColor:     true,
		ShowCurrent: true,
	})
	if err != nil {
		return fmt.Errorf("getting existing repo current branch: %w", err)
	}

	if strings.TrimSpace(string(res.Stdout)) == baseRef {
		// Our existing repo is already checked out to correct branch.
		// Fetch the base ref to make sure our existing repository has the necessary
		// objects and references we'll need to cherry-pick the commits to our
		// branch.
		slog.Default().DebugContext(ctx, "pulling in latest changes")
		res, err := git.Pull(ctx, &libgit.PullOpts{
			Refspec:     []string{baseOrigin, baseRef},
			Autostash:   true,
			SetUpstream: true,
			Rebase:      libgit.RebaseStrategyTrue,
		})
		if err != nil {
			return fmt.Errorf("pulling repo current branch: %s: %w", res.String(), err)
		}

		return nil
	}

	// Fetch the base ref to make sure our existing repository has the necessary
	// objects and references.
	slog.Default().DebugContext(ctx, "fetching repository base ref")
	res, err = git.Fetch(ctx, &libgit.FetchOpts{
		// Fetch the ref but also provide a local tracking branch of the same name
		// e.g. "git fetch origin main:main"
		Refspec:     []string{baseOrigin, fmt.Sprintf("%s:%s", baseRef, baseRef)},
		SetUpstream: true,
		Porcelain:   true,
	})
	if err != nil {
		return fmt.Errorf("fetching base ref: %s: %w", res.String(), err)
	}

	return nil
}

// initializeNewRepo initializes a new repository by cloning the repo fetching
// the `baseRef`.
func initializeNewRepo(
	ctx context.Context,
	git *libgit.Client,
	repoDir string,
	owner string,
	repo string,
	baseOrigin string,
	baseRef string,
) error {
	cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo)
	slog.Default().DebugContext(slogctx.Append(ctx,
		slog.String("owner", owner),
		slog.String("repo", repo),
		slog.String("base-origin", baseOrigin),
		slog.String("base-ref", baseRef),
		slog.String("repo-dir", repoDir),
		slog.String("repo-url", cloneURL),
	), "initializing new clone of repository")

	res, err := git.Clone(ctx, &libgit.CloneOpts{
		Repository:   cloneURL,
		Directory:    repoDir,
		Origin:       baseOrigin,
		Branch:       baseRef,
		SingleBranch: true,
		NoCheckout:   true,
	})
	if err != nil {
		return fmt.Errorf("cloning repository: %s: %w", res.String(), err)
	}

	slog.Default().DebugContext(ctx, "changing working directory to repo-dir")
	err = os.Chdir(repoDir)
	if err != nil {
		return fmt.Errorf("changing directory to the repository dir: %w", err)
	}

	return nil
}
