package graphql

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/vektah/gqlparser/v2/ast"
	"gorm.io/gorm"
)

const maxMutationTargetNodeIDs = 8

var graphqlMutationTargetIDKeys = map[string]struct{}{
	"assignableid":                   {},
	"commentid":                      {},
	"contentid":                      {},
	"id":                             {},
	"issuecommentid":                 {},
	"issueid":                        {},
	"labelableid":                    {},
	"lockableid":                     {},
	"milestoneid":                    {},
	"pullrequestid":                  {},
	"pullrequestreviewid":            {},
	"repositoryid":                   {},
	"repositoryvulnerabilityalertid": {},
	"subjectid":                      {},
	"threadid":                       {},
}

type graphqlMutationNodeTarget struct {
	nodeID                       string
	usePullRequestHeadRepository bool
}

type graphqlMutationTargetSelection struct {
	field                 *ast.Field
	operationNameFallback string
	matched               bool
}

// ResolveMutationRepositories resolves the exact repositories named by one
// GraphQL mutation request. GraphQL owns this translation because mutation
// inputs may identify repositories indirectly through repository-scoped node
// IDs. The caller remains responsible for bounding the request body before
// invoking this method.
func (s *Server) ResolveMutationRepositories(ctx context.Context, body []byte) ([]string, bool, error) {
	if s == nil || s.Svc == nil || s.Svc.DB == nil {
		return nil, false, fmt.Errorf("GraphQL repository resolver is unavailable")
	}
	var request gqlRequest
	if err := json.Unmarshal(body, &request); err != nil {
		return nil, false, fmt.Errorf("decode GraphQL mutation target: %w", err)
	}
	_, operation, err := parseSelectedOperation(request.Query, request.OperationName)
	if err != nil {
		return nil, false, fmt.Errorf("select GraphQL mutation operation: %w", err)
	}
	if operation.Operation != ast.Mutation {
		return nil, false, nil
	}

	targets := make(map[graphqlMutationNodeTarget]struct{})
	selection := s.selectedMutationTarget(request, operation)
	if !selection.matched {
		return nil, true, nil
	}
	if selection.operationNameFallback != "" {
		err = collectOperationNameFallbackTargets(request, selection.operationNameFallback, targets)
	} else if selection.field != nil {
		err = collectMutationFieldTargets(selection.field, request.Variables, targets)
	} else {
		err = fmt.Errorf("GraphQL mutation handler target is unavailable")
	}
	if err != nil {
		return nil, true, fmt.Errorf("resolve GraphQL mutation input: %w", err)
	}
	if len(targets) > maxMutationTargetNodeIDs {
		return nil, true, fmt.Errorf("GraphQL mutation names too many repository-scoped nodes")
	}

	fullNames := make(map[string]struct{})
	for target := range targets {
		repositoryID, found, err := s.resolveMutationNodeRepositoryID(ctx, target.nodeID, target.usePullRequestHeadRepository)
		if err != nil {
			return nil, true, err
		}
		if !found {
			return nil, true, fmt.Errorf("GraphQL mutation repository target is unavailable")
		}
		var repository db.Repository
		if err := s.Svc.DBForCtx(ctx).
			Select("full_name").
			Where("id = ?", repositoryID).
			Take(&repository).Error; err != nil {
			if err == gorm.ErrRecordNotFound {
				return nil, true, fmt.Errorf("GraphQL mutation repository target is unavailable")
			}
			return nil, true, fmt.Errorf("resolve GraphQL mutation repository: %w", err)
		}
		if fullName := strings.TrimSpace(repository.FullName); fullName != "" {
			fullNames[fullName] = struct{}{}
		}
	}

	repositories := make([]string, 0, len(fullNames))
	for repository := range fullNames {
		repositories = append(repositories, repository)
	}
	sort.Strings(repositories)
	return repositories, true, nil
}

// selectedMutationField applies the same ordered matcher set as routeMutation
// and then identifies the exact root field that selected that handler. This
// prevents another field in a multi-field operation from affecting admission
// when the current AGS handler will execute only one mutation handler.
func (s *Server) selectedMutationTarget(request gqlRequest, operation *ast.OperationDefinition) graphqlMutationTargetSelection {
	fields := make(map[string]any)
	mergeSelections(fields, operation.SelectionSet)
	op := strings.ToLower(request.OperationName)
	for _, matcher := range s.getMutationMatchers() {
		if !matcher.match(fields, op, request.Query) {
			continue
		}
		for _, field := range mutationOperationFields(operation.SelectionSet) {
			candidate := make(map[string]any)
			mergeSelections(candidate, ast.SelectionSet{field})
			if matcher.match(candidate, "", "") {
				return graphqlMutationTargetSelection{
					field: field, operationNameFallback: matcher.operationNameFallback, matched: true,
				}
			}
		}
		return graphqlMutationTargetSelection{operationNameFallback: matcher.operationNameFallback, matched: true}
	}
	return graphqlMutationTargetSelection{}
}

func collectOperationNameFallbackTargets(request gqlRequest, fallback string, targets map[graphqlMutationNodeTarget]struct{}) error {
	if fallback == "" {
		return fmt.Errorf("GraphQL mutation handler target is unavailable")
	}
	input := inputMap(request)
	if fallback == "createPullRequest" && strFrom(input, "repositoryId") == "" {
		input = map[string]any{"repositoryId": request.Variables["repositoryId"]}
	}
	collectGraphQLNodeIDs(input, targets, false, false)
	if len(targets) == 0 {
		return fmt.Errorf("GraphQL operationName fallback target is unavailable")
	}
	return nil
}

