package gitengine

import (
	"context"
	"errors"
	"fmt"
	"os/exec"
	"strings"
	"time"
)

var ErrInvalidGitInput = errors.New("invalid git input")

const DefaultCommandTimeout = 2 * time.Minute

type CommandRunner interface {
	Run(workdir string, args ...string) (string, error)
}

type CLI struct {
	Timeout time.Duration
}

func (c CLI) Run(workdir string, args ...string) (string, error) {
	if len(args) == 0 {
		return "", ErrInvalidGitInput
	}
	timeout := c.Timeout
	if timeout <= 0 {
		timeout = DefaultCommandTimeout
	}
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	cmd := exec.CommandContext(ctx, "git", args...)
	if strings.TrimSpace(workdir) != "" {
		cmd.Dir = workdir
	}
	out, err := cmd.CombinedOutput()
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return string(out), fmt.Errorf("git command timed out after %s: %w", timeout, ctx.Err())
	}
	return string(out), err
}

type StatusResult struct {
	Clean  bool
	Output string
}

type CloneOptions struct {
	RemoteURL    string
	WorktreePath string
	Branch       string
}

type CommitOptions struct {
	Message string
	All     bool
}

type DiffOptions struct {
	Staged    bool
	Pathspecs []string
}

type ChangedFile struct {
	Path       string
	ChangeType string
}

func Clone(runner CommandRunner, opts CloneOptions) error {
	remoteURL := strings.TrimSpace(opts.RemoteURL)
	worktreePath := strings.TrimSpace(opts.WorktreePath)
	if remoteURL == "" || worktreePath == "" {
		return ErrInvalidGitInput
	}
	args := []string{"clone"}
	if branch := strings.TrimSpace(opts.Branch); branch != "" {
		args = append(args, "--branch", branch)
	}
	args = append(args, remoteURL, worktreePath)
	_, err := runner.Run("", args...)
	return err
}

func Fetch(runner CommandRunner, workdir, remote string) error {
	if strings.TrimSpace(workdir) == "" {
		return ErrInvalidGitInput
	}
	remote = strings.TrimSpace(remote)
	if remote == "" {
		remote = "origin"
	}
	_, err := runner.Run(workdir, "fetch", "--prune", remote)
	return err
}

type CheckoutOptions struct {
	Branch     string
	StartPoint string
	Reset      bool
}

func Checkout(runner CommandRunner, workdir, branch string) error {
	return CheckoutBranch(runner, workdir, CheckoutOptions{Branch: branch})
}

func CheckoutBranch(runner CommandRunner, workdir string, opts CheckoutOptions) error {
	workdir = strings.TrimSpace(workdir)
	branch := strings.TrimSpace(opts.Branch)
	if workdir == "" || branch == "" {
		return ErrInvalidGitInput
	}
	startPoint := strings.TrimSpace(opts.StartPoint)
	if startPoint == "" {
		_, err := runner.Run(workdir, "checkout", branch)
		return err
	}
	flag := "-b"
	if opts.Reset {
		flag = "-B"
	}
	_, err := runner.Run(workdir, "checkout", flag, branch, startPoint)
	return err
}

func Status(runner CommandRunner, workdir string) (StatusResult, error) {
	if strings.TrimSpace(workdir) == "" {
		return StatusResult{}, ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "status", "--porcelain")
	if err != nil {
		return StatusResult{}, err
	}
	return StatusResult{
		Clean:  strings.TrimSpace(output) == "",
		Output: output,
	}, nil
}

func Diff(runner CommandRunner, workdir string, opts DiffOptions) (string, error) {
	if strings.TrimSpace(workdir) == "" {
		return "", ErrInvalidGitInput
	}
	args := []string{"diff"}
	if opts.Staged {
		args = append(args, "--cached")
	}
	pathspecs := make([]string, 0, len(opts.Pathspecs))
	for _, pathspec := range opts.Pathspecs {
		pathspec = strings.TrimSpace(pathspec)
		if pathspec != "" {
			pathspecs = append(pathspecs, pathspec)
		}
	}
	if len(pathspecs) > 0 {
		args = append(args, "--")
		args = append(args, pathspecs...)
	}
	return runner.Run(workdir, args...)
}

func Commit(runner CommandRunner, workdir string, opts CommitOptions) error {
	if strings.TrimSpace(workdir) == "" || strings.TrimSpace(opts.Message) == "" {
		return ErrInvalidGitInput
	}
	if opts.All {
		if _, err := runner.Run(workdir, "add", "-A"); err != nil {
			return err
		}
	}
	_, err := runner.Run(workdir, "commit", "-m", opts.Message)
	return err
}

func Push(runner CommandRunner, workdir, remote, branch string) error {
	if strings.TrimSpace(workdir) == "" || strings.TrimSpace(branch) == "" {
		return ErrInvalidGitInput
	}
	remote = strings.TrimSpace(remote)
	if remote == "" {
		remote = "origin"
	}
	_, err := runner.Run(workdir, "push", remote, branch)
	return err
}

func HeadRevision(runner CommandRunner, workdir string) (string, error) {
	if strings.TrimSpace(workdir) == "" {
		return "", ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "rev-parse", "HEAD")
	if err != nil {
		return "", err
	}
	revision := strings.TrimSpace(output)
	if revision == "" {
		return "", ErrInvalidGitInput
	}
	return revision, nil
}

func ChangedFiles(runner CommandRunner, workdir, before, after string) ([]string, error) {
	before = strings.TrimSpace(before)
	after = strings.TrimSpace(after)
	if strings.TrimSpace(workdir) == "" || before == "" || after == "" {
		return nil, ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "diff", "--name-only", before+".."+after)
	if err != nil {
		return nil, err
	}
	lines := strings.Split(output, "\n")
	files := make([]string, 0, len(lines))
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line != "" {
			files = append(files, line)
		}
	}
	return files, nil
}

func ChangedFilesWithStatus(runner CommandRunner, workdir, before, after string) ([]ChangedFile, error) {
	before = strings.TrimSpace(before)
	after = strings.TrimSpace(after)
	if strings.TrimSpace(workdir) == "" || before == "" || after == "" {
		return nil, ErrInvalidGitInput
	}
	output, err := runner.Run(workdir, "diff", "--name-status", before+".."+after)
	if err != nil {
		return nil, err
	}
	lines := strings.Split(output, "\n")
	files := make([]ChangedFile, 0, len(lines))
	for _, line := range lines {
		if file, ok := parseNameStatus(line); ok {
			files = append(files, file)
		}
	}
	return files, nil
}

func parseNameStatus(line string) (ChangedFile, bool) {
	line = strings.TrimSuffix(line, "\r")
	if strings.TrimSpace(line) == "" {
		return ChangedFile{}, false
	}
	fields := strings.Split(line, "\t")
	if len(fields) < 2 {
		return ChangedFile{}, false
	}
	status := strings.TrimSpace(fields[0])
	if status == "" {
		return ChangedFile{}, false
	}
	changeType := changeTypeFromGitStatus(status)
	path := fields[len(fields)-1]
	if path == "" {
		return ChangedFile{}, false
	}
	return ChangedFile{Path: path, ChangeType: changeType}, true
}

func changeTypeFromGitStatus(status string) string {
	switch {
	case strings.HasPrefix(status, "A"):
		return "added"
	case strings.HasPrefix(status, "D"):
		return "deleted"
	case strings.HasPrefix(status, "R"):
		return "renamed"
	case strings.HasPrefix(status, "C"):
		return "copied"
	default:
		return "modified"
	}
}
