package prManager

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	gogithub "github.com/google/go-github/v48/github"

	"github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/github"
	"github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/logger"
	"github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/retrier"
)

type PrCreator struct {
	client      *github.Client
	sourceOwner string
	sourceRepo  string
	prRepo      string
	prRepoOwner string
	retrier     *retrier.Retrier
}

type Opts struct {
	SourceOwner string
	SourceRepo  string
	PrRepo      string
	PrRepoOwner string
}

func New(retrier *retrier.Retrier, client *github.Client, opts *Opts) *PrCreator {
	return &PrCreator{
		client:      client,
		sourceOwner: opts.SourceOwner,
		sourceRepo:  opts.SourceRepo,
		prRepo:      opts.PrRepo,
		prRepoOwner: opts.PrRepoOwner,
		retrier:     retrier,
	}
}

// getRef returns the commit branch reference object if it exists or creates it
// from the base branch before returning it.
func (p *PrCreator) getRef(ctx context.Context, commitBranch string, baseBranch string) (ref *gogithub.Reference, err error) {
	if ref, _, err = p.client.Git.GetRef(ctx, p.sourceOwner, p.sourceRepo, "refs/heads/"+commitBranch); err == nil {
		return ref, nil
	}

	if commitBranch == baseBranch {
		return nil, errors.New("the commit-branch does not exist but base-branch is the same as commit-branch")
	}

	var baseRef *gogithub.Reference
	if baseRef, _, err = p.client.Git.GetRef(ctx, p.sourceOwner, p.sourceRepo, "refs/heads/"+baseBranch); err != nil {
		return nil, err
	}
	newRef := &gogithub.Reference{Ref: gogithub.String("refs/heads/" + commitBranch), Object: &gogithub.GitObject{SHA: baseRef.Object.SHA}}
	ref, _, err = p.client.Git.CreateRef(ctx, p.sourceOwner, p.sourceRepo, newRef)
	return ref, err
}

// getTree generates the tree to commit based on the given files and the commit
// of the ref you got in getRef.
func (p *PrCreator) getTree(ctx context.Context, ref *gogithub.Reference, sourceFileBody []byte, destFilePath string) (tree *gogithub.Tree, err error) {
	// Create a tree with what to commit.
	entries := []*gogithub.TreeEntry{}
	entries = append(entries, &gogithub.TreeEntry{Path: gogithub.String(destFilePath), Type: gogithub.String("blob"), Content: gogithub.String(string(sourceFileBody)), Mode: gogithub.String("100644")})

	tree, _, err = p.client.Git.CreateTree(ctx, p.sourceOwner, p.sourceRepo, *ref.Object.SHA, entries)
	return tree, err
}

// pushCommit creates the commit in the given reference using the given tree.
func (p *PrCreator) pushCommit(ctx context.Context, ref *gogithub.Reference, tree *gogithub.Tree, authorName string, authorEmail string, commitMessage string) (err error) {
	// Get the parent commit to attach the commit to.
	parent, _, err := p.client.Repositories.GetCommit(ctx, p.sourceOwner, p.sourceRepo, *ref.Object.SHA, nil)
	if err != nil {
		return err
	}
	// This is not always populated, but is needed.
	parent.Commit.SHA = parent.SHA

	// Create the commit using the tree.
	date := time.Now()
	author := &gogithub.CommitAuthor{
		Date:  &date,
		Name:  &authorName,
		Email: &authorEmail,
	}

	commit := &gogithub.Commit{
		Author:  author,
		Message: &commitMessage,
		Tree:    tree,
		Parents: []*gogithub.Commit{
			parent.Commit,
		},
	}
	newCommit, _, err := p.client.Git.CreateCommit(ctx, p.sourceOwner, p.sourceRepo, commit)
	if err != nil {
		return err
	}

	// Attach the commit to the master branch.
	ref.Object.SHA = newCommit.SHA
	_, _, err = p.client.Git.UpdateRef(ctx, p.sourceOwner, p.sourceRepo, ref, false)
	return err
}

func (p *PrCreator) getPr(ctx context.Context, opts *GetPrOpts) (*gogithub.PullRequest, error) {
	o := &gogithub.PullRequestListOptions{
		Head: fmt.Sprintf("%v:%v", p.prRepoOwner, opts.CommitBranch),
		Base: opts.BaseBranch,
	}
	list, r, err := p.client.PullRequests.List(ctx, p.prRepoOwner, p.prRepo, o)
	if err != nil {
		if r != nil {
			logger.V(3).Info("listing pr response", "status code", r.Response.StatusCode)
		}
		return nil, fmt.Errorf("getting open PR into %v from %v: listing PR: %v", opts.BaseBranch, opts.CommitBranch, err)
	}
	if len(list) > 1 {
		return nil, fmt.Errorf("getting open PR into %v from %v: open PR list is greater than 1, this is impossible or wrong. PR list length: %d", opts.BaseBranch, opts.CommitBranch, len(list))
	}
	if len(list) == 0 {
		return nil, nil
	}
	prNumber := *list[0].Number
	pr, r, err := p.client.PullRequests.Get(ctx, p.prRepoOwner, p.sourceRepo, prNumber)
	if err != nil {
		if r != nil {
			logger.V(3).Info("getting pr response", "status code", r.Response.StatusCode)
		}
		return nil, fmt.Errorf("getting open PR number %d: %v", prNumber, err)
	}
	return pr, nil
}

