// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.

package review

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"github.com/google/go-github/v33/github"
	"golang.org/x/oauth2"

	"github.com/apache/skywalking-eyes/internal/logger"
	comments2 "github.com/apache/skywalking-eyes/pkg/comments"
	header2 "github.com/apache/skywalking-eyes/pkg/header"
)

var (
	Identification = "license-eye hidden identification"

	gh  *github.Client
	ctx context.Context

	owner string
	repo  string
	sha   string
	pr    int

	requiredEnvVars = []string{
		"GITHUB_TOKEN",
		"GITHUB_HEAD_REF",
		"GITHUB_REPOSITORY",
		"GITHUB_EVENT_NAME",
		"GITHUB_EVENT_PATH",
	}

	ghOnce sync.Once
)

func Init() {
	if os.Getenv("GITHUB_TOKEN") == "" {
		logger.Log.Infoln("GITHUB_TOKEN is not set, license-eye won't comment on the pull request")
		return
	}

	if !IsPR() {
		return
	}
	if !IsGHA() {
		panic(fmt.Errorf("this must be run on GitHub Actions or you have to set the environment variables %v manually", requiredEnvVars))
	}

	s, err := GetSha()
	if err != nil {
		logger.Log.Warnln("failed to get sha", err)
		return
	}

	sha = s
	token := os.Getenv("GITHUB_TOKEN")
	ref := os.Getenv("GITHUB_REF")
	fullName := os.Getenv("GITHUB_REPOSITORY")
	logger.Log.Debugln("ref:", ref, "; repo:", fullName, "; sha:", sha)
	ownerRepo := strings.Split(fullName, "/")
	if len(ownerRepo) != 2 {
		logger.Log.Warnln("Length of ownerRepo is not 2", ownerRepo)
		return
	}
	owner, repo = ownerRepo[0], ownerRepo[1]
	matches := regexp.MustCompile(`refs/pull/(\d+)/merge`).FindStringSubmatch(ref)
	if len(matches) < 1 {
		logger.Log.Warnln("Length of ref < 1", matches)
		return
	}
	prString := matches[1]
	if p, err := strconv.Atoi(prString); err == nil {
		pr = p
	} else {
		logger.Log.Warnln("Failed to parse pull request number.", err)
		return
	}

	ctx = context.Background()
	ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
	tc := oauth2.NewClient(ctx, ts)
	gh = github.NewClient(tc)
}

// Header reviews the license header, including suggestions on the pull request and an overview of the checks.
func Header(result *header2.Result, config *header2.ConfigHeader) error {
	ghOnce.Do(Init)

	if !result.HasFailure() || !IsPR() || gh == nil || config.Comment == header2.Never {
		return nil
	}

	commentedFiles := make(map[string]bool)
	for _, comment := range GetAllReviewsComments() {
		decodeString := comment.GetBody()
		if strings.Contains(decodeString, Identification) {
			logger.Log.Debugln("Path:", comment.GetPath())
			commentedFiles[comment.GetPath()] = true
		}
	}
	logger.Log.Debugln("CommentedFiles:", commentedFiles)

	s := "RIGHT"
	l := 1

	var comments []*github.DraftReviewComment
	for _, changedFile := range GetChangedFiles() {
		logger.Log.Debugln("ChangedFile:", changedFile.GetFilename())
		if commentedFiles[changedFile.GetFilename()] {
			logger.Log.Debugln("ChangedFile was reviewed, skipping:", changedFile.GetFilename())
			continue
		}
		for _, invalidFile := range result.Failure {
			if !strings.HasSuffix(invalidFile, changedFile.GetFilename()) {
				continue
			}
			blob, _, err := gh.Git.GetBlob(ctx, owner, repo, changedFile.GetSHA())
			if err != nil {
				logger.Log.Warnln("Failed to get blob:", changedFile.GetFilename(), changedFile.GetSHA())
				continue
			}
			style := comments2.FileCommentStyle(changedFile.GetFilename())
			if style == nil {
				logger.Log.Warnln("Failed to determine the comment style of file:", changedFile.GetFilename())
				continue
			}
			header, err := header2.GenerateLicenseHeader(style, config)
			if err != nil {
				logger.Log.Warnln("Failed to generate comment header:", changedFile.GetFilename())
				continue
			}
			decodeString, err := base64.StdEncoding.DecodeString(blob.GetContent())
			if err != nil {
				logger.Log.Debugln("Failed to decode blob content:", err)
				continue
			}
			body := "```suggestion\n" + header + strings.Split(string(decodeString), "\n")[0] + "\n```\n" + fmt.Sprintf(`<!-- %v -->`, Identification)
			comments = append(comments, &github.DraftReviewComment{
				Path: changedFile.Filename,
				Body: &body,
				Side: &s,
				Line: &l,
			})
		}
	}

	return tryReview(result, config, comments)
}

