/*
Copyright 2019 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// issue.go provides generic functions related to issues

package ghutil

import (
	"fmt"

	"github.com/google/go-github/v27/github"
)

const (
	// IssueOpenState is the state of open github issue
	IssueOpenState IssueStateEnum = "open"
	// IssueCloseState is the state of closed github issue
	IssueCloseState IssueStateEnum = "closed"
	// IssueAllState is the state for all, useful when querying issues
	IssueAllState IssueStateEnum = "all"
)

// IssueStateEnum represents different states of Github Issues
type IssueStateEnum string

// ListRepos lists repos under org
func (gc *GithubClient) ListRepos(org string) ([]string, error) {
	repoListOptions := &github.RepositoryListOptions{}
	genericList, err := gc.depaginate(
		"listing repos",
		maxRetryCount,
		&repoListOptions.ListOptions,
		func() ([]interface{}, *github.Response, error) {
			page, resp, err := gc.Client.Repositories.List(ctx, org, repoListOptions)
			var interfaceList []interface{}
			if nil == err {
				for _, repo := range page {
					interfaceList = append(interfaceList, repo)
				}
			}
			return interfaceList, resp, err
		},
	)
	res := make([]string, len(genericList))
	for i, elem := range genericList {
		res[i] = elem.(*github.Repository).GetName()
	}
	return res, err
}

// ListIssuesByRepo lists issues within given repo, filters by labels if provided
func (gc *GithubClient) ListIssuesByRepo(org, repo string, labels []string) ([]*github.Issue, error) {
	issueListOptions := &github.IssueListByRepoOptions{
		State: string(IssueAllState),
	}
	if len(labels) > 0 {
		issueListOptions.Labels = labels
	}

	genericList, err := gc.depaginate(
		fmt.Sprintf("listing issues with label '%v'", labels),
		maxRetryCount,
		&issueListOptions.ListOptions,
		func() ([]interface{}, *github.Response, error) {
			page, resp, err := gc.Client.Issues.ListByRepo(ctx, org, repo, issueListOptions)
			var interfaceList []interface{}
			if nil == err {
				for _, issue := range page {
					interfaceList = append(interfaceList, issue)
				}
			}
			return interfaceList, resp, err
		},
	)
	res := make([]*github.Issue, len(genericList))
	for i, elem := range genericList {
		res[i] = elem.(*github.Issue)
	}
	return res, err
}

// CreateIssue creates issue
func (gc *GithubClient) CreateIssue(org, repo, title, body string) (*github.Issue, error) {
	issue := &github.IssueRequest{
		Title: &title,
		Body:  &body,
	}

	var res *github.Issue
	_, err := gc.retry(
		fmt.Sprintf("creating issue '%s %s' '%s'", org, repo, title),
		maxRetryCount,
		func() (*github.Response, error) {
			var resp *github.Response
			var err error
			res, resp, err = gc.Client.Issues.Create(ctx, org, repo, issue)
			return resp, err
		},
	)
	return res, err
}

// CloseIssue closes issue
func (gc *GithubClient) CloseIssue(org, repo string, issueNumber int) error {
	return gc.updateIssueState(org, repo, IssueCloseState, issueNumber)
}

// ReopenIssue reopen issue
func (gc *GithubClient) ReopenIssue(org, repo string, issueNumber int) error {
	return gc.updateIssueState(org, repo, IssueOpenState, issueNumber)
}

// ListComments gets all comments from issue
func (gc *GithubClient) ListComments(org, repo string, issueNumber int) ([]*github.IssueComment, error) {
	commentListOptions := &github.IssueListCommentsOptions{}
	genericList, err := gc.depaginate(
		fmt.Sprintf("listing comment for issue '%s %s %d'", org, repo, issueNumber),
		maxRetryCount,
		&commentListOptions.ListOptions,
		func() ([]interface{}, *github.Response, error) {
			page, resp, err := gc.Client.Issues.ListComments(ctx, org, repo, issueNumber, commentListOptions)
			var interfaceList []interface{}
			if nil == err {
				for _, issue := range page {
					interfaceList = append(interfaceList, issue)
				}
			}
			return interfaceList, resp, err
		},
	)
	res := make([]*github.IssueComment, len(genericList))
	for i, elem := range genericList {
		res[i] = elem.(*github.IssueComment)
	}
	return res, err
}

// GetComment gets comment by comment ID
func (gc *GithubClient) GetComment(org, repo string, commentID int64) (*github.IssueComment, error) {
	var res *github.IssueComment
	_, err := gc.retry(
		fmt.Sprintf("getting comment '%s %s %d'", org, repo, commentID),
		maxRetryCount,
		func() (*github.Response, error) {
			var resp *github.Response
			var err error
			res, resp, err = gc.Client.Issues.GetComment(ctx, org, repo, commentID)
			return resp, err
		},
	)
	return res, err
}

// CreateComment adds comment to issue
func (gc *GithubClient) CreateComment(org, repo string, issueNumber int, commentBody string) (*github.IssueComment, error) {
	var res *github.IssueComment
	comment := &github.IssueComment{
		Body: &commentBody,
	}
	_, err := gc.retry(
		fmt.Sprintf("commenting issue '%s %s %d'", org, repo, issueNumber),
		maxRetryCount,
		func() (*github.Response, error) {
			var resp *github.Response
			var err error
			res, resp, err = gc.Client.Issues.CreateComment(ctx, org, repo, issueNumber, comment)
			return resp, err
		},
	)
	return res, err
}

// EditComment edits comment by replacing with provided comment
func (gc *GithubClient) EditComment(org, repo string, commentID int64, commentBody string) error {
	comment := &github.IssueComment{
		Body: &commentBody,
	}
	_, err := gc.retry(
		fmt.Sprintf("editing comment '%s %s %d'", org, repo, commentID),
		maxRetryCount,
		func() (*github.Response, error) {
			_, resp, err := gc.Client.Issues.EditComment(ctx, org, repo, commentID, comment)
			return resp, err
		},
	)
	return err
}

// DeleteComment deletes comment from issue
func (gc *GithubClient) DeleteComment(org, repo string, commentID int64) error {
	_, err := gc.retry(
		fmt.Sprintf("deleting comment '%s %s %d'", org, repo, commentID),
		maxRetryCount,
		func() (*github.Response, error) {
			resp, err := gc.Client.Issues.DeleteComment(ctx, org, repo, commentID)
			return resp, err
		},
	)
	return err
}

// AddLabelsToIssue adds label on issue
func (gc *GithubClient) AddLabelsToIssue(org, repo string, issueNumber int, labels []string) error {
	_, err := gc.retry(
		fmt.Sprintf("add labels '%v' to '%s %s %d'", labels, org, repo, issueNumber),
		maxRetryCount,
		func() (*github.Response, error) {
			_, resp, err := gc.Client.Issues.AddLabelsToIssue(ctx, org, repo, issueNumber, labels)
			return resp, err
		},
	)
	return err
}

// RemoveLabelForIssue removes given label for issue
func (gc *GithubClient) RemoveLabelForIssue(org, repo string, issueNumber int, label string) error {
	_, err := gc.retry(
		fmt.Sprintf("remove label '%s' from '%s %s %d'", label, org, repo, issueNumber),
		maxRetryCount,
		func() (*github.Response, error) {
			return gc.Client.Issues.RemoveLabelForIssue(ctx, org, repo, issueNumber, label)
		},
	)
	return err
}

func (gc *GithubClient) updateIssueState(org, repo string, state IssueStateEnum, issueNumber int) error {
	stateString := string(state)
	issueRequest := &github.IssueRequest{
		State: &stateString,
	}
	_, err := gc.retry(
		fmt.Sprintf("applying '%s' action on issue '%s %s %d'", stateString, org, repo, issueNumber),
		maxRetryCount,
		func() (*github.Response, error) {
			_, resp, err := gc.Client.Issues.Edit(ctx, org, repo, issueNumber, issueRequest)
			return resp, err
		},
	)
	return err
}
