package api

import (
	"fmt"
	"strings"

	"github.com/cli/cli/v2/pkg/set"
)

func squeeze(r rune) rune {
	switch r {
	case '\n', '\t':
		return -1
	default:
		return r
	}
}

func shortenQuery(q string) string {
	return strings.Map(squeeze, q)
}

var assignedActors = shortenQuery(`
	assignedActors(first: 10) {
		nodes {
			...on User {
				id,
				login,
				name,
				__typename
			}
			...on Bot {
				id,
				login,
				__typename
			}
		},
		totalCount
	}
`)

var issueComments = shortenQuery(`
	comments(first: 100) {
		nodes {
			id,
			author{login,...on User{id,name}},
			authorAssociation,
			body,
			createdAt,
			includesCreatedEdit,
			isMinimized,
			minimizedReason,
			reactionGroups{content,users{totalCount}},
			url,
			viewerDidAuthor
		},
		pageInfo{hasNextPage,endCursor},
		totalCount
	}
`)

var issueCommentLast = shortenQuery(`
	comments(last: 1) {
		nodes {
			author{login,...on User{id,name}},
			authorAssociation,
			body,
			createdAt,
			includesCreatedEdit,
			isMinimized,
			minimizedReason,
			reactionGroups{content,users{totalCount}}
		},
		totalCount
	}
`)

var issueClosedByPullRequestsReferences = shortenQuery(`
	closedByPullRequestsReferences(first: 100) {
		nodes {
			id,
			number,
			url,
			repository {
				id,
				name,
				owner {
					id,
					login
				}
			}
		}
		pageInfo{hasNextPage,endCursor}
	}
`)

// prReviewRequests includes ...on Bot to support Copilot as a reviewer on github.com.
// On GHES, Bot is not part of the RequestedReviewer union, but the fragment is
// silently ignored (verified on GHES 3.19).
var prReviewRequests = shortenQuery(`
	reviewRequests(first: 100) {
		nodes {
			requestedReviewer {
				__typename,
				...on User{login,name},
				...on Bot{login},
				...on Team{
					organization{login}
					name,
					slug
				}
			}
		}
	}
`)

var prReviews = shortenQuery(`
	reviews(first: 100) {
		nodes {
			id,
			author{login},
			authorAssociation,
			submittedAt,
			body,
			state,
			commit{oid},
			reactionGroups{content,users{totalCount}}
		}
		pageInfo{hasNextPage,endCursor}
		totalCount
	}
`)

var prLatestReviews = shortenQuery(`
	latestReviews(first: 100) {
		nodes {
			author{login},
			authorAssociation,
			submittedAt,
			body,
			state
		}
	}
`)

var prFiles = shortenQuery(`
	files(first: 100) {
		nodes {
			additions,
			deletions,
			path,
			changeType
		}
	}
`)

var prCommits = shortenQuery(`
	commits(first: 100) {
		nodes {
			commit {
				authors(first:100) {
					nodes {
						name,
						email,
						user{id,login}
					}
				},
				messageHeadline,
				messageBody,
				oid,
				committedDate,
				authoredDate
			}
		}
	}
`)

var prClosingIssuesReferences = shortenQuery(`
	closingIssuesReferences(first: 100) {
		nodes {
			id,
			number,
			url,
			repository {
				id,
				name,
				owner {
					id,
					login
				}
			}
		}
		pageInfo{hasNextPage,endCursor}
	}
`)

var autoMergeRequest = shortenQuery(`
	autoMergeRequest {
		authorEmail,
		commitBody,
		commitHeadline,
		mergeMethod,
		enabledAt,
		enabledBy{login,...on User{id,name}}
	}
`)

func StatusCheckRollupGraphQLWithCountByState() string {
	return shortenQuery(`
	statusCheckRollup: commits(last: 1) {
		nodes {
			commit {
				statusCheckRollup {
					contexts {
						checkRunCount,
						checkRunCountsByState {
							state,
							count
						},
						statusContextCount,
						statusContextCountsByState {
							state,
							count
						}
					}
				}
			}
		}
	}`)
}

func StatusCheckRollupGraphQLWithoutCountByState(after string) string {
	var afterClause string
	if after != "" {
		afterClause = ",after:" + after
	}
	return fmt.Sprintf(shortenQuery(`
	statusCheckRollup: commits(last: 1) {
		nodes {
			commit {
				statusCheckRollup {
					contexts(first:100%s) {
						nodes {
							__typename
							...on StatusContext {
								context,
								state,
								targetUrl,
								createdAt,
								description
							},
							...on CheckRun {
								name,
								checkSuite{workflowRun{workflow{name}}},
								status,
								conclusion,
								startedAt,
								completedAt,
								detailsUrl
							}
						},
						pageInfo{hasNextPage,endCursor}
					}
				}
			}
		}
	}`), afterClause)
}

