package graphql

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
)

func (s *Server) pullRequestStackGQL(ctx context.Context, stack service.PullRequestStackView, queries ...string) map[string]any {
	nodes := make([]any, len(stack.Entries))
	for i, entry := range stack.Entries {
		nodes[i] = s.pullRequestStackEntryGQL(ctx, entry, stack)
	}
	return map[string]any{
		"id": gqlID("PullRequestStack", stack.ID), "number": stack.Number,
		"size": len(stack.Entries), "baseRefName": stack.BaseRef,
		"entries": gqlConnWithArgs(nodes, connectionArgsForField(ctx, strings.Join(queries, "\n"), "stack", "entries")), "__typename": "PullRequestStack",
	}
}

func (s *Server) pullRequestStackEntryGQL(ctx context.Context, entry db.PullRequestStackEntry, stack service.PullRequestStackView) map[string]any {
	return map[string]any{
		"id": gqlID("PullRequestStackEntry", entry.ID), "position": entry.Position,
		"pullRequest": s.prGQL(ctx, entry.PullRequest, "__minimal"),
		"stack": map[string]any{
			"id": gqlID("PullRequestStack", stack.ID), "number": stack.Number,
			"size": len(stack.Entries), "baseRefName": stack.BaseRef,
		},
		"__typename": "PullRequestStackEntry",
	}
}

func (s *Server) mergeQueueGQL(ctx context.Context, view service.MergeQueueView, queries ...string) map[string]any {
	nodes := make([]any, len(view.Entries))
	for i := range view.Entries {
		nodes[i] = s.mergeQueueEntryGQL(ctx, view.Entries[i], &view)
	}
	queue := s.mergeQueueReferenceGQL(ctx, view)
	queue["entries"] = gqlConnWithArgs(nodes, connectionArgsForField(ctx, strings.Join(queries, "\n"), "mergeQueue", "entries"))
	queue["__typename"] = "MergeQueue"
	return queue
}

func (s *Server) mergeQueueReferenceGQL(ctx context.Context, view service.MergeQueueView) map[string]any {
	c := view.Configuration
	resourcePath := fmt.Sprintf("/%s/queue/%s", view.Queue.Repository.FullName, view.Queue.BaseRef)
	return map[string]any{
		"id":                            gqlID("MergeQueue", view.Queue.ID),
		"url":                           s.Svc.HTMLBaseURL(ctx) + resourcePath,
		"resourcePath":                  resourcePath,
		"repository":                    repoGQLLite(view.Queue.Repository),
		"nextEntryEstimatedTimeToMerge": nil,
		"entries":                       emptyConn(),
		"configuration": map[string]any{
			"checkResponseTimeout":          c.CheckResponseTimeoutMinutes,
			"mergeMethod":                   strings.ToUpper(c.MergeMethod),
			"mergingStrategy":               strings.ToUpper(c.GroupingStrategy),
			"maximumEntriesToBuild":         c.MaxEntriesToBuild,
			"maximumEntriesToMerge":         c.MaxEntriesToMerge,
			"minimumEntriesToMerge":         c.MinEntriesToMerge,
			"minimumEntriesToMergeWaitTime": c.MinEntriesToMergeWaitMinutes,
		},
		"__typename": "MergeQueue",
	}
}

func (s *Server) mergeQueueEntryGQL(ctx context.Context, entry db.MergeQueueEntry, view *service.MergeQueueView) map[string]any {
	var queue any = map[string]any{"id": gqlID("MergeQueue", entry.QueueID)}
	if view != nil {
		queue = s.mergeQueueReferenceGQL(ctx, *view)
	}
	return map[string]any{
		"id": gqlID("MergeQueueEntry", entry.ID), "position": entry.Position,
		"state": mergeQueueEntryStateGQL(entry.State), "enqueuedAt": entry.CreatedAt,
		"jump": entry.Jump, "solo": entry.Solo,
		"baseCommit": map[string]any{"oid": entry.BaseSHA}, "headCommit": map[string]any{"oid": entry.HeadSHA},
		"enqueuer": s.authorGQL(ctx, entry.EnqueuedBy), "estimatedTimeToMerge": nil,
		"pullRequest": s.prGQL(ctx, entry.PullRequest, "__minimal"),
		"mergeQueue":  queue, "__typename": "MergeQueueEntry",
	}
}

func mergeQueueEntryStateGQL(state string) string {
	switch state {
	case "awaiting_checks":
		return "AWAITING_CHECKS"
	case "locked":
		return "LOCKED"
	case "mergeable":
		return "MERGEABLE"
	case "unmergeable":
		return "UNMERGEABLE"
	default:
		return "QUEUED"
	}
}

