package featuredetection

import (
	"net/http"

	"github.com/cli/cli/v2/api"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/hashicorp/go-version"
	"golang.org/x/sync/errgroup"

	ghauth "github.com/cli/go-gh/v2/pkg/auth"
)

type Detector interface {
	IssueFeatures() (IssueFeatures, error)
	PullRequestFeatures() (PullRequestFeatures, error)
	RepositoryFeatures() (RepositoryFeatures, error)
	ProjectsV1() gh.ProjectsV1Support
}

type IssueFeatures struct {
	StateReason       bool
	ActorIsAssignable bool
}

var allIssueFeatures = IssueFeatures{
	StateReason:       true,
	ActorIsAssignable: true,
}

type PullRequestFeatures struct {
	MergeQueue bool
	// CheckRunAndStatusContextCounts indicates whether the API supports
	// the checkRunCount, checkRunCountsByState, statusContextCount and statusContextCountsByState
	// fields on the StatusCheckRollupContextConnection
	CheckRunAndStatusContextCounts bool
	CheckRunEvent                  bool
}

var allPullRequestFeatures = PullRequestFeatures{
	MergeQueue:                     true,
	CheckRunAndStatusContextCounts: true,
	CheckRunEvent:                  true,
}

type RepositoryFeatures struct {
	PullRequestTemplateQuery bool
	VisibilityField          bool
	AutoMerge                bool
}

var allRepositoryFeatures = RepositoryFeatures{
	PullRequestTemplateQuery: true,
	VisibilityField:          true,
	AutoMerge:                true,
}

type detector struct {
	host       string
	httpClient *http.Client
}

func NewDetector(httpClient *http.Client, host string) Detector {
	return &detector{
		httpClient: httpClient,
		host:       host,
	}
}

func (d *detector) IssueFeatures() (IssueFeatures, error) {
	if !ghauth.IsEnterprise(d.host) {
		return allIssueFeatures, nil
	}

	features := IssueFeatures{
		StateReason:       false,
		ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
	}

	var featureDetection struct {
		Issue struct {
			Fields []struct {
				Name string
			} `graphql:"fields(includeDeprecated: true)"`
		} `graphql:"Issue: __type(name: \"Issue\")"`
	}

	gql := api.NewClientFromHTTP(d.httpClient)
	err := gql.Query(d.host, "Issue_fields", &featureDetection, nil)
	if err != nil {
		return features, err
	}

	for _, field := range featureDetection.Issue.Fields {
		if field.Name == "stateReason" {
			features.StateReason = true
		}
	}

	return features, nil
}

func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
	// TODO: reinstate the short-circuit once the APIs are fully available on github.com
	// https://github.com/cli/cli/issues/5778
	//
	// if !ghinstance.IsEnterprise(d.host) {
	// 	return allPullRequestFeatures, nil
	// }

	var pullRequestFeatureDetection struct {
		PullRequest struct {
			Fields []struct {
				Name string
			} `graphql:"fields(includeDeprecated: true)"`
		} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
		StatusCheckRollupContextConnection struct {
			Fields []struct {
				Name string
			} `graphql:"fields(includeDeprecated: true)"`
		} `graphql:"StatusCheckRollupContextConnection: __type(name: \"StatusCheckRollupContextConnection\")"`
	}

	// Break feature detection down into two separate queries because the platform
	// only supports two `__type` expressions in one query.
	var pullRequestFeatureDetection2 struct {
		WorkflowRun struct {
			Fields []struct {
				Name string
			} `graphql:"fields(includeDeprecated: true)"`
		} `graphql:"WorkflowRun: __type(name: \"WorkflowRun\")"`
	}

	gql := api.NewClientFromHTTP(d.httpClient)

	var wg errgroup.Group
	wg.Go(func() error {
		return gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil)
	})
	wg.Go(func() error {
		return gql.Query(d.host, "PullRequest_fields2", &pullRequestFeatureDetection2, nil)
	})
	if err := wg.Wait(); err != nil {
		return PullRequestFeatures{}, err
	}

	features := PullRequestFeatures{}

	for _, field := range pullRequestFeatureDetection.PullRequest.Fields {
		if field.Name == "isInMergeQueue" {
			features.MergeQueue = true
		}
	}

	for _, field := range pullRequestFeatureDetection.StatusCheckRollupContextConnection.Fields {
		// We only check for checkRunCount here but it, checkRunCountsByState, statusContextCount and statusContextCountsByState
		// were all introduced in the same version of the API.
		if field.Name == "checkRunCount" {
			features.CheckRunAndStatusContextCounts = true
		}
	}

	for _, field := range pullRequestFeatureDetection2.WorkflowRun.Fields {
		if field.Name == "event" {
			features.CheckRunEvent = true
		}
	}

	return features, nil
}

func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
	if !ghauth.IsEnterprise(d.host) {
		return allRepositoryFeatures, nil
	}

	features := RepositoryFeatures{}

	var featureDetection struct {
		Repository struct {
			Fields []struct {
				Name string
			} `graphql:"fields(includeDeprecated: true)"`
		} `graphql:"Repository: __type(name: \"Repository\")"`
	}

	gql := api.NewClientFromHTTP(d.httpClient)

	err := gql.Query(d.host, "Repository_fields", &featureDetection, nil)
	if err != nil {
		return features, err
	}

	for _, field := range featureDetection.Repository.Fields {
		if field.Name == "pullRequestTemplates" {
			features.PullRequestTemplateQuery = true
		}
		if field.Name == "visibility" {
			features.VisibilityField = true
		}
		if field.Name == "autoMergeAllowed" {
			features.AutoMerge = true
		}
	}

	return features, nil
}

const (
	enterpriseProjectsV1Removed = "3.17.0"
)

func (d *detector) ProjectsV1() gh.ProjectsV1Support {
	if !ghauth.IsEnterprise(d.host) {
		return gh.ProjectsV1Unsupported
	}

	hostVersion, hostVersionErr := resolveEnterpriseVersion(d.httpClient, d.host)
	v1ProjectCutoffVersion, v1ProjectCutoffVersionErr := version.NewVersion(enterpriseProjectsV1Removed)

	if hostVersionErr == nil && v1ProjectCutoffVersionErr == nil && hostVersion.LessThan(v1ProjectCutoffVersion) {
		return gh.ProjectsV1Supported
	}

	return gh.ProjectsV1Unsupported
}

func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) {
	var metaResponse struct {
		InstalledVersion string `json:"installed_version"`
	}

	apiClient := api.NewClientFromHTTP(httpClient)
	err := apiClient.REST(host, "GET", "meta", nil, &metaResponse)
	if err != nil {
		return nil, err
	}

	return version.NewVersion(metaResponse.InstalledVersion)
}
