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

package gitea

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// PRBranchInfo information about a branch
type PRBranchInfo struct {
	Name       string      `json:"label"`
	Ref        string      `json:"ref"`
	Sha        string      `json:"sha"`
	RepoID     int64       `json:"repo_id"`
	Repository *Repository `json:"repo"`
}

// PullRequest represents a pull request
type PullRequest struct {
	ID                      int64      `json:"id"`
	URL                     string     `json:"url"`
	Index                   int64      `json:"number"`
	Poster                  *User      `json:"user"`
	Title                   string     `json:"title"`
	Body                    string     `json:"body"`
	Labels                  []*Label   `json:"labels"`
	Milestone               *Milestone `json:"milestone"`
	Assignee                *User      `json:"assignee"`
	Assignees               []*User    `json:"assignees"`
	RequestedReviewers      []*User    `json:"requested_reviewers"`
	RequestedReviewersTeams []*Team    `json:"requested_reviewers_teams"`
	State                   StateType  `json:"state"`
	Draft                   bool       `json:"draft"`
	IsLocked                bool       `json:"is_locked"`
	Comments                int        `json:"comments"`
	ReviewComments          int        `json:"review_comments,omitempty"`

	HTMLURL  string `json:"html_url"`
	DiffURL  string `json:"diff_url"`
	PatchURL string `json:"patch_url"`

	Mergeable           bool       `json:"mergeable"`
	HasMerged           bool       `json:"merged"`
	Merged              *time.Time `json:"merged_at"`
	MergedCommitID      *string    `json:"merge_commit_sha"`
	MergedBy            *User      `json:"merged_by"`
	AllowMaintainerEdit bool       `json:"allow_maintainer_edit"`

	Base      *PRBranchInfo `json:"base"`
	Head      *PRBranchInfo `json:"head"`
	MergeBase string        `json:"merge_base"`

	Deadline *time.Time `json:"due_date"`
	Created  *time.Time `json:"created_at"`
	Updated  *time.Time `json:"updated_at"`
	Closed   *time.Time `json:"closed_at"`

	Additions    *int `json:"additions,omitempty"`
	Deletions    *int `json:"deletions,omitempty"`
	ChangedFiles *int `json:"changed_files,omitempty"`
	PinOrder     int  `json:"pin_order"`
}

// ChangedFile is a changed file in a diff
type ChangedFile struct {
	Filename         string `json:"filename"`
	PreviousFilename string `json:"previous_filename"`
	Status           string `json:"status"`
	Additions        int    `json:"additions"`
	Deletions        int    `json:"deletions"`
	Changes          int    `json:"changes"`
	HTMLURL          string `json:"html_url"`
	ContentsURL      string `json:"contents_url"`
	RawURL           string `json:"raw_url"`
}

// ListPullRequestsOptions options for listing pull requests
type ListPullRequestsOptions struct {
	ListOptions
	State StateType `json:"state"`
	// oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority
	Sort      string
	Milestone int64
}

// MergeStyle is used specify how a pull is merged
type MergeStyle string

const (
	// MergeStyleMerge merge pull as usual
	MergeStyleMerge MergeStyle = "merge"
	// MergeStyleRebase rebase pull
	MergeStyleRebase MergeStyle = "rebase"
	// MergeStyleRebaseMerge rebase and merge pull
	MergeStyleRebaseMerge MergeStyle = "rebase-merge"
	// MergeStyleSquash squash and merge pull
	MergeStyleSquash MergeStyle = "squash"
	// MergeStyleFastForwardOnly fast-forward merge
	MergeStyleFastForwardOnly MergeStyle = "fast-forward-only"
	// MergeStyleManuallyMerged manually merged
	MergeStyleManuallyMerged MergeStyle = "manually-merged"
)

// QueryEncode turns options into querystring argument
func (opt *ListPullRequestsOptions) QueryEncode() string {
	query := opt.getURLQuery()
	if len(opt.State) > 0 {
		query.Add("state", string(opt.State))
	}
	if len(opt.Sort) > 0 {
		query.Add("sort", opt.Sort)
	}
	if opt.Milestone > 0 {
		query.Add("milestone", fmt.Sprintf("%d", opt.Milestone))
	}
	return query.Encode()
}