func (s *Server) pullRequestTimelineGQL(ctx context.Context, pullRequest db.PullRequest, query string) any {
	events, err := s.Svc.GetPullRequestTimeline(ctx, pullRequest.Repository.FullName, pullRequest.Number)
	if err != nil {
		addGQLExecutionError(ctx, "failed to resolve pull request timeline", pullRequestTimelineErrorPath(query))
		return nil
	}
	args := connectionArgsForField(ctx, query, "", "timelineItems")
	allowed := make(map[string]bool, len(args.ItemTypes))
	for _, itemType := range args.ItemTypes {
		allowed[strings.ToUpper(itemType)] = true
	}
	type timelineNode struct {
		createdAt time.Time
		node      map[string]any
	}
	timelineNodes := make([]timelineNode, 0, len(events))
	var updatedAt any
	for _, event := range events {
		itemType := timelineItemType(event)
		if len(allowed) > 0 && !allowed[itemType] {
			continue
		}
		if args.Since != nil && event.CreatedAt.Before(*args.Since) {
			continue
		}
		if node := s.timelineEventGQL(ctx, event, pullRequest); node != nil {
			timelineNodes = append(timelineNodes, timelineNode{createdAt: event.CreatedAt, node: node})
		}
	}
	if len(allowed) == 0 || allowed["PULL_REQUEST_COMMIT"] {
		commitConnection := s.prCommitsGQL(ctx, pullRequest)
		commitNodes, _ := commitConnection["nodes"].([]any)
		for _, rawNode := range commitNodes {
			node, ok := rawNode.(map[string]any)
			if !ok {
				continue
			}
			commit, _ := node["commit"].(map[string]any)
			createdAt, ok := gqlTimelineTime(commit["committedDate"])
			if !ok || (args.Since != nil && createdAt.Before(*args.Since)) {
				continue
			}
			node["__typename"] = "PullRequestCommit"
			timelineNodes = append(timelineNodes, timelineNode{createdAt: createdAt, node: node})
		}
	}
	sort.SliceStable(timelineNodes, func(i, j int) bool {
		return timelineNodes[i].createdAt.Before(timelineNodes[j].createdAt)
	})
	nodes := make([]any, len(timelineNodes))
	for i := range timelineNodes {
		nodes[i] = timelineNodes[i].node
		updatedAt = timelineNodes[i].createdAt
	}
	connection := gqlConnWithArgs(nodes, args)
	connection["filteredCount"] = len(nodes)
	connection["pageCount"] = len(connection["nodes"].([]any))
	connection["updatedAt"] = updatedAt
	return connection
}

func pullRequestTimelineErrorPath(query string) []any {
	if strings.Contains(query, "node(") || strings.Contains(query, "node (") {
		return []any{"node", "timelineItems"}
	}
	return []any{"repository", "pullRequest", "timelineItems"}
}

func gqlTimelineTime(value any) (time.Time, bool) {
	switch typed := value.(type) {
	case time.Time:
		return typed, true
	case string:
		parsed, err := time.Parse(time.RFC3339, typed)
		return parsed, err == nil
	default:
		return time.Time{}, false
	}
}

func (s *Server) pullRequestMergeQueueEventGQL(ctx context.Context, event db.PullRequestMergeQueueEvent) map[string]any {
	actor := s.authorGQL(ctx, event.Actor)
	return mergeQueueTimelineEventShape(event, actor, actor, s.prGQL(ctx, event.PullRequest, "__minimal"))
}

func timelineItemType(event service.TimelineEvent) string {
	if event.QueueEvent != nil {
		if event.QueueEvent.EventType == "removed" {
			return "REMOVED_FROM_MERGE_QUEUE_EVENT"
		}
		return "ADDED_TO_MERGE_QUEUE_EVENT"
	}
	if event.Comment != nil {
		return "ISSUE_COMMENT"
	}
	if event.Review != nil {
		return "PULL_REQUEST_REVIEW"
	}
	if event.CrossRef != nil {
		return "CROSS_REFERENCED_EVENT"
	}
	return map[string]string{
		"labeled": "LABELED_EVENT", "unlabeled": "UNLABELED_EVENT",
		"closed": "CLOSED_EVENT", "reopened": "REOPENED_EVENT",
		"locked": "LOCKED_EVENT", "unlocked": "UNLOCKED_EVENT",
		"assigned": "ASSIGNED_EVENT", "unassigned": "UNASSIGNED_EVENT",
		"milestoned": "MILESTONED_EVENT", "demilestoned": "DEMILESTONED_EVENT",
		"renamed": "RENAMED_TITLE_EVENT",
	}[event.Event]
}

func (s *Server) timelineEventGQL(ctx context.Context, event service.TimelineEvent, pullRequest db.PullRequest) map[string]any {
	if event.QueueEvent != nil {
		return s.pullRequestMergeQueueEventGQL(ctx, *event.QueueEvent)
	}
	if event.Comment != nil {
		currentLogin := ""
		if current, err := s.Svc.GetCurrentUser(ctx); err == nil {
			currentLogin = current.Login
		}
		result := s.issueCommentGQL(ctx, *event.Comment, currentLogin)
		result["__typename"] = "IssueComment"
		return result
	}
	if event.Review != nil {
		connection := s.reviewsFromList(ctx, []db.PullRequestReview{*event.Review}, pullRequest.Repository.Owner.Login)
		nodes, _ := connection["nodes"].([]any)
		if len(nodes) == 0 {
			return nil
		}
		result := nodes[0].(map[string]any)
		result["__typename"] = "PullRequestReview"
		return result
	}
	if event.EventRec != nil {
		return issueTimelineEventGQL(*event.EventRec)
	}
	if event.CrossRef != nil {
		return map[string]any{
			"id":    gqlID("CrossReferencedEvent", event.CrossRef.Reference.ID),
			"actor": map[string]any{"login": event.Actor}, "createdAt": event.CreatedAt,
			"isCrossRepository": event.CrossRef.SourceRepositoryFullName != pullRequest.Repository.FullName,
			"willCloseTarget":   false, "__typename": "CrossReferencedEvent",
		}
	}
	return nil
}