func RequiredStatusCheckRollupGraphQL(prID, after string, includeEvent bool) string {
	var afterClause string
	if after != "" {
		afterClause = ",after:" + after
	}
	eventField := "event,"
	if !includeEvent {
		eventField = ""
	}
	return fmt.Sprintf(shortenQuery(`
	statusCheckRollup: commits(last: 1) {
		nodes {
			commit {
				statusCheckRollup {
					contexts(first:100%[1]s) {
						nodes {
							__typename
							...on StatusContext {
								context,
								state,
								targetUrl,
								createdAt,
								description,
								isRequired(pullRequestId: %[2]s)
							},
							...on CheckRun {
								name,
								checkSuite{workflowRun{%[3]sworkflow{name}}},
								status,
								conclusion,
								startedAt,
								completedAt,
								detailsUrl,
								isRequired(pullRequestId: %[2]s)
							}
						},
						pageInfo{hasNextPage,endCursor}
					}
				}
			}
		}
	}`), afterClause, prID, eventField)
}

var sharedIssuePRFields = []string{
	"assignees",
	"author",
	"body",
	"closed",
	"comments",
	"createdAt",
	"closedAt",
	"id",
	"labels",
	"milestone",
	"number",
	"projectCards",
	"projectItems",
	"reactionGroups",
	"state",
	"title",
	"updatedAt",
	"url",
}

// Some fields are only valid in the context of issues.
// They need to be enumerated separately in order to be filtered
// from existing code that expects to be able to pass Issue fields
// to PR queries, e.g. the PullRequestGraphql function.
var issueOnlyFields = []string{
	"isPinned",
	"stateReason",
	"closedByPullRequestsReferences",
}

var IssueFields = append(sharedIssuePRFields, issueOnlyFields...)

var PullRequestFields = append(sharedIssuePRFields,
	"additions",
	"autoMergeRequest",
	"baseRefName",
	"baseRefOid",
	"changedFiles",
	"closingIssuesReferences",
	"commits",
	"deletions",
	"files",
	"fullDatabaseId",
	"headRefName",
	"headRefOid",
	"headRepository",
	"headRepositoryOwner",
	"isCrossRepository",
	"isDraft",
	"latestReviews",
	"maintainerCanModify",
	"mergeable",
	"mergeCommit",
	"mergedAt",
	"mergedBy",
	"mergeStateStatus",
	"potentialMergeCommit",
	"reviewDecision",
	"reviewRequests",
	"reviews",
	"statusCheckRollup",
)

