// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/migrations/base"

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

var (
	_ base.Downloader        = &GithubDownloaderV3{}
	_ base.DownloaderFactory = &GithubDownloaderV3Factory{}
)

func init() {
	RegisterDownloaderFactory(&GithubDownloaderV3Factory{})
}

// GithubDownloaderV3Factory defines a github downloader v3 factory
type GithubDownloaderV3Factory struct {
}

// Match returns ture if the migration remote URL matched this downloader factory
func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) {
	u, err := url.Parse(opts.RemoteURL)
	if err != nil {
		return false, err
	}

	return u.Host == "github.com" && opts.AuthUsername != "", nil
}

// New returns a Downloader related to this factory according MigrateOptions
func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) {
	u, err := url.Parse(opts.RemoteURL)
	if err != nil {
		return nil, err
	}

	fields := strings.Split(u.Path, "/")
	oldOwner := fields[1]
	oldName := strings.TrimSuffix(fields[2], ".git")

	log.Trace("Create github downloader: %s/%s", oldOwner, oldName)

	return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil
}

// GithubDownloaderV3 implements a Downloader interface to get repository informations
// from github via APIv3
type GithubDownloaderV3 struct {
	ctx       context.Context
	client    *github.Client
	repoOwner string
	repoName  string
	userName  string
	password  string
}

// NewGithubDownloaderV3 creates a github Downloader via github v3 API
func NewGithubDownloaderV3(userName, password, repoOwner, repoName string) *GithubDownloaderV3 {
	var downloader = GithubDownloaderV3{
		userName:  userName,
		password:  password,
		ctx:       context.Background(),
		repoOwner: repoOwner,
		repoName:  repoName,
	}

	var client *http.Client
	if userName != "" {
		if password == "" {
			ts := oauth2.StaticTokenSource(
				&oauth2.Token{AccessToken: userName},
			)
			client = oauth2.NewClient(downloader.ctx, ts)
		} else {
			client = &http.Client{
				Transport: &http.Transport{
					Proxy: func(req *http.Request) (*url.URL, error) {
						req.SetBasicAuth(userName, password)
						return nil, nil
					},
				},
			}
		}
	}
	downloader.client = github.NewClient(client)
	return &downloader
}

// GetRepoInfo returns a repository information
func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
	gr, _, err := g.client.Repositories.Get(g.ctx, g.repoOwner, g.repoName)
	if err != nil {
		return nil, err
	}
	// convert github repo to stand Repo
	return &base.Repository{
		Owner:       g.repoOwner,
		Name:        gr.GetName(),
		IsPrivate:   *gr.Private,
		Description: gr.GetDescription(),
		OriginalURL: gr.GetHTMLURL(),
		CloneURL:    gr.GetCloneURL(),
	}, nil
}

// GetTopics return github topics
func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
	r, _, err := g.client.Repositories.Get(g.ctx, g.repoOwner, g.repoName)
	return r.Topics, err
}

// GetMilestones returns milestones
func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
	var perPage = 100
	var milestones = make([]*base.Milestone, 0, perPage)
	for i := 1; ; i++ {
		ms, _, err := g.client.Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
			&github.MilestoneListOptions{
				State: "all",
				ListOptions: github.ListOptions{
					Page:    i,
					PerPage: perPage,
				}})
		if err != nil {
			return nil, err
		}

		for _, m := range ms {
			var desc string
			if m.Description != nil {
				desc = *m.Description
			}
			var state = "open"
			if m.State != nil {
				state = *m.State
			}
			milestones = append(milestones, &base.Milestone{
				Title:       *m.Title,
				Description: desc,
				Deadline:    m.DueOn,
				State:       state,
				Created:     *m.CreatedAt,
				Updated:     m.UpdatedAt,
				Closed:      m.ClosedAt,
			})
		}
		if len(ms) < perPage {
			break
		}
	}
	return milestones, nil
}

func convertGithubLabel(label *github.Label) *base.Label {
	var desc string
	if label.Description != nil {
		desc = *label.Description
	}
	return &base.Label{
		Name:        *label.Name,
		Color:       *label.Color,
		Description: desc,
	}
}