func mutationOperationFields(selections ast.SelectionSet) []*ast.Field {
	fields := make([]*ast.Field, 0, len(selections))
	for _, selection := range selections {
		switch typed := selection.(type) {
		case *ast.Field:
			fields = append(fields, typed)
		case *ast.InlineFragment:
			fields = append(fields, mutationOperationFields(typed.SelectionSet)...)
		case *ast.FragmentSpread:
			// The current GraphQL mutation router does not expand top-level
			// fragment spreads before choosing a handler, so such a field is
			// not executed and must not affect admission either.
			continue
		}
	}
	return fields
}

func collectMutationFieldTargets(field *ast.Field, variables map[string]any, targets map[graphqlMutationNodeTarget]struct{}) error {
	ignoreRepositoryID := field.Name == "cloneTemplateRepository"
	usePullRequestHead := field.Name == "updatePullRequestBranch"
	for _, argument := range field.Arguments {
		value, err := argument.Value.Value(variables)
		if err != nil {
			return err
		}
		collectGraphQLNodeIDs(value, targets, ignoreRepositoryID, usePullRequestHead)
	}
	return nil
}

func collectGraphQLNodeIDs(value any, targets map[graphqlMutationNodeTarget]struct{}, ignoreRepositoryID, usePullRequestHead bool) {
	switch typed := value.(type) {
	case map[string]any:
		for key, nested := range typed {
			key = strings.ToLower(strings.TrimSpace(key))
			if isGraphQLMutationTargetIDKey(key, ignoreRepositoryID) {
				collectGraphQLNodeIDValues(nested, targets, usePullRequestHead)
			}
			collectGraphQLNodeIDs(nested, targets, ignoreRepositoryID, usePullRequestHead)
		}
	case []any:
		for _, nested := range typed {
			collectGraphQLNodeIDs(nested, targets, ignoreRepositoryID, usePullRequestHead)
		}
	}
}

func isGraphQLMutationTargetIDKey(key string, ignoreRepositoryID bool) bool {
	if ignoreRepositoryID && key == "repositoryid" {
		return false
	}
	_, ok := graphqlMutationTargetIDKeys[key]
	return ok
}

func collectGraphQLNodeIDValues(value any, targets map[graphqlMutationNodeTarget]struct{}, usePullRequestHead bool) {
	switch typed := value.(type) {
	case string:
		collectGraphQLNodeID(typed, targets, usePullRequestHead)
	case []any:
		for _, nested := range typed {
			collectGraphQLNodeIDValues(nested, targets, usePullRequestHead)
		}
	}
}

func collectGraphQLNodeID(value string, targets map[graphqlMutationNodeTarget]struct{}, usePullRequestHead bool) {
	normalized := normalizeNodeID(strings.TrimSpace(value))
	prefix, rawID, ok := strings.Cut(normalized, "_")
	if !ok || rawID == "" {
		return
	}
	if _, err := strconv.ParseUint(rawID, 10, 64); err != nil {
		return
	}
	switch prefix {
	case "Repository", "Issue", "PullRequest", "IssueComment", "Milestone", "Label",
		"PullRequestReview", "PullRequestReviewThread", "PRReviewComment", "RepositoryVulnerabilityAlert":
		targets[graphqlMutationNodeTarget{
			nodeID:                       normalized,
			usePullRequestHeadRepository: usePullRequestHead && prefix == "PullRequest",
		}] = struct{}{}
	}
}

func (s *Server) resolveMutationNodeRepositoryID(ctx context.Context, nodeID string, usePullRequestHead bool) (uint, bool, error) {
	prefix, rawID, ok := strings.Cut(normalizeNodeID(nodeID), "_")
	if !ok {
		return 0, false, nil
	}
	id, err := strconv.ParseUint(rawID, 10, 64)
	if err != nil || id == 0 {
		return 0, false, nil
	}
	database := s.Svc.DBForCtx(ctx)
	var repositoryID uint
	query := func(model any, column string) error {
		return database.Model(model).Select(column).Where("id = ?", uint(id)).Scan(&repositoryID).Error
	}
	switch prefix {
	case "Repository":
		return uint(id), true, nil
	case "Issue":
		err = query(&db.Issue{}, "repository_id")
	case "PullRequest":
		column := "repository_id"
		if usePullRequestHead {
			column = "head_repository_id"
		}
		err = query(&db.PullRequest{}, column)
	case "IssueComment":
		err = query(&db.IssueComment{}, "repository_id")
	case "Milestone":
		err = query(&db.Milestone{}, "repository_id")
	case "Label":
		err = query(&db.Label{}, "repository_id")
	case "RepositoryVulnerabilityAlert":
		err = query(&db.DependabotAlert{}, "repository_id")
	case "PullRequestReview":
		err = database.Model(&db.PullRequestReview{}).
			Select("pull_requests.repository_id").
			Joins("JOIN pull_requests ON pull_requests.id = pull_request_reviews.pull_request_id").
			Where("pull_request_reviews.id = ?", uint(id)).Scan(&repositoryID).Error
	case "PullRequestReviewThread", "PRReviewComment":
		err = database.Model(&db.PRReviewComment{}).
			Select("pull_requests.repository_id").
			Joins("JOIN pull_requests ON pull_requests.id = pr_review_comments.pull_request_id").
			Where("pr_review_comments.id = ?", uint(id)).Scan(&repositoryID).Error
	default:
		return 0, false, nil
	}
	if err != nil {
		return 0, false, fmt.Errorf("resolve GraphQL mutation node repository: %w", err)
	}
	return repositoryID, repositoryID != 0, nil
}
