package utils

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/alecthomas/chroma/v2/quick"
	"github.com/go-git/go-git/v5"
	"github.com/sergi/go-diff/diffmatchpatch"
	"golang.org/x/sys/execabs"
	"golang.org/x/term"
)

// GitClone clones a git repo
func GitClone(repo, dst string) (string, error) {
	if _, err := execabs.LookPath("git"); err == nil {
		cmd := exec.Command("git", "clone", "--depth", "1", repo, dst)
		dat, err := cmd.CombinedOutput()
		if err != nil {
			return "", fmt.Errorf("failed to clone repo '%s' to %s: %v", repo, dst, err)
		}
		return string(dat), nil
	} else { // use pure Go version of git
		if _, err := git.PlainClone(dst, false, &git.CloneOptions{
			URL:      repo,
			Depth:    1,
			Progress: os.Stderr,
		}); err != nil {
			return "", fmt.Errorf("failed to create local copy of 'appledb' repo: %v", err)
		}
		return "", nil
	}
}

// GitRefresh refreshes a git repo
func GitRefresh(repoPath string) (string, error) {
	if _, err := execabs.LookPath("git"); err == nil {
		cmd := exec.Command("git", "pull", "--rebase")
		cmd.Dir = repoPath
		dat, err := cmd.CombinedOutput()
		if err != nil {
			return "", fmt.Errorf("failed to refresh repo '%s': %v", repoPath, err)
		}
		return string(dat), nil
	} else { // use pure Go version of git
		r, err := git.PlainOpen(repoPath)
		if err != nil {
			return "", fmt.Errorf("failed to open local copy of 'appledb' repo: %v", err)
		}
		w, err := r.Worktree()
		if err != nil {
			return "", fmt.Errorf("failed to get worktree of local copy of 'appledb' repo: %v", err)
		}
		if err = w.Pull(&git.PullOptions{
			RemoteName: "origin",
			// Force:      true,
			Progress: os.Stderr,
		}); err != nil {
			if !errors.Is(err, git.NoErrAlreadyUpToDate) {
				return "", fmt.Errorf("failed to update local copy of 'appledb' repo: %v", err)
			}
		}
		return "", nil
	}
}

type GitDiffConfig struct {
	Tool  string
	Color bool
}

func GitDiff(src, dst string, conf *GitDiffConfig) (string, error) {
	switch conf.Tool {
	case "delta":
		return createDeltaDiffPatch(src, dst, conf)
	case "git":
		return createGitDiffPatch(src, dst, conf)
	case "go":
		return createGoDiff(src, dst, conf)
	default:
		if _, err := exec.LookPath("delta"); err == nil {
			return createDeltaDiffPatch(src, dst, conf)
		} else if _, err := exec.LookPath("git"); err == nil {
			return createGitDiffPatch(src, dst, conf)
		}
		return createGoDiff(src, dst, conf)
	}
}

func createGoDiff(src, dst string, conf *GitDiffConfig) (string, error) {
	dmp := diffmatchpatch.New()

	diffs := dmp.DiffMain(src, dst, false)
	if len(diffs) > 2 {
		diffs = dmp.DiffCleanupSemanticLossless(diffs)
		diffs = dmp.DiffCleanupEfficiency(diffs)
	}

	if len(diffs) == 1 {
		if diffs[0].Type == diffmatchpatch.DiffEqual {
			return "", nil
		}
	}

	return dmp.DiffPrettyText(diffs), nil
}

// createGitDiffPatch renders a unified diff by shelling out to the host git
// binary. The output bytes (hunk boundaries, context-line algorithm) depend on
// the installed git version. The diff cache (internal/diff) does not fold the
// git version into its cache scope, so a cache built on one host may render
// differently than a fresh run on a host with a different git; the cache is
// therefore host-bound by default. See machosJob.OptionsHash for details.
func createGitDiffPatch(src, dst string, conf *GitDiffConfig) (string, error) {
	tmpSrc, err := os.CreateTemp("", "src")
	if err != nil {
		return "", err
	}
	defer os.Remove(tmpSrc.Name())

	os.WriteFile(tmpSrc.Name(), []byte(src), 0644)

	tmpDst, err := os.CreateTemp("", "dst")
	if err != nil {
		return "", err
	}
	defer os.Remove(tmpDst.Name())

	os.WriteFile(tmpDst.Name(), []byte(dst), 0644)

	cmd := exec.Command("git", "diff", "--no-index", tmpSrc.Name(), tmpDst.Name())

	dat, _ := cmd.CombinedOutput()

	out := string(dat)
	// strip the first 4 lines of the patch file
	_, out, _ = strings.Cut(out, "\n")
	_, out, _ = strings.Cut(out, "\n")
	_, out, _ = strings.Cut(out, "\n")
	_, out, _ = strings.Cut(out, "\n")
	// strip the @@ gap lines
	re := regexp.MustCompile("(?m)^@@ .*$")
	out = re.ReplaceAllString(out, "")
	if conf.Color {
		// colorize the diff
		b := new(strings.Builder)
		if err := quick.Highlight(b, out, "diff", "terminal256", "nord"); err != nil {
			return "", err
		}
		return b.String(), nil
	}
	return out, nil
}

func createDeltaDiffPatch(src, dst string, conf *GitDiffConfig) (string, error) {
	tmpSrc, err := os.CreateTemp("", "src")
	if err != nil {
		return "", err
	}
	defer os.Remove(tmpSrc.Name())

	os.WriteFile(tmpSrc.Name(), []byte(src), 0644)

	tmpDst, err := os.CreateTemp("", "dst")
	if err != nil {
		return "", err
	}
	defer os.Remove(tmpDst.Name())

	os.WriteFile(tmpDst.Name(), []byte(dst), 0644)

	width := 120
	if term.IsTerminal(0) {
		twidth, _, err := term.GetSize(0)
		if err != nil {
			return "", err
		}
		width = twidth
	}

	cmd := exec.Command(
		"delta",
		"--dark",
		"--diff-so-fancy",
		"--side-by-side",
		"--file-style", "omit",
		"--hunk-header-style", "omit",
		"--syntax-theme",
		"Nord",
		"--width", strconv.Itoa(width),
		tmpSrc.Name(),
		tmpDst.Name(),
	)

	out, _ := cmd.CombinedOutput()
	// if err != nil {
	// 	return "", fmt.Errorf("delta failed %s: %v", out, err)
	// }

	return string(out), nil
}

func ClangFormat(src, filename string, color bool) string {
	if _, err := exec.LookPath("clang-format"); err == nil {
		cmd := exec.Command(
			"clang-format",
			"-style={AlignConsecutiveDeclarations: true}",
			"--assume-filename",
			filename,
		)
		stdin, err := cmd.StdinPipe()
		if err != nil {
			return src
		}

		go func() {
			defer stdin.Close()
			io.WriteString(stdin, src)
		}()

		out, _ := cmd.CombinedOutput()
		// if err != nil {
		// 	return "", fmt.Errorf("clang-format failed %s: %v", out, err)
		// }

		src = string(out)
	}

	if color {
		b := new(strings.Builder)
		if err := quick.Highlight(b, src, "c", "terminal256", "nord"); err != nil {
			return ""
		}
		return b.String()
	}

	return src
}
