package cmd

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"syscall"

	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
)

var gitCredentialURL = regexp.MustCompile(`(?i)(https?://)[^/@\s]+@`)

func continueSetupWithLatestRelease(ctx context.Context) error {
	executable, repo, err := setupReleaseRepo(ctx)
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig, "setup release: %v", err)
	}

	releaseLock, err := acquireSetupReleaseLock(ctx, repo)
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "setup release: %v", err)
	}
	locked := true
	defer func() {
		if locked {
			releaseLock()
		}
	}()

	tag, err := latestSetupReleaseTag(ctx, repo)
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "setup release: %v", err)
	}
	if _, err := runSetupRemoteGit(ctx, repo, "fetch", "--force", "origin",
		"refs/tags/"+tag+":refs/tags/"+tag); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource,
			"setup release: fetch %s: %v", tag, err)
	}
	releaseSHA, err := runSetupGit(ctx, repo, "rev-parse", tag+"^{commit}")
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource,
			"setup release: resolve %s: %v", tag, err)
	}
	head, err := runSetupGit(ctx, repo, "rev-parse", "HEAD")
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig, "setup release: resolve current HEAD: %v", err)
	}
	if head == releaseSHA {
		_, _ = fmt.Fprintf(os.Stderr, "cp9 setup: already at latest release %s; skipping build\n", tag)
		return nil
	}
	status, err := runSetupGit(ctx, repo, "status", "--porcelain", "--untracked-files=all")
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig, "setup release: inspect working tree: %v", err)
	}
	if status != "" {
		return cp9errors.Newf(cp9errors.CategoryConfig,
			"setup release: working tree is not clean; commit or stash these changes, or run setup checkout=false:\n%s",
			status)
	}

	previousBranch := setupCurrentBranch(ctx, repo)
	previousHEAD := head
	if _, err := runSetupGit(ctx, repo, "checkout", "--detach", releaseSHA); err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig,
			"setup release: checkout %s (%s) in %s: %v; inspect `git status` or run setup checkout=false",
			tag, releaseSHA, repo, err)
	}
	_, _ = fmt.Fprintf(os.Stderr, "cp9 setup: checked out %s; building %s\n", tag, executable)

	if err := runSetupBuild(ctx, repo); err != nil {
		rollbackErr := rollbackSetupCheckout(ctx, repo, previousBranch, previousHEAD)
		if rollbackErr != nil {
			return cp9errors.Newf(cp9errors.CategoryResource,
				"setup release: build %s failed: %v; rollback to %s also failed: %v",
				tag, err, previousHEAD, rollbackErr)
		}
		return cp9errors.Newf(cp9errors.CategoryResource,
			"setup release: build %s failed: %v; restored %s",
			tag, err, previousHEAD)
	}

	releaseLock()
	locked = false
	_, _ = fmt.Fprintf(os.Stderr, "cp9 setup: built %s; continuing with the new binary\n", tag)
	return replaceSetupProcess(executable)
}

func setupReleaseRepo(ctx context.Context) (executable, repo string, err error) {
	executable, err = os.Executable()
	if err != nil {
		return "", "", fmt.Errorf("resolve current executable: %w", err)
	}
	executable, err = filepath.EvalSymlinks(executable)
	if err != nil {
		return "", "", fmt.Errorf("resolve executable symlinks: %w", err)
	}
	if filepath.Base(filepath.Dir(executable)) != "bin" {
		return "", "", fmt.Errorf(
			"executable %s is not <cp9-repo>/bin/cp9; run the repository binary or use setup checkout=false",
			executable)
	}
	repo = filepath.Dir(filepath.Dir(executable))
	topLevel, err := runSetupGit(ctx, repo, "rev-parse", "--show-toplevel")
	if err != nil {
		return "", "", fmt.Errorf("locate repository for %s: %w", executable, err)
	}
	topLevel, err = filepath.EvalSymlinks(topLevel)
	if err != nil {
		return "", "", fmt.Errorf("resolve repository path %s: %w", topLevel, err)
	}
	repo, err = filepath.EvalSymlinks(repo)
	if err != nil {
		return "", "", fmt.Errorf("resolve executable repository path: %w", err)
	}
	if topLevel != repo {
		return "", "", fmt.Errorf(
			"executable %s belongs to %s, but git reports repository %s; use setup checkout=false",
			executable, repo, topLevel)
	}
	return executable, repo, nil
}

func latestSetupReleaseTag(ctx context.Context, repo string) (string, error) {
	output, err := runSetupRemoteGit(ctx, repo, "ls-remote", "--tags", "--refs",
		"--sort=-version:refname", "origin", "refs/tags/v*")
	if err != nil {
		return "", fmt.Errorf("query latest release tag from origin: %w", err)
	}
	line, _, _ := strings.Cut(output, "\n")
	fields := strings.Fields(line)
	if len(fields) != 2 {
		return "", errors.New("origin has no versioned release tag matching refs/tags/v*")
	}
	tag, ok := strings.CutPrefix(fields[1], "refs/tags/")
	if !ok || tag == "" {
		return "", fmt.Errorf("origin returned invalid release ref %q", fields[1])
	}
	return tag, nil
}

