package pulls

import (
	"fmt"
	"net/http"
	"slices"
	"strconv"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/xrpcclient"
	"tangled.org/core/orm"
	"tangled.org/core/patchutil"
	"tangled.org/core/types"

	"github.com/bluesky-social/indigo/atproto/syntax"
	indigoxrpc "github.com/bluesky-social/indigo/xrpc"
	"github.com/go-chi/chi/v5"
)

// htmx fragment
func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "PullActions")

	switch r.Method {
	case http.MethodGet:
		user := s.oauth.GetMultiAccountUser(r)
		if user != nil {
			l = l.With("user", user.Did)
		}

		f, err := s.repoResolver.Resolve(r)
		if err != nil {
			l.Error("failed to get repo and knot", "err", err)
			return
		}

		pull, ok := r.Context().Value("pull").(*models.Pull)
		if !ok {
			l.Error("failed to get pull")
			s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
			return
		}
		l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid)

		// can be nil  if this pull is not stacked
		stack, _ := r.Context().Value("stack").(models.Stack)

		roundNumberStr := chi.URLParam(r, "round")
		roundNumber, err := strconv.Atoi(roundNumberStr)
		if err != nil {
			roundNumber = pull.LastRoundNumber()
		}
		if roundNumber >= len(pull.Submissions) {
			http.Error(w, "bad round id", http.StatusBadRequest)
			l.Error("failed to parse round id", "err", err, "round_number", roundNumber)
			return
		}

		mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
		branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
		resubmitResult := pages.Unknown
		if user.Did == pull.OwnerDid {
			resubmitResult = s.resubmitCheck(r, f, pull, stack)
		}

		s.pages.PullActionsFragment(w, pages.PullActionsParams{
			LoggedInUser:       user,
			RepoInfo:           s.repoResolver.GetRepoInfo(r, user),
			Pull:               pull,
			RoundNumber:        roundNumber,
			MergeCheck:         mergeCheckResponse,
			ResubmitCheck:      resubmitResult,
			BranchDeleteStatus: branchDeleteStatus,
			Stack:              stack,
		})
		return
	}
}