// GetLabels returns labels
func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
	var perPage = 100
	var labels = make([]*base.Label, 0, perPage)
	for i := 1; ; i++ {
		ls, _, err := g.client.Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
			&github.ListOptions{
				Page:    i,
				PerPage: perPage,
			})
		if err != nil {
			return nil, err
		}

		for _, label := range ls {
			labels = append(labels, convertGithubLabel(label))
		}
		if len(ls) < perPage {
			break
		}
	}
	return labels, nil
}

func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
	var (
		name string
		desc string
	)
	if rel.Body != nil {
		desc = *rel.Body
	}
	if rel.Name != nil {
		name = *rel.Name
	}

	r := &base.Release{
		TagName:         *rel.TagName,
		TargetCommitish: *rel.TargetCommitish,
		Name:            name,
		Body:            desc,
		Draft:           *rel.Draft,
		Prerelease:      *rel.Prerelease,
		Created:         rel.CreatedAt.Time,
		Published:       rel.PublishedAt.Time,
	}

	for _, asset := range rel.Assets {
		u, _ := url.Parse(*asset.BrowserDownloadURL)
		u.User = url.UserPassword(g.userName, g.password)
		r.Assets = append(r.Assets, base.ReleaseAsset{
			URL:           u.String(),
			Name:          *asset.Name,
			ContentType:   asset.ContentType,
			Size:          asset.Size,
			DownloadCount: asset.DownloadCount,
			Created:       asset.CreatedAt.Time,
			Updated:       asset.UpdatedAt.Time,
		})
	}
	return r
}

// GetReleases returns releases
func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
	var perPage = 100
	var releases = make([]*base.Release, 0, perPage)
	for i := 1; ; i++ {
		ls, _, err := g.client.Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
			&github.ListOptions{
				Page:    i,
				PerPage: perPage,
			})
		if err != nil {
			return nil, err
		}

		for _, release := range ls {
			releases = append(releases, g.convertGithubRelease(release))
		}
		if len(ls) < perPage {
			break
		}
	}
	return releases, nil
}

func convertGithubReactions(reactions *github.Reactions) *base.Reactions {
	return &base.Reactions{
		TotalCount: *reactions.TotalCount,
		PlusOne:    *reactions.PlusOne,
		MinusOne:   *reactions.MinusOne,
		Laugh:      *reactions.Laugh,
		Confused:   *reactions.Confused,
		Heart:      *reactions.Heart,
		Hooray:     *reactions.Hooray,
	}
}

// GetIssues returns issues according start and limit
func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
	opt := &github.IssueListByRepoOptions{
		Sort:      "created",
		Direction: "asc",
		State:     "all",
		ListOptions: github.ListOptions{
			PerPage: perPage,
			Page:    page,
		},
	}

	var allIssues = make([]*base.Issue, 0, perPage)

	issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
	if err != nil {
		return nil, false, fmt.Errorf("error while listing repos: %v", err)
	}
	for _, issue := range issues {
		if issue.IsPullRequest() {
			continue
		}
		var body string
		if issue.Body != nil {
			body = *issue.Body
		}
		var milestone string
		if issue.Milestone != nil {
			milestone = *issue.Milestone.Title
		}
		var labels = make([]*base.Label, 0, len(issue.Labels))
		for _, l := range issue.Labels {
			labels = append(labels, convertGithubLabel(&l))
		}
		var reactions *base.Reactions
		if issue.Reactions != nil {
			reactions = convertGithubReactions(issue.Reactions)
		}

		var email string
		if issue.User.Email != nil {
			email = *issue.User.Email
		}
		allIssues = append(allIssues, &base.Issue{
			Title:       *issue.Title,
			Number:      int64(*issue.Number),
			PosterID:    *issue.User.ID,
			PosterName:  *issue.User.Login,
			PosterEmail: email,
			Content:     body,
			Milestone:   milestone,
			State:       *issue.State,
			Created:     *issue.CreatedAt,
			Labels:      labels,
			Reactions:   reactions,
			Closed:      issue.ClosedAt,
			IsLocked:    *issue.Locked,
		})
	}

	return allIssues, len(issues) < perPage, nil
}

