// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package protection

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/harness/gitness/app/services/codeowners"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"

	"golang.org/x/exp/slices"
)

type (
	MergeVerifier interface {
		MergeVerify(ctx context.Context, in MergeVerifyInput) (MergeVerifyOutput, []types.RuleViolations, error)
		RequiredChecks(ctx context.Context, in RequiredChecksInput) (RequiredChecksOutput, error)
	}

	MergeVerifyInput struct {
		ResolveUserGroupID func(ctx context.Context, userGroupIDs []int64) ([]int64, error)
		Actor              *types.Principal
		AllowBypass        bool
		IsRepoOwner        bool
		TargetRepo         *types.Repository
		SourceRepo         *types.Repository
		PullReq            *types.PullReq
		Reviewers          []*types.PullReqReviewer
		Method             enum.MergeMethod
		CheckResults       []types.CheckResult
		CodeOwners         *codeowners.Evaluation
	}

	MergeVerifyOutput struct {
		AllowedMethods                      []enum.MergeMethod
		DeleteSourceBranch                  bool
		MinimumRequiredApprovalsCount       int
		MinimumRequiredApprovalsCountLatest int
		RequiresCodeOwnersApproval          bool
		RequiresCodeOwnersApprovalLatest    bool
		RequiresCommentResolution           bool
		RequiresNoChangeRequests            bool
	}

	RequiredChecksInput struct {
		ResolveUserGroupID func(ctx context.Context, userGroupIDs []int64) ([]int64, error)
		Actor              *types.Principal
		IsRepoOwner        bool
		Repo               *types.Repository
		PullReq            *types.PullReq
	}

	RequiredChecksOutput struct {
		RequiredIdentifiers   map[string]struct{}
		BypassableIdentifiers map[string]struct{}
	}
)

// ensures that the DefPullReq type implements Sanitizer and MergeVerifier interface.
var (
	_ Sanitizer     = (*DefPullReq)(nil)
	_ MergeVerifier = (*DefPullReq)(nil)
)

const (
	codePullReqApprovalReqMinCount              = "pullreq.approvals.require_minimum_count"
	codePullReqApprovalReqMinCountLatest        = "pullreq.approvals.require_minimum_count:latest_commit"
	codePullReqApprovalReqLatestCommit          = "pullreq.approvals.require_latest_commit"
	codePullReqApprovalReqChangeRequested       = "pullreq.approvals.require_change_requested"
	codePullReqApprovalReqChangeRequestedOldSHA = "pullreq.approvals.require_change_requested_old_SHA"

	codePullReqApprovalReqCodeOwnersNoApproval       = "pullreq.approvals.require_code_owners:no_approval"
	codePullReqApprovalReqCodeOwnersChangeRequested  = "pullreq.approvals.require_code_owners:change_requested"
	codePullReqApprovalReqCodeOwnersNoLatestApproval = "pullreq.approvals.require_code_owners:no_latest_approval"

	codePullReqMergeStrategiesAllowed = "pullreq.merge.strategies_allowed"
	codePullReqMergeDeleteBranch      = "pullreq.merge.delete_branch"
	codePullReqMergeBlock             = "pullreq.merge.blocked"

	codePullReqCommentsReqResolveAll      = "pullreq.comments.require_resolve_all"
	codePullReqStatusChecksReqIdentifiers = "pullreq.status_checks.required_identifiers"
)