// IssueGraphQL constructs a GraphQL query fragment for a set of issue fields.
func IssueGraphQL(fields []string) string {
	var q []string
	for _, field := range fields {
		switch field {
		case "author":
			q = append(q, `author{login,...on User{id,name}}`)
		case "mergedBy":
			q = append(q, `mergedBy{login,...on User{id,name}}`)
		case "headRepositoryOwner":
			q = append(q, `headRepositoryOwner{id,login,...on User{name}}`)
		case "headRepository":
			q = append(q, `headRepository{id,name}`)
		case "assignees":
			q = append(q, `assignees(first:100){nodes{id,login,name,databaseId},totalCount}`)
		case "assignedActors":
			q = append(q, assignedActors)
		case "labels":
			q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`)
		case "projectCards":
			q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`)
		case "projectItems":
			q = append(q, `projectItems(first:100){nodes{id, project{id,title}, status:fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue{optionId,name}}},totalCount}`)
		case "milestone":
			q = append(q, `milestone{number,title,description,dueOn}`)
		case "reactionGroups":
			q = append(q, `reactionGroups{content,users{totalCount}}`)
		case "mergeCommit":
			q = append(q, `mergeCommit{oid}`)
		case "potentialMergeCommit":
			q = append(q, `potentialMergeCommit{oid}`)
		case "autoMergeRequest":
			q = append(q, autoMergeRequest)
		case "comments":
			q = append(q, issueComments)
		case "lastComment": // pseudo-field
			q = append(q, issueCommentLast)
		case "reviewRequests":
			q = append(q, prReviewRequests)
		case "reviews":
			q = append(q, prReviews)
		case "latestReviews":
			q = append(q, prLatestReviews)
		case "files":
			q = append(q, prFiles)
		case "commits":
			q = append(q, prCommits)
		case "lastCommit": // pseudo-field
			q = append(q, `commits(last:1){nodes{commit{oid}}}`)
		case "commitsCount": // pseudo-field
			q = append(q, `commits{totalCount}`)
		case "requiresStrictStatusChecks": // pseudo-field
			q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`)
		case "statusCheckRollup":
			q = append(q, StatusCheckRollupGraphQLWithoutCountByState(""))
		case "statusCheckRollupWithCountByState": // pseudo-field
			q = append(q, StatusCheckRollupGraphQLWithCountByState())
		case "closingIssuesReferences":
			q = append(q, prClosingIssuesReferences)
		case "closedByPullRequestsReferences":
			q = append(q, issueClosedByPullRequestsReferences)
		default:
			q = append(q, field)
		}
	}
	return strings.Join(q, ",")
}

// PullRequestGraphQL constructs a GraphQL query fragment for a set of pull request fields.
// It will try to sanitize the fields to just those available on pull request.
func PullRequestGraphQL(fields []string) string {
	s := set.NewStringSet()
	s.AddValues(fields)
	s.RemoveValues(issueOnlyFields)
	return IssueGraphQL(s.ToSlice())
}

var RepositoryFields = []string{
	"id",
	"name",
	"nameWithOwner",
	"owner",
	"parent",
	"templateRepository",
	"description",
	"homepageUrl",
	"openGraphImageUrl",
	"usesCustomOpenGraphImage",
	"url",
	"sshUrl",
	"mirrorUrl",
	"securityPolicyUrl",

	"createdAt",
	"pushedAt",
	"updatedAt",
	"archivedAt",

	"isBlankIssuesEnabled",
	"isSecurityPolicyEnabled",
	"hasIssuesEnabled",
	"hasProjectsEnabled",
	"hasWikiEnabled",
	"hasDiscussionsEnabled",
	"mergeCommitAllowed",
	"squashMergeAllowed",
	"rebaseMergeAllowed",

	"forkCount",
	"stargazerCount",
	"watchers",
	"issues",
	"pullRequests",

	"codeOfConduct",
	"contactLinks",
	"defaultBranchRef",
	"deleteBranchOnMerge",
	"diskUsage",
	"fundingLinks",
	"isArchived",
	"isEmpty",
	"isFork",
	"isInOrganization",
	"isMirror",
	"isPrivate",
	"visibility",
	"isTemplate",
	"isUserConfigurationRepository",
	"licenseInfo",
	"viewerCanAdminister",
	"viewerDefaultCommitEmail",
	"viewerDefaultMergeMethod",
	"viewerHasStarred",
	"viewerPermission",
	"viewerPossibleCommitEmails",
	"viewerSubscription",

	"repositoryTopics",
	"primaryLanguage",
	"languages",
	"issueTemplates",
	"pullRequestTemplates",
	"labels",
	"milestones",
	"latestRelease",

	"assignableUsers",
	"mentionableUsers",
	"projects",
	"projectsV2",

	// "branchProtectionRules", // too complex to expose
	// "collaborators", // does it make sense to expose without affiliation filter?
}

func RepositoryGraphQL(fields []string) string {
	var q []string
	for _, field := range fields {
		switch field {
		case "codeOfConduct":
			q = append(q, "codeOfConduct{key,name,url}")
		case "contactLinks":
			q = append(q, "contactLinks{about,name,url}")
		case "fundingLinks":
			q = append(q, "fundingLinks{platform,url}")
		case "licenseInfo":
			q = append(q, "licenseInfo{key,name,nickname}")
		case "owner":
			q = append(q, "owner{id,login}")
		case "parent":
			q = append(q, "parent{id,name,owner{id,login}}")
		case "templateRepository":
			q = append(q, "templateRepository{id,name,owner{id,login}}")
		case "repositoryTopics":
			q = append(q, "repositoryTopics(first:100){nodes{topic{name}}}")
		case "issueTemplates":
			q = append(q, "issueTemplates{name,title,body,about}")
		case "pullRequestTemplates":
			q = append(q, "pullRequestTemplates{body,filename}")
		case "labels":
			q = append(q, "labels(first:100){nodes{id,color,name,description}}")
		case "languages":
			q = append(q, "languages(first:100){edges{size,node{name}}}")
		case "primaryLanguage":
			q = append(q, "primaryLanguage{name}")
		case "latestRelease":
			q = append(q, "latestRelease{publishedAt,tagName,name,url}")
		case "milestones":
			q = append(q, "milestones(first:100,states:OPEN){nodes{number,title,description,dueOn}}")
		case "assignableUsers":
			q = append(q, "assignableUsers(first:100){nodes{id,login,name}}")
		case "mentionableUsers":
			q = append(q, "mentionableUsers(first:100){nodes{id,login,name}}")
		case "projects":
			q = append(q, "projects(first:100,states:OPEN){nodes{id,name,number,body,resourcePath}}")
		case "projectsV2":
			q = append(q, "projectsV2(first:100,query:\"is:open\"){nodes{id,number,title,resourcePath,closed,url}}")
		case "watchers":
			q = append(q, "watchers{totalCount}")
		case "issues":
			q = append(q, "issues(states:OPEN){totalCount}")
		case "pullRequests":
			q = append(q, "pullRequests(states:OPEN){totalCount}")
		case "defaultBranchRef":
			q = append(q, "defaultBranchRef{name}")
		default:
			q = append(q, field)
		}
	}
	return strings.Join(q, ",")
}
