package claudebox

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"os/exec"
	"strings"
)

// GitOperations provides local git operations in the container.
type GitOperations struct {
	workDir    string
	giteaToken string
	gitUser    string
	gitEmail   string
	logger     *slog.Logger
}

// GitOperationsConfig holds configuration for git operations.
type GitOperationsConfig struct {
	// WorkDir is the default working directory.
	WorkDir string

	// GiteaToken is the token for HTTPS push authentication.
	GiteaToken string

	// GitUser is the git commit author name.
	GitUser string

	// GitEmail is the git commit author email.
	GitEmail string

	// Logger is an optional logger for debug output.
	Logger *slog.Logger
}

// NewGitOperations creates a new git operations helper.
func NewGitOperations(cfg GitOperationsConfig) *GitOperations {
	if cfg.GitUser == "" {
		cfg.GitUser = "rdev-worker"
	}
	if cfg.GitEmail == "" {
		cfg.GitEmail = "worker@threesix.ai"
	}
	logger := cfg.Logger
	if logger == nil {
		logger = slog.Default()
	}
	return &GitOperations{
		workDir:    cfg.WorkDir,
		giteaToken: cfg.GiteaToken,
		gitUser:    cfg.GitUser,
		gitEmail:   cfg.GitEmail,
		logger:     logger,
	}
}

// CloneResult contains the result of a git clone operation.
type CloneResult struct {
	Cloned bool // True if repo was cloned, false if already existed
	Error  error
}

// CloneRepo clones a git repository into the workspace if it doesn't exist.
// If the workspace already contains a git repo, it pulls the latest changes.
func (g *GitOperations) CloneRepo(ctx context.Context, workDir, cloneURL string) *CloneResult {
	result := &CloneResult{}

	g.logger.Info("git clone request",
		"work_dir", workDir,
		"clone_url", g.redactToken(cloneURL),
		"has_token", g.giteaToken != "")

	if cloneURL == "" {
		result.Error = fmt.Errorf("git clone URL is required")
		g.logger.Error("git clone failed: empty URL")
		return result
	}

	// Check if already a git repo with the correct remote
	isRepo := g.isGitRepo(ctx, workDir)
	g.logger.Info("workspace check", "is_git_repo", isRepo, "work_dir", workDir)

	if isRepo {
		currentRemote, err := g.runGitOutput(ctx, workDir, "config", "--get", "remote.origin.url")
		currentRemote = strings.TrimSpace(currentRemote)
		g.logger.Info("existing repo detected",
			"current_remote", g.redactToken(currentRemote),
			"expected_remote", g.redactToken(cloneURL),
			"remote_match", currentRemote == cloneURL,
			"remote_err", err)

		if err == nil && currentRemote == cloneURL {
			// Pull latest changes
			g.logger.Info("pulling latest changes", "work_dir", workDir)
			if err := g.runGit(ctx, workDir, "pull", "--ff-only"); err != nil {
				// Pull failed but repo exists - continue with existing state
				g.logger.Warn("git pull failed, continuing with existing state",
					"error", err,
					"work_dir", workDir)
			}
			g.logger.Info("git clone complete (existing repo)", "cloned", false)
			return result
		}

		// Different remote - clear and re-clone
		g.logger.Info("clearing workspace for re-clone", "work_dir", workDir)
		if err := g.clearDir(ctx, workDir); err != nil {
			result.Error = fmt.Errorf("clear workspace: %w", err)
			g.logger.Error("failed to clear workspace", "error", result.Error)
			return result
		}
	}

	// Check if directory exists and is non-empty (would cause clone to fail)
	if !isRepo {
		checkCmd := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("ls -A %s 2>/dev/null | head -1", workDir))
		output, _ := checkCmd.Output()
		if len(strings.TrimSpace(string(output))) > 0 {
			g.logger.Warn("workspace not empty but not a git repo, clearing",
				"work_dir", workDir,
				"first_file", strings.TrimSpace(string(output)))
			if err := g.clearDir(ctx, workDir); err != nil {
				result.Error = fmt.Errorf("clear non-empty workspace: %w", err)
				g.logger.Error("failed to clear non-empty workspace", "error", result.Error)
				return result
			}
		}
	}

	// Inject token for authentication
	authCloneURL := cloneURL
	if g.giteaToken != "" {
		authCloneURL = strings.Replace(cloneURL, "https://", "https://token:"+g.giteaToken+"@", 1)
	} else {
		g.logger.Warn("no gitea token configured, clone may fail for private repos")
	}

	// Clone the repository
	g.logger.Info("executing git clone", "work_dir", workDir)
	cmd := exec.CommandContext(ctx, "git", "clone", authCloneURL, workDir)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		errMsg := g.redactToken(stderr.String())
		stdoutMsg := g.redactToken(stdout.String())
		result.Error = fmt.Errorf("git clone exit %v: %s", err, errMsg)
		g.logger.Error("git clone failed",
			"error", err,
			"stderr", errMsg,
			"stdout", stdoutMsg,
			"work_dir", workDir)
		return result
	}

	result.Cloned = true
	g.logger.Info("git clone complete", "cloned", true, "work_dir", workDir)
	return result
}

// CommitAndPushResult contains the result of commit and push operations.
type CommitAndPushResult struct {
	HasChanges   bool
	CommitSHA    string
	FilesChanged []string
	Pushed       bool
	Error        error
}