func tryReview(result *header2.Result, config *header2.ConfigHeader, comments []*github.DraftReviewComment) error {
	tryBestEffortToComment := func() error {
		if err := doReview(result, comments); err != nil {
			logger.Log.Warnln("Failed to create review comment, fallback to a plain comment:", err)
			_ = doReview(result, nil)
			return err
		}
		return nil
	}

	if config.Comment == header2.Always {
		if err := tryBestEffortToComment(); err != nil {
			return err
		}
	} else if config.Comment == header2.OnFailure && len(comments) > 0 {
		if err := tryBestEffortToComment(); err != nil {
			return err
		}
	}
	return nil
}

func doReview(result *header2.Result, comments []*github.DraftReviewComment) error {
	logger.Log.Debugln("Comments:", comments)

	c := Markdown(result)
	e := "COMMENT"
	if _, _, err := gh.PullRequests.CreateReview(ctx, owner, repo, pr, &github.PullRequestReviewRequest{
		CommitID: &sha,
		Body:     &c,
		Event:    &e,
		Comments: comments,
	}); err != nil {
		return err
	}
	return nil
}

// GetChangedFiles returns the changed files in this pull request.
func GetChangedFiles() []*github.CommitFile {
	prsvc := gh.PullRequests
	options := &github.ListOptions{Page: 1, PerPage: 100}

	var allFiles []*github.CommitFile
	for files, response, err := prsvc.ListFiles(ctx, owner, repo, pr, options); err == nil; {
		allFiles = append(allFiles, files...)
		if response.NextPage <= options.Page {
			break
		}
		options = &github.ListOptions{Page: response.NextPage, PerPage: options.PerPage}
	}
	return allFiles
}

// GetAllReviewsComments returns all review comments of the pull request.
func GetAllReviewsComments() []*github.PullRequestComment {
	prsvc := gh.PullRequests
	options := &github.PullRequestListCommentsOptions{ListOptions: github.ListOptions{Page: 1, PerPage: 100}}

	var allComments []*github.PullRequestComment
	for comments, response, err := prsvc.ListComments(ctx, owner, repo, pr, options); err == nil; {
		allComments = append(allComments, comments...)
		if response.NextPage <= options.Page {
			break
		}
		options = &github.PullRequestListCommentsOptions{
			ListOptions: github.ListOptions{Page: response.NextPage, PerPage: options.PerPage},
		}
	}
	return allComments
}

func IsGHA() bool {
	for _, key := range requiredEnvVars {
		if val := os.Getenv(key); val == "" {
			return false
		}
	}
	return true
}

func IsPR() bool {
	return os.Getenv("GITHUB_EVENT_NAME") == "pull_request"
}

func Markdown(result *header2.Result) string {
	return fmt.Sprintf(`
<!-- %s -->
[license-eye](https://github.com/apache/skywalking-eyes/tree/main/cmd/license-eye) has checked %d files.
| Valid | Invalid | Ignored | Fixed |
| --- | --- | --- | --- |
| %d | %d | %d | %d |
<details>
  <summary>Click to see the invalid file list</summary>

  %v
</details>

<details>
	<summary>Use this command to fix any missing license headers</summary>

	`+"```bash\n"+
		"docker run -it --rm -v $(pwd):/github/workspace apache/skywalking-eyes header fix\n"+
		"```"+
		`
</details>
`,
		Identification,
		len(result.Success)+len(result.Failure)+len(result.Ignored),
		len(result.Success),
		len(result.Failure),
		len(result.Ignored),
		len(result.Fixed),
		"- "+strings.Join(result.Failure, "\n- "),
	)
}

type Event struct {
	PR github.PullRequest `json:"pull_request"`
}

func GetSha() (string, error) {
	filepath := os.Getenv("GITHUB_EVENT_PATH")
	logger.Log.Debugln("GITHUB_EVENT_PATH: ", filepath)
	if filepath == "" {
		return "", fmt.Errorf("failed to get event path")
	}
	content, err := os.ReadFile(filepath)
	if err != nil {
		return "", err
	}
	logger.Log.Debugln(filepath, "content:", string(content))

	var event Event
	if err := json.Unmarshal(content, &event); err != nil {
		return "", err
	}
	return *event.PR.Head.SHA, nil
}
