package create

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/cli/cli/v2/api"
	"github.com/shurcooL/githubv4"
)

// repoCreateInput is input parameters for the repoCreate method
type repoCreateInput struct {
	Name                 string
	HomepageURL          string
	Description          string
	Visibility           string
	OwnerLogin           string
	TeamSlug             string
	TemplateRepositoryID string
	HasIssuesEnabled     bool
	HasWikiEnabled       bool
	GitIgnoreTemplate    string
	LicenseTemplate      string
	IncludeAllBranches   bool
	InitReadme           bool
}

// createRepositoryInputV3 is the payload for the repo create REST API
type createRepositoryInputV3 struct {
	Name              string `json:"name"`
	HomepageURL       string `json:"homepage,omitempty"`
	Description       string `json:"description,omitempty"`
	IsPrivate         bool   `json:"private"`
	Visibility        string `json:"visibility,omitempty"`
	TeamID            uint64 `json:"team_id,omitempty"`
	HasIssuesEnabled  bool   `json:"has_issues"`
	HasWikiEnabled    bool   `json:"has_wiki"`
	GitIgnoreTemplate string `json:"gitignore_template,omitempty"`
	LicenseTemplate   string `json:"license_template,omitempty"`
	InitReadme        bool   `json:"auto_init,omitempty"`
}

// createRepositoryInput is the payload for the repo create GraphQL mutation
type createRepositoryInput struct {
	Name             string `json:"name"`
	HomepageURL      string `json:"homepageUrl,omitempty"`
	Description      string `json:"description,omitempty"`
	Visibility       string `json:"visibility"`
	OwnerID          string `json:"ownerId,omitempty"`
	TeamID           string `json:"teamId,omitempty"`
	HasIssuesEnabled bool   `json:"hasIssuesEnabled"`
	HasWikiEnabled   bool   `json:"hasWikiEnabled"`
}

// cloneTemplateRepositoryInput is the payload for creating a repo from a template using GraphQL
type cloneTemplateRepositoryInput struct {
	Name               string `json:"name"`
	Visibility         string `json:"visibility"`
	Description        string `json:"description,omitempty"`
	OwnerID            string `json:"ownerId"`
	RepositoryID       string `json:"repositoryId"`
	IncludeAllBranches bool   `json:"includeAllBranches"`
}

type updateRepositoryInput struct {
	RepositoryID     string `json:"repositoryId"`
	HasWikiEnabled   bool   `json:"hasWikiEnabled"`
	HasIssuesEnabled bool   `json:"hasIssuesEnabled"`
	HomepageURL      string `json:"homepageUrl,omitempty"`
}

// repoCreate creates a new GitHub repository
func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) {
	isOrg := false
	var ownerID string
	var teamID string
	var teamIDv3 uint64

	apiClient := api.NewClientFromHTTP(client)

	if input.TeamSlug != "" {
		team, err := resolveOrganizationTeam(apiClient, hostname, input.OwnerLogin, input.TeamSlug)
		if err != nil {
			return nil, err
		}
		teamIDv3 = team.ID
		teamID = team.NodeID
		ownerID = team.Organization.NodeID
		isOrg = true
	} else if input.OwnerLogin != "" {
		owner, err := resolveOwner(apiClient, hostname, input.OwnerLogin)
		if err != nil {
			return nil, err
		}
		ownerID = owner.NodeID
		isOrg = owner.IsOrganization()
	}

	if input.TemplateRepositoryID != "" {
		var response struct {
			CloneTemplateRepository struct {
				Repository api.Repository
			}
		}

		if ownerID == "" {
			var err error
			ownerID, err = api.CurrentUserID(apiClient, hostname)
			if err != nil {
				return nil, err
			}
		}

		variables := map[string]interface{}{
			"input": cloneTemplateRepositoryInput{
				Name:               input.Name,
				Description:        input.Description,
				Visibility:         strings.ToUpper(input.Visibility),
				OwnerID:            ownerID,
				RepositoryID:       input.TemplateRepositoryID,
				IncludeAllBranches: input.IncludeAllBranches,
			},
		}

		err := apiClient.GraphQL(hostname, `
		mutation CloneTemplateRepository($input: CloneTemplateRepositoryInput!) {
			cloneTemplateRepository(input: $input) {
				repository {
					id
					name
					owner { login }
					url
				}
			}
		}
		`, variables, &response)
		if err != nil {
			return nil, err
		}

		if !input.HasWikiEnabled || !input.HasIssuesEnabled || input.HomepageURL != "" {
			updateVariables := map[string]interface{}{
				"input": updateRepositoryInput{
					RepositoryID:     response.CloneTemplateRepository.Repository.ID,
					HasWikiEnabled:   input.HasWikiEnabled,
					HasIssuesEnabled: input.HasIssuesEnabled,
					HomepageURL:      input.HomepageURL,
				},
			}

			if err := apiClient.GraphQL(hostname, `
				mutation UpdateRepository($input: UpdateRepositoryInput!) {
					updateRepository(input: $input) {
						repository {
							id
						}
					}
				}
			`, updateVariables, nil); err != nil {
				return nil, err
			}
		}

		return api.InitRepoHostname(&response.CloneTemplateRepository.Repository, hostname), nil
	}

	if input.GitIgnoreTemplate != "" || input.LicenseTemplate != "" || input.InitReadme {
		inputv3 := createRepositoryInputV3{
			Name:              input.Name,
			HomepageURL:       input.HomepageURL,
			Description:       input.Description,
			IsPrivate:         strings.EqualFold(input.Visibility, "PRIVATE"),
			TeamID:            teamIDv3,
			HasIssuesEnabled:  input.HasIssuesEnabled,
			HasWikiEnabled:    input.HasWikiEnabled,
			GitIgnoreTemplate: input.GitIgnoreTemplate,
			LicenseTemplate:   input.LicenseTemplate,
			InitReadme:        input.InitReadme,
		}

		path := "user/repos"
		if isOrg {
			path = fmt.Sprintf("orgs/%s/repos", input.OwnerLogin)
			inputv3.Visibility = strings.ToLower(input.Visibility)
		}

		body := &bytes.Buffer{}
		enc := json.NewEncoder(body)
		if err := enc.Encode(inputv3); err != nil {
			return nil, err
		}

		repo, err := api.CreateRepoTransformToV4(apiClient, hostname, "POST", path, body)
		if err != nil {
			return nil, err
		}
		return repo, nil
	}

	var response struct {
		CreateRepository struct {
			Repository api.Repository
		}
	}

	variables := map[string]interface{}{
		"input": createRepositoryInput{
			Name:             input.Name,
			Description:      input.Description,
			HomepageURL:      input.HomepageURL,
			Visibility:       strings.ToUpper(input.Visibility),
			OwnerID:          ownerID,
			TeamID:           teamID,
			HasIssuesEnabled: input.HasIssuesEnabled,
			HasWikiEnabled:   input.HasWikiEnabled,
		},
	}

	err := apiClient.GraphQL(hostname, `
	mutation RepositoryCreate($input: CreateRepositoryInput!) {
		createRepository(input: $input) {
			repository {
				id
				name
				owner { login }
				url
			}
		}
	}
	`, variables, &response)
	if err != nil {
		return nil, err
	}

	return api.InitRepoHostname(&response.CreateRepository.Repository, hostname), nil
}