// ListRepoPullRequests list PRs of one repository
func (c *PullRequestsService) ListRepoPullRequests(ctx context.Context, owner, repo string, opt ListPullRequestsOptions) ([]*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	opt.setDefaults()
	prs := make([]*PullRequest, 0, opt.PageSize)

	link, _ := url.Parse(fmt.Sprintf("/repos/%s/%s/pulls", owner, repo))
	link.RawQuery = opt.QueryEncode()
	resp, err := c.getParsedResponse(ctx, "GET", link.String(), jsonHeader, nil, &prs)
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_14_0) != nil {
		for i := range prs {
			if err := fixPullHeadSha(ctx, c.Client, prs[i]); err != nil {
				return prs, resp, err
			}
		}
	}
	return prs, resp, err
}

// GetPullRequest get information of one PR
func (c *PullRequestsService) GetPullRequest(ctx context.Context, owner, repo string, index int64) (*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	pr := new(PullRequest)
	resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/repos/%s/%s/pulls/%d", owner, repo, index), nil, nil, pr)
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_14_0) != nil {
		if err := fixPullHeadSha(ctx, c.Client, pr); err != nil {
			return pr, resp, err
		}
	}
	return pr, resp, err
}

// GetPullRequestByBaseHead gets a pull request by its base and head branches.
func (c *PullRequestsService) GetPullRequestByBaseHead(ctx context.Context, owner, repo, base, head string) (*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo, &base); err != nil {
		return nil, nil, err
	}
	if err := c.checkServerVersionGreaterThanOrEqual(ctx, version1_22_0); err != nil {
		return nil, nil, err
	}
	head = pathEscapeSegments(head)
	pr := new(PullRequest)
	resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/repos/%s/%s/pulls/%s/%s", owner, repo, base, head), nil, nil, pr)
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_14_0) != nil {
		if err := fixPullHeadSha(ctx, c.Client, pr); err != nil {
			return pr, resp, err
		}
	}
	return pr, resp, err
}

// CreatePullRequestOption options when creating a pull request
type CreatePullRequestOption struct {
	Head          string     `json:"head"`
	Base          string     `json:"base"`
	Title         string     `json:"title"`
	Body          string     `json:"body"`
	Assignee      string     `json:"assignee"`
	Assignees     []string   `json:"assignees"`
	Reviewers     []string   `json:"reviewers"`
	TeamReviewers []string   `json:"team_reviewers"`
	Milestone     int64      `json:"milestone"`
	Labels        []int64    `json:"labels"`
	Deadline      *time.Time `json:"due_date"`
}

// CreatePullRequest create pull request with options
func (c *PullRequestsService) CreatePullRequest(ctx context.Context, owner, repo string, opt CreatePullRequestOption) (*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return nil, nil, err
	}
	pr := new(PullRequest)
	resp, err := c.getParsedResponse(ctx, "POST",
		fmt.Sprintf("/repos/%s/%s/pulls", owner, repo),
		jsonHeader, bytes.NewReader(body), pr)
	return pr, resp, err
}

// EditPullRequestOption options when modify pull request
type EditPullRequestOption struct {
	Title               string     `json:"title"`
	Body                *string    `json:"body"`
	Base                string     `json:"base"`
	Assignee            string     `json:"assignee"`
	Assignees           []string   `json:"assignees"`
	Milestone           int64      `json:"milestone"`
	Labels              []int64    `json:"labels"`
	State               *StateType `json:"state"`
	Deadline            *time.Time `json:"due_date"`
	RemoveDeadline      *bool      `json:"unset_due_date"`
	AllowMaintainerEdit *bool      `json:"allow_maintainer_edit"`
}

// Validate the EditPullRequestOption struct
func (opt EditPullRequestOption) Validate(ctx context.Context, c *Client) error {
	if len(opt.Title) != 0 && len(strings.TrimSpace(opt.Title)) == 0 {
		return fmt.Errorf("title is empty")
	}
	if len(opt.Base) != 0 {
		if err := c.checkServerVersionGreaterThanOrEqual(ctx, version1_12_0); err != nil {
			return fmt.Errorf("can not change base gitea to old")
		}
	}
	return nil
}

// EditPullRequest modify pull request with PR id and options
func (c *PullRequestsService) EditPullRequest(ctx context.Context, owner, repo string, index int64, opt EditPullRequestOption) (*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	if err := opt.Validate(ctx, c.Client); err != nil {
		return nil, nil, err
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return nil, nil, err
	}
	pr := new(PullRequest)
	resp, err := c.getParsedResponse(ctx, "PATCH",
		fmt.Sprintf("/repos/%s/%s/pulls/%d", owner, repo, index),
		jsonHeader, bytes.NewReader(body), pr)
	return pr, resp, err
}