//nolint:gocognit,gocyclo,cyclop // well aware of this
func (v *DefPullReq) MergeVerify(
	_ context.Context,
	in MergeVerifyInput,
) (MergeVerifyOutput, []types.RuleViolations, error) {
	var out MergeVerifyOutput
	var violations types.RuleViolations

	// set static merge verify output that comes from the PR definition
	out.DeleteSourceBranch = v.Merge.DeleteBranch
	out.RequiresCommentResolution = v.Comments.RequireResolveAll
	out.RequiresNoChangeRequests = v.Approvals.RequireNoChangeRequest

	// output that depends on approval of latest commit
	if v.Approvals.RequireLatestCommit {
		out.RequiresCodeOwnersApprovalLatest = v.Approvals.RequireCodeOwners
		out.MinimumRequiredApprovalsCountLatest = v.Approvals.RequireMinimumCount
	} else {
		out.RequiresCodeOwnersApproval = v.Approvals.RequireCodeOwners
		out.MinimumRequiredApprovalsCount = v.Approvals.RequireMinimumCount
	}

	// pullreq.approvals

	approvedBy := make([]types.PrincipalInfo, 0, len(in.Reviewers))
	for _, reviewer := range in.Reviewers {
		switch reviewer.ReviewDecision {
		case enum.PullReqReviewDecisionApproved:
			if v.Approvals.RequireLatestCommit && reviewer.SHA != in.PullReq.SourceSHA {
				continue
			}
			approvedBy = append(approvedBy, reviewer.Reviewer)
		case enum.PullReqReviewDecisionChangeReq:
			if v.Approvals.RequireNoChangeRequest {
				if reviewer.SHA == in.PullReq.SourceSHA {
					violations.Addf(
						codePullReqApprovalReqChangeRequested,
						"Reviewer %s requested changes",
						reviewer.Reviewer.DisplayName,
					)
				} else {
					violations.Addf(
						codePullReqApprovalReqChangeRequestedOldSHA,
						"Reviewer %s requested changes for an older commit",
						reviewer.Reviewer.DisplayName,
					)
				}
			}
		case enum.PullReqReviewDecisionPending,
			enum.PullReqReviewDecisionReviewed:
		}
	}

	if len(approvedBy) < v.Approvals.RequireMinimumCount {
		if v.Approvals.RequireLatestCommit {
			violations.Addf(codePullReqApprovalReqMinCountLatest,
				"Insufficient number of approvals of the latest commit. Have %d but need at least %d.",
				len(approvedBy), v.Approvals.RequireMinimumCount)
		} else {
			violations.Addf(codePullReqApprovalReqMinCount,
				"Insufficient number of approvals. Have %d but need at least %d.",
				len(approvedBy), v.Approvals.RequireMinimumCount)
		}
	}

	if v.Approvals.RequireCodeOwners {
		for _, entry := range in.CodeOwners.EvaluationEntries {
			reviewDecision, approvers := getCodeOwnerApprovalStatus(entry)

			if reviewDecision == enum.PullReqReviewDecisionPending {
				violations.Addf(codePullReqApprovalReqCodeOwnersNoApproval,
					"Code owners approval pending for %q", entry.Pattern)
				continue
			}

			if reviewDecision == enum.PullReqReviewDecisionChangeReq {
				violations.Addf(codePullReqApprovalReqCodeOwnersChangeRequested,
					"Code owners requested changes for %q", entry.Pattern)
				continue
			}

			// pull req approved. check other settings
			if !v.Approvals.RequireLatestCommit {
				continue
			}
			latestSHAApproved := slices.ContainsFunc(approvers, func(ev codeowners.OwnerEvaluation) bool {
				return ev.ReviewSHA == in.PullReq.SourceSHA
			})
			if !latestSHAApproved {
				violations.Addf(codePullReqApprovalReqCodeOwnersNoLatestApproval,
					"Code owners approval pending on latest commit for %q", entry.Pattern)
			}
		}
	}

	// pullreq.comments

	if v.Comments.RequireResolveAll && in.PullReq.UnresolvedCount > 0 {
		violations.Addf(codePullReqCommentsReqResolveAll,
			"All comments must be resolved. There are %d unresolved comments.",
			in.PullReq.UnresolvedCount)
	}

	// pullreq.status_checks

	var violatingStatusCheckIdentifiers []string
	for _, requiredIdentifier := range v.StatusChecks.RequireIdentifiers {
		var succeeded bool
		for i := range in.CheckResults {
			if in.CheckResults[i].Identifier == requiredIdentifier {
				succeeded = in.CheckResults[i].Status == enum.CheckStatusSuccess
				break
			}
		}

		if !succeeded {
			violatingStatusCheckIdentifiers = append(violatingStatusCheckIdentifiers, requiredIdentifier)
		}
	}

	if len(violatingStatusCheckIdentifiers) > 0 {
		violations.Addf(
			codePullReqStatusChecksReqIdentifiers,
			"The following status checks are required to be completed successfully: %s",
			strings.Join(violatingStatusCheckIdentifiers, ", "),
		)
	}

	// pullreq.merge

	out.AllowedMethods = enum.MergeMethods

	// Note: Empty allowed strategies list means all are allowed
	if len(v.Merge.StrategiesAllowed) > 0 {
		// if the Method isn't provided return allowed strategies
		out.AllowedMethods = v.Merge.StrategiesAllowed

		if in.Method != "" {
			// if the Method is provided report violations if any
			if !slices.Contains(v.Merge.StrategiesAllowed, in.Method) {
				violations.Addf(codePullReqMergeStrategiesAllowed,
					"The requested merge strategy %q is not allowed. Allowed strategies are %v.",
					in.Method, v.Merge.StrategiesAllowed)
			}
		}
	}

	if v.Merge.Block {
		violations.Addf(
			codePullReqMergeBlock,
			"The merge for the branch %s is not allowed.", in.PullReq.TargetBranch)
	}

	if len(violations.Violations) > 0 {
		return out, []types.RuleViolations{violations}, nil
	}

	return out, nil, nil
}

func (v *DefPullReq) RequiredChecks(
	_ context.Context,
	_ RequiredChecksInput,
) (RequiredChecksOutput, error) {
	m := make(map[string]struct{}, len(v.StatusChecks.RequireIdentifiers))
	for _, id := range v.StatusChecks.RequireIdentifiers {
		m[id] = struct{}{}
	}
	return RequiredChecksOutput{
		RequiredIdentifiers: m,
	}, nil
}