func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff bool) {
	l := s.logger.With("handler", "repoPullHelper", "interdiff", interdiff)

	user := s.oauth.GetMultiAccountUser(r)
	if user != nil {
		l = l.With("user", user.Did)
	}

	f, err := s.repoResolver.Resolve(r)
	if err != nil {
		l.Error("failed to get repo and knot", "err", err)
		return
	}

	pull, ok := r.Context().Value("pull").(*models.Pull)
	if !ok {
		l.Error("failed to get pull")
		s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
		return
	}
	l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid)

	backlinks, err := db.GetBacklinks(s.db, pull.AtUri())
	if err != nil {
		l.Error("failed to get pull backlinks", "err", err)
		s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.")
		return
	}

	roundId := chi.URLParam(r, "round")
	roundIdInt := pull.LastRoundNumber()
	if r, err := strconv.Atoi(roundId); err == nil {
		roundIdInt = r
	}
	if roundIdInt >= len(pull.Submissions) {
		http.Error(w, "bad round id", http.StatusBadRequest)
		l.Error("failed to parse round id", "err", err, "round_number", roundIdInt)
		return
	}

	var diffOpts types.DiffOpts
	if d := r.URL.Query().Get("diff"); d == "split" {
		diffOpts.Split = true
	}

	// can be nil  if this pull is not stacked
	stack, _ := r.Context().Value("stack").(models.Stack)

	mergeCheckResponse := s.mergeCheck(r, f, pull, stack)
	branchDeleteStatus := s.branchDeleteStatus(r, f, pull)
	resubmitResult := pages.Unknown
	if user != nil && user.Did == pull.OwnerDid {
		resubmitResult = s.resubmitCheck(r, f, pull, stack)
	}

	m := make(map[string]models.Pipeline)

	var shas []string
	for _, s := range pull.Submissions {
		shas = append(shas, s.SourceRev)
	}
	for _, p := range stack {
		shas = append(shas, p.LatestSha())
	}

	ps, err := db.GetPipelineStatuses(
		s.db,
		len(shas),
		orm.FilterEq("p.repo_owner", f.Did),
		orm.FilterEq("p.repo_name", f.Name),
		orm.FilterEq("p.knot", f.Knot),
		orm.FilterIn("p.sha", shas),
	)
	if err != nil {
		l.Error("failed to fetch pipeline statuses", "err", err)
		// non-fatal
	}

	for _, p := range ps {
		m[p.Sha] = p
	}

	reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri())
	if err != nil {
		l.Error("failed to get pull reactions", "err", err)
	}

	userReactions := map[models.ReactionKind]bool{}
	if user != nil {
		userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri())
	}

	labelDefs, err := db.GetLabelDefinitions(
		s.db,
		orm.FilterIn("at_uri", f.Labels),
		orm.FilterContains("scope", tangled.RepoPullNSID),
	)
	if err != nil {
		l.Error("failed to fetch labels", "err", err)
		s.pages.Error503(w)
		return
	}

	defs := make(map[string]*models.LabelDefinition)
	for _, l := range labelDefs {
		defs[l.AtUri().String()] = &l
	}

	vouchRelationships := make(map[syntax.DID]*models.VouchRelationship)
	if user != nil {
		participants := pull.Participants()
		vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), participants)
		if err != nil {
			l.Error("failed to fetch vouch relationships", "err", err)
		}
	}

	patch := pull.Submissions[roundIdInt].CombinedPatch()
	var diff types.DiffRenderer
	diff = patchutil.AsNiceDiff(patch, pull.TargetBranch)

	if interdiff {
		currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch())
		if err != nil {
			l.Error("failed to interdiff; current patch malformed", "err", err, "round_number", roundIdInt)
			s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
			return
		}

		previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch())
		if err != nil {
			l.Error("failed to interdiff; previous patch malformed", "err", err, "round_number", roundIdInt)
			s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
			return
		}

		diff = patchutil.Interdiff(previousPatch, currentPatch)
	}

	err = s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
		LoggedInUser:       user,
		RepoInfo:           s.repoResolver.GetRepoInfo(r, user),
		Pull:               pull,
		Stack:              stack,
		Backlinks:          backlinks,
		BranchDeleteStatus: branchDeleteStatus,
		MergeCheck:         mergeCheckResponse,
		ResubmitCheck:      resubmitResult,
		Pipelines:          m,
		Diff:               diff,
		DiffOpts:           diffOpts,
		ActiveRound:        roundIdInt,
		IsInterdiff:        interdiff,

		Reactions:   reactionMap,
		UserReacted: userReactions,

		LabelDefs:          defs,
		VouchRelationships: vouchRelationships,
	})
	if err != nil {
		l.Error("failed to render page", "err", err)
	}
}

func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "RepoSinglePull")

	pull, ok := r.Context().Value("pull").(*models.Pull)
	if !ok {
		l.Error("failed to get pull")
		s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
		return
	}

	http.Redirect(w, r, r.URL.String()+fmt.Sprintf("/round/%d", pull.LastRoundNumber()), http.StatusFound)
}

func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
	if pull.State == models.PullMerged {
		return types.MergeCheckResponse{}
	}

	xrpcc := s.knotClient(f.Knot)

	// combine patches of substack
	subStack := stack.Below(pull)
	// collect the portion of the stack that is mergeable
	mergeable := subStack.Mergeable()
	// combine each patch
	patch := mergeable.CombinedPatch()

	resp, err := tangled.RepoMergeCheck(
		r.Context(),
		xrpcc,
		&tangled.RepoMergeCheck_Input{
			Did:    f.Did,
			Name:   f.Name,
			Branch: pull.TargetBranch,
			Patch:  patch,
		},
	)
	if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
		s.logger.Error("failed to check for mergeability", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "target_branch", pull.TargetBranch)
		return types.MergeCheckResponse{
			Error: fmt.Sprintf("failed to check merge status: %s", xrpcerr.Error()),
		}
	}

	return mergeCheckResponseFrom(resp)
}

