package xrpc

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/bluesky-social/indigo/atproto/atclient"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/dgraph-io/ristretto"
	"tangled.org/core/api/tangled"
)

type MergeInput struct {
	TargetRepo   syntax.DID
	TargetBranch string
	SourceRepo   syntax.DID
	SourceCommit string
}

type MergeCheckCache struct {
	cache *ristretto.Cache
}

func (m *MergeCheckCache) cacheKey(input MergeInput) string {
	raw := strings.Join([]string{
		input.TargetRepo.String(),
		input.TargetBranch,
		input.SourceRepo.String(),
		input.SourceCommit,
	}, "\x00")
	sum := sha256.Sum256([]byte(raw))
	return fmt.Sprintf("%x", sum)
}

func (m *MergeCheckCache) cacheVal(out *tangled.GitMergeCheck_Output) any {
	return *out
}

func (m *MergeCheckCache) Set(input MergeInput, mergeCheck *tangled.GitMergeCheck_Output) {
	key := m.cacheKey(input)
	val := m.cacheVal(mergeCheck)
	m.cache.Set(key, val, 0)
}

func (m *MergeCheckCache) Get(input MergeInput) (tangled.GitMergeCheck_Output, bool) {
	key := m.cacheKey(input)
	if val, ok := m.cache.Get(key); ok {
		if out, ok := val.(tangled.GitMergeCheck_Output); ok {
			// cache hit
			return out, true
		}
	}

	// cache miss
	return tangled.GitMergeCheck_Output{}, false
}

var mergeCheckCache MergeCheckCache

func init() {
	cache, _ := ristretto.NewCache(&ristretto.Config{
		NumCounters:            1e7,
		MaxCost:                1 << 30,
		BufferItems:            64,
		TtlTickerDurationInSec: 60 * 60 * 24 * 2, // 2 days
	})
	mergeCheckCache = MergeCheckCache{cache}
}

func (x *Xrpc) GitMergeCheck(w http.ResponseWriter, r *http.Request) {
	var input tangled.GitMergeCheck_Input
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "failed to decode json body"})
		return
	}
	l := x.Logger.With("handler", "MergeCheck2", "input", input)
	l.Debug("request")

	if err := gitMergeCheck_Input_Validate(input); err != nil {
		writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "InvalidRequest", Message: err.Error()})
		return
	}

	mergeInput := MergeInput{
		TargetRepo:   syntax.DID(input.Repo),
		TargetBranch: input.Branch,
		SourceRepo:   syntax.DID(input.Source.Repo),
		SourceCommit: input.Source.Commit,
	}

	// check cache
	if cached, ok := mergeCheckCache.Get(mergeInput); ok {
		l.Debug("cache hit")
		writeJson(w, http.StatusOK, cached)
		return
	}

	output, status, apierr := x.mergeCheck(r.Context(), input)
	if apierr != nil {
		l.Error("failed", "kind", apierr.Name, "error", apierr.Message)
		writeJson(w, status, apierr)
		return
	}

	// update cache
	mergeCheckCache.Set(mergeInput, &output)

	writeJson(w, status, output)
}