type DefApprovals struct {
	RequireCodeOwners      bool `json:"require_code_owners,omitempty"`
	RequireMinimumCount    int  `json:"require_minimum_count,omitempty"`
	RequireLatestCommit    bool `json:"require_latest_commit,omitempty"`
	RequireNoChangeRequest bool `json:"require_no_change_request,omitempty"`
}

func (v *DefApprovals) Sanitize() error {
	if v.RequireMinimumCount < 0 {
		return errors.New("minimum count must be zero or a positive integer")
	}

	if v.RequireLatestCommit && v.RequireMinimumCount == 0 && !v.RequireCodeOwners {
		return errors.New("require latest commit can only be used with require code owners or require minimum count")
	}

	return nil
}

type DefComments struct {
	RequireResolveAll bool `json:"require_resolve_all,omitempty"`
}

func (DefComments) Sanitize() error {
	return nil
}

type DefStatusChecks struct {
	RequireIdentifiers []string `json:"require_identifiers,omitempty"`
}

// TODO [CODE-1363]: remove after identifier migration.
func (c DefStatusChecks) MarshalJSON() ([]byte, error) {
	// alias allows us to embed the original object while avoiding an infinite loop of marshaling.
	type alias DefStatusChecks
	return json.Marshal(&struct {
		alias
		RequireUIDs []string `json:"require_uids"`
	}{
		alias:       (alias)(c),
		RequireUIDs: c.RequireIdentifiers,
	})
}

// TODO [CODE-1363]: remove if we don't have any require_uids left in our DB.
func (c *DefStatusChecks) UnmarshalJSON(data []byte) error {
	// alias allows us to extract the original object while avoiding an infinite loop of unmarshaling.
	type alias DefStatusChecks
	res := struct {
		alias
		RequireUIDs []string `json:"require_uids"`
	}{}

	err := json.Unmarshal(data, &res)
	if err != nil {
		return fmt.Errorf("failed to unmarshal to alias type with required uids: %w", err)
	}

	*c = DefStatusChecks(res.alias)
	if len(c.RequireIdentifiers) == 0 {
		c.RequireIdentifiers = res.RequireUIDs
	}

	return nil
}

func (c *DefStatusChecks) Sanitize() error {
	if err := validateIdentifierSlice(c.RequireIdentifiers); err != nil {
		return fmt.Errorf("required identifiers error: %w", err)
	}

	return nil
}

type DefMerge struct {
	StrategiesAllowed []enum.MergeMethod `json:"strategies_allowed,omitempty"`
	DeleteBranch      bool               `json:"delete_branch,omitempty"`
	Block             bool               `json:"block,omitempty"`
}

func (v *DefMerge) Sanitize() error {
	m := make(map[enum.MergeMethod]struct{}, 0)
	for _, strategy := range v.StrategiesAllowed {
		if _, ok := strategy.Sanitize(); !ok {
			return fmt.Errorf("unrecognized merge strategy: %s", strategy)
		}

		if _, ok := m[strategy]; ok {
			return fmt.Errorf("duplicate entry in merge strategy list: %s", strategy)
		}

		m[strategy] = struct{}{}
	}

	slices.Sort(v.StrategiesAllowed)

	return nil
}

type DefPush struct {
	Block bool `json:"block,omitempty"`
}

func (v *DefPush) Sanitize() error {
	return nil
}

type DefPullReq struct {
	Approvals    DefApprovals    `json:"approvals"`
	Comments     DefComments     `json:"comments"`
	StatusChecks DefStatusChecks `json:"status_checks"`
	Merge        DefMerge        `json:"merge"`
}

func (v *DefPullReq) Sanitize() error {
	if err := v.Approvals.Sanitize(); err != nil {
		return fmt.Errorf("approvals: %w", err)
	}

	if err := v.Comments.Sanitize(); err != nil {
		return fmt.Errorf("comments: %w", err)
	}

	if err := v.StatusChecks.Sanitize(); err != nil {
		return fmt.Errorf("status checks: %w", err)
	}

	if err := v.Merge.Sanitize(); err != nil {
		return fmt.Errorf("merge: %w", err)
	}

	return nil
}

func getCodeOwnerApprovalStatus(
	entry codeowners.EvaluationEntry,
) (enum.PullReqReviewDecision, []codeowners.OwnerEvaluation) {
	approvers := make([]codeowners.OwnerEvaluation, 0)

	// users
	for _, o := range entry.OwnerEvaluations {
		if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq {
			return enum.PullReqReviewDecisionChangeReq, nil
		}
		if o.ReviewDecision == enum.PullReqReviewDecisionApproved {
			approvers = append(approvers, o)
		}
	}

	// usergroups
	for _, u := range entry.UserGroupOwnerEvaluations {
		for _, o := range u.Evaluations {
			if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq {
				return enum.PullReqReviewDecisionChangeReq, nil
			}
			if o.ReviewDecision == enum.PullReqReviewDecisionApproved {
				approvers = append(approvers, o)
			}
		}
	}

	if len(approvers) > 0 {
		return enum.PullReqReviewDecisionApproved, approvers
	}
	return enum.PullReqReviewDecisionPending, nil
}