// MergePullRequestOption options when merging a pull request
type MergePullRequestOption struct {
	Style                  MergeStyle `json:"Do"`
	MergeCommitID          string     `json:"MergeCommitID"`
	Title                  string     `json:"MergeTitleField"`
	Message                string     `json:"MergeMessageField"`
	DeleteBranchAfterMerge *bool      `json:"delete_branch_after_merge,omitempty"`
	ForceMerge             bool       `json:"force_merge"`
	HeadCommitId           string     `json:"head_commit_id"`
	MergeWhenChecksSucceed bool       `json:"merge_when_checks_succeed"`
}

// Validate the MergePullRequestOption struct
func (opt MergePullRequestOption) Validate(ctx context.Context, c *Client) error {
	if opt.Style == MergeStyleSquash {
		if err := c.checkServerVersionGreaterThanOrEqual(ctx, version1_11_5); err != nil {
			return err
		}
	}
	return nil
}

// MergePullRequest merge a PR to repository by PR id
func (c *PullRequestsService) MergePullRequest(ctx context.Context, owner, repo string, index int64, opt MergePullRequestOption) (bool, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return false, nil, err
	}
	if err := opt.Validate(ctx, c.Client); err != nil {
		return false, nil, err
	}
	if opt.HeadCommitId == "" {
		pr, _, err := c.GetPullRequest(ctx, owner, repo, index)
		if err != nil {
			return false, nil, err
		}
		if pr != nil && pr.Head != nil {
			opt.HeadCommitId = pr.Head.Sha
		}
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return false, nil, err
	}
	status, resp, err := c.getStatusCode(ctx, "POST", fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", owner, repo, index), jsonHeader, bytes.NewReader(body))
	if err != nil {
		return false, resp, err
	}
	return status == 200 || status == 201, resp, nil
}

// IsPullRequestMerged test if one PR is merged to one repository
func (c *PullRequestsService) IsPullRequestMerged(ctx context.Context, owner, repo string, index int64) (bool, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return false, nil, err
	}
	status, resp, err := c.getStatusCode(ctx, "GET", fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", owner, repo, index), nil, nil)
	if err != nil {
		return false, resp, err
	}

	return status == 204, resp, nil
}

// PullRequestDiffOptions options for GET /repos/<owner>/<repo>/pulls/<idx>.[diff|patch]
type PullRequestDiffOptions struct {
	// Include binary file changes when requesting a .diff
	Binary bool
}

// QueryEncode converts the options to a query string
func (o PullRequestDiffOptions) QueryEncode() string {
	query := make(url.Values)
	query.Add("binary", fmt.Sprintf("%v", o.Binary))
	return query.Encode()
}

type pullRequestDiffType string

const (
	pullRequestDiffTypeDiff  pullRequestDiffType = "diff"
	pullRequestDiffTypePatch pullRequestDiffType = "patch"
)

// getPullRequestDiffOrPatch gets the patch or diff file as bytes for a PR
func (c *PullRequestsService) getPullRequestDiffOrPatch(ctx context.Context, owner, repo string, kind pullRequestDiffType, index int64, opts PullRequestDiffOptions) ([]byte, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	if err := c.checkServerVersionGreaterThanOrEqual(ctx, version1_13_0); err != nil {
		r, _, err2 := c.GetRepo(ctx, owner, repo)
		if err2 != nil {
			return nil, nil, err
		}
		if r.Private {
			return nil, nil, err
		}
		url := fmt.Sprintf("/%s/%s/pulls/%d.%s?%s", owner, repo, index, kind, opts.QueryEncode())
		return c.getWebResponse(ctx, "GET", url, nil)
	}
	return c.getResponse(ctx, "GET", fmt.Sprintf("/repos/%s/%s/pulls/%d.%s", owner, repo, index, kind), nil, nil)
}

// GetPullRequestPatch gets the git patchset of a PR
func (c *PullRequestsService) GetPullRequestPatch(ctx context.Context, owner, repo string, index int64) ([]byte, *Response, error) {
	return c.getPullRequestDiffOrPatch(ctx, owner, repo, pullRequestDiffTypePatch, index, PullRequestDiffOptions{})
}

// GetPullRequestDiff gets the diff of a PR. For Gitea >= 1.16, you must set includeBinary to get an applicable diff
func (c *PullRequestsService) GetPullRequestDiff(ctx context.Context, owner, repo string, index int64, opts PullRequestDiffOptions) ([]byte, *Response, error) {
	return c.getPullRequestDiffOrPatch(ctx, owner, repo, pullRequestDiffTypeDiff, index, opts)
}