func issueTimelineEventGQL(event db.IssueEvent) map[string]any {
	typename := map[string]string{
		"labeled": "LabeledEvent", "unlabeled": "UnlabeledEvent",
		"closed": "ClosedEvent", "reopened": "ReopenedEvent",
		"locked": "LockedEvent", "unlocked": "UnlockedEvent",
		"assigned": "AssignedEvent", "unassigned": "UnassignedEvent",
		"milestoned": "MilestonedEvent", "demilestoned": "DemilestonedEvent",
		"renamed": "RenamedTitleEvent",
	}[event.EventType]
	if typename == "" {
		return nil
	}
	result := map[string]any{
		"id": gqlID(typename, event.ID), "actor": map[string]any{"login": event.ActorLogin},
		"createdAt": event.CreatedAt, "__typename": typename,
	}
	switch event.EventType {
	case "labeled", "unlabeled":
		if event.LabelName != nil {
			result["label"] = map[string]any{"name": *event.LabelName}
		}
	case "assigned", "unassigned":
		if event.AssigneeLogin != nil {
			result["assignee"] = map[string]any{"login": *event.AssigneeLogin}
		}
	case "milestoned", "demilestoned":
		if event.MilestoneTitle != nil {
			result["milestoneTitle"] = *event.MilestoneTitle
		}
	case "renamed":
		if event.OldTitle != nil {
			result["previousTitle"] = *event.OldTitle
		}
		if event.NewTitle != nil {
			result["currentTitle"] = *event.NewTitle
		}
	case "locked":
		if event.LockReason != nil {
			result["lockReason"] = *event.LockReason
		}
	}
	return result
}

func mergeQueueTimelineEventShape(event db.PullRequestMergeQueueEvent, actor, enqueuer, pullRequest any) map[string]any {
	typename := "AddedToMergeQueueEvent"
	if event.EventType == "removed" {
		typename = "RemovedFromMergeQueueEvent"
	}
	result := map[string]any{
		"id": gqlID(typename, event.ID), "actor": actor,
		"createdAt": event.CreatedAt, "enqueuer": enqueuer,
		"mergeQueue":  map[string]any{"id": gqlID("MergeQueue", event.QueueID)},
		"pullRequest": pullRequest, "__typename": typename,
	}
	if event.EventType == "removed" {
		var beforeCommit any
		if event.BeforeSHA != "" {
			beforeCommit = map[string]any{"oid": event.BeforeSHA}
		}
		result["beforeCommit"] = beforeCommit
		result["reason"] = event.Reason
	}
	return result
}

func (s *Server) doEnqueuePullRequest(ctx context.Context, req gqlRequest) map[string]any {
	inp := inputMap(req)
	prID := parseNodeID(strFrom(inp, "pullRequestId"), "PullRequest")
	if prID == 0 {
		return errResp("invalid pull request ID")
	}
	jump, _ := inp["jump"].(bool)
	entries, err := s.Svc.EnqueuePullRequest(ctx, prID, strFrom(inp, "expectedHeadOid"), jump)
	if err != nil {
		return errResp(err.Error())
	}
	if len(entries) == 0 {
		return errResp("pull request was not enqueued")
	}
	entry := entries[len(entries)-1]
	view, err := s.Svc.GetMergeQueue(ctx, entry.QueueID)
	if err != nil {
		return errResp(err.Error())
	}
	return wrap("enqueuePullRequest", map[string]any{
		"clientMutationId": strFrom(inp, "clientMutationId"),
		"mergeQueueEntry":  s.mergeQueueEntryGQL(ctx, entry, &view),
	})
}

func (s *Server) doDequeuePullRequest(ctx context.Context, req gqlRequest) map[string]any {
	inp := inputMap(req)
	prID := parseNodeID(strFrom(inp, "id"), "PullRequest")
	if prID == 0 {
		return errResp("invalid pull request ID")
	}
	entries, err := s.Svc.DequeuePullRequest(ctx, prID)
	if err != nil {
		return errResp(err.Error())
	}
	var entry any
	if len(entries) > 0 {
		view, viewErr := s.Svc.GetMergeQueue(ctx, entries[0].QueueID)
		if viewErr != nil {
			return errResp(viewErr.Error())
		}
		entry = s.mergeQueueEntryGQL(ctx, entries[0], &view)
	}
	return wrap("dequeuePullRequest", map[string]any{
		"clientMutationId": strFrom(inp, "clientMutationId"), "mergeQueueEntry": entry,
	})
}
