package git

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// Repo represents a Git repository working tree.
type Repo struct {
	root string
}

// Open creates a new Repo rooted at the given directory.
func Open(root string) *Repo {
	return &Repo{root: root}
}

// Current discovers and returns the Repo containing the current working directory.
func Current() (*Repo, error) {
	cmd := exec.Command("git", "rev-parse", "--show-toplevel")
	out, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("not in a git repository: %w", err)
	}
	return &Repo{root: strings.TrimSpace(string(out))}, nil
}

// Root returns the top-level path of the repository.
func (r *Repo) Root() string {
	return r.root
}

// Run executes a Git command in the root of this Repo.
func (r *Repo) Run(args ...string) (string, error) {
	cmd := exec.Command("git", args...)
	cmd.Dir = r.root
	out, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("git %s failed: %w\n%s", strings.Join(args, " "), err, string(out))
	}
	return strings.TrimSpace(string(out)), nil
}

// CurrentBranch returns the name of the checked-out branch, or the
// short commit hash if detached.
func (r *Repo) CurrentBranch() (string, error) {
	branch, err := r.Run("symbolic-ref", "--short", "-q", "HEAD")
	if err == nil {
		return branch, nil
	}
	return r.RevParse("HEAD")
}

// RevParse resolves a ref (for example, "HEAD", "main") to a 7-character short commit hash.
func (r *Repo) RevParse(ref string) (string, error) {
	return r.Run("rev-parse", "--short", ref)
}

// IsDirty returns true if the working tree has uncommitted staged, unstaged, or untracked changes.
func (r *Repo) IsDirty() (bool, error) {
	out, err := r.Run("status", "--porcelain")
	if err != nil {
		return false, err
	}
	return len(out) > 0, nil
}

// ChangedFiles returns a deduplicated list of relative paths for all modified, staged, and untracked files.
func (r *Repo) ChangedFiles() ([]string, error) {
	diffOut, err := r.Run("diff", "--name-only", "HEAD", "--")
	if err != nil {
		return nil, err
	}
	untrackedOut, err := r.Run("ls-files", "--others", "--exclude-standard")
	if err != nil {
		return nil, err
	}

	seen := make(map[string]bool)
	var files []string
	for f := range strings.SplitSeq(diffOut+"\n"+untrackedOut, "\n") {
		f = strings.TrimSpace(f)
		if f == "" || seen[f] {
			continue
		}
		seen[f] = true
		files = append(files, f)
	}
	return files, nil
}

// AddWorktree creates a detached worktree at dest pointing to ref.
// It returns a cleanup function that forcibly removes the worktree.
func (r *Repo) AddWorktree(dest, ref string) (cleanup func(), err error) {
	if _, err := r.Run("worktree", "add", "--detach", "-q", dest, ref); err != nil {
		return nil, err
	}
	cleanup = func() {
		_, _ = r.Run("worktree", "remove", "--force", dest)
	}
	return cleanup, nil
}

// SyncChangesTo copies all uncommitted and untracked files to a target directory,
// removing destination files that were deleted locally.
func (r *Repo) SyncChangesTo(destDir string) error {
	files, err := r.ChangedFiles()
	if err != nil {
		return err
	}

	for _, relPath := range files {
		src := filepath.Join(r.root, relPath)
		dst := filepath.Join(destDir, relPath)

		fi, err := os.Stat(src)
		if os.IsNotExist(err) {
			_ = os.Remove(dst)
			continue
		}
		if err != nil || fi.IsDir() {
			continue
		}

		if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
			return err
		}
		if err := copyFile(src, dst); err != nil {
			return err
		}
	}
	return nil
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	if _, err := io.Copy(out, in); err != nil {
		return err
	}
	return out.Sync()
}