type ownerResponse struct {
	NodeID string `json:"node_id"`
	Type   string `json:"type"`
}

func (r *ownerResponse) IsOrganization() bool {
	return r.Type == "Organization"
}

func resolveOwner(client *api.Client, hostname, orgName string) (*ownerResponse, error) {
	var response ownerResponse
	err := client.REST(hostname, "GET", fmt.Sprintf("users/%s", orgName), nil, &response)
	return &response, err
}

type teamResponse struct {
	ID           uint64 `json:"id"`
	NodeID       string `json:"node_id"`
	Organization struct {
		NodeID string `json:"node_id"`
	}
}

func resolveOrganizationTeam(client *api.Client, hostname, orgName, teamSlug string) (*teamResponse, error) {
	var response teamResponse
	err := client.REST(hostname, "GET", fmt.Sprintf("orgs/%s/teams/%s", orgName, teamSlug), nil, &response)
	return &response, err
}

func listTemplateRepositories(client *http.Client, hostname, owner string) ([]api.Repository, error) {
	ownerConnection := "repositoryOwner(login: $owner)"

	variables := map[string]interface{}{
		"perPage": githubv4.Int(100),
		"owner":   githubv4.String(owner),
	}
	inputs := []string{"$perPage:Int!", "$endCursor:String", "$owner:String!"}

	type result struct {
		RepositoryOwner struct {
			Login        string
			Repositories struct {
				Nodes      []api.Repository
				TotalCount int
				PageInfo   struct {
					HasNextPage bool
					EndCursor   string
				}
			}
		}
	}

	query := fmt.Sprintf(`query RepositoryList(%s) {
		%s {
			login
			repositories(first: $perPage, after: $endCursor, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC }) {
				nodes{
					id
					name
					isTemplate
					defaultBranchRef {
						name
					}
				}
				totalCount
				pageInfo{hasNextPage,endCursor}
			}
		}
	}`, strings.Join(inputs, ","), ownerConnection)

	apiClient := api.NewClientFromHTTP(client)
	var templateRepositories []api.Repository
	for {
		var res result
		err := apiClient.GraphQL(hostname, query, variables, &res)
		if err != nil {
			return nil, err
		}

		owner := res.RepositoryOwner

		for _, repo := range owner.Repositories.Nodes {
			if repo.IsTemplate {
				templateRepositories = append(templateRepositories, repo)
			}
		}

		if !owner.Repositories.PageInfo.HasNextPage {
			break
		}
		variables["endCursor"] = githubv4.String(owner.Repositories.PageInfo.EndCursor)
	}

	return templateRepositories, nil
}

// listGitIgnoreTemplates uses API v3 here because gitignore template isn't supported by GraphQL yet.
func listGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, error) {
	var gitIgnoreTemplates []string
	client := api.NewClientFromHTTP(httpClient)
	err := client.REST(hostname, "GET", "gitignore/templates", nil, &gitIgnoreTemplates)
	if err != nil {
		return []string{}, err
	}
	return gitIgnoreTemplates, nil
}

// listLicenseTemplates uses API v3 here because license template isn't supported by GraphQL yet.
func listLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) {
	var licenseTemplates []api.License
	client := api.NewClientFromHTTP(httpClient)
	err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates)
	if err != nil {
		return nil, err
	}
	return licenseTemplates, nil
}

// Returns the current username and any orgs that user is a member of.
func userAndOrgs(httpClient *http.Client, hostname string) (string, []string, error) {
	client := api.NewClientFromHTTP(httpClient)
	return api.CurrentLoginNameAndOrgs(client, hostname)
}
