package gogithub

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

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

	"github.com/aws/eks-anywhere/pkg/git"
	"github.com/aws/eks-anywhere/pkg/logger"
	"github.com/aws/eks-anywhere/pkg/retrier"
)

type GoGithub struct {
	Opts   Options
	Client Client
}

type Options struct {
	Auth git.TokenAuth
}

func New(ctx context.Context, opts Options) *GoGithub {
	return &GoGithub{
		Opts:   opts,
		Client: newClient(ctx, opts),
	}
}

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

type Client interface {
	CreateRepo(ctx context.Context, org string, repo *goGithub.Repository) (*goGithub.Repository, *goGithub.Response, error)
	AddDeployKeyToRepo(ctx context.Context, owner, repo string, key *goGithub.Key) error
	Repo(ctx context.Context, owner, repo string) (*goGithub.Repository, *goGithub.Response, error)
	User(ctx context.Context, user string) (*goGithub.User, *goGithub.Response, error)
	Organization(ctx context.Context, org string) (*goGithub.Organization, *goGithub.Response, error)
	GetContents(ctx context.Context, owner, repo, path string, opt *goGithub.RepositoryContentGetOptions) (
		fileContent *goGithub.RepositoryContent, directoryContent []*goGithub.RepositoryContent, resp *goGithub.Response, err error,
	)
	DeleteRepo(ctx context.Context, owner, repo string) (*goGithub.Response, error)
}

type githubClient struct {
	client *goGithub.Client
}

var HttpClient HTTPClient

func init() {
	HttpClient = &http.Client{}
}

func (ggc *githubClient) CreateRepo(ctx context.Context, org string, repo *goGithub.Repository) (*goGithub.Repository, *goGithub.Response, error) {
	return ggc.client.Repositories.Create(ctx, org, repo)
}

func (ggc *githubClient) Repo(ctx context.Context, owner, repo string) (*goGithub.Repository, *goGithub.Response, error) {
	return ggc.client.Repositories.Get(ctx, owner, repo)
}

func (ggc *githubClient) User(ctx context.Context, user string) (*goGithub.User, *goGithub.Response, error) {
	return ggc.client.Users.Get(ctx, user)
}

func (ggc *githubClient) Organization(ctx context.Context, org string) (*goGithub.Organization, *goGithub.Response, error) {
	return ggc.client.Organizations.Get(ctx, org)
}

func (ggc *githubClient) GetContents(ctx context.Context, owner, repo, path string, opt *goGithub.RepositoryContentGetOptions) (fileContent *goGithub.RepositoryContent, directoryContent []*goGithub.RepositoryContent, resp *goGithub.Response, err error) {
	return ggc.client.Repositories.GetContents(ctx, owner, repo, path, opt)
}

func (ggc *githubClient) DeleteRepo(ctx context.Context, owner, repo string) (*goGithub.Response, error) {
	return ggc.client.Repositories.Delete(ctx, owner, repo)
}

func (ggc *githubClient) AddDeployKeyToRepo(ctx context.Context, owner, repo string, key *goGithub.Key) error {
	_, resp, err := ggc.client.Repositories.CreateKey(ctx, owner, repo, key)
	if err != nil {
		logger.Info("createKey response", "resp", resp)
		return fmt.Errorf("adding deploy key to repo: %v", err)
	}
	return err
}

// CreateRepo creates an empty Github Repository. The repository must be initialized locally or
// file must be added to it via the github api before it can be successfully cloned.
func (g *GoGithub) CreateRepo(ctx context.Context, opts git.CreateRepoOpts) (repository *git.Repository, err error) {
	logger.V(3).Info("Attempting to create new Github repo", "repo", opts.Name, "owner", opts.Owner)
	r := &goGithub.Repository{
		Name:        &opts.Name,
		Private:     &opts.Privacy,
		Description: &opts.Description,
		AutoInit:    &opts.AutoInit,
	}

	org := ""
	if !opts.Personal {
		org = opts.Owner
		logger.V(4).Info("Not a personal repository; using repository Owner as Org", "org", org, "owner", opts.Owner)
	}

	repo, _, err := g.Client.CreateRepo(ctx, org, r)
	if err != nil {
		return nil, fmt.Errorf("failed to create new Github repo %s: %v", opts.Name, err)
	}
	logger.V(3).Info("Successfully created new Github repo", "repo", repo.GetName(), "owner", opts.Owner)
	return &git.Repository{
		Name:         repo.GetName(),
		CloneUrl:     repo.GetCloneURL(),
		Owner:        repo.GetOwner().GetName(),
		Organization: repo.GetOrganization().GetName(),
	}, err
}

