package git

import (
	"errors"
	"fmt"
	"log/slog"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	knotconfig "tangled.org/core/knotserver/config"
	"tangled.org/core/knotserver/sandbox"
)

func Fork(repoPath, source string, cfg *knotconfig.Config) error {
	return ForkWithSandbox(repoPath, source, cfg, nil)
}

// ForkWithSandbox clones source into repoPath, optionally wrapping the
// post-clone configure step in sb. The initial clone itself is not sandboxed
// because the target directory doesn't exist yet when the ruleset is applied.
func ForkWithSandbox(repoPath, source string, cfg *knotconfig.Config, sb sandbox.Backend) error {
	u, err := validateSource(source)
	if err != nil {
		return err
	}

	localClone := false
	if o := optimizeClone(u, cfg); o != nil {
		u = o
		localClone = true
	}

	cloneCmd := exec.Command("git", cloneArgs(u, repoPath, localClone)...)
	cloneCmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
	if err := cloneCmd.Run(); err != nil {
		return fmt.Errorf("failed to bare clone repository: %w", err)
	}

	// ensure repoPath exists before attempting to sandbox the configure step.
	if _, statErr := os.Stat(repoPath); statErr != nil {
		return fmt.Errorf("clone did not create %s: %w", repoPath, statErr)
	}

	configureCmd := exec.Command("git", "-C", repoPath, "config", "receive.hideRefs", "refs/hidden")
	if sb != nil {
		configureCmd, err = sb.Wrap(repoPath, configureCmd)
		if err != nil {
			return fmt.Errorf("sandbox wrap for git config: %w", err)
		}
	} else {
		configureCmd.Dir = repoPath
	}
	if err := configureCmd.Run(); err != nil {
		return fmt.Errorf("failed to configure hidden refs: %w", err)
	}

	return nil
}

// validateSource parses a user-supplied clone source and rejects anything that
// isn't a plain http(s) URL. This blocks argument injection (a leading "-" that
// git would read as an option) and non-http transports such as ext::, file://,
// or ssh that could be abused for command execution or local file access.
func validateSource(source string) (*url.URL, error) {
	if strings.HasPrefix(source, "-") {
		return nil, fmt.Errorf("invalid source: must not start with '-'")
	}

	u, err := url.Parse(source)
	if err != nil {
		return nil, fmt.Errorf("failed to parse source URL: %w", err)
	}

	switch u.Scheme {
	case "http", "https":
	default:
		return nil, fmt.Errorf("invalid source: scheme must be http or https, got %q", u.Scheme)
	}

	if u.Host == "" {
		return nil, fmt.Errorf("invalid source: missing host")
	}

	return u, nil
}

// cloneArgs builds the argument list for a hardened `git clone`.
//
//	protocol.allow=never denies every transport by default; only http and
//	https are re-enabled. This blocks ext::, ssh, git, and other transports
//	that can potentially lead to command execution. The file transport is allowed
//	only for the trusted local-clone optimization.
func cloneArgs(u *url.URL, repoPath string, localClone bool) []string {
	args := []string{
		"-c", "protocol.allow=never",
		"-c", "protocol.http.allow=always",
		"-c", "protocol.https.allow=always",
	}
	if localClone {
		args = append(args, "-c", "protocol.file.allow=always")
	}
	args = append(args, "clone", "--bare", "--", u.String(), repoPath)
	return args
}

func optimizeClone(u *url.URL, cfg *knotconfig.Config) *url.URL {
	// only optimize if it's the same host
	if u.Host != cfg.Server.Hostname {
		return nil
	}

	local := filepath.Join(cfg.Repo.ScanPath, u.Path)

	// sanity check: is there a git repo there?
	if _, err := PlainOpen(local); err != nil {
		return nil
	}

	// create optimized file:// URL
	optimized := &url.URL{
		Scheme: "file",
		Path:   local,
	}

	slog.Debug("performing local clone", "url", optimized.String())
	return optimized
}

func (g *GitRepo) Sync() error {
	branch := g.h.String()

	fetchOpts := &git.FetchOptions{
		RefSpecs: []config.RefSpec{
			config.RefSpec("+" + branch + ":" + branch), // +refs/heads/master:refs/heads/master
		},
	}

	err := g.r.Fetch(fetchOpts)
	if errors.Is(git.NoErrAlreadyUpToDate, err) {
		return nil
	} else if err != nil {
		return fmt.Errorf("failed to fetch origin branch: %s: %w", branch, err)
	}
	return nil
}

// TrackHiddenRemoteRef tracks a hidden remote in the repository. For example,
// if the feature branch on the fork (forkRef) is feature-1, and the remoteRef,
// i.e. the branch we want to merge into, is main, this will result in a refspec:
//
//	+refs/heads/main:refs/hidden/feature-1/main
func (g *GitRepo) TrackHiddenRemoteRef(forkRef, remoteRef string) error {
	fetchOpts := &git.FetchOptions{
		RefSpecs: []config.RefSpec{
			config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/hidden/%s/%s", remoteRef, forkRef, remoteRef)),
		},
		RemoteName: "origin",
	}

	err := g.r.Fetch(fetchOpts)
	if errors.Is(git.NoErrAlreadyUpToDate, err) {
		return nil
	} else if err != nil {
		return fmt.Errorf("failed to fetch hidden remote: %s: %w", forkRef, err)
	}
	return nil
}