// CancelScheduledAutoMerge cancels a scheduled automatic merge for a pull request.
func (c *PullRequestsService) CancelScheduledAutoMerge(ctx context.Context, owner, repo string, index int64) (*Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, err
	}
	if err := c.checkServerVersionGreaterThanOrEqual(ctx, version1_18_0); err != nil {
		return nil, err
	}
	return c.doRequestWithStatusHandle(ctx, "DELETE", fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", owner, repo, index), nil, nil)
}

// ListPullRequestCommitsOptions options for listing pull requests
type ListPullRequestCommitsOptions struct {
	ListOptions
}

// ListPullRequestCommits list commits for a pull request
func (c *PullRequestsService) ListPullRequestCommits(ctx context.Context, owner, repo string, index int64, opt ListPullRequestCommitsOptions) ([]*Commit, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	link, _ := url.Parse(fmt.Sprintf("/repos/%s/%s/pulls/%d/commits", owner, repo, index))
	opt.setDefaults()
	commits := make([]*Commit, 0, opt.PageSize)
	link.RawQuery = opt.getURLQuery().Encode()
	resp, err := c.getParsedResponse(ctx, "GET", link.String(), nil, nil, &commits)
	return commits, resp, err
}

// fixPullHeadSha is a workaround for https://github.com/go-gitea/gitea/issues/12675
// When no head sha is available, this is because the branch got deleted in the base repo.
// pr.Head.Ref points in this case not to the head repo branch name, but the base repo ref,
// which stays available to resolve the commit sha. This is fixed for gitea >= 1.14.0
func fixPullHeadSha(ctx context.Context, client *Client, pr *PullRequest) error {
	if pr.Base != nil && pr.Base.Repository != nil && pr.Base.Repository.Owner != nil &&
		pr.Head != nil && pr.Head.Ref != "" && pr.Head.Sha == "" {
		owner := pr.Base.Repository.Owner.UserName
		repo := pr.Base.Repository.Name
		refs, _, err := client.GetRepoRefs(ctx, owner, repo, pr.Head.Ref)
		if err != nil {
			return err
		} else if len(refs) == 0 {
			return fmt.Errorf("unable to resolve PR ref '%s'", pr.Head.Ref)
		}
		pr.Head.Sha = refs[0].Object.SHA
	}
	return nil
}

// ListPullRequestFilesOptions options for listing pull request files
type ListPullRequestFilesOptions struct {
	ListOptions
}

// ListPullRequestFiles list changed files for a pull request
func (c *PullRequestsService) ListPullRequestFiles(ctx context.Context, owner, repo string, index int64, opt ListPullRequestFilesOptions) ([]*ChangedFile, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	link, _ := url.Parse(fmt.Sprintf("/repos/%s/%s/pulls/%d/files", owner, repo, index))
	opt.setDefaults()
	files := make([]*ChangedFile, 0, opt.PageSize)
	link.RawQuery = opt.getURLQuery().Encode()
	resp, err := c.getParsedResponse(ctx, "GET", link.String(), nil, nil, &files)
	return files, resp, err
}

// ListRepoPinnedPullRequests lists a repo's pinned pull requests.
func (c *PullRequestsService) ListRepoPinnedPullRequests(ctx context.Context, owner, repo string) ([]*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	prs := make([]*PullRequest, 0, 5)
	resp, err := c.getParsedResponse(ctx, "GET",
		fmt.Sprintf("/repos/%s/%s/pulls/pinned", owner, repo),
		jsonHeader, nil, &prs)
	return prs, resp, err
}

// GetCommitPullRequest gets the pull request associated with a commit SHA
func (c *PullRequestsService) GetCommitPullRequest(ctx context.Context, owner, repo, sha string) (*PullRequest, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo, &sha); err != nil {
		return nil, nil, err
	}
	pr := new(PullRequest)
	resp, err := c.getParsedResponse(ctx, "GET",
		fmt.Sprintf("/repos/%s/%s/commits/%s/pull", owner, repo, sha),
		jsonHeader, nil, &pr)
	return pr, resp, err
}

// UpdatePullRequest updates a pull request with new commits from the base branch
func (c *PullRequestsService) UpdatePullRequest(ctx context.Context, owner, repo string, index int64) (*Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, err
	}
	status, resp, err := c.getStatusCode(ctx, "POST",
		fmt.Sprintf("/repos/%s/%s/pulls/%d/update", owner, repo, index),
		jsonHeader, nil)
	if err != nil {
		return resp, err
	}
	if status != http.StatusOK {
		return resp, fmt.Errorf("unexpected status: %d", status)
	}
	return resp, nil
}
