package xrpc

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"tangled.org/core/api/tangled"
	"tangled.org/core/knotserver/db"
	"tangled.org/core/knotserver/git"
	"tangled.org/core/patchutil"
	"tangled.org/core/rbac"
	"tangled.org/core/tid"
	"tangled.org/core/types"
	xrpcerr "tangled.org/core/xrpc/errors"
)

func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) {
	l := x.Logger.With("handler", "Merge")
	fail := func(e xrpcerr.XrpcError) {
		l.Error("failed", "kind", e.Tag, "error", e.Message)
		writeError(w, e, http.StatusBadRequest)
	}

	actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
	if !ok {
		fail(xrpcerr.MissingActorDidError)
		return
	}

	var data tangled.RepoMerge_Input
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		fail(xrpcerr.GenericError(err))
		return
	}

	did := data.Did
	name := data.Name

	if did == "" || name == "" {
		fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
		return
	}

	repoDid, err := x.Db.GetRepoDid(did, name)
	if err != nil {
		fail(xrpcerr.RepoNotFoundError)
		return
	}
	repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid)
	if err != nil {
		fail(xrpcerr.RepoNotFoundError)
		return
	}

	if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil {
		l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid)
		writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
		return
	}

	gr, err := git.Open(repoPath, data.Branch)
	if err != nil {
		fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
		return
	}

	mo := git.MergeOptions{}
	if data.AuthorName != nil {
		mo.AuthorName = *data.AuthorName
	}
	if data.AuthorEmail != nil {
		mo.AuthorEmail = *data.AuthorEmail
	}
	if data.CommitBody != nil {
		mo.CommitBody = *data.CommitBody
	}
	if data.CommitMessage != nil {
		mo.CommitMessage = *data.CommitMessage
	}

	mo.CommitterName = x.Config.Git.UserName
	mo.CommitterEmail = x.Config.Git.UserEmail
	mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)

	err = gr.MergeWithOptions(data.Patch, data.Branch, mo)
	if err != nil {
		var mergeErr *git.ErrMerge
		if errors.As(err, &mergeErr) {
			conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
			for i, conflict := range mergeErr.Conflicts {
				conflicts[i] = types.ConflictInfo{
					Filename: conflict.Filename,
					Reason:   conflict.Reason,
				}
			}

			conflictErr := xrpcerr.NewXrpcError(
				xrpcerr.WithTag("MergeConflict"),
				xrpcerr.WithMessage(fmt.Sprintf("Merge failed due to conflicts: %s", mergeErr.Message)),
			)
			writeError(w, conflictErr, http.StatusConflict)
			return
		} else {
			l.Error("failed to merge", "error", err.Error())
			writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
			return
		}
	}

	oldSha := gr.Hash()
	if err := gr.Refresh(); err != nil {
		l.Error("failed to refresh", "error", err)
	}
	newSha := gr.Hash()

	go func() {
		refUpdate := tangled.GitRefUpdate{
			Repo:         repoDid,
			OwnerDid:     &data.Did,
			Ref:          data.Branch,
			OldSha:       oldSha.String(),
			NewSha:       newSha.String(),
			CommitterDid: actorDid.String(),
		}
		eventJson, err := json.Marshal(refUpdate)
		if err != nil {
			return
		}

		x.Db.InsertEvent(db.Event{
			Rkey:      tid.TID(),
			Nsid:      tangled.GitRefUpdateNSID,
			EventJson: string(eventJson),
		}, x.Notifier)
	}()

	w.WriteHeader(http.StatusOK)
}