func (g *GoGithub) GetAccessTokenPermissions(accessToken string) (string, error) {
	req, err := http.NewRequest("HEAD", "https://api.github.com/users/codertocat", nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "token "+accessToken)

	var resp *http.Response
	r := retrier.New(3 * time.Minute)
	err = r.Retry(func() error {
		resp, err = HttpClient.Do(req)
		if err != nil {
			return fmt.Errorf("getting Github Personal Access Token permissions %v", err)
		}
		return nil
	})

	permissionsScopes := resp.Header.Get("X-Oauth-Scopes")
	defer resp.Body.Close()

	return permissionsScopes, nil
}

func (g *GoGithub) CheckAccessTokenPermissions(checkPATPermission string, allPermissionScopes string) error {
	logger.Info("Checking Github Access Token permissions")

	allPermissions := strings.Split(allPermissionScopes, ", ")
	for _, permission := range allPermissions {
		if permission == checkPATPermission {
			return nil
		}
	}
	return errors.New("github access token does not have repo permissions")
}

// GetRepo describes a remote repository, return the repo name if it exists.
// If the repo does not exist, resulting in a 404 exception, it returns a `RepoDoesNotExist` error.
func (g *GoGithub) GetRepo(ctx context.Context, opts git.GetRepoOpts) (*git.Repository, error) {
	r := opts.Repository
	o := opts.Owner
	logger.V(3).Info("Describing Github repository", "name", r, "owner", o)
	repo, _, err := g.Client.Repo(ctx, o, r)
	if err != nil {
		if isNotFound(err) {
			return nil, &git.RepositoryDoesNotExistError{Err: err}
		}
		return nil, fmt.Errorf("unexpected error when describing repository %s: %w", r, err)
	}
	return &git.Repository{
		Name:         repo.GetName(),
		CloneUrl:     repo.GetCloneURL(),
		Owner:        repo.GetOwner().GetName(),
		Organization: repo.GetOrganization().GetName(),
	}, err
}

func (g *GoGithub) AuthenticatedUser(ctx context.Context) (*goGithub.User, error) {
	githubUser, _, err := g.Client.User(ctx, "") // passing the empty string will fetch the authenticated
	if err != nil {
		return nil, fmt.Errorf("failed while getting the authenticated github user %v", err)
	}
	return githubUser, nil
}

func (g *GoGithub) Organization(ctx context.Context, org string) (*goGithub.Organization, error) {
	organization, _, err := g.Client.Organization(ctx, org)
	if err != nil {
		return nil, fmt.Errorf("failed while getting github organization %s details %v", org, err)
	}
	return organization, nil
}

// PathExists checks if a path exists in the remote repository. If the owner, repository or branch doesn't exist,
// it returns false and no error.
func (g *GoGithub) PathExists(ctx context.Context, owner, repo, branch, path string) (bool, error) {
	_, _, _, err := g.Client.GetContents(
		ctx,
		owner,
		repo,
		path,
		&goGithub.RepositoryContentGetOptions{Ref: branch},
	)

	if isNotFound(err) {
		return false, nil
	}

	if err != nil {
		return false, fmt.Errorf("failed checking if path %s exists in remote github repository: %v", path, err)
	}

	return true, nil
}

func (g *GoGithub) AddDeployKeyToRepo(ctx context.Context, opts git.AddDeployKeyOpts) error {
	logger.V(3).Info("Adding deploy key to repository", "repository", opts.Repository, "owner", opts.Owner)
	k := &goGithub.Key{
		Key:      &opts.Key,
		Title:    &opts.Title,
		ReadOnly: &opts.ReadOnly,
	}
	return g.Client.AddDeployKeyToRepo(ctx, opts.Owner, opts.Repository, k)
}

// DeleteRepo deletes a Github repository.
func (g *GoGithub) DeleteRepo(ctx context.Context, opts git.DeleteRepoOpts) error {
	r := opts.Repository
	o := opts.Owner
	logger.V(3).Info("Deleting Github repository", "name", r, "owner", o)
	_, err := g.Client.DeleteRepo(ctx, o, r)
	if err != nil {
		return fmt.Errorf("deleting repository %s: %v", r, err)
	}
	return nil
}

func newClient(ctx context.Context, opts Options) Client {
	ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: opts.Auth.Token})
	tc := oauth2.NewClient(ctx, ts)
	return &githubClient{goGithub.NewClient(tc)}
}

func isNotFound(err error) bool {
	var e *goGithub.ErrorResponse
	return errors.As(err, &e) && e.Response.StatusCode == http.StatusNotFound
}