// CommitAndPush commits and optionally pushes changes.
func (g *GitOperations) CommitAndPush(ctx context.Context, workDir, message string, push bool) *CommitAndPushResult {
	result := &CommitAndPushResult{}

	// Configure git user
	if err := g.runGit(ctx, workDir, "config", "user.name", g.gitUser); err != nil {
		result.Error = fmt.Errorf("git config user.name: %w", err)
		return result
	}
	if err := g.runGit(ctx, workDir, "config", "user.email", g.gitEmail); err != nil {
		result.Error = fmt.Errorf("git config user.email: %w", err)
		return result
	}

	// Check for changes
	status, err := g.runGitOutput(ctx, workDir, "status", "--porcelain")
	if err != nil {
		result.Error = fmt.Errorf("git status: %w", err)
		return result
	}
	if strings.TrimSpace(status) == "" {
		return result // No changes
	}
	result.HasChanges = true

	// Stage all changes
	if err := g.runGit(ctx, workDir, "add", "-A"); err != nil {
		result.Error = fmt.Errorf("git add: %w", err)
		return result
	}

	// Get list of staged files
	diffOutput, err := g.runGitOutput(ctx, workDir, "diff", "--cached", "--name-only")
	if err != nil {
		result.Error = fmt.Errorf("git diff: %w", err)
		return result
	}
	for _, f := range strings.Split(strings.TrimSpace(diffOutput), "\n") {
		if f != "" {
			result.FilesChanged = append(result.FilesChanged, f)
		}
	}

	// Commit
	if err := g.runGit(ctx, workDir, "commit", "-m", message); err != nil {
		result.Error = fmt.Errorf("git commit: %w", err)
		return result
	}

	// Get commit SHA
	sha, err := g.runGitOutput(ctx, workDir, "rev-parse", "HEAD")
	if err != nil {
		result.Error = fmt.Errorf("git rev-parse: %w", err)
		return result
	}
	result.CommitSHA = strings.TrimSpace(sha)

	// Push if requested
	if push {
		// Configure credential helper
		if g.giteaToken != "" {
			credHelper := fmt.Sprintf("!f() { echo username=token; echo password=%s; }; f", g.giteaToken)
			if err := g.runGit(ctx, workDir, "config", "credential.helper", credHelper); err != nil {
				g.logger.Debug("credential helper config failed, continuing with push", "error", err)
			}
		}

		if err := g.runGit(ctx, workDir, "push", "origin", "HEAD"); err != nil {
			result.Error = fmt.Errorf("git push: %w", err)
			return result
		}
		result.Pushed = true
	}

	return result
}

// GitStatusResult contains git status information.
type GitStatusResult struct {
	IsRepo       bool     `json:"is_repo"`
	HasChanges   bool     `json:"has_changes"`
	ChangedFiles []string `json:"changed_files,omitempty"`
	Branch       string   `json:"branch,omitempty"`
}

// Status returns the git status of the workspace.
func (g *GitOperations) Status(ctx context.Context, workDir string) (*GitStatusResult, error) {
	result := &GitStatusResult{}

	if !g.isGitRepo(ctx, workDir) {
		return result, nil
	}
	result.IsRepo = true

	// Get current branch
	branch, err := g.runGitOutput(ctx, workDir, "rev-parse", "--abbrev-ref", "HEAD")
	if err == nil {
		result.Branch = strings.TrimSpace(branch)
	}

	// Get status
	status, err := g.runGitOutput(ctx, workDir, "status", "--porcelain")
	if err != nil {
		return result, fmt.Errorf("git status: %w", err)
	}

	lines := strings.Split(strings.TrimSpace(status), "\n")
	for _, line := range lines {
		if len(line) > 3 {
			result.ChangedFiles = append(result.ChangedFiles, strings.TrimSpace(line[3:]))
		}
	}
	result.HasChanges = len(result.ChangedFiles) > 0

	return result, nil
}

// isGitRepo checks if the directory is a git repository.
func (g *GitOperations) isGitRepo(ctx context.Context, workDir string) bool {
	cmd := exec.CommandContext(ctx, "test", "-d", workDir+"/.git")
	return cmd.Run() == nil
}

// clearDir clears the contents of a directory.
func (g *GitOperations) clearDir(ctx context.Context, dir string) error {
	cmd := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("rm -rf %s/* %s/.[!.]*", dir, dir))
	return cmd.Run()
}

// runGit executes a git command.
func (g *GitOperations) runGit(ctx context.Context, workDir string, args ...string) error {
	cmd := exec.CommandContext(ctx, "git", append([]string{"-C", workDir}, args...)...)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		errMsg := g.redactToken(stderr.String())
		return fmt.Errorf("%s: %s", err, errMsg)
	}
	return nil
}

// runGitOutput executes a git command and returns stdout.
func (g *GitOperations) runGitOutput(ctx context.Context, workDir string, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, "git", append([]string{"-C", workDir}, args...)...)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		errMsg := g.redactToken(stderr.String())
		return "", fmt.Errorf("%s: %s", err, errMsg)
	}
	return stdout.String(), nil
}

// redactToken removes the Gitea token from output.
func (g *GitOperations) redactToken(s string) string {
	if g.giteaToken == "" {
		return s
	}
	return strings.ReplaceAll(s, g.giteaToken, "[REDACTED]")
}