func (p *PrCreator) createPR(ctx context.Context, opts *CreatePrOpts) (pr *gogithub.PullRequest, err error) {
	if opts.PrSubject == "" {
		return nil, fmt.Errorf("PR subject is required")
	}

	if p.prRepoOwner != "" && p.prRepoOwner != p.sourceOwner {
		opts.CommitBranch = fmt.Sprintf("%s:%s", p.sourceOwner, opts.CommitBranch)
	} else {
		p.prRepoOwner = p.sourceOwner
	}

	if p.prRepo == "" {
		p.prRepo = p.sourceRepo
	}

	newPR := &gogithub.NewPullRequest{
		Title:               &opts.PrSubject,
		Head:                &opts.CommitBranch,
		Base:                &opts.PrBranch,
		Body:                &opts.PrDescription,
		MaintainerCanModify: gogithub.Bool(true),
	}

	var pullRequest *gogithub.PullRequest
	var resp *gogithub.Response
	err = p.retrier.Retry(func() error {
		pullRequest, resp, err = p.client.PullRequests.Create(ctx, p.prRepoOwner, p.prRepo, newPR)
		if resp.StatusCode == github.SecondaryRateLimitStatusCode {
			if strings.Contains(err.Error(), github.SecondaryRateLimitResponse) {
				return fmt.Errorf("rate limited while attempting to create github pull request: %v", err)
			}
		}
		if err != nil && strings.Contains(err.Error(), github.PullRequestAlreadyExistsForBranchError) {
			// there can only be one PR per branch; if there's already an existing PR for the branch, we won't create one, but continue
			logger.V(1).Info("A Pull Request already exists for the given branch", "branch", opts.CommitBranch)
			getPrOpts := &GetPrOpts{
				CommitBranch: opts.CommitBranch,
				BaseBranch:   "main",
			}
			pullRequest, err = p.getPr(ctx, getPrOpts)
			if err != nil {
				return err
			}
			return nil
		}
		if err != nil {
			return fmt.Errorf("creating Github pull request: %v; resp: %v", err, resp)
		}
		logger.V(1).Info("Github Pull Request Created", "Pull Request URL", pullRequest.GetHTMLURL())
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("creating github pull request: %v", err)
	}
	return pullRequest, nil
}

type CreatePrOpts struct {
	CommitBranch    string
	BaseBranch      string
	AuthorName      string
	AuthorEmail     string
	CommitMessage   string
	PrSubject       string
	PrBranch        string
	PrDescription   string
	DestFileGitPath string
	SourceFileBody  []byte
}

func (p *PrCreator) CreatePr(ctx context.Context, opts *CreatePrOpts) (string, error) {
	// If opts.CommitMessage is not empty then create the required refs and other objects to create the commit
	// otherwise this will try and create a pr from the repos referenced. This requires the branch to exist.
	// Checkout the goGithub repo's Create function
	// https://github.com/google/go-github/blob/a0e8f35c5cefc688733d566ec3701e86972df056/github/pulls.go#L258
	if opts.CommitMessage != "" {
		ref, err := p.getRef(ctx, opts.CommitBranch, opts.BaseBranch)
		if err != nil {
			return "", fmt.Errorf("creating pull request: get/create the commit reference: %s", err)
		}
		if ref == nil {
			return "", fmt.Errorf("creating pull request: the reference is nil")
		}

		tree, err := p.getTree(ctx, ref, opts.SourceFileBody, opts.DestFileGitPath)
		if err != nil {
			return "", fmt.Errorf("creating the tree based on the provided files: %s", err)
		}

		if err := p.pushCommit(ctx, ref, tree, opts.AuthorName, opts.AuthorEmail, opts.CommitMessage); err != nil {
			return "", fmt.Errorf("creating the commit: %s", err)
		}
	}

	pr, err := p.createPR(ctx, opts)
	if err != nil {
		return "", fmt.Errorf("creating pull request: %s", err)
	}
	return pr.GetHTMLURL(), nil
}

type GetPrOpts struct {
	CommitBranch string
	BaseBranch   string
}

func (p *PrCreator) GetPr(ctx context.Context, opts *GetPrOpts) (string, error) {
	pr, err := p.getPr(ctx, opts)
	if err != nil {
		return "", fmt.Errorf("getting pull request: %s", err)
	}
	return pr.GetHTMLURL(), nil
}