// GetComments returns comments according issueNumber
func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
	var allComments = make([]*base.Comment, 0, 100)
	opt := &github.IssueListCommentsOptions{
		Sort:      "created",
		Direction: "asc",
		ListOptions: github.ListOptions{
			PerPage: 100,
		},
	}
	for {
		comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
		if err != nil {
			return nil, fmt.Errorf("error while listing repos: %v", err)
		}
		for _, comment := range comments {
			var email string
			if comment.User.Email != nil {
				email = *comment.User.Email
			}
			var reactions *base.Reactions
			if comment.Reactions != nil {
				reactions = convertGithubReactions(comment.Reactions)
			}
			allComments = append(allComments, &base.Comment{
				IssueIndex:  issueNumber,
				PosterID:    *comment.User.ID,
				PosterName:  *comment.User.Login,
				PosterEmail: email,
				Content:     *comment.Body,
				Created:     *comment.CreatedAt,
				Reactions:   reactions,
			})
		}
		if resp.NextPage == 0 {
			break
		}
		opt.Page = resp.NextPage
	}
	return allComments, nil
}

// GetPullRequests returns pull requests according page and perPage
func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
	opt := &github.PullRequestListOptions{
		Sort:      "created",
		Direction: "asc",
		State:     "all",
		ListOptions: github.ListOptions{
			PerPage: perPage,
			Page:    page,
		},
	}
	var allPRs = make([]*base.PullRequest, 0, perPage)

	prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
	if err != nil {
		return nil, fmt.Errorf("error while listing repos: %v", err)
	}
	for _, pr := range prs {
		var body string
		if pr.Body != nil {
			body = *pr.Body
		}
		var milestone string
		if pr.Milestone != nil {
			milestone = *pr.Milestone.Title
		}
		var labels = make([]*base.Label, 0, len(pr.Labels))
		for _, l := range pr.Labels {
			labels = append(labels, convertGithubLabel(l))
		}

		// FIXME: This API missing reactions, we may need another extra request to get reactions

		var email string
		if pr.User.Email != nil {
			email = *pr.User.Email
		}
		var merged bool
		// pr.Merged is not valid, so use MergedAt to test if it's merged
		if pr.MergedAt != nil {
			merged = true
		}

		var (
			headRepoName string
			cloneURL     string
			headRef      string
			headSHA      string
		)
		if pr.Head.Repo != nil {
			if pr.Head.Repo.Name != nil {
				headRepoName = *pr.Head.Repo.Name
			}
			if pr.Head.Repo.CloneURL != nil {
				cloneURL = *pr.Head.Repo.CloneURL
			}
		}
		if pr.Head.Ref != nil {
			headRef = *pr.Head.Ref
		}
		if pr.Head.SHA != nil {
			headSHA = *pr.Head.SHA
		}
		var mergeCommitSHA string
		if pr.MergeCommitSHA != nil {
			mergeCommitSHA = *pr.MergeCommitSHA
		}

		var headUserName string
		if pr.Head.User != nil && pr.Head.User.Login != nil {
			headUserName = *pr.Head.User.Login
		}

		allPRs = append(allPRs, &base.PullRequest{
			Title:          *pr.Title,
			Number:         int64(*pr.Number),
			PosterName:     *pr.User.Login,
			PosterID:       *pr.User.ID,
			PosterEmail:    email,
			Content:        body,
			Milestone:      milestone,
			State:          *pr.State,
			Created:        *pr.CreatedAt,
			Closed:         pr.ClosedAt,
			Labels:         labels,
			Merged:         merged,
			MergeCommitSHA: mergeCommitSHA,
			MergedTime:     pr.MergedAt,
			IsLocked:       pr.ActiveLockReason != nil,
			Head: base.PullRequestBranch{
				Ref:       headRef,
				SHA:       headSHA,
				RepoName:  headRepoName,
				OwnerName: headUserName,
				CloneURL:  cloneURL,
			},
			Base: base.PullRequestBranch{
				Ref:       *pr.Base.Ref,
				SHA:       *pr.Base.SHA,
				RepoName:  *pr.Base.Repo.Name,
				OwnerName: *pr.Base.User.Login,
			},
			PatchURL: *pr.PatchURL,
		})
	}

	return allPRs, nil
}