func mergeCheckResponseFrom(resp *tangled.RepoMergeCheck_Output) types.MergeCheckResponse {
	conflicts := make([]types.ConflictInfo, len(resp.Conflicts))
	for i, c := range resp.Conflicts {
		conflicts[i] = types.ConflictInfo{Filename: c.Filename, Reason: c.Reason}
	}
	out := types.MergeCheckResponse{
		IsConflicted: resp.Is_conflicted,
		Conflicts:    conflicts,
	}
	if resp.Message != nil {
		out.Message = *resp.Message
	}
	if resp.Error != nil {
		out.Error = *resp.Error
	}
	return out
}

func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *models.BranchDeleteStatus {
	if pull.State != models.PullMerged {
		return nil
	}

	user := s.oauth.GetMultiAccountUser(r)
	if user == nil {
		return nil
	}

	var branch string
	// check if the branch exists
	// NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates
	if pull.IsBranchBased() {
		branch = pull.PullSource.Branch
	} else if pull.IsForkBased() {
		branch = pull.PullSource.Branch
		repo = pull.PullSource.Repo
	} else {
		return nil
	}

	// deleted fork
	if repo == nil {
		return nil
	}

	// user can only delete branch if they are a collaborator in the repo that the branch belongs to
	perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.RepoIdentifier())
	if !slices.Contains(perms, "repo:push") {
		return nil
	}

	xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url}
	resp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, branch, repo.RepoAt().String())
	if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
		s.logger.Error("failed to get branch", "xrpcerr", xrpcerr, "err", err)
		return nil
	}

	return &models.BranchDeleteStatus{
		Repo:   repo,
		Branch: resp.Name,
	}
}

func (s *Pulls) resubmitCheck(r *http.Request, repo *models.Repo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
	if pull.State == models.PullMerged || pull.State == models.PullAbandoned || pull.PullSource == nil {
		return pages.Unknown
	}

	var sourceRepo syntax.ATURI
	if pull.PullSource.RepoAt != nil {
		sourceRepo = *pull.PullSource.RepoAt
	} else {
		sourceRepo = repo.RepoAt()
	}

	xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url}
	branchResp, err := tangled.GitTempGetBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.String())
	if err != nil {
		if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
			s.logger.Error("failed to call XRPC repo.branches", "xrpcerr", xrpcerr, "err", err, "pull_id", pull.PullId, "branch", pull.PullSource.Branch)
			return pages.Unknown
		}
		s.logger.Error("failed to reach knotserver", "err", err, "pull_id", pull.PullId)
		return pages.Unknown
	}

	targetBranch := branchResp

	top := stack[0]
	latestSourceRev := top.LatestSha()

	if latestSourceRev != targetBranch.Hash {
		return pages.ShouldResubmit
	}

	return pages.ShouldNotResubmit
}

func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
	s.repoPullHelper(w, r, false)
}

func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
	s.repoPullHelper(w, r, true)
}

func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "RepoPullPatchRaw")

	pull, ok := r.Context().Value("pull").(*models.Pull)
	if !ok {
		l.Error("failed to get pull")
		s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
		return
	}
	l = l.With("pull_id", pull.PullId)

	roundId := chi.URLParam(r, "round")
	roundIdInt, err := strconv.Atoi(roundId)
	if err != nil || roundIdInt >= len(pull.Submissions) {
		http.Error(w, "bad round id", http.StatusBadRequest)
		l.Error("failed to parse round id", "err", err, "round_id_str", roundId)
		return
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Write([]byte(pull.Submissions[roundIdInt].Patch))
}