func (x *Xrpc) mergeCheck(ctx context.Context, input tangled.GitMergeCheck_Input) (tangled.GitMergeCheck_Output, int, *atclient.ErrorBody) {
	l := x.Logger.With("handler", "mergeCheck")

	fail := func(status int, name, clientMsg string, detail ...any) (tangled.GitMergeCheck_Output, int, *atclient.ErrorBody) {
		l.Error(clientMsg, append([]any{"name", name}, detail...)...)
		return tangled.GitMergeCheck_Output{}, status, &atclient.ErrorBody{Name: name, Message: clientMsg}
	}

	baseRepoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Repo)
	if err != nil {
		return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "repo", input.Repo, "err", err)
	}
	sourceRepoDid := syntax.DID(input.Source.Repo)

	var sourceRepoUrl string
	var sourceRepoPath string // non-empty only when the source is local to this knot
	if p, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, input.Source.Repo); err == nil {
		sourceRepoPath = p
		sourceRepoUrl = "file://" + p
	} else {
		ident, err := x.Resolver.Directory().LookupDID(ctx, sourceRepoDid)
		if err != nil {
			return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "err", err)
		}
		sourceKnot := ident.GetServiceEndpoint("atproto_pds")
		u, err := url.Parse(sourceKnot)
		if err != nil {
			return fail(http.StatusNotFound, "RepoNotFound", "unknown repository", "source", sourceRepoDid, "knot", sourceKnot, "err", err)
		}
		sourceRepoUrl = u.JoinPath(sourceRepoDid.String()).String()
	}

	env := append(os.Environ(), "GIT_TERMINAL_PROMPT=0")

	// 1. create temp repo with git alternate to the base repo's objects.
	tmpRepoPath, cleanup, err := createTemporaryRepoForMerge(ctx, x.Sandbox, baseRepoPath, input.Branch)
	if err != nil {
		return fail(http.StatusInternalServerError, "InternalError", "failed to prepare merge check", "err", err)
	}
	defer cleanup()

	runGit := func(args ...string) ([]byte, []byte, error) {
		args = append([]string{"-C", tmpRepoPath}, args...)
		return gitWithSandbox(ctx, x.Sandbox, env, []string{tmpRepoPath}, args...)
	}

	// 2. fetch source commit and pin it to a "tracking" branch.
	fetchPaths := []string{tmpRepoPath}
	if sourceRepoPath != "" {
		fetchPaths = append(fetchPaths, sourceRepoPath)
	}
	if _, stderr, err := gitWithSandbox(ctx, x.Sandbox, env, fetchPaths, "-C", tmpRepoPath, "fetch", sourceRepoUrl, input.Source.Commit); err != nil {
		return fail(http.StatusNotFound, "CommitNotFound", "source commit unavailable", "commit", input.Source.Commit, "err", err, "stderr", strings.TrimSpace(string(stderr)))
	}
	if _, stderr, err := runGit("branch", "tracking", input.Source.Commit); err != nil {
		return fail(http.StatusNotFound, "CommitNotFound", "source commit unavailable", "commit", input.Source.Commit, "err", err, "stderr", strings.TrimSpace(string(stderr)))
	}

	// 3. populate the working tree on the base branch.
	if _, stderr, err := runGit("checkout", "-f", "base"); err != nil {
		return fail(http.StatusInternalServerError, "InternalError", "failed to perform merge check", "step", "checkout base", "err", err, "stderr", strings.TrimSpace(string(stderr)))
	}

	// 4. attempt a 3-way merge without committing.
	if _, stderr, err := runGit("merge", "--no-commit", "--no-ff", "tracking"); err != nil {
		lsOut, _, _ := runGit("ls-files", "--unmerged")
		files := parseUnmergedFiles(lsOut)
		if len(files) == 0 {
			return fail(http.StatusInternalServerError, "InternalError", "failed to perform merge check", "step", "merge", "err", err, "stderr", strings.TrimSpace(string(stderr)))
		}

		conflicts := make([]*tangled.GitMergeCheck_ConflictInfo, 0, len(files))
		for _, f := range files {
			conflicts = append(conflicts, &tangled.GitMergeCheck_ConflictInfo{
				Filename: f,
				Reason:   "merge conflict",
			})
		}
		msg := strings.TrimSpace(string(stderr))
		l.Debug("merge check found conflicts", "files", files)
		return tangled.GitMergeCheck_Output{
			IsConflicted: true,
			Conflicts:    conflicts,
			Message:      &msg,
		}, http.StatusOK, nil
	}

	return tangled.GitMergeCheck_Output{IsConflicted: false}, http.StatusOK, nil
}

// lexgen doesn't give Validate() method...
func gitMergeCheck_Input_Validate(input tangled.GitMergeCheck_Input) error {
	if _, err := syntax.ParseDID(input.Repo); err != nil {
		return fmt.Errorf("repo: invalid DID: %w", err)
	}
	if input.Branch == "" {
		return fmt.Errorf("branch: required")
	}
	if input.Source == nil {
		return fmt.Errorf("source: required")
	}
	if _, err := syntax.ParseDID(input.Source.Repo); err != nil {
		return fmt.Errorf("source.repo: invalid DID: %w", err)
	}
	if input.Source.Commit == "" {
		return fmt.Errorf("source.commit: required")
	}
	return nil
}