func acquireSetupReleaseLock(ctx context.Context, repo string) (func(), error) {
	lockPath, err := runSetupGit(ctx, repo, "rev-parse", "--git-path", "cp9-setup-release.lock")
	if err != nil {
		return nil, fmt.Errorf("resolve setup lock path: %w", err)
	}
	if !filepath.IsAbs(lockPath) {
		lockPath = filepath.Join(repo, lockPath)
	}
	lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open setup lock %s: %w", lockPath, err)
	}
	if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		_ = lockFile.Close()
		return nil, errors.New("another cp9 setup is already checking out or building this repository")
	}
	return func() {
		_ = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
		_ = lockFile.Close()
	}, nil
}

func runSetupBuild(ctx context.Context, repo string) error {
	var output bytes.Buffer
	command := exec.CommandContext(ctx, "make", "build")
	command.Dir = repo
	command.Stdin = os.Stdin
	command.Stdout = io.MultiWriter(os.Stderr, &output)
	command.Stderr = io.MultiWriter(os.Stderr, &output)
	if err := command.Run(); err != nil {
		return fmt.Errorf("%w\n%s", err, strings.TrimSpace(redactSetupCommandOutput(output.String())))
	}
	return nil
}

func rollbackSetupCheckout(ctx context.Context, repo, branch, head string) error {
	args := []string{"checkout", "--detach", head}
	if branch != "" {
		args = []string{"checkout", branch}
	}
	_, err := runSetupGit(ctx, repo, args...)
	return err
}

func setupCurrentBranch(ctx context.Context, repo string) string {
	command := exec.CommandContext(ctx, "git", "-C", repo, "symbolic-ref", "--quiet", "--short", "HEAD")
	output, err := command.Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(output))
}

func replaceSetupProcess(executable string) error {
	args := append([]string{executable}, os.Args[1:]...)
	// #nosec G702 -- setupReleaseRepo validates the executable and arguments are forwarded without a shell.
	if err := syscall.Exec(executable, args, os.Environ()); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource,
			"setup release: new binary %s failed: %v", executable, err)
	}
	return nil
}

func runSetupGit(ctx context.Context, repo string, args ...string) (string, error) {
	// #nosec G204 -- git is fixed and every argument is passed directly without a shell.
	command := exec.CommandContext(ctx, "git", append([]string{"-C", repo}, args...)...)
	output, err := command.CombinedOutput()
	text := strings.TrimSpace(redactSetupCommandOutput(string(output)))
	if err != nil {
		if text == "" {
			return "", err
		}
		return "", fmt.Errorf("%w: %s", err, text)
	}
	return text, nil
}

func runSetupRemoteGit(ctx context.Context, repo string, args ...string) (string, error) {
	output, gitErr := runSetupGit(ctx, repo, args...)
	if gitErr == nil {
		return output, nil
	}

	fallbackArgs, ok := setupGHGitArgs(ctx, repo)
	if !ok {
		return "", gitErr
	}
	_, _ = fmt.Fprintf(os.Stderr,
		"cp9 setup: git remote access failed (%v); retrying with logged-in gh credentials\n",
		gitErr)
	output, fallbackErr := runSetupGit(ctx, repo, append(fallbackArgs, args...)...)
	if fallbackErr != nil {
		return "", fmt.Errorf("%w; gh-authenticated retry failed: %w", gitErr, fallbackErr)
	}
	return output, nil
}

func setupGHGitArgs(ctx context.Context, repo string) ([]string, bool) {
	origin, err := runSetupGit(ctx, repo, "remote", "get-url", "origin")
	if err != nil {
		return nil, false
	}
	rewrite, ok := githubOriginRewrite(origin)
	if !ok {
		return nil, false
	}
	if err := exec.CommandContext(ctx, "gh", "auth", "status", "--hostname", "github.com").Run(); err != nil {
		return nil, false
	}

	args := []string{
		"-c", "credential.helper=",
		"-c", "credential.helper=!gh auth git-credential",
	}
	if rewrite != "" {
		args = append(args, "-c", "url.https://github.com/.insteadOf="+rewrite)
	}
	return args, true
}

func githubOriginRewrite(origin string) (string, bool) {
	switch {
	case strings.HasPrefix(origin, "git@github.com:"):
		return "git@github.com:", true
	case strings.HasPrefix(origin, "ssh://git@github.com/"):
		return "ssh://git@github.com/", true
	case strings.HasPrefix(origin, "https://github.com/"):
		return "", true
	case strings.HasPrefix(origin, "http://github.com/"):
		return "http://github.com/", true
	default:
		return "", false
	}
}

func redactSetupCommandOutput(output string) string {
	return gitCredentialURL.ReplaceAllString(output, "${1}<redacted>@")
}
